Merge branch 'master' of github.com:edx/edx-platform into peter-fogg/single-click-video-creation
This commit is contained in:
@@ -1 +1 @@
|
||||
mitx
|
||||
edx-platform
|
||||
|
||||
1
AUTHORS
1
AUTHORS
@@ -72,3 +72,4 @@ Giulio Gratta <giulio@giuliogratta.com>
|
||||
David Baumgold <david@davidbaumgold.com>
|
||||
Jason Bau <jbau@stanford.edu>
|
||||
Frances Botsford <frances@edx.org>
|
||||
Slater Victoroff <slater.r.victoroff@gmail.com>
|
||||
|
||||
44
README.md
44
README.md
@@ -1,19 +1,18 @@
|
||||
This is edX, a platform for online course delivery. The project is primarily
|
||||
written in [Python](http://python.org/), using the
|
||||
[Django](https://www.djangoproject.com/) framework. We also use some
|
||||
[Ruby](http://www.ruby-lang.org/) and some [NodeJS](http://nodejs.org/).
|
||||
This is the main edX platform which consists of LMS and Studio.
|
||||
|
||||
See [code.edx.org](http://code.edx.org/) for other parts of the edX code base.
|
||||
|
||||
Installation
|
||||
============
|
||||
The installation process is a bit messy at the moment. Here's a high-level
|
||||
overview of what you should do to get started.
|
||||
|
||||
**TLDR:** There is a `scripts/create-dev-env.sh` script that will attempt to set all
|
||||
of this up for you. If you're in a hurry, run that script. Otherwise, I suggest
|
||||
that you understand what the script is doing, and why, by reading this document.
|
||||
There is a `scripts/create-dev-env.sh` that will attempt to set up a development
|
||||
environment.
|
||||
|
||||
If you want to better understand what the script is doing, keep reading.
|
||||
|
||||
Directory Hierarchy
|
||||
-------------------
|
||||
|
||||
This code assumes that it is checked out in a directory that has three sibling
|
||||
directories: `data` (used for XML course data), `db` (used to hold a
|
||||
[sqlite](https://sqlite.org/) database), and `log` (used to hold logs). If you
|
||||
@@ -77,6 +76,7 @@ environment), and Node has a library installer called
|
||||
Once you've got your languages and virtual environments set up, install
|
||||
the libraries like so:
|
||||
|
||||
$ pip install -r requirements/edx/pre.txt
|
||||
$ pip install -r requirements/edx/base.txt
|
||||
$ pip install -r requirements/edx/post.txt
|
||||
$ bundle install
|
||||
@@ -144,10 +144,28 @@ in the `data` directory, instead of in Mongo. To run this older version, run:
|
||||
|
||||
$ rake lms
|
||||
|
||||
Further Documentation
|
||||
=====================
|
||||
Once you've got your project up and running, you can check out the `docs`
|
||||
directory to see more documentation about how edX is structured.
|
||||
License
|
||||
-------
|
||||
|
||||
The code in this repository is licensed under version 3 of the AGPL unless
|
||||
otherwise noted.
|
||||
|
||||
Please see ``LICENSE.txt`` for details.
|
||||
|
||||
How to Contribute
|
||||
-----------------
|
||||
|
||||
Contributions are very welcome. The easiest way is to fork this repo, and then
|
||||
make a pull request from your fork. The first time you make a pull request, you
|
||||
may be asked to sign a Contributor Agreement.
|
||||
|
||||
Reporting Security Issues
|
||||
-------------------------
|
||||
|
||||
Please do not report security issues in public. Please email security@edx.org
|
||||
|
||||
Mailing List and IRC Channel
|
||||
----------------------------
|
||||
|
||||
You can discuss this code on the [edx-code Google Group](https://groups.google.com/forum/#!forum/edx-code) or in the
|
||||
`edx-code` IRC channel on Freenode.
|
||||
|
||||
@@ -27,3 +27,59 @@ def click_component_from_menu(instance_id, expected_css):
|
||||
assert_equal(1, len(world.css_find(elem_css)))
|
||||
world.css_click(elem_css)
|
||||
assert_equal(1, len(world.css_find(expected_css)))
|
||||
|
||||
@world.absorb
|
||||
def edit_component_and_select_settings():
|
||||
world.css_click('a.edit-button')
|
||||
world.css_click('#settings-mode')
|
||||
|
||||
|
||||
@world.absorb
|
||||
def verify_setting_entry(setting, display_name, value, explicitly_set):
|
||||
assert_equal(display_name, setting.find_by_css('.setting-label')[0].value)
|
||||
assert_equal(value, setting.find_by_css('.setting-input')[0].value)
|
||||
settingClearButton = setting.find_by_css('.setting-clear')[0]
|
||||
assert_equal(explicitly_set, settingClearButton.has_class('active'))
|
||||
assert_equal(not explicitly_set, settingClearButton.has_class('inactive'))
|
||||
|
||||
|
||||
@world.absorb
|
||||
def verify_all_setting_entries(expected_entries):
|
||||
settings = world.browser.find_by_css('.wrapper-comp-setting')
|
||||
assert_equal(len(expected_entries), len(settings))
|
||||
for (counter, setting) in enumerate(settings):
|
||||
world.verify_setting_entry(
|
||||
setting, expected_entries[counter][0],
|
||||
expected_entries[counter][1], expected_entries[counter][2]
|
||||
)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def save_component_and_reopen(step):
|
||||
world.css_click("a.save-button")
|
||||
# We have a known issue that modifications are still shown within the edit window after cancel (though)
|
||||
# they are not persisted. Refresh the browser to make sure the changes WERE persisted after Save.
|
||||
reload_the_page(step)
|
||||
edit_component_and_select_settings()
|
||||
|
||||
|
||||
@world.absorb
|
||||
def cancel_component(step):
|
||||
world.css_click("a.cancel-button")
|
||||
# We have a known issue that modifications are still shown within the edit window after cancel (though)
|
||||
# they are not persisted. Refresh the browser to make sure the changes were not persisted.
|
||||
reload_the_page(step)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def revert_setting_entry(label):
|
||||
get_setting_entry(label).find_by_css('.setting-clear')[0].click()
|
||||
|
||||
|
||||
@world.absorb
|
||||
def get_setting_entry(label):
|
||||
settings = world.browser.find_by_css('.wrapper-comp-setting')
|
||||
for setting in settings:
|
||||
if setting.find_by_css('.setting-label')[0].value == label:
|
||||
return setting
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
Feature: Discussion Component Editor
|
||||
As a course author, I want to be able to create discussion components.
|
||||
|
||||
Scenario: User can view metadata
|
||||
Given I have created a Discussion Tag
|
||||
And I edit and select Settings
|
||||
Then I see three alphabetized settings and their expected values
|
||||
|
||||
Scenario: User can modify display name
|
||||
Given I have created a Discussion Tag
|
||||
And I edit and select Settings
|
||||
Then I can modify the display name
|
||||
And my display name change is persisted on save
|
||||
23
cms/djangoapps/contentstore/features/discussion-editor.py
Normal file
23
cms/djangoapps/contentstore/features/discussion-editor.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# disable missing docstring
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world, step
|
||||
|
||||
|
||||
@step('I have created a Discussion Tag$')
|
||||
def i_created_discussion_tag(step):
|
||||
world.create_component_instance(
|
||||
step, '.large-discussion-icon',
|
||||
'i4x://edx/templates/discussion/Discussion_Tag',
|
||||
'.xmodule_DiscussionModule'
|
||||
)
|
||||
|
||||
|
||||
@step('I see three alphabetized settings and their expected values$')
|
||||
def i_see_only_the_settings_and_values(step):
|
||||
world.verify_all_setting_entries(
|
||||
[
|
||||
['Category', "Week 1", True],
|
||||
['Display Name', "Discussion Tag", True],
|
||||
['Subcategory', "Topic-Level Student-Visible Label", True]
|
||||
])
|
||||
13
cms/djangoapps/contentstore/features/html-editor.feature
Normal file
13
cms/djangoapps/contentstore/features/html-editor.feature
Normal file
@@ -0,0 +1,13 @@
|
||||
Feature: HTML Editor
|
||||
As a course author, I want to be able to create HTML blocks.
|
||||
|
||||
Scenario: User can view metadata
|
||||
Given I have created a Blank HTML Page
|
||||
And I edit and select Settings
|
||||
Then I see only the HTML display name setting
|
||||
|
||||
Scenario: User can modify display name
|
||||
Given I have created a Blank HTML Page
|
||||
And I edit and select Settings
|
||||
Then I can modify the display name
|
||||
And my display name change is persisted on save
|
||||
17
cms/djangoapps/contentstore/features/html-editor.py
Normal file
17
cms/djangoapps/contentstore/features/html-editor.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# disable missing docstring
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world, step
|
||||
|
||||
|
||||
@step('I have created a Blank HTML Page$')
|
||||
def i_created_blank_html_page(step):
|
||||
world.create_component_instance(
|
||||
step, '.large-html-icon', 'i4x://edx/templates/html/Blank_HTML_Page',
|
||||
'.xmodule_HtmlModule'
|
||||
)
|
||||
|
||||
|
||||
@step('I see only the HTML display name setting$')
|
||||
def i_see_only_the_html_display_name(step):
|
||||
world.verify_all_setting_entries([['Display Name', "Blank HTML Page", True]])
|
||||
67
cms/djangoapps/contentstore/features/problem-editor.feature
Normal file
67
cms/djangoapps/contentstore/features/problem-editor.feature
Normal file
@@ -0,0 +1,67 @@
|
||||
Feature: Problem Editor
|
||||
As a course author, I want to be able to create problems and edit their settings.
|
||||
|
||||
Scenario: User can view metadata
|
||||
Given I have created a Blank Common Problem
|
||||
And I edit and select Settings
|
||||
Then I see five alphabetized settings and their expected values
|
||||
And Edit High Level Source is not visible
|
||||
|
||||
Scenario: User can modify String values
|
||||
Given I have created a Blank Common Problem
|
||||
And I edit and select Settings
|
||||
Then I can modify the display name
|
||||
And my display name change is persisted on save
|
||||
|
||||
Scenario: User can specify special characters in String values
|
||||
Given I have created a Blank Common Problem
|
||||
And I edit and select Settings
|
||||
Then I can specify special characters in the display name
|
||||
And my special characters and persisted on save
|
||||
|
||||
Scenario: User can revert display name to unset
|
||||
Given I have created a Blank Common Problem
|
||||
And I edit and select Settings
|
||||
Then I can revert the display name to unset
|
||||
And my display name is unset on save
|
||||
|
||||
Scenario: User can select values in a Select
|
||||
Given I have created a Blank Common Problem
|
||||
And I edit and select Settings
|
||||
Then I can select Per Student for Randomization
|
||||
And my change to randomization is persisted
|
||||
And I can revert to the default value for randomization
|
||||
|
||||
Scenario: User can modify float input values
|
||||
Given I have created a Blank Common Problem
|
||||
And I edit and select Settings
|
||||
Then I can set the weight to "3.5"
|
||||
And my change to weight is persisted
|
||||
And I can revert to the default value of unset for weight
|
||||
|
||||
Scenario: User cannot type letters in float number field
|
||||
Given I have created a Blank Common Problem
|
||||
And I edit and select Settings
|
||||
Then if I set the weight to "abc", it remains unset
|
||||
|
||||
Scenario: User cannot type decimal values integer number field
|
||||
Given I have created a Blank Common Problem
|
||||
And I edit and select Settings
|
||||
Then if I set the max attempts to "2.34", it displays initially as "234", and is persisted as "234"
|
||||
|
||||
Scenario: User cannot type out of range values in an integer number field
|
||||
Given I have created a Blank Common Problem
|
||||
And I edit and select Settings
|
||||
Then if I set the max attempts to "-3", it displays initially as "-3", and is persisted as "1"
|
||||
|
||||
Scenario: Settings changes are not saved on Cancel
|
||||
Given I have created a Blank Common Problem
|
||||
And I edit and select Settings
|
||||
Then I can set the weight to "3.5"
|
||||
And I can modify the display name
|
||||
Then If I press Cancel my changes are not persisted
|
||||
|
||||
Scenario: Edit High Level source is available for LaTeX problem
|
||||
Given I have created a LaTeX Problem
|
||||
And I edit and select Settings
|
||||
Then Edit High Level Source is visible
|
||||
187
cms/djangoapps/contentstore/features/problem-editor.py
Normal file
187
cms/djangoapps/contentstore/features/problem-editor.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# disable missing docstring
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_equal
|
||||
|
||||
DISPLAY_NAME = "Display Name"
|
||||
MAXIMUM_ATTEMPTS = "Maximum Attempts"
|
||||
PROBLEM_WEIGHT = "Problem Weight"
|
||||
RANDOMIZATION = 'Randomization'
|
||||
SHOW_ANSWER = "Show Answer"
|
||||
|
||||
|
||||
############### ACTIONS ####################
|
||||
@step('I have created a Blank Common Problem$')
|
||||
def i_created_blank_common_problem(step):
|
||||
world.create_component_instance(
|
||||
step,
|
||||
'.large-problem-icon',
|
||||
'i4x://edx/templates/problem/Blank_Common_Problem',
|
||||
'.xmodule_CapaModule'
|
||||
)
|
||||
|
||||
|
||||
@step('I edit and select Settings$')
|
||||
def i_edit_and_select_settings(step):
|
||||
world.edit_component_and_select_settings()
|
||||
|
||||
|
||||
@step('I see five alphabetized settings and their expected values$')
|
||||
def i_see_five_settings_with_values(step):
|
||||
world.verify_all_setting_entries(
|
||||
[
|
||||
[DISPLAY_NAME, "Blank Common Problem", True],
|
||||
[MAXIMUM_ATTEMPTS, "", False],
|
||||
[PROBLEM_WEIGHT, "", False],
|
||||
[RANDOMIZATION, "Never", True],
|
||||
[SHOW_ANSWER, "Finished", True]
|
||||
])
|
||||
|
||||
|
||||
@step('I can modify the display name')
|
||||
def i_can_modify_the_display_name(step):
|
||||
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill('modified')
|
||||
verify_modified_display_name()
|
||||
|
||||
|
||||
@step('my display name change is persisted on save')
|
||||
def my_display_name_change_is_persisted_on_save(step):
|
||||
world.save_component_and_reopen(step)
|
||||
verify_modified_display_name()
|
||||
|
||||
|
||||
@step('I can specify special characters in the display name')
|
||||
def i_can_modify_the_display_name_with_special_chars(step):
|
||||
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill("updated ' \" &")
|
||||
verify_modified_display_name_with_special_chars()
|
||||
|
||||
|
||||
@step('my special characters and persisted on save')
|
||||
def special_chars_persisted_on_save(step):
|
||||
world.save_component_and_reopen(step)
|
||||
verify_modified_display_name_with_special_chars()
|
||||
|
||||
|
||||
@step('I can revert the display name to unset')
|
||||
def can_revert_display_name_to_unset(step):
|
||||
world.revert_setting_entry(DISPLAY_NAME)
|
||||
verify_unset_display_name()
|
||||
|
||||
|
||||
@step('my display name is unset on save')
|
||||
def my_display_name_is_persisted_on_save(step):
|
||||
world.save_component_and_reopen(step)
|
||||
verify_unset_display_name()
|
||||
|
||||
|
||||
@step('I can select Per Student for Randomization')
|
||||
def i_can_select_per_student_for_randomization(step):
|
||||
world.browser.select(RANDOMIZATION, "Per Student")
|
||||
verify_modified_randomization()
|
||||
|
||||
|
||||
@step('my change to randomization is persisted')
|
||||
def my_change_to_randomization_is_persisted(step):
|
||||
world.save_component_and_reopen(step)
|
||||
verify_modified_randomization()
|
||||
|
||||
|
||||
@step('I can revert to the default value for randomization')
|
||||
def i_can_revert_to_default_for_randomization(step):
|
||||
world.revert_setting_entry(RANDOMIZATION)
|
||||
world.save_component_and_reopen(step)
|
||||
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Always", False)
|
||||
|
||||
|
||||
@step('I can set the weight to "(.*)"?')
|
||||
def i_can_set_weight(step, weight):
|
||||
set_weight(weight)
|
||||
verify_modified_weight()
|
||||
|
||||
|
||||
@step('my change to weight is persisted')
|
||||
def my_change_to_weight_is_persisted(step):
|
||||
world.save_component_and_reopen(step)
|
||||
verify_modified_weight()
|
||||
|
||||
|
||||
@step('I can revert to the default value of unset for weight')
|
||||
def i_can_revert_to_default_for_unset_weight(step):
|
||||
world.revert_setting_entry(PROBLEM_WEIGHT)
|
||||
world.save_component_and_reopen(step)
|
||||
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
|
||||
|
||||
|
||||
@step('if I set the weight to "(.*)", it remains unset')
|
||||
def set_the_weight_to_abc(step, bad_weight):
|
||||
set_weight(bad_weight)
|
||||
# We show the clear button immediately on type, hence the "True" here.
|
||||
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", True)
|
||||
world.save_component_and_reopen(step)
|
||||
# But no change was actually ever sent to the model, so on reopen, explicitly_set is False
|
||||
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
|
||||
|
||||
|
||||
@step('if I set the max attempts to "(.*)", it displays initially as "(.*)", and is persisted as "(.*)"')
|
||||
def set_the_max_attempts(step, max_attempts_set, max_attempts_displayed, max_attempts_persisted):
|
||||
world.get_setting_entry(MAXIMUM_ATTEMPTS).find_by_css('.setting-input')[0].fill(max_attempts_set)
|
||||
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, max_attempts_displayed, True)
|
||||
world.save_component_and_reopen(step)
|
||||
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, max_attempts_persisted, True)
|
||||
|
||||
|
||||
@step('Edit High Level Source is not visible')
|
||||
def edit_high_level_source_not_visible(step):
|
||||
verify_high_level_source(step, False)
|
||||
|
||||
|
||||
@step('Edit High Level Source is visible')
|
||||
def edit_high_level_source_visible(step):
|
||||
verify_high_level_source(step, True)
|
||||
|
||||
|
||||
@step('If I press Cancel my changes are not persisted')
|
||||
def cancel_does_not_save_changes(step):
|
||||
world.cancel_component(step)
|
||||
step.given("I edit and select Settings")
|
||||
step.given("I see five alphabetized settings and their expected values")
|
||||
|
||||
|
||||
@step('I have created a LaTeX Problem')
|
||||
def create_latex_problem(step):
|
||||
world.click_new_component_button(step, '.large-problem-icon')
|
||||
# Go to advanced tab (waiting for the tab to be visible)
|
||||
world.css_find('#ui-id-2')
|
||||
world.css_click('#ui-id-2')
|
||||
world.click_component_from_menu("i4x://edx/templates/problem/Problem_Written_in_LaTeX", '.xmodule_CapaModule')
|
||||
|
||||
|
||||
def verify_high_level_source(step, visible):
|
||||
assert_equal(visible, world.is_css_present('.launch-latex-compiler'))
|
||||
world.cancel_component(step)
|
||||
assert_equal(visible, world.is_css_present('.upload-button'))
|
||||
|
||||
|
||||
def verify_modified_weight():
|
||||
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "3.5", True)
|
||||
|
||||
|
||||
def verify_modified_randomization():
|
||||
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Per Student", True)
|
||||
|
||||
|
||||
def verify_modified_display_name():
|
||||
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'modified', True)
|
||||
|
||||
|
||||
def verify_modified_display_name_with_special_chars():
|
||||
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, "updated ' \" &", True)
|
||||
|
||||
|
||||
def verify_unset_display_name():
|
||||
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '', False)
|
||||
|
||||
|
||||
def set_weight(weight):
|
||||
world.get_setting_entry(PROBLEM_WEIGHT).find_by_css('.setting-input')[0].fill(weight)
|
||||
@@ -10,9 +10,7 @@ from nose.tools import assert_equal
|
||||
|
||||
@step('I have opened a new course section in Studio$')
|
||||
def i_have_opened_a_new_course_section(step):
|
||||
world.clear_courses()
|
||||
log_into_studio()
|
||||
create_a_course()
|
||||
open_new_course()
|
||||
add_section()
|
||||
|
||||
|
||||
|
||||
13
cms/djangoapps/contentstore/features/video-editor.feature
Normal file
13
cms/djangoapps/contentstore/features/video-editor.feature
Normal file
@@ -0,0 +1,13 @@
|
||||
Feature: Video Component Editor
|
||||
As a course author, I want to be able to create video components.
|
||||
|
||||
Scenario: User can view metadata
|
||||
Given I have created a Video component
|
||||
And I edit and select Settings
|
||||
Then I see only the Video display name setting
|
||||
|
||||
Scenario: User can modify display name
|
||||
Given I have created a Video component
|
||||
And I edit and select Settings
|
||||
Then I can modify the display name
|
||||
And my display name change is persisted on save
|
||||
9
cms/djangoapps/contentstore/features/video-editor.py
Normal file
9
cms/djangoapps/contentstore/features/video-editor.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# disable missing docstring
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world, step
|
||||
|
||||
|
||||
@step('I see only the video display name setting$')
|
||||
def i_see_only_the_video_display_name(step):
|
||||
world.verify_all_setting_entries([['Display Name', "default", True]])
|
||||
@@ -3,4 +3,4 @@ Feature: Video Component
|
||||
|
||||
Scenario: Autoplay is disabled in Studio
|
||||
Given I have created a Video component
|
||||
Then when I view it it does not autoplay
|
||||
Then when I view the video it does not have autoplay enabled
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
|
||||
@step('when I view it it does not autoplay')
|
||||
@step('when I view the video it does not have autoplay enabled')
|
||||
def does_not_autoplay(step):
|
||||
assert world.css_find('.video')[0]['data-autoplay'] == 'False'
|
||||
assert world.css_find('.video_control')[0].has_class('play')
|
||||
|
||||
@@ -34,6 +34,8 @@ from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from contentstore.views.component import ADVANCED_COMPONENT_TYPES
|
||||
|
||||
from django_comment_common.utils import are_permissions_roles_seeded
|
||||
|
||||
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
||||
@@ -75,6 +77,31 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.client = Client()
|
||||
self.client.login(username=uname, password=password)
|
||||
|
||||
def test_advanced_components_in_edit_unit(self):
|
||||
store = modulestore('direct')
|
||||
import_from_xml(store, 'common/test/data/', ['simple'])
|
||||
|
||||
course = store.get_item(Location(['i4x', 'edX', 'simple',
|
||||
'course', '2012_Fall', None]), depth=None)
|
||||
|
||||
course.advanced_modules = ADVANCED_COMPONENT_TYPES
|
||||
|
||||
store.update_metadata(course.location, own_metadata(course))
|
||||
|
||||
# just pick one vertical
|
||||
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
|
||||
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# This could be made better, but for now let's just assert that we see the advanced modules mentioned in the page
|
||||
# response HTML
|
||||
self.assertIn('Video Alpha', resp.content)
|
||||
self.assertIn('Word cloud', resp.content)
|
||||
self.assertIn('Annotation', resp.content)
|
||||
self.assertIn('Open Ended Response', resp.content)
|
||||
self.assertIn('Peer Grading Interface', resp.content)
|
||||
|
||||
def check_edit_unit(self, test_course_name):
|
||||
import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name])
|
||||
|
||||
|
||||
@@ -111,6 +111,18 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
# Now login should work
|
||||
self.login(self.email, self.pw)
|
||||
|
||||
def test_login_link_on_activation_age(self):
|
||||
self.create_account(self.username, self.email, self.pw)
|
||||
# we want to test the rendering of the activation page when the user isn't logged in
|
||||
self.client.logout()
|
||||
resp = self._activate_user(self.email)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# check the the HTML has links to the right login page. Note that this is merely a content
|
||||
# check and thus could be fragile should the wording change on this page
|
||||
expected = 'You can now <a href="' + reverse('login') + '">login</a>.'
|
||||
self.assertIn(expected, resp.content)
|
||||
|
||||
def test_private_pages_auth(self):
|
||||
"""Make sure pages that do require login work."""
|
||||
auth_pages = (
|
||||
|
||||
@@ -42,7 +42,7 @@ COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
|
||||
|
||||
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
|
||||
NOTE_COMPONENT_TYPES = ['notes']
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud', 'videoalpha'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
|
||||
ADVANCED_COMPONENT_CATEGORY = 'advanced'
|
||||
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
|
||||
|
||||
@@ -149,8 +149,7 @@ def edit_unit(request, location):
|
||||
component_templates[category].append((
|
||||
template.display_name_with_default,
|
||||
template.location.url(),
|
||||
hasattr(template, 'markdown') and template.markdown is not None,
|
||||
template.cms.empty,
|
||||
hasattr(template, 'markdown') and template.markdown is not None
|
||||
))
|
||||
|
||||
components = [
|
||||
|
||||
@@ -226,7 +226,8 @@ PIPELINE_JS = {
|
||||
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
|
||||
) + ['js/hesitate.js', 'js/base.js',
|
||||
'js/models/feedback.js', 'js/views/feedback.js',
|
||||
'js/models/section.js', 'js/views/section.js'],
|
||||
'js/models/section.js', 'js/views/section.js',
|
||||
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js'],
|
||||
'output_filename': 'js/cms-application.js',
|
||||
'test_order': 0
|
||||
},
|
||||
|
||||
1
cms/static/coffee/fixtures/metadata-editor.underscore
Symbolic link
1
cms/static/coffee/fixtures/metadata-editor.underscore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../templates/js/metadata-editor.underscore
|
||||
1
cms/static/coffee/fixtures/metadata-number-entry.underscore
Symbolic link
1
cms/static/coffee/fixtures/metadata-number-entry.underscore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../templates/js/metadata-number-entry.underscore
|
||||
1
cms/static/coffee/fixtures/metadata-option-entry.underscore
Symbolic link
1
cms/static/coffee/fixtures/metadata-option-entry.underscore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../templates/js/metadata-option-entry.underscore
|
||||
1
cms/static/coffee/fixtures/metadata-string-entry.underscore
Symbolic link
1
cms/static/coffee/fixtures/metadata-string-entry.underscore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../templates/js/metadata-string-entry.underscore
|
||||
58
cms/static/coffee/spec/models/metadata_spec.coffee
Normal file
58
cms/static/coffee/spec/models/metadata_spec.coffee
Normal file
@@ -0,0 +1,58 @@
|
||||
describe "CMS.Models.Metadata", ->
|
||||
it "knows when the value has not been modified", ->
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'original', 'explicitly_set': false})
|
||||
expect(model.isModified()).toBeFalsy()
|
||||
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'original', 'explicitly_set': true})
|
||||
model.setValue('original')
|
||||
expect(model.isModified()).toBeFalsy()
|
||||
|
||||
it "knows when the value has been modified", ->
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'original', 'explicitly_set': false})
|
||||
model.setValue('original')
|
||||
expect(model.isModified()).toBeTruthy()
|
||||
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'original', 'explicitly_set': true})
|
||||
model.setValue('modified')
|
||||
expect(model.isModified()).toBeTruthy()
|
||||
|
||||
it "tracks when values have been explicitly set", ->
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'original', 'explicitly_set': false})
|
||||
expect(model.isExplicitlySet()).toBeFalsy()
|
||||
model.setValue('original')
|
||||
expect(model.isExplicitlySet()).toBeTruthy()
|
||||
|
||||
it "has both 'display value' and a 'value' methods", ->
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'default', 'explicitly_set': false})
|
||||
expect(model.getValue()).toBeNull
|
||||
expect(model.getDisplayValue()).toBe('default')
|
||||
model.setValue('modified')
|
||||
expect(model.getValue()).toBe('modified')
|
||||
expect(model.getDisplayValue()).toBe('modified')
|
||||
|
||||
it "has a clear method for reverting to the default", ->
|
||||
model = new CMS.Models.Metadata(
|
||||
{'value': 'original', 'default_value' : 'default', 'explicitly_set': true})
|
||||
model.clear()
|
||||
expect(model.getValue()).toBeNull
|
||||
expect(model.getDisplayValue()).toBe('default')
|
||||
expect(model.isExplicitlySet()).toBeFalsy()
|
||||
|
||||
it "has a getter for field name", ->
|
||||
model = new CMS.Models.Metadata({'field_name': 'foo'})
|
||||
expect(model.getFieldName()).toBe('foo')
|
||||
|
||||
it "has a getter for options", ->
|
||||
model = new CMS.Models.Metadata({'options': ['foo', 'bar']})
|
||||
expect(model.getOptions()).toEqual(['foo', 'bar'])
|
||||
|
||||
it "has a getter for type", ->
|
||||
model = new CMS.Models.Metadata({'type': 'Integer'})
|
||||
expect(model.getType()).toBe(CMS.Models.Metadata.INTEGER_TYPE)
|
||||
|
||||
300
cms/static/coffee/spec/views/metadata_edit_spec.coffee
Normal file
300
cms/static/coffee/spec/views/metadata_edit_spec.coffee
Normal file
@@ -0,0 +1,300 @@
|
||||
describe "Test Metadata Editor", ->
|
||||
editorTemplate = readFixtures('metadata-editor.underscore')
|
||||
numberEntryTemplate = readFixtures('metadata-number-entry.underscore')
|
||||
stringEntryTemplate = readFixtures('metadata-string-entry.underscore')
|
||||
optionEntryTemplate = readFixtures('metadata-option-entry.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-number-entry", type: "text/template"}).text(numberEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-string-entry", type: "text/template"}).text(stringEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-option-entry", type: "text/template"}).text(optionEntryTemplate))
|
||||
|
||||
genericEntry = {
|
||||
default_value: 'default value',
|
||||
display_name: "Display Name",
|
||||
explicitly_set: true,
|
||||
field_name: "display_name",
|
||||
help: "Specifies the name for this component.",
|
||||
inheritable: false,
|
||||
options: [],
|
||||
type: CMS.Models.Metadata.GENERIC_TYPE,
|
||||
value: "Word cloud"
|
||||
}
|
||||
|
||||
selectEntry = {
|
||||
default_value: "answered",
|
||||
display_name: "Show Answer",
|
||||
explicitly_set: false,
|
||||
field_name: "show_answer",
|
||||
help: "When should you show the answer",
|
||||
inheritable: true,
|
||||
options: [
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Never", "value": "never"}
|
||||
],
|
||||
type: CMS.Models.Metadata.SELECT_TYPE,
|
||||
value: "always"
|
||||
}
|
||||
|
||||
integerEntry = {
|
||||
default_value: 5,
|
||||
display_name: "Inputs",
|
||||
explicitly_set: false,
|
||||
field_name: "num_inputs",
|
||||
help: "Number of text boxes for student to input words/sentences.",
|
||||
inheritable: false,
|
||||
options: {min: 1},
|
||||
type: CMS.Models.Metadata.INTEGER_TYPE,
|
||||
value: 5
|
||||
}
|
||||
|
||||
floatEntry = {
|
||||
default_value: 2.7,
|
||||
display_name: "Weight",
|
||||
explicitly_set: true,
|
||||
field_name: "weight",
|
||||
help: "Weight for this problem",
|
||||
inheritable: true,
|
||||
options: {min: 1.3, max:100.2, step:0.1},
|
||||
type: CMS.Models.Metadata.FLOAT_TYPE,
|
||||
value: 10.2
|
||||
}
|
||||
|
||||
# Test for the editor that creates the individual views.
|
||||
describe "CMS.Views.Metadata.Editor creates editors for each field", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.MetadataCollection(
|
||||
[
|
||||
integerEntry,
|
||||
floatEntry,
|
||||
selectEntry,
|
||||
genericEntry,
|
||||
{
|
||||
default_value: null,
|
||||
display_name: "Unknown",
|
||||
explicitly_set: true,
|
||||
field_name: "unknown_type",
|
||||
help: "Mystery property.",
|
||||
inheritable: false,
|
||||
options: [
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Never", "value": "never"}],
|
||||
type: "unknown type",
|
||||
value: null
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
it "creates child views on initialize, and sorts them alphabetically", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: @model})
|
||||
childModels = view.collection.models
|
||||
expect(childModels.length).toBe(5)
|
||||
childViews = view.$el.find('.setting-input')
|
||||
expect(childViews.length).toBe(5)
|
||||
|
||||
verifyEntry = (index, display_name, type) ->
|
||||
expect(childModels[index].get('display_name')).toBe(display_name)
|
||||
expect(childViews[index].type).toBe(type)
|
||||
|
||||
verifyEntry(0, 'Display Name', 'text')
|
||||
verifyEntry(1, 'Inputs', 'number')
|
||||
verifyEntry(2, 'Show Answer', 'select-one')
|
||||
verifyEntry(3, 'Unknown', 'text')
|
||||
verifyEntry(4, 'Weight', 'number')
|
||||
|
||||
it "returns its display name", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: @model})
|
||||
expect(view.getDisplayName()).toBe("Word cloud")
|
||||
|
||||
it "returns an empty string if there is no display name property with a valid value", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: new CMS.Models.MetadataCollection()})
|
||||
expect(view.getDisplayName()).toBe("")
|
||||
|
||||
view = new CMS.Views.Metadata.Editor({collection: new CMS.Models.MetadataCollection([
|
||||
{
|
||||
default_value: null,
|
||||
display_name: "Display Name",
|
||||
explicitly_set: false,
|
||||
field_name: "display_name",
|
||||
help: "",
|
||||
inheritable: false,
|
||||
options: [],
|
||||
type: CMS.Models.Metadata.GENERIC_TYPE,
|
||||
value: null
|
||||
|
||||
}])
|
||||
})
|
||||
expect(view.getDisplayName()).toBe("")
|
||||
|
||||
it "has no modified values by default", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: @model})
|
||||
expect(view.getModifiedMetadataValues()).toEqual({})
|
||||
|
||||
it "returns modified values only", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: @model})
|
||||
childModels = view.collection.models
|
||||
childModels[0].setValue('updated display name')
|
||||
childModels[1].setValue(20)
|
||||
expect(view.getModifiedMetadataValues()).toEqual({
|
||||
display_name : 'updated display name',
|
||||
num_inputs: 20
|
||||
})
|
||||
|
||||
# Tests for individual views.
|
||||
assertInputType = (view, expectedType) ->
|
||||
input = view.$el.find('.setting-input')
|
||||
expect(input.length).toBe(1)
|
||||
expect(input[0].type).toBe(expectedType)
|
||||
|
||||
assertValueInView = (view, expectedValue) ->
|
||||
expect(view.getValueFromEditor()).toBe(expectedValue)
|
||||
|
||||
assertCanUpdateView = (view, newValue) ->
|
||||
view.setValueInEditor(newValue)
|
||||
expect(view.getValueFromEditor()).toBe(newValue)
|
||||
|
||||
assertClear = (view, modelValue, editorValue=modelValue) ->
|
||||
view.clear()
|
||||
expect(view.model.getValue()).toBe(null)
|
||||
expect(view.model.getDisplayValue()).toBe(modelValue)
|
||||
expect(view.getValueFromEditor()).toBe(editorValue)
|
||||
|
||||
assertUpdateModel = (view, originalValue, newValue) ->
|
||||
view.setValueInEditor(newValue)
|
||||
expect(view.model.getValue()).toBe(originalValue)
|
||||
view.updateModel()
|
||||
expect(view.model.getValue()).toBe(newValue)
|
||||
|
||||
describe "CMS.Views.Metadata.String is a basic string input with clear functionality", ->
|
||||
beforeEach ->
|
||||
model = new CMS.Models.Metadata(genericEntry)
|
||||
@view = new CMS.Views.Metadata.String({model: model})
|
||||
|
||||
it "uses a text input type", ->
|
||||
assertInputType(@view, 'text')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@view, 'Word cloud')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@view, "updated ' \" &")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@view, 'default value')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@view, 'Word cloud', 'updated')
|
||||
|
||||
describe "CMS.Views.Metadata.Option is an option input type with clear functionality", ->
|
||||
beforeEach ->
|
||||
model = new CMS.Models.Metadata(selectEntry)
|
||||
@view = new CMS.Views.Metadata.Option({model: model})
|
||||
|
||||
it "uses a select input type", ->
|
||||
assertInputType(@view, 'select-one')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@view, 'always')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@view, "never")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@view, 'answered')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@view, null, 'never')
|
||||
|
||||
it "does not update to a value that is not an option", ->
|
||||
@view.setValueInEditor("not an option")
|
||||
expect(@view.getValueFromEditor()).toBe('always')
|
||||
|
||||
describe "CMS.Views.Metadata.Number supports integer or float type and has clear functionality", ->
|
||||
beforeEach ->
|
||||
integerModel = new CMS.Models.Metadata(integerEntry)
|
||||
@integerView = new CMS.Views.Metadata.Number({model: integerModel})
|
||||
|
||||
floatModel = new CMS.Models.Metadata(floatEntry)
|
||||
@floatView = new CMS.Views.Metadata.Number({model: floatModel})
|
||||
|
||||
it "uses a number input type", ->
|
||||
assertInputType(@integerView, 'number')
|
||||
assertInputType(@floatView, 'number')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@integerView, '5')
|
||||
assertValueInView(@floatView, '10.2')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@integerView, "12")
|
||||
assertCanUpdateView(@floatView, "-2.4")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@integerView, 5, '5')
|
||||
assertClear(@floatView, 2.7, '2.7')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@integerView, null, '90')
|
||||
assertUpdateModel(@floatView, 10.2, '-9.5')
|
||||
|
||||
it "knows the difference between integer and float", ->
|
||||
expect(@integerView.isIntegerField()).toBeTruthy()
|
||||
expect(@floatView.isIntegerField()).toBeFalsy()
|
||||
|
||||
it "sets attribtues related to min, max, and step", ->
|
||||
verifyAttributes = (view, min, step, max=null) ->
|
||||
inputEntry = view.$el.find('input')
|
||||
expect(Number(inputEntry.attr('min'))).toEqual(min)
|
||||
expect(Number(inputEntry.attr('step'))).toEqual(step)
|
||||
if max is not null
|
||||
expect(Number(inputEntry.attr('max'))).toEqual(max)
|
||||
|
||||
verifyAttributes(@integerView, 1, 1)
|
||||
verifyAttributes(@floatView, 1.3, .1, 100.2)
|
||||
|
||||
it "corrects values that are out of range", ->
|
||||
verifyValueAfterChanged = (view, value, expectedResult) ->
|
||||
view.setValueInEditor(value)
|
||||
view.changed()
|
||||
expect(view.getValueFromEditor()).toBe(expectedResult)
|
||||
|
||||
verifyValueAfterChanged(@integerView, '-4', '1')
|
||||
verifyValueAfterChanged(@integerView, '1', '1')
|
||||
verifyValueAfterChanged(@integerView, '0', '1')
|
||||
verifyValueAfterChanged(@integerView, '3001', '3001')
|
||||
|
||||
verifyValueAfterChanged(@floatView, '-4', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '1.3', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '1.2', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '100.2', '100.2')
|
||||
verifyValueAfterChanged(@floatView, '100.3', '100.2')
|
||||
|
||||
it "disallows invalid characters", ->
|
||||
verifyValueAfterKeyPressed = (view, character, reject) ->
|
||||
event = {
|
||||
type : 'keypress',
|
||||
which : character.charCodeAt(0),
|
||||
keyCode: character.charCodeAt(0),
|
||||
preventDefault : () -> 'no op'
|
||||
}
|
||||
spyOn(event, 'preventDefault')
|
||||
view.$el.find('input').trigger(event)
|
||||
if (reject)
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
else
|
||||
expect(event.preventDefault).not.toHaveBeenCalled()
|
||||
|
||||
verifyDisallowedChars = (view) ->
|
||||
verifyValueAfterKeyPressed(view, 'a', true)
|
||||
verifyValueAfterKeyPressed(view, '.', view.isIntegerField())
|
||||
verifyValueAfterKeyPressed(view, '[', true)
|
||||
verifyValueAfterKeyPressed(view, '@', true)
|
||||
|
||||
for i in [0...9]
|
||||
verifyValueAfterKeyPressed(view, String(i), false)
|
||||
|
||||
verifyDisallowedChars(@integerView)
|
||||
verifyDisallowedChars(@floatView)
|
||||
@@ -73,13 +73,3 @@ describe "CMS.Views.ModuleEdit", ->
|
||||
expect(XModule.loadModule).toHaveBeenCalled()
|
||||
expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display'))
|
||||
|
||||
describe "changedMetadata", ->
|
||||
it "returns empty if no metadata loaded", ->
|
||||
expect(@moduleEdit.changedMetadata()).toEqual({})
|
||||
|
||||
it "returns only changed values", ->
|
||||
@moduleEdit.originalMetadata = {'foo', 'bar'}
|
||||
spyOn(@moduleEdit, 'metadata').andReturn({'a': '', 'b': 'before', 'c': ''})
|
||||
@moduleEdit.loadEdit()
|
||||
@moduleEdit.metadata.andReturn({'a': '', 'b': 'after', 'd': 'only_after'})
|
||||
expect(@moduleEdit.changedMetadata()).toEqual({'b' : 'after', 'd' : 'only_after'})
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
class CMS.Views.ModuleEdit extends Backbone.View
|
||||
tagName: 'li'
|
||||
className: 'component'
|
||||
editorMode: 'editor-mode'
|
||||
|
||||
events:
|
||||
"click .component-editor .cancel-button": 'clickCancelButton'
|
||||
"click .component-editor .save-button": 'clickSaveButton'
|
||||
"click .component-actions .edit-button": 'clickEditButton'
|
||||
"click .component-actions .delete-button": 'onDelete'
|
||||
"click .mode a": 'clickModeButton'
|
||||
|
||||
initialize: ->
|
||||
@onDelete = @options.onDelete
|
||||
@@ -20,29 +22,30 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
loadEdit: ->
|
||||
if not @module
|
||||
@module = XModule.loadModule(@$el.find('.xmodule_edit'))
|
||||
@originalMetadata = @metadata()
|
||||
# At this point, metadata-edit.html will be loaded, and the metadata (as JSON) is available.
|
||||
metadataEditor = @$el.find('.metadata_edit')
|
||||
metadataData = metadataEditor.data('metadata')
|
||||
models = [];
|
||||
for key of metadataData
|
||||
models.push(metadataData[key])
|
||||
@metadataEditor = new CMS.Views.Metadata.Editor({
|
||||
el: metadataEditor,
|
||||
collection: new CMS.Models.MetadataCollection(models)
|
||||
})
|
||||
|
||||
metadata: ->
|
||||
# cdodge: package up metadata which is separated into a number of input fields
|
||||
# there's probably a better way to do this, but at least this lets me continue to move onwards
|
||||
_metadata = {}
|
||||
# Need to update set "active" class on data editor if there is one.
|
||||
# If we are only showing settings, hide the data editor controls and update settings accordingly.
|
||||
if @hasDataEditor()
|
||||
@selectMode(@editorMode)
|
||||
else
|
||||
@hideDataEditor()
|
||||
|
||||
$metadata = @$component_editor().find('.metadata_edit')
|
||||
|
||||
if $metadata
|
||||
# walk through the set of elments which have the 'xmetadata_name' attribute and
|
||||
# build up a object to pass back to the server on the subsequent POST
|
||||
_metadata[$(el).data("metadata-name")] = el.value for el in $('[data-metadata-name]', $metadata)
|
||||
|
||||
return _metadata
|
||||
title = interpolate(gettext('<em>Editing:</em> %s'),
|
||||
[@metadataEditor.getDisplayName()])
|
||||
@$el.find('.component-name').html(title)
|
||||
|
||||
changedMetadata: ->
|
||||
currentMetadata = @metadata()
|
||||
changedMetadata = {}
|
||||
for key of currentMetadata
|
||||
if currentMetadata[key] != @originalMetadata[key]
|
||||
changedMetadata[key] = currentMetadata[key]
|
||||
return changedMetadata
|
||||
return @metadataEditor.getModifiedMetadataValues()
|
||||
|
||||
cloneTemplate: (parent, template) ->
|
||||
$.post("/clone_item", {
|
||||
@@ -77,7 +80,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
@render()
|
||||
@$el.removeClass('editing')
|
||||
).fail( ->
|
||||
showToastMessage("There was an error saving your changes. Please try again.", null, 3)
|
||||
showToastMessage(gettext("There was an error saving your changes. Please try again."), null, 3)
|
||||
)
|
||||
|
||||
clickCancelButton: (event) ->
|
||||
@@ -96,3 +99,38 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
$modalCover.show().addClass('is-fixed')
|
||||
@$component_editor().slideDown(150)
|
||||
@loadEdit()
|
||||
|
||||
clickModeButton: (event) ->
|
||||
event.preventDefault()
|
||||
if not @hasDataEditor()
|
||||
return
|
||||
@selectMode(event.currentTarget.parentElement.id)
|
||||
|
||||
hasDataEditor: =>
|
||||
return @$el.find('.wrapper-comp-editor').length > 0
|
||||
|
||||
selectMode: (mode) =>
|
||||
dataEditor = @$el.find('.wrapper-comp-editor')
|
||||
settingsEditor = @$el.find('.wrapper-comp-settings')
|
||||
editorModeButton = @$el.find('#editor-mode').find("a")
|
||||
settingsModeButton = @$el.find('#settings-mode').find("a")
|
||||
|
||||
if mode == @editorMode
|
||||
# Because of CodeMirror editor, cannot hide the data editor when it is first loaded. Therefore
|
||||
# we have to use a class of is-inactive instead of is-active.
|
||||
dataEditor.removeClass('is-inactive')
|
||||
editorModeButton.addClass('is-set')
|
||||
settingsEditor.removeClass('is-active')
|
||||
settingsModeButton.removeClass('is-set')
|
||||
else
|
||||
dataEditor.addClass('is-inactive')
|
||||
editorModeButton.removeClass('is-set')
|
||||
settingsEditor.addClass('is-active')
|
||||
settingsModeButton.addClass('is-set')
|
||||
|
||||
hideDataEditor: =>
|
||||
editorModeButtonParent = @$el.find('#editor-mode')
|
||||
editorModeButtonParent.addClass('inactive-mode')
|
||||
editorModeButtonParent.removeClass('active-mode')
|
||||
@$el.find('.wrapper-comp-settings').addClass('is-active')
|
||||
@$el.find('#settings-mode').find("a").addClass('is-set')
|
||||
|
||||
113
cms/static/js/models/metadata_model.js
Normal file
113
cms/static/js/models/metadata_model.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Model used for metadata setting editors. This model does not do its own saving,
|
||||
* as that is done by module_edit.coffee.
|
||||
*/
|
||||
CMS.Models.Metadata = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
"field_name": null,
|
||||
"display_name": null,
|
||||
"value" : null,
|
||||
"explicitly_set": null,
|
||||
"default_value" : null,
|
||||
"options" : null,
|
||||
"type" : null
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.original_value = this.get('value');
|
||||
this.original_explicitly_set = this.get('explicitly_set');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the stored value is different, or if the "explicitly_set"
|
||||
* property has changed.
|
||||
*/
|
||||
isModified : function() {
|
||||
if (!this.get('explicitly_set') && !this.original_explicitly_set) {
|
||||
return false;
|
||||
}
|
||||
if (this.get('explicitly_set') && this.original_explicitly_set) {
|
||||
return this.get('value') !== this.original_value;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if a non-default/non-inherited value has been set.
|
||||
*/
|
||||
isExplicitlySet: function() {
|
||||
return this.get('explicitly_set');
|
||||
},
|
||||
|
||||
/**
|
||||
* The value, as shown in the UI. This may be an inherited or default value.
|
||||
*/
|
||||
getDisplayValue : function () {
|
||||
return this.get('value');
|
||||
},
|
||||
|
||||
/**
|
||||
* The value, as should be returned to the server. if 'isExplicitlySet'
|
||||
* returns false, this method returns null to indicate that the value
|
||||
* is not set at this level.
|
||||
*/
|
||||
getValue: function() {
|
||||
return this.get('explicitly_set') ? this.get('value') : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the displayed value.
|
||||
*/
|
||||
setValue: function (value) {
|
||||
this.set({
|
||||
explicitly_set: true,
|
||||
value: value
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the field name, which should be used for persisting the metadata
|
||||
* field to the server.
|
||||
*/
|
||||
getFieldName: function () {
|
||||
return this.get('field_name');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the options. This may be a array of possible values, or an object
|
||||
* with properties like "max", "min" and "step".
|
||||
*/
|
||||
getOptions: function () {
|
||||
return this.get('options');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the type of this metadata field. Possible values are SELECT_TYPE,
|
||||
* INTEGER_TYPE, and FLOAT_TYPE, GENERIC_TYPE.
|
||||
*/
|
||||
getType: function() {
|
||||
return this.get('type');
|
||||
},
|
||||
|
||||
/**
|
||||
* Reverts the value to the default_value specified at construction, and updates the
|
||||
* explicitly_set property.
|
||||
*/
|
||||
clear: function() {
|
||||
this.set({
|
||||
explicitly_set: false,
|
||||
value: this.get('default_value')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Models.MetadataCollection = Backbone.Collection.extend({
|
||||
model : CMS.Models.Metadata,
|
||||
comparator: "display_name"
|
||||
});
|
||||
|
||||
CMS.Models.Metadata.SELECT_TYPE = "Select";
|
||||
CMS.Models.Metadata.INTEGER_TYPE = "Integer";
|
||||
CMS.Models.Metadata.FLOAT_TYPE = "Float";
|
||||
CMS.Models.Metadata.GENERIC_TYPE = "Generic";
|
||||
312
cms/static/js/views/metadata_editor_view.js
Normal file
312
cms/static/js/views/metadata_editor_view.js
Normal file
@@ -0,0 +1,312 @@
|
||||
if (!CMS.Views['Metadata']) CMS.Views.Metadata = {};
|
||||
|
||||
CMS.Views.Metadata.Editor = Backbone.View.extend({
|
||||
|
||||
// Model is CMS.Models.MetadataCollection,
|
||||
initialize : function() {
|
||||
var tpl = $("#metadata-editor-tpl").text();
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load metadata editor template");
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
|
||||
this.$el.html(this.template({numEntries: this.collection.length}));
|
||||
var counter = 0;
|
||||
|
||||
var self = this;
|
||||
this.collection.each(
|
||||
function (model) {
|
||||
var data = {
|
||||
el: self.$el.find('.metadata_entry')[counter++],
|
||||
model: model
|
||||
};
|
||||
if (model.getType() === CMS.Models.Metadata.SELECT_TYPE) {
|
||||
new CMS.Views.Metadata.Option(data);
|
||||
}
|
||||
else if (model.getType() === CMS.Models.Metadata.INTEGER_TYPE ||
|
||||
model.getType() === CMS.Models.Metadata.FLOAT_TYPE) {
|
||||
new CMS.Views.Metadata.Number(data);
|
||||
}
|
||||
else {
|
||||
// Everything else is treated as GENERIC_TYPE, which uses String editor.
|
||||
new CMS.Views.Metadata.String(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the just the modified metadata values, in the format used to persist to the server.
|
||||
*/
|
||||
getModifiedMetadataValues: function () {
|
||||
var modified_values = {};
|
||||
this.collection.each(
|
||||
function (model) {
|
||||
if (model.isModified()) {
|
||||
modified_values[model.getFieldName()] = model.getValue();
|
||||
}
|
||||
}
|
||||
);
|
||||
return modified_values;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a display name for the component related to this metadata. This method looks to see
|
||||
* if there is a metadata entry called 'display_name', and if so, it returns its value. If there
|
||||
* is no such entry, or if display_name does not have a value set, it returns an empty string.
|
||||
*/
|
||||
getDisplayName: function () {
|
||||
var displayName = '';
|
||||
this.collection.each(
|
||||
function (model) {
|
||||
if (model.get('field_name') === 'display_name') {
|
||||
var displayNameValue = model.get('value');
|
||||
// It is possible that there is no display name value set. In that case, return empty string.
|
||||
displayName = displayNameValue ? displayNameValue : '';
|
||||
}
|
||||
}
|
||||
);
|
||||
return displayName;
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.Metadata.AbstractEditor = Backbone.View.extend({
|
||||
|
||||
// Model is CMS.Models.Metadata.
|
||||
initialize : function() {
|
||||
var self = this;
|
||||
var templateName = _.result(this, 'templateName');
|
||||
// Backbone model cid is only unique within the collection.
|
||||
this.uniqueId = _.uniqueId(templateName + "_");
|
||||
|
||||
var tpl = document.getElementById(templateName).text;
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load template: " + templateName);
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
this.$el.html(this.template({model: this.model, uniqueId: this.uniqueId}));
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* The ID/name of the template. Subclasses must override this.
|
||||
*/
|
||||
templateName: '',
|
||||
|
||||
/**
|
||||
* Returns the value currently displayed in the editor/view. Subclasses should implement this method.
|
||||
*/
|
||||
getValueFromEditor : function () {},
|
||||
|
||||
/**
|
||||
* Sets the value currently displayed in the editor/view. Subclasses should implement this method.
|
||||
*/
|
||||
setValueInEditor : function (value) {},
|
||||
|
||||
/**
|
||||
* Sets the value in the model, using the value currently displayed in the view.
|
||||
*/
|
||||
updateModel: function () {
|
||||
this.model.setValue(this.getValueFromEditor());
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the value currently set in the model (reverting to the default).
|
||||
*/
|
||||
clear: function () {
|
||||
this.model.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows the clear button, if it is not already showing.
|
||||
*/
|
||||
showClearButton: function() {
|
||||
if (!this.$el.hasClass('is-set')) {
|
||||
this.$el.addClass('is-set');
|
||||
this.getClearButton().removeClass('inactive');
|
||||
this.getClearButton().addClass('active');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the clear button.
|
||||
*/
|
||||
getClearButton: function () {
|
||||
return this.$el.find('.setting-clear');
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the editor, updating the value displayed in the view, as well as the state of
|
||||
* the clear button.
|
||||
*/
|
||||
render: function () {
|
||||
if (!this.template) return;
|
||||
|
||||
this.setValueInEditor(this.model.getDisplayValue());
|
||||
|
||||
if (this.model.isExplicitlySet()) {
|
||||
this.showClearButton();
|
||||
}
|
||||
else {
|
||||
this.$el.removeClass('is-set');
|
||||
this.getClearButton().addClass('inactive');
|
||||
this.getClearButton().removeClass('active');
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.Metadata.String = CMS.Views.Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change input" : "updateModel",
|
||||
"keypress .setting-input" : "showClearButton" ,
|
||||
"click .setting-clear" : "clear"
|
||||
},
|
||||
|
||||
templateName: "metadata-string-entry",
|
||||
|
||||
getValueFromEditor : function () {
|
||||
return this.$el.find('#' + this.uniqueId).val();
|
||||
},
|
||||
|
||||
setValueInEditor : function (value) {
|
||||
this.$el.find('input').val(value);
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.Metadata.Number = CMS.Views.Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change input" : "updateModel",
|
||||
"keypress .setting-input" : "keyPressed",
|
||||
"change .setting-input" : "changed",
|
||||
"click .setting-clear" : "clear"
|
||||
},
|
||||
|
||||
render: function () {
|
||||
CMS.Views.Metadata.AbstractEditor.prototype.render.apply(this);
|
||||
if (!this.initialized) {
|
||||
var numToString = function (val) {
|
||||
return val.toFixed(4);
|
||||
};
|
||||
var min = "min";
|
||||
var max = "max";
|
||||
var step = "step";
|
||||
var options = this.model.getOptions();
|
||||
if (options.hasOwnProperty(min)) {
|
||||
this.min = Number(options[min]);
|
||||
this.$el.find('input').attr(min, numToString(this.min));
|
||||
}
|
||||
if (options.hasOwnProperty(max)) {
|
||||
this.max = Number(options[max]);
|
||||
this.$el.find('input').attr(max, numToString(this.max));
|
||||
}
|
||||
var stepValue = undefined;
|
||||
if (options.hasOwnProperty(step)) {
|
||||
// Parse step and convert to String. Polyfill doesn't like float values like ".1" (expects "0.1").
|
||||
stepValue = numToString(Number(options[step]));
|
||||
}
|
||||
else if (this.isIntegerField()) {
|
||||
stepValue = "1";
|
||||
}
|
||||
if (stepValue !== undefined) {
|
||||
this.$el.find('input').attr(step, stepValue);
|
||||
}
|
||||
|
||||
// Manually runs polyfill for input number types to correct for Firefox non-support.
|
||||
// inputNumber will be undefined when unit test is running.
|
||||
if ($.fn.inputNumber) {
|
||||
this.$el.find('.setting-input-number').inputNumber();
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
templateName: "metadata-number-entry",
|
||||
|
||||
getValueFromEditor : function () {
|
||||
return this.$el.find('#' + this.uniqueId).val();
|
||||
},
|
||||
|
||||
setValueInEditor : function (value) {
|
||||
this.$el.find('input').val(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if this view is restricted to integers, as opposed to floating points values.
|
||||
*/
|
||||
isIntegerField : function () {
|
||||
return this.model.getType() === 'Integer';
|
||||
},
|
||||
|
||||
keyPressed: function (e) {
|
||||
this.showClearButton();
|
||||
// This first filtering if statement is take from polyfill to prevent
|
||||
// non-numeric input (for browsers that don't use polyfill because they DO have a number input type).
|
||||
var _ref, _ref1;
|
||||
if (((_ref = e.keyCode) !== 8 && _ref !== 9 && _ref !== 35 && _ref !== 36 && _ref !== 37 && _ref !== 39) &&
|
||||
((_ref1 = e.which) !== 45 && _ref1 !== 46 && _ref1 !== 48 && _ref1 !== 49 && _ref1 !== 50 && _ref1 !== 51
|
||||
&& _ref1 !== 52 && _ref1 !== 53 && _ref1 !== 54 && _ref1 !== 55 && _ref1 !== 56 && _ref1 !== 57)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
// For integers, prevent decimal points.
|
||||
if (this.isIntegerField() && e.keyCode === 46) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
changed: function () {
|
||||
// Limit value to the range specified by min and max (necessary for browsers that aren't using polyfill).
|
||||
var value = this.getValueFromEditor();
|
||||
if ((this.max !== undefined) && value > this.max) {
|
||||
value = this.max;
|
||||
} else if ((this.min != undefined) && value < this.min) {
|
||||
value = this.min;
|
||||
}
|
||||
this.setValueInEditor(value);
|
||||
this.updateModel();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
CMS.Views.Metadata.Option = CMS.Views.Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change select" : "updateModel",
|
||||
"click .setting-clear" : "clear"
|
||||
},
|
||||
|
||||
templateName: "metadata-option-entry",
|
||||
|
||||
getValueFromEditor : function () {
|
||||
var selectedText = this.$el.find('#' + this.uniqueId).find(":selected").text();
|
||||
var selectedValue;
|
||||
_.each(this.model.getOptions(), function (modelValue) {
|
||||
if (modelValue === selectedText) {
|
||||
selectedValue = modelValue;
|
||||
}
|
||||
else if (modelValue['display_name'] === selectedText) {
|
||||
selectedValue = modelValue['value'];
|
||||
}
|
||||
});
|
||||
return selectedValue;
|
||||
},
|
||||
|
||||
setValueInEditor : function (value) {
|
||||
// Value here is the json value as used by the field. The choice may instead be showing display names.
|
||||
// Find the display name matching the value passed in.
|
||||
_.each(this.model.getOptions(), function (modelValue) {
|
||||
if (modelValue['value'] === value) {
|
||||
value = modelValue['display_name'];
|
||||
}
|
||||
});
|
||||
this.$el.find('#' + this.uniqueId + " option").filter(function() {
|
||||
return $(this).text() === value;
|
||||
}).prop('selected', true);
|
||||
}
|
||||
});
|
||||
@@ -814,7 +814,7 @@ hr.divide {
|
||||
line-height: 26px;
|
||||
color: $white;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
|
||||
&:after {
|
||||
content: '▾';
|
||||
|
||||
@@ -149,11 +149,11 @@ abbr[title] {
|
||||
margin-left: 20px;
|
||||
}
|
||||
li {
|
||||
opacity: .8;
|
||||
opacity: 0.8;
|
||||
|
||||
&:ui-state-active {
|
||||
background-color: rgba(255, 255, 255, .3);
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
font-weight: 400;
|
||||
}
|
||||
a:focus {
|
||||
|
||||
@@ -95,12 +95,12 @@
|
||||
// bounce in
|
||||
@mixin bounceIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
@include transform(scale(0.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
@include transform(scale(1.05));
|
||||
}
|
||||
|
||||
@@ -128,12 +128,12 @@
|
||||
// bounce in
|
||||
@mixin bounceOut {
|
||||
0% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
@include transform(scale(0.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
@include transform(scale(1.05));
|
||||
}
|
||||
|
||||
@@ -146,12 +146,12 @@
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
@include transform(scale(1.05));
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
@include transform(scale(0.3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ code {
|
||||
|
||||
.CodeMirror {
|
||||
font-size: 13px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
left: -7px;
|
||||
top: 47px;
|
||||
width: 140px;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -558,7 +558,7 @@ body.signin .nav-not-signedin-signup {
|
||||
|
||||
.wrapper-nav-sub {
|
||||
@include transition (opacity 1.0s ease-in-out 0s);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
|
||||
&.is-shown {
|
||||
|
||||
@@ -627,7 +627,7 @@
|
||||
pointer-events: none;
|
||||
|
||||
.prompt {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ body.course.checklists {
|
||||
.task-support {
|
||||
@extend .t-copy-sub2;
|
||||
@include transition(opacity .15s .25s ease-in-out);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -267,7 +267,7 @@ body.course.checklists {
|
||||
float: right;
|
||||
width: flex-grid(2,9);
|
||||
margin: ($baseline/2) 0 0 flex-gutter();
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
text-align: right;
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ body.dashboard {
|
||||
top: 15px;
|
||||
right: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -162,7 +162,7 @@ body.index {
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
right: ($baseline/2);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
|
||||
[class^="icon-"] {
|
||||
@include font-size(18);
|
||||
|
||||
@@ -21,7 +21,7 @@ body.course.settings {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message-status {
|
||||
.message-status {
|
||||
display: none;
|
||||
@include border-top-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@@ -386,6 +386,11 @@ body.course.settings {
|
||||
#course-overview {
|
||||
height: ($baseline*20);
|
||||
}
|
||||
|
||||
//adds back in CodeMirror border removed due to Unit page styling of component editors
|
||||
.CodeMirror {
|
||||
border: 1px solid $gray-l2;
|
||||
}
|
||||
}
|
||||
|
||||
// specific fields - video
|
||||
@@ -698,7 +703,7 @@ body.course.settings {
|
||||
|
||||
.tip {
|
||||
@include transition (opacity 0.5s ease-in-out 0s);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
bottom: ($baseline*1.25);
|
||||
}
|
||||
@@ -713,7 +718,7 @@ body.course.settings {
|
||||
input.error {
|
||||
|
||||
& + .tip {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,38 +41,23 @@ body.course.static-pages {
|
||||
@include edit-box;
|
||||
@include box-shadow(none);
|
||||
display: none;
|
||||
padding: 20px;
|
||||
padding: 0;
|
||||
border-radius: 2px 2px 0 0;
|
||||
|
||||
.metadata_edit {
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
//Overrides general edit-box mixin
|
||||
.row {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
// This duplicates the styling from Unit page editing
|
||||
.module-actions {
|
||||
@include box-shadow(inset 0 1px 1px $shadow);
|
||||
padding: 0px 0 10px 10px;
|
||||
background-color: $gray-l6;
|
||||
|
||||
.save-button {
|
||||
margin: ($baseline/2) 8px 0 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 8px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-top: 10px;
|
||||
margin: 15px 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,3 +200,4 @@ body.course.static-pages {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ body.course.unit {
|
||||
border: none;
|
||||
|
||||
.rendered-component {
|
||||
padding: 0 20px;
|
||||
padding: 0 $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ body.course.unit {
|
||||
.unit-body {
|
||||
|
||||
.unit-name-input {
|
||||
padding: 20px 40px;
|
||||
padding: $baseline 2*$baseline;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
@@ -73,15 +73,15 @@ body.course.unit {
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Component List Meta
|
||||
.components {
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
margin: 20px 40px;
|
||||
|
||||
|
||||
margin: $baseline 2*$baseline;
|
||||
|
||||
.title {
|
||||
margin: 0 0 15px 0;
|
||||
@@ -91,23 +91,26 @@ body.course.unit {
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// New Components
|
||||
&.new-component-item {
|
||||
margin: 20px 0px;
|
||||
margin: $baseline 0px;
|
||||
border-top: 1px solid $mediumGrey;
|
||||
box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset;
|
||||
background-color: $lightGrey;
|
||||
margin-bottom: 0px;
|
||||
padding-bottom: 20px;
|
||||
padding-bottom: $baseline;
|
||||
|
||||
.new-component-button {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
padding: $baseline;
|
||||
text-align: center;
|
||||
color: #edf1f5;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 20px 0px;
|
||||
margin: $baseline 0px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
@@ -132,7 +135,7 @@ body.course.unit {
|
||||
height: 100px;
|
||||
color: #fff;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: $baseline;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
line-height: 14px;
|
||||
@@ -144,7 +147,7 @@ body.course.unit {
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
padding: $baseline/2;
|
||||
@include box-sizing(border-box);
|
||||
color: #fff;
|
||||
}
|
||||
@@ -153,7 +156,7 @@ body.course.unit {
|
||||
|
||||
.new-component-templates {
|
||||
display: none;
|
||||
margin: 20px 40px 20px 40px;
|
||||
margin: $baseline 2*$baseline;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $mediumGrey;
|
||||
background-color: #fff;
|
||||
@@ -161,7 +164,7 @@ body.course.unit {
|
||||
@include clearfix;
|
||||
|
||||
.cancel-button {
|
||||
margin: 20px 0px 10px 10px;
|
||||
margin: $baseline 0px $baseline/2 $baseline/2;
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
@@ -171,9 +174,9 @@ body.course.unit {
|
||||
|
||||
// specific menu types
|
||||
&.new-component-problem {
|
||||
padding-bottom:10px;
|
||||
padding-bottom: $baseline/2;
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
[class^="icon-"], .editor-indicator {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -208,7 +211,7 @@ body.course.unit {
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
|
||||
li:first-child {
|
||||
margin-left: 20px;
|
||||
margin-left: $baseline;
|
||||
}
|
||||
|
||||
li {
|
||||
@@ -219,21 +222,21 @@ body.course.unit {
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
background-color: tint($lightBluishGrey, 10%);
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
opacity:.8;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity:1;
|
||||
opacity: 0.9;
|
||||
background-color: tint($lightBluishGrey, 20%);
|
||||
}
|
||||
|
||||
&.ui-state-active {
|
||||
border: 0px;
|
||||
@include active;
|
||||
opacity:1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
a{
|
||||
a {
|
||||
display: block;
|
||||
padding: 15px 25px;
|
||||
font-size: 15px;
|
||||
@@ -280,14 +283,14 @@ body.course.unit {
|
||||
a {
|
||||
@include clearfix();
|
||||
display: block;
|
||||
padding: 7px 20px;
|
||||
padding: 7px $baseline;
|
||||
border-bottom: none;
|
||||
font-weight: 500;
|
||||
|
||||
.name {
|
||||
float: left;
|
||||
|
||||
.ss-icon {
|
||||
[class^="icon-"] {
|
||||
@include transition(opacity .15s);
|
||||
display: inline-block;
|
||||
top: 1px;
|
||||
@@ -308,14 +311,14 @@ body.course.unit {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
[class^="icon-"], .editor-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
|
||||
.ss-icon {
|
||||
[class^="icon-"] {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
@@ -355,6 +358,9 @@ body.course.unit {
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// Component Drag and Drop, Non-Edit Module Rendering, Styling
|
||||
.component {
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
border-radius: 3px;
|
||||
@@ -401,7 +407,7 @@ body.course.unit {
|
||||
}
|
||||
|
||||
.xmodule_display {
|
||||
padding: 40px 20px 20px;
|
||||
padding: 2*$baseline $baseline $baseline;
|
||||
overflow-x: auto;
|
||||
|
||||
h1 {
|
||||
@@ -409,36 +415,24 @@ body.course.unit {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Component Editing
|
||||
.wrapper-component-editor {
|
||||
z-index: 9999;
|
||||
position: relative;
|
||||
background: $lightBluishGrey2;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
@include edit-box;
|
||||
@include box-shadow(none);
|
||||
display: none;
|
||||
padding: 20px;
|
||||
padding: 0;
|
||||
border-radius: 2px 2px 0 0;
|
||||
|
||||
.metadata_edit {
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: $baseline/2;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -449,214 +443,286 @@ body.course.unit {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-top: 10px;
|
||||
margin: 15px 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-settings {
|
||||
.window-contents {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
|
||||
div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-button, .view-button {
|
||||
@include white-button;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.publish-button {
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.delete-draft {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
.preview-button,
|
||||
.publish-button,
|
||||
.view-button {
|
||||
font-size: 11px;
|
||||
margin-top: 10px;
|
||||
padding: 6px 15px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-history {
|
||||
&.collapsed {
|
||||
h4 {
|
||||
border-bottom: none;
|
||||
border-radius: 3px;
|
||||
.row {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// Module Actions, also used for Static Pages
|
||||
.module-actions {
|
||||
@include box-shadow(inset 0 1px 1px $shadow);
|
||||
padding: 0 0 $baseline $baseline;
|
||||
background-color: $gray-l6;
|
||||
|
||||
ol {
|
||||
border: 1px solid #ced2db;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
padding: 6px 8px 8px 10px;
|
||||
background: #edf1f5;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #fffcf1;
|
||||
|
||||
.item-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: #d1dae3;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
.save-button {
|
||||
margin: ($baseline/2) 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-location {
|
||||
.url {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
|
||||
.draft-tag,
|
||||
.hidden-tag,
|
||||
.private-tag,
|
||||
.has-new-draft-tag {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.window-contents > ol {
|
||||
@include tree-view;
|
||||
|
||||
.section-item {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
ol {
|
||||
.section-item {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
ol ol {
|
||||
.section-item {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin: 0 0 10px 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-draft {
|
||||
.visibility,
|
||||
|
||||
.edit-draft-message,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-public {
|
||||
.delete-draft,
|
||||
.component-actions,
|
||||
.new-component-item,
|
||||
.editing-draft-alert,
|
||||
.publish-draft-message,
|
||||
.preview-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-private {
|
||||
.delete-draft,
|
||||
.publish-draft,
|
||||
.editing-draft-alert,
|
||||
.create-draft,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// editing units from courseware
|
||||
// Edit Header (Component Name, Mode-Editor, Mode-Settings)
|
||||
.component-edit-header {
|
||||
@include box-sizing(border-box);
|
||||
padding: 18px 0 18px $baseline;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: $blue;
|
||||
border-bottom: 1px solid $blue-d2;
|
||||
color: $white;
|
||||
|
||||
//Component Name
|
||||
.component-name {
|
||||
@extend .t-copy-sub1;
|
||||
width: 50%;
|
||||
color: $white;
|
||||
font-weight: 600;
|
||||
|
||||
em {
|
||||
display: inline-block;
|
||||
margin-right: ($baseline/4);
|
||||
font-weight: 400;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
//Nav-Edit Modes
|
||||
.nav-edit-modes {
|
||||
list-style: none;
|
||||
right: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
padding: 12px ($baseline*0.75);
|
||||
|
||||
.mode {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
&.inactive-mode{
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.active-mode a {
|
||||
|
||||
@include blue-button;
|
||||
|
||||
&.is-set {
|
||||
@include transition(box-shadow 0.5 ease-in-out);
|
||||
@include linear-gradient($blue, $blue);
|
||||
color: $blue-d1;
|
||||
box-shadow: inset 0 1px 2px 1px $shadow-l1;
|
||||
background-color: $blue-d4;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 1px 2px 1px $shadow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Editor Wrapper
|
||||
.wrapper-comp-editor {
|
||||
display: block;
|
||||
|
||||
// Because the editor may be a CodeMirror editor (which must be visible at the time it is created
|
||||
// in order for it to properly initialize), we must toggle "is-inactive" instead of the more common "is-active".
|
||||
&.is-inactive {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Settings Wrapper
|
||||
.wrapper-comp-settings {
|
||||
display: none;
|
||||
|
||||
&.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
//settings-list
|
||||
.list-input.settings-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
overflow: auto;
|
||||
max-height: 400px;
|
||||
|
||||
//chrome scrollbar visibility correction
|
||||
&::-webkit-scrollbar {
|
||||
-webkit-appearance: none;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 8px;
|
||||
border: 2px solid $gray-l2;
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
//component-setting-entry
|
||||
.field.comp-setting-entry {
|
||||
background-color: $white;
|
||||
padding: $baseline;
|
||||
border-bottom: 1px solid $gray-l2;
|
||||
opacity: 0.7;
|
||||
|
||||
&:last-child {
|
||||
//margin-bottom: 0;
|
||||
}
|
||||
|
||||
//no required component settings currently
|
||||
&.required {
|
||||
label {
|
||||
//font-weight: 600;
|
||||
}
|
||||
label:after {
|
||||
//margin-left: ($baseline/4);
|
||||
//content: "*";
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include transition(opacity 0.25s ease-in-out);
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
&.is-set {
|
||||
opacity: 1.0;
|
||||
background-color: $white;
|
||||
|
||||
.setting-input {
|
||||
color: $blue-l1;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-comp-setting{
|
||||
display: inline-block;
|
||||
min-width: 300px;
|
||||
width: 45%;
|
||||
top: 0;
|
||||
vertical-align: top;
|
||||
margin-bottom:5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
label.setting-label {
|
||||
@extend .t-copy-sub1;
|
||||
@include transition(color, 0.15s, ease-in-out);
|
||||
font-weight: 400;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: 0;
|
||||
min-width: 100px;
|
||||
width: 35%;
|
||||
|
||||
&.is-focused {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
input, select, input[type="number"] {
|
||||
@include placeholder($gray-l4);
|
||||
@include font-size(16);
|
||||
@include size(100%,100%);
|
||||
padding: ($baseline/2);
|
||||
min-width: 100px;
|
||||
width: 45%;
|
||||
|
||||
//&.long {
|
||||
//
|
||||
//}
|
||||
|
||||
//&.short {
|
||||
//}
|
||||
|
||||
//&.error {
|
||||
// border-color: $red;
|
||||
//}
|
||||
|
||||
//&:focus {
|
||||
// + .tip {
|
||||
// color: $gray;
|
||||
// }
|
||||
border-radius: 3px;
|
||||
border: 1px solid $gray-l2;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
|
||||
width: 38.5%;
|
||||
@include box-shadow(0 1px 2px $shadow-l1 inset);
|
||||
//For webkit browsers which render number fields differently, make input wider.
|
||||
-moz-column-width: {
|
||||
width: 32%;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #FFFCF1;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
//@include box-shadow(0 1px 2px $shadow-l1 inset);
|
||||
|
||||
&:focus {
|
||||
@include box-shadow(0 0 1px $shadow);
|
||||
@include transition(opacity 0.25s ease-in-out);
|
||||
background-color: $yellow;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $yellow;
|
||||
}
|
||||
}
|
||||
|
||||
.action.setting-clear {
|
||||
@include font-size(11);
|
||||
color: $gray;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
vertical-align: middle;
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
margin: 0 $baseline/2;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
border: 1px solid $gray-l1;
|
||||
background-color: $gray-l4;
|
||||
|
||||
&:hover {
|
||||
@include box-shadow(0 1px 1px $shadow);
|
||||
@include transition(opacity 0.15s ease-in-out);
|
||||
background-color: $blue-s3;
|
||||
border: 1px solid $blue-s3;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.tip.setting-help {
|
||||
@include font-size(12);
|
||||
display: inline-block;
|
||||
font-color: $gray-l6;
|
||||
min-width: 260px;
|
||||
width: 50%;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Editing Units from Courseware
|
||||
body.unit {
|
||||
|
||||
.component {
|
||||
@@ -678,3 +744,224 @@ body.unit {
|
||||
}
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Unit Page Sidebar
|
||||
.unit-settings {
|
||||
.window-contents {
|
||||
padding: $baseline/2 $baseline;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: none;
|
||||
padding: $baseline/2;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
|
||||
div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-button, .view-button {
|
||||
@include white-button;
|
||||
margin-bottom: $baseline/2;
|
||||
}
|
||||
|
||||
.publish-button {
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.delete-draft {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
.preview-button,
|
||||
.publish-button,
|
||||
.view-button {
|
||||
font-size: 11px;
|
||||
margin-top: $baseline/2;
|
||||
padding: 6px 15px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-history {
|
||||
&.collapsed {
|
||||
h4 {
|
||||
border-bottom: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
border: 1px solid #ced2db;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
padding: 6px 8px 8px $baseline/2;
|
||||
background: #edf1f5;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #fffcf1;
|
||||
|
||||
.item-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: #d1dae3;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-location {
|
||||
.url {
|
||||
@include box-shadow(none);
|
||||
width: 100%;
|
||||
margin-bottom: $baseline/2;
|
||||
}
|
||||
|
||||
.draft-tag,
|
||||
.hidden-tag,
|
||||
.private-tag,
|
||||
.has-new-draft-tag {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.window-contents > ol {
|
||||
@include tree-view;
|
||||
|
||||
.section-item {
|
||||
@include box-sizing(border-box);
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ol {
|
||||
.section-item {
|
||||
padding-left: $baseline;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin-left: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
ol ol {
|
||||
.section-item {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin: 0 0 $baseline 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-draft {
|
||||
.visibility,
|
||||
|
||||
.edit-draft-message,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-public {
|
||||
.delete-draft,
|
||||
.component-actions,
|
||||
.new-component-item,
|
||||
.editing-draft-alert,
|
||||
.publish-draft-message,
|
||||
.preview-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-private {
|
||||
.delete-draft,
|
||||
.publish-draft,
|
||||
.editing-draft-alert,
|
||||
.create-draft,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Latex Compiler
|
||||
.launch-latex-compiler {
|
||||
background-color: $white;
|
||||
padding: $baseline/2 0 $baseline/2 $baseline;
|
||||
border-bottom: 1px solid $gray-l2;
|
||||
opacity: 0.8;
|
||||
|
||||
|
||||
&:hover {
|
||||
@include transition(opacity 0.25s ease-in-out);
|
||||
opacity: 1.0s;
|
||||
}
|
||||
}
|
||||
|
||||
// hides latex compiler button if settings mode is-active
|
||||
div.wrapper-comp-editor.is-inactive ~ div.launch-latex-compiler{
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ body.course.updates {
|
||||
@include edit-box;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 10001;
|
||||
width: 800px;
|
||||
padding: 30px;
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
<div class="wrapper wrapper-view">
|
||||
<%include file="widgets/header.html" />
|
||||
|
||||
## remove this block after advanced settings notification is rewritten
|
||||
<%block name="view_alerts"></%block>
|
||||
<div id="page-alert"></div>
|
||||
|
||||
<%block name="content"></%block>
|
||||
@@ -72,9 +74,13 @@
|
||||
<%include file="widgets/footer.html" />
|
||||
<%include file="widgets/tender.html" />
|
||||
|
||||
## remove this block after advanced settings notification is rewritten
|
||||
<%block name="view_notifications"></%block>
|
||||
<div id="page-notification"></div>
|
||||
</div>
|
||||
|
||||
## remove this block after advanced settings notification is rewritten
|
||||
<%block name="view_prompts"></%block>
|
||||
<div id="page-prompt"></div>
|
||||
<%block name="jsextra"></%block>
|
||||
</body>
|
||||
|
||||
@@ -1,19 +1,41 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/models/metadata_model.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/metadata_editor_view.js')}"></script>
|
||||
<script src="${static.url('js/vendor/html5-input-polyfills/number-polyfill.js')}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/html5-input-polyfills/number-polyfill.css')}" />
|
||||
|
||||
<div class="wrapper wrapper-component-editor">
|
||||
<div class="component-editor">
|
||||
<div class="module-editor">
|
||||
${editor}
|
||||
</div>
|
||||
<div class="row module-actions">
|
||||
<a href="#" class="save-button">Save</a>
|
||||
<a href="#" class="cancel-button">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="component-edit-header">
|
||||
<span class="component-name"></span>
|
||||
<ul class="nav-edit-modes">
|
||||
<li id="editor-mode" class="mode active-mode" aria-controls="editor-tab" role="tab">
|
||||
<a href="#">${_("Editor")}</a>
|
||||
</li>
|
||||
<li id="settings-mode" class="mode active-mode" aria-controls="settings-tab" role="tab">
|
||||
<a href="#">${_("Settings")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div> <!-- Editor Header -->
|
||||
|
||||
<div class="component-edit-modes">
|
||||
<div class="module-editor">
|
||||
${editor}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row module-actions">
|
||||
<a href="#" class="save-button">${_("Save")}</a>
|
||||
<a href="#" class="cancel-button">${_("Cancel")}</a>
|
||||
</div> <!-- Module Actions-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="component-actions">
|
||||
<a href="#" class="edit-button standard"><span class="edit-icon"></span>Edit</a>
|
||||
<a href="#" class="delete-button standard"><span class="delete-icon"></span>Delete</a>
|
||||
<a href="#" class="edit-button standard"><span class="edit-icon"></span>${_("Edit")}</a>
|
||||
<a href="#" class="delete-button standard"><span class="delete-icon"></span>${_("Delete")}</a>
|
||||
</div>
|
||||
<a data-tooltip="Drag to reorder" href="#" class="drag-handle"></a>
|
||||
<a data-tooltip='${_("Drag to reorder")}' href="#" class="drag-handle"></a>
|
||||
${preview}
|
||||
|
||||
|
||||
6
cms/templates/js/metadata-editor.underscore
Normal file
6
cms/templates/js/metadata-editor.underscore
Normal file
@@ -0,0 +1,6 @@
|
||||
<ul class="list-input settings-list">
|
||||
<% _.each(_.range(numEntries), function() { %>
|
||||
<li class="field comp-setting-entry metadata_entry" id="settings-listing">
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
8
cms/templates/js/metadata-number-entry.underscore
Normal file
8
cms/templates/js/metadata-number-entry.underscore
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="wrapper-comp-setting">
|
||||
<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name') %></label>
|
||||
<input class="input setting-input setting-input-number" type="number" id="<%= uniqueId %>" value='<%= model.get("value") %>'/>
|
||||
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
|
||||
<i class="icon-undo"></i><span class="sr">"<%= gettext("Clear Value") %>"</span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="tip setting-help"><%= model.get('help') %></span>
|
||||
16
cms/templates/js/metadata-option-entry.underscore
Normal file
16
cms/templates/js/metadata-option-entry.underscore
Normal file
@@ -0,0 +1,16 @@
|
||||
<div class="wrapper-comp-setting">
|
||||
<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name') %></label>
|
||||
<select class="input setting-input" id="<%= uniqueId %>" name="<%= model.get('display_name') %>">
|
||||
<% _.each(model.get('options'), function(option) { %>
|
||||
<% if (option.display_name !== undefined) { %>
|
||||
<option value="<%= option['display_name'] %>"><%= option['display_name'] %></option>
|
||||
<% } else { %>
|
||||
<option value="<%= option %>"><%= option %></option>
|
||||
<% } %>
|
||||
<% }) %>
|
||||
</select>
|
||||
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
|
||||
<i class="icon-undo"></i><span class="sr">"<%= gettext("Clear Value") %>"</span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="tip setting-help"><%= model.get('help') %></span>
|
||||
8
cms/templates/js/metadata-string-entry.underscore
Normal file
8
cms/templates/js/metadata-string-entry.underscore
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="wrapper-comp-setting">
|
||||
<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name') %></label>
|
||||
<input class="input setting-input" type="text" id="<%= uniqueId %>" value='<%= model.get("value") %>'/>
|
||||
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
|
||||
<i class="icon-undo"></i><span class="sr">"<%= gettext("Clear Value") %>"</span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="tip setting-help"><%= model.get('help') %></span>
|
||||
@@ -3,6 +3,12 @@
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
%if not user_logged_in:
|
||||
<%block name="bodyclass">
|
||||
not-signedin
|
||||
</%block>
|
||||
%endif
|
||||
|
||||
<%block name="content">
|
||||
<section class="container activation">
|
||||
|
||||
@@ -18,7 +24,7 @@
|
||||
%if user_logged_in:
|
||||
Visit your <a href="/">dashboard</a> to see your courses.
|
||||
%else:
|
||||
You can now <a href="#login-modal" rel="leanModal">login</a>.
|
||||
You can now <a href="${reverse('login')}">login</a>.
|
||||
%endif
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -87,22 +87,13 @@
|
||||
% endif
|
||||
<div class="tab current" id="tab1">
|
||||
<ul class="new-component-template">
|
||||
% for name, location, has_markdown, is_empty in templates:
|
||||
% for name, location, has_markdown in templates:
|
||||
% if has_markdown or type != "problem":
|
||||
% if is_empty:
|
||||
<li class="editor-md empty">
|
||||
<a href="#" data-location="${location}" id="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
% else:
|
||||
<li class="editor-md">
|
||||
<a href="#" data-location="${location}" id="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
<li class="editor-md">
|
||||
<a href="#" id="${location}" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
%endfor
|
||||
@@ -111,23 +102,13 @@
|
||||
% if type == "problem":
|
||||
<div class="tab" id="tab2">
|
||||
<ul class="new-component-template">
|
||||
% for name, location, has_markdown, is_empty in templates:
|
||||
% for name, location, has_markdown in templates:
|
||||
% if not has_markdown:
|
||||
% if is_empty:
|
||||
<li class="editor-manual empty">
|
||||
<a href="#" data-location="${location}" id="${location}">
|
||||
<span class="name">${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
% else:
|
||||
<li class="editor-manual">
|
||||
<a href="#" data-location="${location}" id="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
% endif
|
||||
<li class="editor-manual">
|
||||
<a href="#" id="${location}" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<div class="wrapper-comp-editor" id="editor-tab">
|
||||
<section class="html-editor editor">
|
||||
<ul class="editor-tabs">
|
||||
<li><a href="#" class="visual-tab tab current" data-tab="visual">${_("Visual")}</a></li>
|
||||
<li><a href="#" class="html-tab tab" data-tab="advanced">${_("HTML")}</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="row">
|
||||
<textarea class="tiny-mce">${data | h}</textarea>
|
||||
<textarea name="" class="edit-box">${data | h}</textarea>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
<section class="html-editor editor">
|
||||
<ul class="editor-tabs">
|
||||
<li><a href="#" class="visual-tab tab current" data-tab="visual">Visual</a></li>
|
||||
<li><a href="#" class="html-tab tab" data-tab="advanced">HTML</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="row">
|
||||
<textarea class="tiny-mce">${data | h}</textarea>
|
||||
<textarea name="" class="edit-box">${data | h}</textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,49 +1,43 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%
|
||||
import hashlib
|
||||
from xmodule.fields import StringyInteger, StringyFloat
|
||||
import copy
|
||||
import json
|
||||
hlskey = hashlib.md5(module.location.url()).hexdigest()
|
||||
%>
|
||||
<section class="metadata_edit">
|
||||
<ul>
|
||||
% for field_name, field_value in editable_metadata_fields.items():
|
||||
<li>
|
||||
% if field_name == 'source_code':
|
||||
% if field_value['explicitly_set'] is True:
|
||||
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
|
||||
% endif
|
||||
% else:
|
||||
<label>${field_value['field'].display_name}:</label>
|
||||
<input type='text' data-metadata-name='${field_value["field"].display_name}'
|
||||
## This is a hack to keep current behavior for weight and attempts (empty will parse OK as unset).
|
||||
## This hack will go away with our custom editors.
|
||||
% if field_value["value"] == None and (isinstance(field_value["field"], StringyFloat) or isinstance(field_value["field"], StringyInteger)):
|
||||
value = ''
|
||||
% else:
|
||||
value='${field_value["field"].to_json(field_value["value"])}'
|
||||
% endif
|
||||
size='60' />
|
||||
## Change to True to see all the information being passed through.
|
||||
% if False:
|
||||
<label>Help: ${field_value['field'].help}</label>
|
||||
<label>Type: ${type(field_value['field']).__name__}</label>
|
||||
<label>Inheritable: ${field_value['inheritable']}</label>
|
||||
<label>Showing inherited value: ${field_value['inheritable'] and not field_value['explicitly_set']}</label>
|
||||
<label>Explicitly set: ${field_value['explicitly_set']}</label>
|
||||
<label>Default value: ${field_value['default_value']}</label>
|
||||
% if field_value['field'].values:
|
||||
<label>Possible values:</label>
|
||||
% for value in field_value['field'].values:
|
||||
<label>${value}</label>
|
||||
% endfor
|
||||
% endif
|
||||
% endif
|
||||
% endif
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
% if 'source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set']:
|
||||
## js templates
|
||||
<script id="metadata-editor-tpl" type="text/template">
|
||||
<%static:include path="js/metadata-editor.underscore" />
|
||||
</script>
|
||||
|
||||
<script id="metadata-number-entry" type="text/template">
|
||||
<%static:include path="js/metadata-number-entry.underscore" />
|
||||
</script>
|
||||
|
||||
<script id="metadata-string-entry" type="text/template">
|
||||
<%static:include path="js/metadata-string-entry.underscore" />
|
||||
</script>
|
||||
|
||||
<script id="metadata-option-entry" type="text/template">
|
||||
<%static:include path="js/metadata-option-entry.underscore" />
|
||||
</script>
|
||||
|
||||
<% showHighLevelSource='source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set'] %>
|
||||
<% metadata_field_copy = copy.copy(editable_metadata_fields) %>
|
||||
## Delete 'source_code' field (if it exists) so metadata editor view does not attempt to render it.
|
||||
% if 'source_code' in editable_metadata_fields:
|
||||
## source-edit.html needs access to the 'source_code' value, so delete from a copy.
|
||||
<% del metadata_field_copy['source_code'] %>
|
||||
% endif
|
||||
|
||||
% if showHighLevelSource:
|
||||
<div class="launch-latex-compiler">
|
||||
<a href="#hls-modal-${hlskey}" id="hls-trig-${hlskey}">${_("Launch Latex Source Compiler")}</a>
|
||||
</div>
|
||||
<%include file="source-edit.html" />
|
||||
% endif
|
||||
% endif
|
||||
|
||||
</section>
|
||||
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='${json.dumps(metadata_field_copy) | h}'/>
|
||||
@@ -1,4 +1,4 @@
|
||||
<%include file="metadata-edit.html" />
|
||||
<div class="wrapper-comp-editor" id="editor-tab">
|
||||
<section class="combinedopenended-editor editor">
|
||||
<div class="row">
|
||||
%if enable_markdown:
|
||||
@@ -93,3 +93,5 @@
|
||||
</div>
|
||||
</article>
|
||||
</script>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<%include file="metadata-edit.html" />
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<div class="wrapper-comp-editor" id="editor-tab">
|
||||
<section class="problem-editor editor">
|
||||
<div class="row">
|
||||
%if enable_markdown:
|
||||
<div class="editor-bar">
|
||||
<ul class="format-buttons">
|
||||
<li><a href="#" class="header-button" data-tooltip="Heading 1"><span
|
||||
<li><a href="#" class="header-button" data-tooltip='${_("Heading 1")}'><span
|
||||
class="problem-editor-icon heading1"></span></a></li>
|
||||
<li><a href="#" class="multiple-choice-button" data-tooltip="Multiple Choice"><span
|
||||
<li><a href="#" class="multiple-choice-button" data-tooltip='${_("Multiple Choice")}'><span
|
||||
class="problem-editor-icon multiple-choice"></span></a></li>
|
||||
<li><a href="#" class="checks-button" data-tooltip="Checkboxes"><span
|
||||
<li><a href="#" class="checks-button" data-tooltip='${_("Checkboxes")}'><span
|
||||
class="problem-editor-icon checks"></span></a></li>
|
||||
<li><a href="#" class="string-button" data-tooltip="Text Input"><span
|
||||
<li><a href="#" class="string-button" data-tooltip='${_("Text Input")}'><span
|
||||
class="problem-editor-icon string"></span></a></li>
|
||||
<li><a href="#" class="number-button" data-tooltip="Numerical Input"><span
|
||||
<li><a href="#" class="number-button" data-tooltip='${_("Numerical Input")}'><span
|
||||
class="problem-editor-icon number"></span></a></li>
|
||||
<li><a href="#" class="dropdown-button" data-tooltip="Dropdown"><span
|
||||
<li><a href="#" class="dropdown-button" data-tooltip='${_("Dropdown")}'><span
|
||||
class="problem-editor-icon dropdown"></span></a></li>
|
||||
<li><a href="#" class="explanation-button" data-tooltip="Explanation"><span
|
||||
<li><a href="#" class="explanation-button" data-tooltip='${_("Explanation")}'><span
|
||||
class="problem-editor-icon explanation"></span></a></li>
|
||||
</ul>
|
||||
<ul class="editor-tabs">
|
||||
<li><a href="#" class="xml-tab advanced-toggle" data-tab="xml">Advanced Editor</a></li>
|
||||
<li><a href="#" class="cheatsheet-toggle" data-tooltip="Toggle Cheatsheet">?</a></li>
|
||||
<li><a href="#" class="xml-tab advanced-toggle" data-tab="xml">${_("Advanced Editor")}</a></li>
|
||||
<li><a href="#" class="cheatsheet-toggle" data-tooltip='${_("Toggle Cheatsheet")}'>?</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<textarea class="markdown-box">${markdown | h}</textarea>
|
||||
@@ -34,7 +36,7 @@
|
||||
<article class="simple-editor-cheatsheet">
|
||||
<div class="cheatsheet-wrapper">
|
||||
<div class="row">
|
||||
<h6>Heading 1</h6>
|
||||
<h6>${_("Heading 1")}</h6>
|
||||
<div class="col sample heading-1">
|
||||
<img src="/static/img/header-example.png" />
|
||||
</div>
|
||||
@@ -45,7 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Multiple Choice</h6>
|
||||
<h6>${_("Multiple Choice")}</h6>
|
||||
<div class="col sample multiple-choice">
|
||||
<img src="/static/img/choice-example.png" />
|
||||
</div>
|
||||
@@ -56,7 +58,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Checkboxes</h6>
|
||||
<h6>${_("Checkboxes")}</h6>
|
||||
<div class="col sample check-multiple">
|
||||
<img src="/static/img/multi-example.png" />
|
||||
</div>
|
||||
@@ -67,7 +69,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Text Input</h6>
|
||||
<h6>${_("Text Input")}</h6>
|
||||
<div class="col sample string-response">
|
||||
<img src="/static/img/string-example.png" />
|
||||
</div>
|
||||
@@ -76,7 +78,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Numerical Input</h6>
|
||||
<h6>${_("Numerical Input")}</h6>
|
||||
<div class="col sample numerical-response">
|
||||
<img src="/static/img/number-example.png" />
|
||||
</div>
|
||||
@@ -85,7 +87,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Dropdown</h6>
|
||||
<h6>${_("Dropdown")}</h6>
|
||||
<div class="col sample option-reponse">
|
||||
<img src="/static/img/select-example.png" />
|
||||
</div>
|
||||
@@ -94,7 +96,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Explanation</h6>
|
||||
<h6>${_("Explanation")}</h6>
|
||||
<div class="col sample explanation">
|
||||
<img src="/static/img/explanation-example.png" />
|
||||
</div>
|
||||
@@ -105,3 +107,5 @@
|
||||
</div>
|
||||
</article>
|
||||
</script>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div class="wrapper-comp-editor" id="editor-tab">
|
||||
<section class="raw-edit">
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea>
|
||||
</section>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
<section class="raw-edit">
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea>
|
||||
</section>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<%include file="metadata-edit.html" />
|
||||
<div class="content">
|
||||
<section class="modules">
|
||||
<ol>
|
||||
@@ -50,5 +49,6 @@
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
</section>
|
||||
|
||||
|
||||
@@ -28,4 +28,4 @@ class CmsNamespace(Namespace):
|
||||
"""
|
||||
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
|
||||
published_by = String(help="Id of the user who published this module", scope=Scope.settings)
|
||||
empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ class UserFactory(sf.UserFactory):
|
||||
"""
|
||||
User account for lms / cms
|
||||
"""
|
||||
FACTORY_DJANGO_GET_OR_CREATE = ('username',)
|
||||
pass
|
||||
|
||||
|
||||
@@ -21,6 +22,7 @@ class UserProfileFactory(sf.UserProfileFactory):
|
||||
"""
|
||||
Demographics etc for the User
|
||||
"""
|
||||
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
|
||||
pass
|
||||
|
||||
|
||||
@@ -29,6 +31,7 @@ class RegistrationFactory(sf.RegistrationFactory):
|
||||
"""
|
||||
Activation key for registering the user account
|
||||
"""
|
||||
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from lettuce import world
|
||||
import time
|
||||
from urllib import quote_plus
|
||||
from selenium.common.exceptions import WebDriverException
|
||||
from selenium.common.exceptions import WebDriverException, StaleElementReferenceException
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
@@ -63,7 +63,7 @@ def css_click(css_selector):
|
||||
# Occassionally, MathJax or other JavaScript can cover up
|
||||
# an element temporarily.
|
||||
# If this happens, wait a second, then try again
|
||||
time.sleep(1)
|
||||
world.wait(1)
|
||||
world.browser.find_by_css(css_selector).click()
|
||||
|
||||
|
||||
@@ -79,6 +79,14 @@ def css_click_at(css, x=10, y=10):
|
||||
e.action_chains.perform()
|
||||
|
||||
|
||||
@world.absorb
|
||||
def id_click(elem_id):
|
||||
"""
|
||||
Perform a click on an element as specified by its id
|
||||
"""
|
||||
world.css_click('#%s' % elem_id)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_fill(css_selector, text):
|
||||
world.browser.find_by_css(css_selector).first.fill(text)
|
||||
@@ -94,7 +102,12 @@ def css_text(css_selector):
|
||||
|
||||
# Wait for the css selector to appear
|
||||
if world.is_css_present(css_selector):
|
||||
return world.browser.find_by_css(css_selector).first.text
|
||||
try:
|
||||
return world.browser.find_by_css(css_selector).first.text
|
||||
except StaleElementReferenceException:
|
||||
# The DOM was still redrawing. Wait a second and try again.
|
||||
world.wait(1)
|
||||
return world.browser.find_by_css(css_selector).first.text
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
@@ -209,30 +209,3 @@ def accepts(request, media_type):
|
||||
accept = parse_accept_header(request.META.get("HTTP_ACCEPT", ""))
|
||||
return media_type in [t for (t, p, q) in accept]
|
||||
|
||||
|
||||
def debug_request(request):
|
||||
"""Return a pretty printed version of the request"""
|
||||
|
||||
return HttpResponse("""<html>
|
||||
<h1>request:</h1>
|
||||
<pre>{0}</pre>
|
||||
|
||||
<h1>request.GET</h1>:
|
||||
|
||||
<pre>{1}</pre>
|
||||
|
||||
<h1>request.POST</h1>:
|
||||
<pre>{2}</pre>
|
||||
|
||||
<h1>request.REQUEST</h1>:
|
||||
<pre>{3}</pre>
|
||||
|
||||
|
||||
|
||||
</html>
|
||||
""".format(
|
||||
pprint.pformat(request),
|
||||
pprint.pformat(dict(request.GET)),
|
||||
pprint.pformat(dict(request.POST)),
|
||||
pprint.pformat(dict(request.REQUEST)),
|
||||
))
|
||||
|
||||
15
common/lib/calc/.coveragerc
Normal file
15
common/lib/calc/.coveragerc
Normal file
@@ -0,0 +1,15 @@
|
||||
# .coveragerc for common/lib/calc
|
||||
[run]
|
||||
data_file = reports/common/lib/calc/.coverage
|
||||
source = common/lib/calc
|
||||
branch = true
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
||||
|
||||
[html]
|
||||
title = Calc Python Test Coverage Report
|
||||
directory = reports/common/lib/calc/cover
|
||||
|
||||
[xml]
|
||||
output = reports/common/lib/calc/coverage.xml
|
||||
@@ -144,6 +144,8 @@ def evaluator(variables, functions, string, cs=False):
|
||||
return x
|
||||
|
||||
def parallel(x): # Parallel resistors [ 1 2 ] => 2/3
|
||||
# convert from pyparsing.ParseResults, which doesn't support '0 in x'
|
||||
x = list(x)
|
||||
if len(x) == 1:
|
||||
return x[0]
|
||||
if 0 in x:
|
||||
@@ -180,8 +182,8 @@ def evaluator(variables, functions, string, cs=False):
|
||||
|
||||
number_part = Word(nums)
|
||||
|
||||
# 0.33 or 7 or .34
|
||||
inner_number = (number_part + Optional("." + number_part)) | ("." + number_part)
|
||||
# 0.33 or 7 or .34 or 16.
|
||||
inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part)
|
||||
|
||||
# 0.33k or -17
|
||||
number = (Optional(minus | plus) + inner_number
|
||||
@@ -230,27 +232,3 @@ def evaluator(variables, functions, string, cs=False):
|
||||
expr << Optional((plus | minus)) + term + ZeroOrMore((plus | minus) + term) # -5 + 4 - 3
|
||||
expr = expr.setParseAction(sum_parse_action)
|
||||
return (expr + stringEnd).parseString(string)[0]
|
||||
|
||||
if __name__ == '__main__':
|
||||
variables = {'R1': 2.0, 'R3': 4.0}
|
||||
functions = {'sin': numpy.sin, 'cos': numpy.cos}
|
||||
print "X", evaluator(variables, functions, "10000||sin(7+5)-6k")
|
||||
print "X", evaluator(variables, functions, "13")
|
||||
print evaluator({'R1': 2.0, 'R3': 4.0}, {}, "13")
|
||||
|
||||
print evaluator({'e1': 1, 'e2': 1.0, 'R3': 7, 'V0': 5, 'R5': 15, 'I1': 1, 'R4': 6}, {}, "e2")
|
||||
|
||||
print evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5")
|
||||
print evaluator({}, {}, "-1")
|
||||
print evaluator({}, {}, "-(7+5)")
|
||||
print evaluator({}, {}, "-0.33")
|
||||
print evaluator({}, {}, "-.33")
|
||||
print evaluator({}, {}, "5+1*j")
|
||||
print evaluator({}, {}, "j||1")
|
||||
print evaluator({}, {}, "e^(j*pi)")
|
||||
print evaluator({}, {}, "fact(5)")
|
||||
print evaluator({}, {}, "factorial(5)")
|
||||
try:
|
||||
print evaluator({}, {}, "5+7 QWSEKO")
|
||||
except UndefinedVariable:
|
||||
print "Successfully caught undefined variable"
|
||||
|
||||
377
common/lib/calc/tests/test_calc.py
Normal file
377
common/lib/calc/tests/test_calc.py
Normal file
@@ -0,0 +1,377 @@
|
||||
"""
|
||||
Unit tests for calc.py
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import numpy
|
||||
import calc
|
||||
from pyparsing import ParseException
|
||||
|
||||
|
||||
class EvaluatorTest(unittest.TestCase):
|
||||
"""
|
||||
Run tests for calc.evaluator
|
||||
Go through all functionalities as specifically as possible--
|
||||
work from number input to functions and complex expressions
|
||||
Also test custom variable substitutions (i.e.
|
||||
`evaluator({'x':3.0},{}, '3*x')`
|
||||
gives 9.0) and more.
|
||||
"""
|
||||
|
||||
def test_number_input(self):
|
||||
"""
|
||||
Test different kinds of float inputs
|
||||
|
||||
See also
|
||||
test_trailing_period (slightly different)
|
||||
test_exponential_answer
|
||||
test_si_suffix
|
||||
"""
|
||||
easy_eval = lambda x: calc.evaluator({}, {}, x)
|
||||
|
||||
self.assertEqual(easy_eval("13"), 13)
|
||||
self.assertEqual(easy_eval("3.14"), 3.14)
|
||||
self.assertEqual(easy_eval(".618033989"), 0.618033989)
|
||||
|
||||
self.assertEqual(easy_eval("-13"), -13)
|
||||
self.assertEqual(easy_eval("-3.14"), -3.14)
|
||||
self.assertEqual(easy_eval("-.618033989"), -0.618033989)
|
||||
|
||||
def test_period(self):
|
||||
"""
|
||||
The string '.' should not evaluate to anything.
|
||||
"""
|
||||
self.assertRaises(ParseException, calc.evaluator, {}, {}, '.')
|
||||
self.assertRaises(ParseException, calc.evaluator, {}, {}, '1+.')
|
||||
|
||||
def test_trailing_period(self):
|
||||
"""
|
||||
Test that things like '4.' will be 4 and not throw an error
|
||||
"""
|
||||
try:
|
||||
self.assertEqual(4.0, calc.evaluator({}, {}, '4.'))
|
||||
except ParseException:
|
||||
self.fail("'4.' is a valid input, but threw an exception")
|
||||
|
||||
def test_exponential_answer(self):
|
||||
"""
|
||||
Test for correct interpretation of scientific notation
|
||||
"""
|
||||
answer = 50
|
||||
correct_responses = ["50", "50.0", "5e1", "5e+1",
|
||||
"50e0", "50.0e0", "500e-1"]
|
||||
incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"]
|
||||
|
||||
for input_str in correct_responses:
|
||||
result = calc.evaluator({}, {}, input_str)
|
||||
fail_msg = "Expected '{0}' to equal {1}".format(
|
||||
input_str, answer)
|
||||
self.assertEqual(answer, result, msg=fail_msg)
|
||||
|
||||
for input_str in incorrect_responses:
|
||||
result = calc.evaluator({}, {}, input_str)
|
||||
fail_msg = "Expected '{0}' to not equal {1}".format(
|
||||
input_str, answer)
|
||||
self.assertNotEqual(answer, result, msg=fail_msg)
|
||||
|
||||
def test_si_suffix(self):
|
||||
"""
|
||||
Test calc.py's unique functionality of interpreting si 'suffixes'.
|
||||
|
||||
For instance 'k' stand for 'kilo-' so '1k' should be 1,000
|
||||
"""
|
||||
test_mapping = [('4.2%', 0.042), ('2.25k', 2250), ('8.3M', 8300000),
|
||||
('9.9G', 9.9e9), ('1.2T', 1.2e12), ('7.4c', 0.074),
|
||||
('5.4m', 0.0054), ('8.7u', 0.0000087),
|
||||
('5.6n', 5.6e-9), ('4.2p', 4.2e-12)]
|
||||
|
||||
for (expr, answer) in test_mapping:
|
||||
tolerance = answer * 1e-6 # Make rel. tolerance, because of floats
|
||||
fail_msg = "Failure in testing suffix '{0}': '{1}' was not {2}"
|
||||
fail_msg = fail_msg.format(expr[-1], expr, answer)
|
||||
self.assertAlmostEqual(calc.evaluator({}, {}, expr), answer,
|
||||
delta=tolerance, msg=fail_msg)
|
||||
|
||||
def test_operator_sanity(self):
|
||||
"""
|
||||
Test for simple things like '5+2' and '5/2'
|
||||
"""
|
||||
var1 = 5.0
|
||||
var2 = 2.0
|
||||
operators = [('+', 7), ('-', 3), ('*', 10), ('/', 2.5), ('^', 25)]
|
||||
|
||||
for (operator, answer) in operators:
|
||||
input_str = "{0} {1} {2}".format(var1, operator, var2)
|
||||
result = calc.evaluator({}, {}, input_str)
|
||||
fail_msg = "Failed on operator '{0}': '{1}' was not {2}".format(
|
||||
operator, input_str, answer)
|
||||
self.assertEqual(answer, result, msg=fail_msg)
|
||||
|
||||
def test_raises_zero_division_err(self):
|
||||
"""
|
||||
Ensure division by zero gives an error
|
||||
"""
|
||||
self.assertRaises(ZeroDivisionError, calc.evaluator,
|
||||
{}, {}, '1/0')
|
||||
self.assertRaises(ZeroDivisionError, calc.evaluator,
|
||||
{}, {}, '1/0.0')
|
||||
self.assertRaises(ZeroDivisionError, calc.evaluator,
|
||||
{'x': 0.0}, {}, '1/x')
|
||||
|
||||
def test_parallel_resistors(self):
|
||||
"""
|
||||
Test the parallel resistor operator ||
|
||||
|
||||
The formula is given by
|
||||
a || b || c ...
|
||||
= 1 / (1/a + 1/b + 1/c + ...)
|
||||
It is the resistance of a parallel circuit of resistors with resistance
|
||||
a, b, c, etc&. See if this evaulates correctly.
|
||||
"""
|
||||
self.assertEqual(calc.evaluator({}, {}, '1||1'), 0.5)
|
||||
self.assertEqual(calc.evaluator({}, {}, '1||1||2'), 0.4)
|
||||
self.assertEqual(calc.evaluator({}, {}, "j||1"), 0.5 + 0.5j)
|
||||
|
||||
def test_parallel_resistors_with_zero(self):
|
||||
"""
|
||||
Check the behavior of the || operator with 0
|
||||
"""
|
||||
self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0||1')))
|
||||
self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0.0||1')))
|
||||
self.assertTrue(numpy.isnan(calc.evaluator({'x': 0.0}, {}, 'x||1')))
|
||||
|
||||
def assert_function_values(self, fname, ins, outs, tolerance=1e-3):
|
||||
"""
|
||||
Helper function to test many values at once
|
||||
|
||||
Test the accuracy of evaluator's use of the function given by fname
|
||||
Specifically, the equality of `fname(ins[i])` against outs[i].
|
||||
This is used later to test a whole bunch of f(x) = y at a time
|
||||
"""
|
||||
|
||||
for (arg, val) in zip(ins, outs):
|
||||
input_str = "{0}({1})".format(fname, arg)
|
||||
result = calc.evaluator({}, {}, input_str)
|
||||
fail_msg = "Failed on function {0}: '{1}' was not {2}".format(
|
||||
fname, input_str, val)
|
||||
self.assertAlmostEqual(val, result, delta=tolerance, msg=fail_msg)
|
||||
|
||||
def test_trig_functions(self):
|
||||
"""
|
||||
Test the trig functions provided in calc.py
|
||||
|
||||
which are: sin, cos, tan, arccos, arcsin, arctan
|
||||
"""
|
||||
|
||||
angles = ['-pi/4', '0', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j']
|
||||
sin_values = [-0.707, 0, 0.5, 0.588, -0.707, 0.707, 1.298 + 0.635j]
|
||||
cos_values = [0.707, 1, 0.866, 0.809, -0.707, 0.707, 0.834 - 0.989j]
|
||||
tan_values = [-1, 0, 0.577, 0.727, 1, 1, 0.272 + 1.084j]
|
||||
# Cannot test tan(pi/2) b/c pi/2 is a float and not precise...
|
||||
|
||||
self.assert_function_values('sin', angles, sin_values)
|
||||
self.assert_function_values('cos', angles, cos_values)
|
||||
self.assert_function_values('tan', angles, tan_values)
|
||||
|
||||
# Include those where the real part is between -pi/2 and pi/2
|
||||
arcsin_inputs = ['-0.707', '0', '0.5', '0.588', '1.298 + 0.635*j']
|
||||
arcsin_angles = [-0.785, 0, 0.524, 0.629, 1 + 1j]
|
||||
self.assert_function_values('arcsin', arcsin_inputs, arcsin_angles)
|
||||
# Rather than throwing an exception, numpy.arcsin gives nan
|
||||
# self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(-1.1)')))
|
||||
# self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(1.1)')))
|
||||
# Disabled for now because they are giving a runtime warning... :-/
|
||||
|
||||
# Include those where the real part is between 0 and pi
|
||||
arccos_inputs = ['1', '0.866', '0.809', '0.834-0.989*j']
|
||||
arccos_angles = [0, 0.524, 0.628, 1 + 1j]
|
||||
self.assert_function_values('arccos', arccos_inputs, arccos_angles)
|
||||
# self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arccos(-1.1)')))
|
||||
# self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arccos(1.1)')))
|
||||
|
||||
# Has the same range as arcsin
|
||||
arctan_inputs = ['-1', '0', '0.577', '0.727', '0.272 + 1.084*j']
|
||||
arctan_angles = arcsin_angles
|
||||
self.assert_function_values('arctan', arctan_inputs, arctan_angles)
|
||||
|
||||
def test_other_functions(self):
|
||||
"""
|
||||
Test the non-trig functions provided in calc.py
|
||||
|
||||
Specifically:
|
||||
sqrt, log10, log2, ln, abs,
|
||||
fact, factorial
|
||||
"""
|
||||
|
||||
# Test sqrt
|
||||
self.assert_function_values('sqrt',
|
||||
[0, 1, 2, 1024], # -1
|
||||
[0, 1, 1.414, 32]) # 1j
|
||||
# sqrt(-1) is NAN not j (!!).
|
||||
|
||||
# Test logs
|
||||
self.assert_function_values('log10',
|
||||
[0.1, 1, 3.162, 1000000, '1+j'],
|
||||
[-1, 0, 0.5, 6, 0.151 + 0.341j])
|
||||
self.assert_function_values('log2',
|
||||
[0.5, 1, 1.414, 1024, '1+j'],
|
||||
[-1, 0, 0.5, 10, 0.5 + 1.133j])
|
||||
self.assert_function_values('ln',
|
||||
[0.368, 1, 1.649, 2.718, 42, '1+j'],
|
||||
[-1, 0, 0.5, 1, 3.738, 0.347 + 0.785j])
|
||||
|
||||
# Test abs
|
||||
self.assert_function_values('abs', [-1, 0, 1, 'j'], [1, 0, 1, 1])
|
||||
|
||||
# Test factorial
|
||||
fact_inputs = [0, 1, 3, 7]
|
||||
fact_values = [1, 1, 6, 5040]
|
||||
self.assert_function_values('fact', fact_inputs, fact_values)
|
||||
self.assert_function_values('factorial', fact_inputs, fact_values)
|
||||
|
||||
self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(-1)")
|
||||
self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(0.5)")
|
||||
self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(-1)")
|
||||
self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(0.5)")
|
||||
|
||||
def test_constants(self):
|
||||
"""
|
||||
Test the default constants provided in calc.py
|
||||
|
||||
which are: j (complex number), e, pi, k, c, T, q
|
||||
"""
|
||||
|
||||
# Of the form ('expr', python value, tolerance (or None for exact))
|
||||
default_variables = [('j', 1j, None),
|
||||
('e', 2.7183, 1e-3),
|
||||
('pi', 3.1416, 1e-3),
|
||||
# c = speed of light
|
||||
('c', 2.998e8, 1e5),
|
||||
# 0 deg C = T Kelvin
|
||||
('T', 298.15, 0.01),
|
||||
# Note k = scipy.constants.k = 1.3806488e-23
|
||||
('k', 1.3806488e-23, 1e-26),
|
||||
# Note q = scipy.constants.e = 1.602176565e-19
|
||||
('q', 1.602176565e-19, 1e-22)]
|
||||
for (variable, value, tolerance) in default_variables:
|
||||
fail_msg = "Failed on constant '{0}', not within bounds".format(
|
||||
variable)
|
||||
result = calc.evaluator({}, {}, variable)
|
||||
if tolerance is None:
|
||||
self.assertEqual(value, result, msg=fail_msg)
|
||||
else:
|
||||
self.assertAlmostEqual(value, result,
|
||||
delta=tolerance, msg=fail_msg)
|
||||
|
||||
def test_complex_expression(self):
|
||||
"""
|
||||
Calculate combinations of operators and default functions
|
||||
"""
|
||||
|
||||
self.assertAlmostEqual(
|
||||
calc.evaluator({}, {}, "(2^2+1.0)/sqrt(5e0)*5-1"),
|
||||
10.180,
|
||||
delta=1e-3)
|
||||
|
||||
self.assertAlmostEqual(
|
||||
calc.evaluator({}, {}, "1+1/(1+1/(1+1/(1+1)))"),
|
||||
1.6,
|
||||
delta=1e-3)
|
||||
self.assertAlmostEqual(
|
||||
calc.evaluator({}, {}, "10||sin(7+5)"),
|
||||
-0.567, delta=0.01)
|
||||
self.assertAlmostEqual(calc.evaluator({}, {}, "sin(e)"),
|
||||
0.41, delta=0.01)
|
||||
self.assertAlmostEqual(calc.evaluator({}, {}, "k*T/q"),
|
||||
0.025, delta=1e-3)
|
||||
self.assertAlmostEqual(calc.evaluator({}, {}, "e^(j*pi)"),
|
||||
-1, delta=1e-5)
|
||||
|
||||
def test_simple_vars(self):
|
||||
"""
|
||||
Substitution of variables into simple equations
|
||||
"""
|
||||
variables = {'x': 9.72, 'y': 7.91, 'loooooong': 6.4}
|
||||
|
||||
# Should not change value of constant
|
||||
# even with different numbers of variables...
|
||||
self.assertEqual(calc.evaluator({'x': 9.72}, {}, '13'), 13)
|
||||
self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, '13'), 13)
|
||||
self.assertEqual(calc.evaluator(variables, {}, '13'), 13)
|
||||
|
||||
# Easy evaluation
|
||||
self.assertEqual(calc.evaluator(variables, {}, 'x'), 9.72)
|
||||
self.assertEqual(calc.evaluator(variables, {}, 'y'), 7.91)
|
||||
self.assertEqual(calc.evaluator(variables, {}, 'loooooong'), 6.4)
|
||||
|
||||
# Test a simple equation
|
||||
self.assertAlmostEqual(calc.evaluator(variables, {}, '3*x-y'),
|
||||
21.25, delta=0.01) # = 3 * 9.72 - 7.91
|
||||
self.assertAlmostEqual(calc.evaluator(variables, {}, 'x*y'),
|
||||
76.89, delta=0.01)
|
||||
|
||||
self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, "13"), 13)
|
||||
self.assertEqual(calc.evaluator(variables, {}, "13"), 13)
|
||||
self.assertEqual(
|
||||
calc.evaluator({
|
||||
'a': 2.2997471478310274, 'k': 9, 'm': 8,
|
||||
'x': 0.66009498411213041},
|
||||
{}, "5"),
|
||||
5)
|
||||
|
||||
def test_variable_case_sensitivity(self):
|
||||
"""
|
||||
Test the case sensitivity flag and corresponding behavior
|
||||
"""
|
||||
self.assertEqual(
|
||||
calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "r1*r3"),
|
||||
8.0)
|
||||
|
||||
variables = {'t': 1.0}
|
||||
self.assertEqual(calc.evaluator(variables, {}, "t"), 1.0)
|
||||
self.assertEqual(calc.evaluator(variables, {}, "T"), 1.0)
|
||||
self.assertEqual(calc.evaluator(variables, {}, "t", cs=True), 1.0)
|
||||
# Recall 'T' is a default constant, with value 298.15
|
||||
self.assertAlmostEqual(calc.evaluator(variables, {}, "T", cs=True),
|
||||
298, delta=0.2)
|
||||
|
||||
def test_simple_funcs(self):
|
||||
"""
|
||||
Subsitution of custom functions
|
||||
"""
|
||||
variables = {'x': 4.712}
|
||||
functions = {'id': lambda x: x}
|
||||
self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81)
|
||||
self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81)
|
||||
self.assertEqual(calc.evaluator(variables, functions, 'id(x)'), 4.712)
|
||||
|
||||
functions.update({'f': numpy.sin})
|
||||
self.assertAlmostEqual(calc.evaluator(variables, functions, 'f(x)'),
|
||||
-1, delta=1e-3)
|
||||
|
||||
def test_function_case_sensitivity(self):
|
||||
"""
|
||||
Test the case sensitivity of functions
|
||||
"""
|
||||
functions = {'f': lambda x: x,
|
||||
'F': lambda x: x + 1}
|
||||
# Test case insensitive evaluation
|
||||
# Both evaulations should call the same function
|
||||
self.assertEqual(calc.evaluator({}, functions, 'f(6)'),
|
||||
calc.evaluator({}, functions, 'F(6)'))
|
||||
# Test case sensitive evaluation
|
||||
self.assertNotEqual(calc.evaluator({}, functions, 'f(6)', cs=True),
|
||||
calc.evaluator({}, functions, 'F(6)', cs=True))
|
||||
|
||||
def test_undefined_vars(self):
|
||||
"""
|
||||
Check to see if the evaluator catches undefined variables
|
||||
"""
|
||||
variables = {'R1': 2.0, 'R3': 4.0}
|
||||
|
||||
self.assertRaises(calc.UndefinedVariable, calc.evaluator,
|
||||
{}, {}, "5+7 QWSEKO")
|
||||
self.assertRaises(calc.UndefinedVariable, calc.evaluator,
|
||||
{'r1': 5}, {}, "r1+r2")
|
||||
self.assertRaises(calc.UndefinedVariable, calc.evaluator,
|
||||
variables, {}, "r1*r3", cs=True)
|
||||
@@ -469,6 +469,7 @@ class LoncapaProblem(object):
|
||||
random_seed=self.seed,
|
||||
python_path=python_path,
|
||||
cache=self.system.cache,
|
||||
slug=self.problem_id,
|
||||
)
|
||||
except Exception as err:
|
||||
log.exception("Error while execing script code: " + all_code)
|
||||
|
||||
@@ -140,6 +140,8 @@ class LoncapaResponse(object):
|
||||
self.context = context
|
||||
self.system = system
|
||||
|
||||
self.id = xml.get('id')
|
||||
|
||||
for abox in inputfields:
|
||||
if abox.tag not in self.allowed_inputfields:
|
||||
msg = "%s: cannot have input field %s" % (
|
||||
@@ -286,7 +288,7 @@ class LoncapaResponse(object):
|
||||
}
|
||||
|
||||
try:
|
||||
safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'])
|
||||
safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'], slug=self.id)
|
||||
except Exception as err:
|
||||
msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
|
||||
msg += "\nSee XML source line %s" % getattr(
|
||||
@@ -935,7 +937,6 @@ class CustomResponse(LoncapaResponse):
|
||||
# if <customresponse> has an "expect" (or "answer") attribute then save
|
||||
# that
|
||||
self.expect = xml.get('expect') or xml.get('answer')
|
||||
self.myid = xml.get('id')
|
||||
|
||||
log.debug('answer_ids=%s' % self.answer_ids)
|
||||
|
||||
@@ -972,7 +973,7 @@ class CustomResponse(LoncapaResponse):
|
||||
'ans': ans,
|
||||
}
|
||||
globals_dict.update(kwargs)
|
||||
safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'])
|
||||
safe_exec.safe_exec(code, globals_dict, python_path=self.context['python_path'], slug=self.id)
|
||||
return globals_dict['cfn_return']
|
||||
return check_function
|
||||
|
||||
@@ -981,7 +982,7 @@ class CustomResponse(LoncapaResponse):
|
||||
if not self.code:
|
||||
if answer is None:
|
||||
log.error("[courseware.capa.responsetypes.customresponse] missing"
|
||||
" code checking script! id=%s" % self.myid)
|
||||
" code checking script! id=%s" % self.id)
|
||||
self.code = ''
|
||||
else:
|
||||
answer_src = answer.get('src')
|
||||
@@ -1034,7 +1035,7 @@ class CustomResponse(LoncapaResponse):
|
||||
# note that this doesn't help the "cfn" version - only the exec version
|
||||
self.context.update({
|
||||
# my ID
|
||||
'response_id': self.myid,
|
||||
'response_id': self.id,
|
||||
|
||||
# expected answer (if given as attribute)
|
||||
'expect': self.expect,
|
||||
@@ -1089,7 +1090,7 @@ class CustomResponse(LoncapaResponse):
|
||||
# exec the check function
|
||||
if isinstance(self.code, basestring):
|
||||
try:
|
||||
safe_exec.safe_exec(self.code, self.context, cache=self.system.cache)
|
||||
safe_exec.safe_exec(self.code, self.context, cache=self.system.cache, slug=self.id)
|
||||
except Exception as err:
|
||||
self._handle_exec_exception(err)
|
||||
|
||||
@@ -1813,7 +1814,7 @@ class SchematicResponse(LoncapaResponse):
|
||||
]
|
||||
self.context.update({'submission': submission})
|
||||
try:
|
||||
safe_exec.safe_exec(self.code, self.context, cache=self.system.cache)
|
||||
safe_exec.safe_exec(self.code, self.context, cache=self.system.cache, slug=self.id)
|
||||
except Exception as err:
|
||||
msg = 'Error %s in evaluating SchematicResponse' % err
|
||||
raise ResponseError(msg)
|
||||
|
||||
@@ -71,7 +71,7 @@ def update_hash(hasher, obj):
|
||||
|
||||
|
||||
@statsd.timed('capa.safe_exec.time')
|
||||
def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None):
|
||||
def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None, slug=None):
|
||||
"""
|
||||
Execute python code safely.
|
||||
|
||||
@@ -87,6 +87,9 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None
|
||||
to cache the execution, taking into account the code, the values of the globals,
|
||||
and the random seed.
|
||||
|
||||
`slug` is an arbitrary string, a description that's meaningful to the
|
||||
caller, that will be used in log messages.
|
||||
|
||||
"""
|
||||
# Check the cache for a previous result.
|
||||
if cache:
|
||||
@@ -112,7 +115,7 @@ def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None
|
||||
try:
|
||||
codejail_safe_exec(
|
||||
code_prolog + LAZY_IMPORTS + code, globals_dict,
|
||||
python_path=python_path,
|
||||
python_path=python_path, slug=slug,
|
||||
)
|
||||
except SafeExecException as e:
|
||||
emsg = e.message
|
||||
|
||||
@@ -10,7 +10,6 @@ import random
|
||||
import unittest
|
||||
import textwrap
|
||||
import mock
|
||||
import textwrap
|
||||
|
||||
from . import new_loncapa_problem, test_system
|
||||
|
||||
@@ -190,7 +189,7 @@ class SymbolicResponseTest(ResponseTest):
|
||||
|
||||
def test_grade_single_input(self):
|
||||
problem = self.build_problem(math_display=True,
|
||||
expect="2*x+3*y")
|
||||
expect="2*x+3*y")
|
||||
|
||||
# Correct answers
|
||||
correct_inputs = [
|
||||
@@ -223,7 +222,6 @@ class SymbolicResponseTest(ResponseTest):
|
||||
for (input_str, input_mathml) in incorrect_inputs:
|
||||
self._assert_symbolic_grade(problem, input_str, input_mathml, 'incorrect')
|
||||
|
||||
|
||||
def test_complex_number_grade(self):
|
||||
problem = self.build_problem(math_display=True,
|
||||
expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]",
|
||||
@@ -241,7 +239,7 @@ class SymbolicResponseTest(ResponseTest):
|
||||
# Correct answer
|
||||
with mock.patch.object(requests, 'post') as mock_post:
|
||||
|
||||
# Simulate what the LaTeX-to-MathML server would
|
||||
# Simulate what the LaTeX-to-MathML server would
|
||||
# send for the correct response input
|
||||
mock_post.return_value.text = correct_snuggletex_response
|
||||
|
||||
@@ -323,7 +321,7 @@ class SymbolicResponseTest(ResponseTest):
|
||||
dynamath_input,
|
||||
expected_correctness):
|
||||
input_dict = {'1_2_1': str(student_input),
|
||||
'1_2_1_dynamath': str(dynamath_input) }
|
||||
'1_2_1_dynamath': str(dynamath_input)}
|
||||
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
|
||||
@@ -349,10 +347,18 @@ class OptionResponseTest(ResponseTest):
|
||||
|
||||
|
||||
class FormulaResponseTest(ResponseTest):
|
||||
"""
|
||||
Test the FormulaResponse class
|
||||
"""
|
||||
from response_xml_factory import FormulaResponseXMLFactory
|
||||
xml_factory_class = FormulaResponseXMLFactory
|
||||
|
||||
def test_grade(self):
|
||||
"""
|
||||
Test basic functionality of FormulaResponse
|
||||
|
||||
Specifically, if it can understand equivalence of formulae
|
||||
"""
|
||||
# Sample variables x and y in the range [-10, 10]
|
||||
sample_dict = {'x': (-10, 10), 'y': (-10, 10)}
|
||||
|
||||
@@ -373,6 +379,9 @@ class FormulaResponseTest(ResponseTest):
|
||||
self.assert_grade(problem, input_formula, "incorrect")
|
||||
|
||||
def test_hint(self):
|
||||
"""
|
||||
Test the hint-giving functionality of FormulaResponse
|
||||
"""
|
||||
# Sample variables x and y in the range [-10, 10]
|
||||
sample_dict = {'x': (-10, 10), 'y': (-10, 10)}
|
||||
|
||||
@@ -401,6 +410,10 @@ class FormulaResponseTest(ResponseTest):
|
||||
'Try including the variable x')
|
||||
|
||||
def test_script(self):
|
||||
"""
|
||||
Test if python script can be used to generate answers
|
||||
"""
|
||||
|
||||
# Calculate the answer using a script
|
||||
script = "calculated_ans = 'x+x'"
|
||||
|
||||
@@ -419,7 +432,9 @@ class FormulaResponseTest(ResponseTest):
|
||||
self.assert_grade(problem, '3*x', 'incorrect')
|
||||
|
||||
def test_parallel_resistors(self):
|
||||
"""Test parallel resistors"""
|
||||
"""
|
||||
Test parallel resistors
|
||||
"""
|
||||
sample_dict = {'R1': (10, 10), 'R2': (2, 2), 'R3': (5, 5), 'R4': (1, 1)}
|
||||
|
||||
# Test problem
|
||||
@@ -440,8 +455,11 @@ class FormulaResponseTest(ResponseTest):
|
||||
self.assert_grade(problem, input_formula, "incorrect")
|
||||
|
||||
def test_default_variables(self):
|
||||
"""Test the default variables provided in common/lib/capa/capa/calc.py"""
|
||||
# which are: j (complex number), e, pi, k, c, T, q
|
||||
"""
|
||||
Test the default variables provided in calc.py
|
||||
|
||||
which are: j (complex number), e, pi, k, c, T, q
|
||||
"""
|
||||
|
||||
# Sample x in the range [-10,10]
|
||||
sample_dict = {'x': (-10, 10)}
|
||||
@@ -464,11 +482,14 @@ class FormulaResponseTest(ResponseTest):
|
||||
msg="Failed on variable {0}; the given, incorrect answer was {1} but graded 'correct'".format(var, incorrect))
|
||||
|
||||
def test_default_functions(self):
|
||||
"""Test the default functions provided in common/lib/capa/capa/calc.py"""
|
||||
# which are: sin, cos, tan, sqrt, log10, log2, ln,
|
||||
# arccos, arcsin, arctan, abs,
|
||||
# fact, factorial
|
||||
"""
|
||||
Test the default functions provided in common/lib/capa/capa/calc.py
|
||||
|
||||
which are:
|
||||
sin, cos, tan, sqrt, log10, log2, ln,
|
||||
arccos, arcsin, arctan, abs,
|
||||
fact, factorial
|
||||
"""
|
||||
w = random.randint(3, 10)
|
||||
sample_dict = {'x': (-10, 10), # Sample x in the range [-10,10]
|
||||
'y': (1, 10), # Sample y in the range [1,10] - logs, arccos need positive inputs
|
||||
@@ -496,8 +517,10 @@ class FormulaResponseTest(ResponseTest):
|
||||
msg="Failed on function {0}; the given, incorrect answer was {1} but graded 'correct'".format(func, incorrect))
|
||||
|
||||
def test_grade_infinity(self):
|
||||
# This resolves a bug where a problem with relative tolerance would
|
||||
# pass with any arbitrarily large student answer.
|
||||
"""
|
||||
Test that a large input on a problem with relative tolerance isn't
|
||||
erroneously marked as correct.
|
||||
"""
|
||||
|
||||
sample_dict = {'x': (1, 2)}
|
||||
|
||||
@@ -514,8 +537,9 @@ class FormulaResponseTest(ResponseTest):
|
||||
self.assert_grade(problem, input_formula, "incorrect")
|
||||
|
||||
def test_grade_nan(self):
|
||||
# Attempt to produce a value which causes the student's answer to be
|
||||
# evaluated to nan. See if this is resolved correctly.
|
||||
"""
|
||||
Test that expressions that evaluate to NaN are not marked as correct.
|
||||
"""
|
||||
|
||||
sample_dict = {'x': (1, 2)}
|
||||
|
||||
@@ -532,6 +556,18 @@ class FormulaResponseTest(ResponseTest):
|
||||
input_formula = "x + 0*1e999"
|
||||
self.assert_grade(problem, input_formula, "incorrect")
|
||||
|
||||
def test_raises_zero_division_err(self):
|
||||
"""
|
||||
See if division by zero raises an error.
|
||||
"""
|
||||
sample_dict = {'x': (1, 2)}
|
||||
problem = self.build_problem(sample_dict=sample_dict,
|
||||
num_samples=10,
|
||||
tolerance="1%",
|
||||
answer="x") # Answer doesn't matter
|
||||
input_dict = {'1_2_1': '1/0'}
|
||||
self.assertRaises(StudentInputError, problem.grade_answers, input_dict)
|
||||
|
||||
|
||||
class StringResponseTest(ResponseTest):
|
||||
from response_xml_factory import StringResponseXMLFactory
|
||||
@@ -592,7 +628,7 @@ class StringResponseTest(ResponseTest):
|
||||
problem = self.build_problem(
|
||||
answer="Michigan",
|
||||
hintfn="gimme_a_hint",
|
||||
script = textwrap.dedent("""
|
||||
script=textwrap.dedent("""
|
||||
def gimme_a_hint(answer_ids, student_answers, new_cmap, old_cmap):
|
||||
aid = answer_ids[0]
|
||||
answer = student_answers[aid]
|
||||
@@ -898,6 +934,14 @@ class NumericalResponseTest(ResponseTest):
|
||||
incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"]
|
||||
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
|
||||
|
||||
def test_raises_zero_division_err(self):
|
||||
"""See if division by zero is handled correctly"""
|
||||
problem = self.build_problem(question_text="What 5 * 10?",
|
||||
explanation="The answer is 50",
|
||||
answer="5e+1") # Answer doesn't matter
|
||||
input_dict = {'1_2_1': '1/0'}
|
||||
self.assertRaises(StudentInputError, problem.grade_answers, input_dict)
|
||||
|
||||
|
||||
class CustomResponseTest(ResponseTest):
|
||||
from response_xml_factory import CustomResponseXMLFactory
|
||||
@@ -947,8 +991,8 @@ class CustomResponseTest(ResponseTest):
|
||||
#
|
||||
# 'answer_given' is the answer the student gave (if there is just one input)
|
||||
# or an ordered list of answers (if there are multiple inputs)
|
||||
#
|
||||
# The function should return a dict of the form
|
||||
#
|
||||
# The function should return a dict of the form
|
||||
# { 'ok': BOOL, 'msg': STRING }
|
||||
#
|
||||
script = textwrap.dedent("""
|
||||
|
||||
@@ -4,5 +4,5 @@ setup(
|
||||
name="capa",
|
||||
version="0.1",
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
install_requires=["distribute==0.6.28"],
|
||||
install_requires=["distribute>=0.6.28"],
|
||||
)
|
||||
|
||||
@@ -66,22 +66,51 @@ class ComplexEncoder(json.JSONEncoder):
|
||||
|
||||
class CapaFields(object):
|
||||
attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
|
||||
max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
|
||||
max_attempts = StringyInteger(
|
||||
display_name="Maximum Attempts",
|
||||
help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.",
|
||||
values={"min": 1}, scope=Scope.settings
|
||||
)
|
||||
due = Date(help="Date that this problem is due by", scope=Scope.settings)
|
||||
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
|
||||
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed",
|
||||
values=["answered", "always", "attempted", "closed", "never"])
|
||||
showanswer = String(
|
||||
display_name="Show Answer",
|
||||
help="Defines when to show the answer to the problem. A default value can be set in Advanced Settings.",
|
||||
scope=Scope.settings, default="closed",
|
||||
values=[
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Attempted", "value": "attempted"},
|
||||
{"display_name": "Closed", "value": "closed"},
|
||||
{"display_name": "Finished", "value": "finished"},
|
||||
{"display_name": "Past Due", "value": "past_due"},
|
||||
{"display_name": "Never", "value": "never"}]
|
||||
)
|
||||
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
|
||||
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
|
||||
rerandomize = Randomization(
|
||||
display_name="Randomization", help="Defines how often inputs are randomized when a student loads the problem. This setting only applies to problems that can have randomly generated numeric values. A default value can be set in Advanced Settings.",
|
||||
default="always", scope=Scope.settings, values=[{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "On Reset", "value": "onreset"},
|
||||
{"display_name": "Never", "value": "never"},
|
||||
{"display_name": "Per Student", "value": "per_student"}]
|
||||
)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
|
||||
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
|
||||
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state)
|
||||
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
|
||||
seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state)
|
||||
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
|
||||
weight = StringyFloat(
|
||||
display_name="Problem Weight",
|
||||
help="Defines the number of points each problem is worth. If the value is not set, each response field in the problem is worth one point.",
|
||||
values={"min": 0, "step": .1},
|
||||
scope=Scope.settings
|
||||
)
|
||||
markdown = String(help="Markdown source of this module", scope=Scope.settings)
|
||||
source_code = String(help="Source code for LaTeX and Word problems. This feature is not well-supported.", scope=Scope.settings)
|
||||
source_code = String(
|
||||
help="Source code for LaTeX and Word problems. This feature is not well-supported.",
|
||||
scope=Scope.settings
|
||||
)
|
||||
|
||||
|
||||
class CapaModule(CapaFields, XModule):
|
||||
|
||||
@@ -5,7 +5,7 @@ from pkg_resources import resource_string
|
||||
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from .x_module import XModule
|
||||
from xblock.core import Integer, Scope, String, Boolean, List
|
||||
from xblock.core import Integer, Scope, String, List
|
||||
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
|
||||
from collections import namedtuple
|
||||
from .fields import Date, StringyFloat, StringyInteger, StringyBoolean
|
||||
@@ -48,27 +48,49 @@ class VersionInteger(Integer):
|
||||
|
||||
|
||||
class CombinedOpenEndedFields(object):
|
||||
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings)
|
||||
display_name = String(
|
||||
display_name="Display Name",
|
||||
help="This name appears in the horizontal navigation at the top of the page.",
|
||||
default="Open Ended Grading", scope=Scope.settings
|
||||
)
|
||||
current_task_number = StringyInteger(help="Current task that the student is on.", default=0, scope=Scope.user_state)
|
||||
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state)
|
||||
state = String(help="Which step within the current task that the student is on.", default="initial",
|
||||
scope=Scope.user_state)
|
||||
student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0,
|
||||
scope=Scope.user_state)
|
||||
ready_to_reset = StringyBoolean(help="If the problem is ready to be reset or not.", default=False,
|
||||
scope=Scope.user_state)
|
||||
attempts = StringyInteger(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings)
|
||||
is_graded = StringyBoolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
|
||||
accept_file_upload = StringyBoolean(help="Whether or not the problem accepts file uploads.", default=False,
|
||||
scope=Scope.settings)
|
||||
skip_spelling_checks = StringyBoolean(help="Whether or not to skip initial spelling checks.", default=True,
|
||||
scope=Scope.settings)
|
||||
ready_to_reset = StringyBoolean(
|
||||
help="If the problem is ready to be reset or not.", default=False,
|
||||
scope=Scope.user_state
|
||||
)
|
||||
attempts = StringyInteger(
|
||||
display_name="Maximum Attempts",
|
||||
help="The number of times the student can try to answer this problem.", default=1,
|
||||
scope=Scope.settings, values = {"min" : 1 }
|
||||
)
|
||||
is_graded = StringyBoolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
|
||||
accept_file_upload = StringyBoolean(
|
||||
display_name="Allow File Uploads",
|
||||
help="Whether or not the student can submit files as a response.", default=False, scope=Scope.settings
|
||||
)
|
||||
skip_spelling_checks = StringyBoolean(
|
||||
display_name="Disable Quality Filter",
|
||||
help="If False, the Quality Filter is enabled and submissions with poor spelling, short length, or poor grammar will not be peer reviewed.",
|
||||
default=False, scope=Scope.settings
|
||||
)
|
||||
due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings)
|
||||
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
|
||||
scope=Scope.settings)
|
||||
graceperiod = String(
|
||||
help="Amount of time after the due date that submissions will be accepted",
|
||||
default=None,
|
||||
scope=Scope.settings
|
||||
)
|
||||
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
|
||||
weight = StringyFloat(
|
||||
display_name="Problem Weight",
|
||||
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
|
||||
scope=Scope.settings, values = {"min" : 0 , "step": ".1"}
|
||||
)
|
||||
markdown = String(help="Markdown source of this module", scope=Scope.settings)
|
||||
|
||||
|
||||
@@ -244,6 +266,6 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(CombinedOpenEndedDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.extend([CombinedOpenEndedDescriptor.due, CombinedOpenEndedDescriptor.graceperiod,
|
||||
CombinedOpenEndedDescriptor.markdown])
|
||||
CombinedOpenEndedDescriptor.markdown, CombinedOpenEndedDescriptor.version])
|
||||
return non_editable_fields
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
position: relative;
|
||||
@include linear-gradient(top, #d4dee8, #c9d5e2);
|
||||
padding: 5px;
|
||||
border: 1px solid #3c3c3c;
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-bottom-color: #a5aaaf;
|
||||
@include clearfix;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.advanced-toggle {
|
||||
@include white-button;
|
||||
height: auto;
|
||||
margin-top: -1px;
|
||||
margin-top: -4px;
|
||||
padding: 3px 9px;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
color: $darkGrey !important;
|
||||
pointer-events: none;
|
||||
cursor: none;
|
||||
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
padding: 0;
|
||||
margin: 0 5px 0 15px;
|
||||
margin: -1px 5px 0 15px;
|
||||
border-radius: 22px;
|
||||
border: 1px solid #a5aaaf;
|
||||
background: #e5ecf3;
|
||||
@@ -99,6 +99,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.problem-editor {
|
||||
// adding padding to simple editor only - adjacent selector is needed since there are no toggles for CodeMirror
|
||||
.markdown-box+.CodeMirror {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.problem-editor-icon {
|
||||
display: inline-block;
|
||||
width: 26px;
|
||||
|
||||
@@ -170,7 +170,7 @@ nav.sequence-nav {
|
||||
font-family: $sans-serif;
|
||||
line-height: lh();
|
||||
left: 0px;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
padding: 6px;
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
@@ -204,7 +204,7 @@ nav.sequence-nav {
|
||||
p {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,12 +248,12 @@ nav.sequence-nav {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: normal;
|
||||
opacity: .4;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,12 +320,12 @@ nav.sequence-bottom {
|
||||
outline: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
background-position: center 15px;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: .4;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
|
||||
@@ -41,7 +41,7 @@ div.video {
|
||||
|
||||
&:hover {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ div.video {
|
||||
|
||||
ol.video_speeds {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
@@ -208,7 +208,7 @@ div.video {
|
||||
}
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
@@ -221,7 +221,7 @@ div.video {
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
width: 133px;
|
||||
z-index: 10;
|
||||
@@ -264,7 +264,7 @@ div.video {
|
||||
&.open {
|
||||
.volume-slider-container {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ div.video {
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
width: 45px;
|
||||
height: 125px;
|
||||
@@ -395,7 +395,7 @@ div.video {
|
||||
font-weight: 800;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
padding: 0 lh(.5);
|
||||
position: relative;
|
||||
text-indent: -9999px;
|
||||
@@ -410,7 +410,7 @@ div.video {
|
||||
}
|
||||
|
||||
&.off {
|
||||
opacity: .7;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,7 +418,7 @@ div.video {
|
||||
|
||||
&:hover section.video-controls {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
div.slider {
|
||||
|
||||
@@ -41,7 +41,7 @@ div.video {
|
||||
|
||||
&:hover {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ div.video {
|
||||
|
||||
ol.video_speeds {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
@@ -208,7 +208,7 @@ div.video {
|
||||
}
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
@@ -221,7 +221,7 @@ div.video {
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
width: 133px;
|
||||
z-index: 10;
|
||||
@@ -264,7 +264,7 @@ div.video {
|
||||
&.open {
|
||||
.volume-slider-container {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ div.video {
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
width: 45px;
|
||||
height: 125px;
|
||||
@@ -395,7 +395,7 @@ div.video {
|
||||
font-weight: 800;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
padding: 0 lh(.5);
|
||||
position: relative;
|
||||
text-indent: -9999px;
|
||||
@@ -410,7 +410,7 @@ div.video {
|
||||
}
|
||||
|
||||
&.off {
|
||||
opacity: .7;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,7 +418,7 @@ div.video {
|
||||
|
||||
&:hover section.video-controls {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
div.slider {
|
||||
|
||||
@@ -8,8 +8,16 @@ from xblock.core import String, Scope
|
||||
|
||||
class DiscussionFields(object):
|
||||
discussion_id = String(scope=Scope.settings)
|
||||
discussion_category = String(scope=Scope.settings)
|
||||
discussion_target = String(scope=Scope.settings)
|
||||
discussion_category = String(
|
||||
display_name="Category",
|
||||
help="A category name for the discussion. This name appears in the left pane of the discussion forum for the course.",
|
||||
scope=Scope.settings
|
||||
)
|
||||
discussion_target = String(
|
||||
display_name="Subcategory",
|
||||
help="A subcategory name for the discussion. This name appears in the left pane of the discussion forum for the course.",
|
||||
scope=Scope.settings
|
||||
)
|
||||
sort_key = String(scope=Scope.settings)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
"""
|
||||
Modules that get shown to the users when an error has occured while
|
||||
loading or rendering other modules
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import json
|
||||
@@ -22,12 +27,19 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ErrorFields(object):
|
||||
"""
|
||||
XBlock fields used by the ErrorModules
|
||||
"""
|
||||
contents = String(scope=Scope.content)
|
||||
error_msg = String(scope=Scope.content)
|
||||
display_name = String(scope=Scope.settings)
|
||||
|
||||
|
||||
class ErrorModule(ErrorFields, XModule):
|
||||
"""
|
||||
Module that gets shown to staff when there has been an error while
|
||||
loading or rendering other modules
|
||||
"""
|
||||
|
||||
def get_html(self):
|
||||
'''Show an error to staff.
|
||||
@@ -42,6 +54,10 @@ class ErrorModule(ErrorFields, XModule):
|
||||
|
||||
|
||||
class NonStaffErrorModule(ErrorFields, XModule):
|
||||
"""
|
||||
Module that gets shown to students when there has been an error while
|
||||
loading or rendering other modules
|
||||
"""
|
||||
def get_html(self):
|
||||
'''Show an error to a student.
|
||||
TODO (vshnayder): proper style, divs, etc.
|
||||
@@ -61,7 +77,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
|
||||
module_class = ErrorModule
|
||||
|
||||
@classmethod
|
||||
def _construct(self, system, contents, error_msg, location):
|
||||
def _construct(cls, system, contents, error_msg, location):
|
||||
|
||||
if location.name is None:
|
||||
location = location._replace(
|
||||
@@ -80,7 +96,7 @@ class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
|
||||
'contents': contents,
|
||||
'display_name': 'Error: ' + location.name
|
||||
}
|
||||
return ErrorDescriptor(
|
||||
return cls(
|
||||
system,
|
||||
location,
|
||||
model_data,
|
||||
|
||||
@@ -289,6 +289,9 @@ class @CombinedOpenEnded
|
||||
if @child_type == "openended"
|
||||
@submit_button.hide()
|
||||
@queueing()
|
||||
if @task_number==1 and @task_count==1
|
||||
@grader_status = $('.grader-status')
|
||||
@grader_status.html("<p>Response submitted for scoring.</p>")
|
||||
else if @child_state == 'post_assessment'
|
||||
if @child_type=="openended"
|
||||
@skip_button.show()
|
||||
@@ -311,6 +314,8 @@ class @CombinedOpenEnded
|
||||
if @task_number<@task_count
|
||||
@next_problem()
|
||||
else
|
||||
if @task_number==1 and @task_count==1
|
||||
@show_combined_rubric_current()
|
||||
@show_results_current()
|
||||
@reset_button.show()
|
||||
|
||||
|
||||
@@ -268,7 +268,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
query = {'_id.org': location.org,
|
||||
'_id.course': location.course,
|
||||
'_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical',
|
||||
'wrapper', 'problemset', 'conditional']}
|
||||
'wrapper', 'problemset', 'conditional', 'randomize']}
|
||||
}
|
||||
# we just want the Location, children, and inheritable metadata
|
||||
record_filter = {'_id': 1, 'definition.children': 1}
|
||||
|
||||
@@ -85,7 +85,7 @@ class MockControllerQueryService(object):
|
||||
def __init__(self, config, system):
|
||||
pass
|
||||
|
||||
def check_if_name_is_unique(self, **params):
|
||||
def check_if_name_is_unique(self, *args, **kwargs):
|
||||
"""
|
||||
Mock later if needed. Stub function for now.
|
||||
@param params:
|
||||
@@ -93,7 +93,7 @@ class MockControllerQueryService(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def check_for_eta(self, **params):
|
||||
def check_for_eta(self, *args, **kwargs):
|
||||
"""
|
||||
Mock later if needed. Stub function for now.
|
||||
@param params:
|
||||
@@ -101,19 +101,19 @@ class MockControllerQueryService(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def check_combined_notifications(self, **params):
|
||||
def check_combined_notifications(self, *args, **kwargs):
|
||||
combined_notifications = '{"flagged_submissions_exist": false, "version": 1, "new_student_grading_to_view": false, "success": true, "staff_needs_to_grade": false, "student_needs_to_peer_grade": true, "overall_need_to_check": true}'
|
||||
return combined_notifications
|
||||
|
||||
def get_grading_status_list(self, **params):
|
||||
def get_grading_status_list(self, *args, **kwargs):
|
||||
grading_status_list = '{"version": 1, "problem_list": [{"problem_name": "Science Question -- Machine Assessed", "grader_type": "NA", "eta_available": true, "state": "Waiting to be Graded", "eta": 259200, "location": "i4x://MITx/oe101x/combinedopenended/Science_SA_ML"}, {"problem_name": "Humanities Question -- Peer Assessed", "grader_type": "NA", "eta_available": true, "state": "Waiting to be Graded", "eta": 259200, "location": "i4x://MITx/oe101x/combinedopenended/Humanities_SA_Peer"}], "success": true}'
|
||||
return grading_status_list
|
||||
|
||||
def get_flagged_problem_list(self, **params):
|
||||
def get_flagged_problem_list(self, *args, **kwargs):
|
||||
flagged_problem_list = '{"version": 1, "success": false, "error": "No flagged submissions exist for course: MITx/oe101x/2012_Fall"}'
|
||||
return flagged_problem_list
|
||||
|
||||
def take_action_on_flags(self, **params):
|
||||
def take_action_on_flags(self, *args, **kwargs):
|
||||
"""
|
||||
Mock later if needed. Stub function for now.
|
||||
@param params:
|
||||
|
||||
@@ -10,7 +10,7 @@ from .x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from .timeinfo import TimeInfo
|
||||
from xblock.core import Object, Integer, Boolean, String, Scope
|
||||
from xblock.core import Object, String, Scope
|
||||
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
|
||||
|
||||
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
|
||||
@@ -22,24 +22,43 @@ USE_FOR_SINGLE_LOCATION = False
|
||||
LINK_TO_LOCATION = ""
|
||||
TRUE_DICT = [True, "True", "true", "TRUE"]
|
||||
MAX_SCORE = 1
|
||||
IS_GRADED = True
|
||||
IS_GRADED = False
|
||||
|
||||
EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please notify course staff."
|
||||
|
||||
|
||||
class PeerGradingFields(object):
|
||||
use_for_single_location = StringyBoolean(help="Whether to use this for a single location or as a panel.",
|
||||
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
|
||||
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
|
||||
scope=Scope.settings)
|
||||
is_graded = StringyBoolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
|
||||
use_for_single_location = StringyBoolean(
|
||||
display_name="Show Single Problem",
|
||||
help='When True, only the single problem specified by "Link to Problem Location" is shown. '
|
||||
'When False, a panel is displayed with all problems available for peer grading.',
|
||||
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings
|
||||
)
|
||||
link_to_location = String(
|
||||
display_name="Link to Problem Location",
|
||||
help='The location of the problem being graded. Only used when "Show Single Problem" is True.',
|
||||
default=LINK_TO_LOCATION, scope=Scope.settings
|
||||
)
|
||||
is_graded = StringyBoolean(
|
||||
display_name="Graded",
|
||||
help='Defines whether the student gets credit for grading this problem. Only used when "Show Single Problem" is True.',
|
||||
default=IS_GRADED, scope=Scope.settings
|
||||
)
|
||||
due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings)
|
||||
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
|
||||
max_grade = StringyInteger(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
|
||||
scope=Scope.settings)
|
||||
student_data_for_location = Object(help="Student data for a given peer grading problem.",
|
||||
scope=Scope.user_state)
|
||||
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
|
||||
max_grade = StringyInteger(
|
||||
help="The maximum grade that a student can receive for this problem.", default=MAX_SCORE,
|
||||
scope=Scope.settings, values={"min": 0}
|
||||
)
|
||||
student_data_for_location = Object(
|
||||
help="Student data for a given peer grading problem.",
|
||||
scope=Scope.user_state
|
||||
)
|
||||
weight = StringyFloat(
|
||||
display_name="Problem Weight",
|
||||
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
|
||||
scope=Scope.settings, values={"min": 0, "step": ".1"}
|
||||
)
|
||||
|
||||
|
||||
class PeerGradingModule(PeerGradingFields, XModule):
|
||||
@@ -590,3 +609,11 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
|
||||
|
||||
#Specify whether or not to pass in open ended interface
|
||||
needs_open_ended_interface = True
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(PeerGradingDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.extend([PeerGradingFields.due_date, PeerGradingFields.grace_period_string,
|
||||
PeerGradingFields.max_grade])
|
||||
return non_editable_fields
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Open Ended Response
|
||||
attempts: 1
|
||||
is_graded: False
|
||||
version: 1
|
||||
skip_spelling_checks: False
|
||||
accept_file_upload: False
|
||||
weight: ""
|
||||
markdown: ""
|
||||
data: |
|
||||
<combinedopenended>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Blank HTML Page
|
||||
empty: True
|
||||
|
||||
data: |
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Peer Grading Interface
|
||||
use_for_single_location: False
|
||||
link_to_location: None
|
||||
is_graded: False
|
||||
max_grade: 1
|
||||
weight: ""
|
||||
data: |
|
||||
<peergrading>
|
||||
</peergrading>
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
metadata:
|
||||
display_name: Circuit Schematic Builder
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
data: |
|
||||
<problem >
|
||||
Please make a voltage divider that splits the provided voltage evenly.
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Custom Python-Evaluated Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
data: |
|
||||
<problem>
|
||||
<p>
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
metadata:
|
||||
display_name: Blank Common Problem
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
showanswer: finished
|
||||
markdown: ""
|
||||
weight: ""
|
||||
empty: True
|
||||
attempts: ""
|
||||
data: |
|
||||
<problem>
|
||||
</problem>
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
metadata:
|
||||
display_name: Blank Advanced Problem
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
empty: True
|
||||
showanswer: finished
|
||||
data: |
|
||||
<problem>
|
||||
</problem>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Math Expression Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
data: |
|
||||
<problem>
|
||||
<p>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Image Mapped Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
data: |
|
||||
<problem>
|
||||
<p>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Multiple Choice
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
markdown:
|
||||
"A multiple choice problem presents radio buttons for student input. Students can only select a single
|
||||
option presented. Multiple Choice questions have been the subject of many areas of research due to the early
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Numerical Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
markdown:
|
||||
"A numerical input problem accepts a line of text input from the
|
||||
student, and evaluates the input for correctness based on its
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Dropdown
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
markdown:
|
||||
"Dropdown problems give a limited set of options for students to respond with, and present those options
|
||||
in a format that encourages them to search for a specific answer rather than being immediately presented
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Text Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
# Note, the extra newlines are needed to make the yaml parser add blank lines instead of folding
|
||||
markdown:
|
||||
"A text input problem accepts a line of text from the
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Word cloud
|
||||
version: 1
|
||||
num_inputs: 5
|
||||
num_top_words: 250
|
||||
display_student_percents: True
|
||||
data: {}
|
||||
children: []
|
||||
|
||||
51
common/lib/xmodule/xmodule/tests/test_error_module.py
Normal file
51
common/lib/xmodule/xmodule/tests/test_error_module.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
Tests for ErrorModule and NonStaffErrorModule
|
||||
"""
|
||||
import unittest
|
||||
from xmodule.tests import test_system
|
||||
import xmodule.error_module as error_module
|
||||
|
||||
|
||||
class TestErrorModule(unittest.TestCase):
|
||||
"""
|
||||
Tests for ErrorModule and ErrorDescriptor
|
||||
"""
|
||||
def setUp(self):
|
||||
self.system = test_system()
|
||||
self.org = "org"
|
||||
self.course = "course"
|
||||
self.fake_xml = "<problem />"
|
||||
self.broken_xml = "<problem>"
|
||||
self.error_msg = "Error"
|
||||
|
||||
def test_error_module_create(self):
|
||||
descriptor = error_module.ErrorDescriptor.from_xml(
|
||||
self.fake_xml, self.system, self.org, self.course)
|
||||
self.assertTrue(isinstance(descriptor, error_module.ErrorDescriptor))
|
||||
|
||||
def test_error_module_rendering(self):
|
||||
descriptor = error_module.ErrorDescriptor.from_xml(
|
||||
self.fake_xml, self.system, self.org, self.course, self.error_msg)
|
||||
module = descriptor.xmodule(self.system)
|
||||
rendered_html = module.get_html()
|
||||
self.assertIn(self.error_msg, rendered_html)
|
||||
self.assertIn(self.fake_xml, rendered_html)
|
||||
|
||||
|
||||
class TestNonStaffErrorModule(TestErrorModule):
|
||||
"""
|
||||
Tests for NonStaffErrorModule and NonStaffErrorDescriptor
|
||||
"""
|
||||
|
||||
def test_non_staff_error_module_create(self):
|
||||
descriptor = error_module.NonStaffErrorDescriptor.from_xml(
|
||||
self.fake_xml, self.system, self.org, self.course)
|
||||
self.assertTrue(isinstance(descriptor, error_module.NonStaffErrorDescriptor))
|
||||
|
||||
def test_non_staff_error_module_rendering(self):
|
||||
descriptor = error_module.NonStaffErrorDescriptor.from_xml(
|
||||
self.fake_xml, self.system, self.org, self.course)
|
||||
module = descriptor.xmodule(self.system)
|
||||
rendered_html = module.get_html()
|
||||
self.assertNotIn(self.error_msg, rendered_html)
|
||||
self.assertNotIn(self.fake_xml, rendered_html)
|
||||
@@ -460,8 +460,8 @@ class ImportTestCase(BaseCourseTestCase):
|
||||
)
|
||||
module = modulestore.get_instance(course.id, location)
|
||||
self.assertEqual(len(module.get_children()), 0)
|
||||
self.assertEqual(module.num_inputs, '5')
|
||||
self.assertEqual(module.num_top_words, '250')
|
||||
self.assertEqual(module.num_inputs, 5)
|
||||
self.assertEqual(module.num_top_words, 250)
|
||||
|
||||
def test_cohort_config(self):
|
||||
"""
|
||||
|
||||
@@ -1,69 +1,141 @@
|
||||
# disable missing docstring
|
||||
#pylint: disable=C0111
|
||||
|
||||
from xmodule.x_module import XModuleFields
|
||||
from xblock.core import Scope, String, Object
|
||||
from xmodule.fields import Date, StringyInteger
|
||||
from xblock.core import Scope, String, Object, Boolean
|
||||
from xmodule.fields import Date, StringyInteger, StringyFloat
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
import unittest
|
||||
from . import test_system
|
||||
from .import test_system
|
||||
from mock import Mock
|
||||
|
||||
|
||||
class CrazyJsonString(String):
|
||||
def to_json(self, value):
|
||||
return value + " JSON"
|
||||
|
||||
|
||||
class TestFields(object):
|
||||
# Will be returned by editable_metadata_fields.
|
||||
max_attempts = StringyInteger(scope=Scope.settings, default=1000)
|
||||
max_attempts = StringyInteger(scope=Scope.settings, default=1000, values={'min': 1, 'max': 10})
|
||||
# Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields.
|
||||
due = Date(scope=Scope.settings)
|
||||
# Will not be returned by editable_metadata_fields because is not Scope.settings.
|
||||
student_answers = Object(scope=Scope.user_state)
|
||||
# Will be returned, and can override the inherited value from XModule.
|
||||
display_name = String(scope=Scope.settings, default='local default')
|
||||
display_name = String(scope=Scope.settings, default='local default', display_name='Local Display Name',
|
||||
help='local help')
|
||||
# Used for testing select type, effect of to_json method
|
||||
string_select = CrazyJsonString(
|
||||
scope=Scope.settings,
|
||||
default='default value',
|
||||
values=[{'display_name': 'first', 'value': 'value a'},
|
||||
{'display_name': 'second', 'value': 'value b'}]
|
||||
)
|
||||
# Used for testing select type
|
||||
float_select = StringyFloat(scope=Scope.settings, default=.999, values=[1.23, 0.98])
|
||||
# Used for testing float type
|
||||
float_non_select = StringyFloat(scope=Scope.settings, default=.999, values={'min': 0, 'step': .3})
|
||||
# Used for testing that Booleans get mapped to select type
|
||||
boolean_select = Boolean(scope=Scope.settings)
|
||||
|
||||
|
||||
class EditableMetadataFieldsTest(unittest.TestCase):
|
||||
|
||||
def test_display_name_field(self):
|
||||
editable_fields = self.get_xml_editable_fields({})
|
||||
# Tests that the xblock fields (currently tags and name) get filtered out.
|
||||
# Also tests that xml_attributes is filtered out of XmlDescriptor.
|
||||
self.assertEqual(1, len(editable_fields), "Expected only 1 editable field for xml descriptor.")
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=False, inheritable=False, value=None, default_value=None)
|
||||
self.assert_field_values(
|
||||
editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=False, inheritable=False, value=None, default_value=None
|
||||
)
|
||||
|
||||
def test_override_default(self):
|
||||
# Tests that explicitly_set is correct when a value overrides the default (not inheritable).
|
||||
editable_fields = self.get_xml_editable_fields({'display_name': 'foo'})
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=True, inheritable=False, value='foo', default_value=None)
|
||||
self.assert_field_values(
|
||||
editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=True, inheritable=False, value='foo', default_value=None
|
||||
)
|
||||
|
||||
def test_additional_field(self):
|
||||
descriptor = self.get_descriptor({'max_attempts' : '7'})
|
||||
def test_integer_field(self):
|
||||
descriptor = self.get_descriptor({'max_attempts': '7'})
|
||||
editable_fields = descriptor.editable_metadata_fields
|
||||
self.assertEqual(2, len(editable_fields))
|
||||
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts,
|
||||
explicitly_set=True, inheritable=False, value=7, default_value=1000)
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=False, inheritable=False, value='local default', default_value='local default')
|
||||
self.assertEqual(6, len(editable_fields))
|
||||
self.assert_field_values(
|
||||
editable_fields, 'max_attempts', TestFields.max_attempts,
|
||||
explicitly_set=True, inheritable=False, value=7, default_value=1000, type='Integer',
|
||||
options=TestFields.max_attempts.values
|
||||
)
|
||||
self.assert_field_values(
|
||||
editable_fields, 'display_name', TestFields.display_name,
|
||||
explicitly_set=False, inheritable=False, value='local default', default_value='local default'
|
||||
)
|
||||
|
||||
editable_fields = self.get_descriptor({}).editable_metadata_fields
|
||||
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts,
|
||||
explicitly_set=False, inheritable=False, value=1000, default_value=1000)
|
||||
self.assert_field_values(
|
||||
editable_fields, 'max_attempts', TestFields.max_attempts,
|
||||
explicitly_set=False, inheritable=False, value=1000, default_value=1000, type='Integer',
|
||||
options=TestFields.max_attempts.values
|
||||
)
|
||||
|
||||
def test_inherited_field(self):
|
||||
model_val = {'display_name' : 'inherited'}
|
||||
model_val = {'display_name': 'inherited'}
|
||||
descriptor = self.get_descriptor(model_val)
|
||||
# Mimic an inherited value for display_name (inherited and inheritable are the same in this case).
|
||||
descriptor._inherited_metadata = model_val
|
||||
descriptor._inheritable_metadata = model_val
|
||||
editable_fields = descriptor.editable_metadata_fields
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=False, inheritable=True, value='inherited', default_value='inherited')
|
||||
self.assert_field_values(
|
||||
editable_fields, 'display_name', TestFields.display_name,
|
||||
explicitly_set=False, inheritable=True, value='inherited', default_value='inherited'
|
||||
)
|
||||
|
||||
descriptor = self.get_descriptor({'display_name' : 'explicit'})
|
||||
descriptor = self.get_descriptor({'display_name': 'explicit'})
|
||||
# Mimic the case where display_name WOULD have been inherited, except we explicitly set it.
|
||||
descriptor._inheritable_metadata = {'display_name' : 'inheritable value'}
|
||||
descriptor._inheritable_metadata = {'display_name': 'inheritable value'}
|
||||
descriptor._inherited_metadata = {}
|
||||
editable_fields = descriptor.editable_metadata_fields
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=True, inheritable=True, value='explicit', default_value='inheritable value')
|
||||
self.assert_field_values(
|
||||
editable_fields, 'display_name', TestFields.display_name,
|
||||
explicitly_set=True, inheritable=True, value='explicit', default_value='inheritable value'
|
||||
)
|
||||
|
||||
def test_type_and_options(self):
|
||||
# test_display_name_field verifies that a String field is of type "Generic".
|
||||
# test_integer_field verifies that a StringyInteger field is of type "Integer".
|
||||
|
||||
descriptor = self.get_descriptor({})
|
||||
editable_fields = descriptor.editable_metadata_fields
|
||||
|
||||
# Tests for select
|
||||
self.assert_field_values(
|
||||
editable_fields, 'string_select', TestFields.string_select,
|
||||
explicitly_set=False, inheritable=False, value='default value', default_value='default value',
|
||||
type='Select', options=[{'display_name': 'first', 'value': 'value a JSON'},
|
||||
{'display_name': 'second', 'value': 'value b JSON'}]
|
||||
)
|
||||
|
||||
self.assert_field_values(
|
||||
editable_fields, 'float_select', TestFields.float_select,
|
||||
explicitly_set=False, inheritable=False, value=.999, default_value=.999,
|
||||
type='Select', options=[1.23, 0.98]
|
||||
)
|
||||
|
||||
self.assert_field_values(
|
||||
editable_fields, 'boolean_select', TestFields.boolean_select,
|
||||
explicitly_set=False, inheritable=False, value=None, default_value=None,
|
||||
type='Select', options=[{'display_name': "True", "value": True}, {'display_name': "False", "value": False}]
|
||||
)
|
||||
|
||||
# Test for float
|
||||
self.assert_field_values(
|
||||
editable_fields, 'float_non_select', TestFields.float_non_select,
|
||||
explicitly_set=False, inheritable=False, value=.999, default_value=.999,
|
||||
type='Float', options={'min': 0, 'step': .3}
|
||||
)
|
||||
|
||||
|
||||
# Start of helper methods
|
||||
def get_xml_editable_fields(self, model_data):
|
||||
@@ -73,7 +145,6 @@ class EditableMetadataFieldsTest(unittest.TestCase):
|
||||
|
||||
def get_descriptor(self, model_data):
|
||||
class TestModuleDescriptor(TestFields, XmlDescriptor):
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(TestModuleDescriptor, self).non_editable_metadata_fields
|
||||
@@ -84,10 +155,19 @@ class EditableMetadataFieldsTest(unittest.TestCase):
|
||||
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
return TestModuleDescriptor(system=system, location=None, model_data=model_data)
|
||||
|
||||
def assert_field_values(self, editable_fields, name, field, explicitly_set, inheritable, value, default_value):
|
||||
def assert_field_values(self, editable_fields, name, field, explicitly_set, inheritable, value, default_value,
|
||||
type='Generic', options=[]):
|
||||
test_field = editable_fields[name]
|
||||
self.assertEqual(field, test_field['field'])
|
||||
|
||||
self.assertEqual(field.name, test_field['field_name'])
|
||||
self.assertEqual(field.display_name, test_field['display_name'])
|
||||
self.assertEqual(field.help, test_field['help'])
|
||||
|
||||
self.assertEqual(field.to_json(value), test_field['value'])
|
||||
self.assertEqual(field.to_json(default_value), test_field['default_value'])
|
||||
|
||||
self.assertEqual(options, test_field['options'])
|
||||
self.assertEqual(type, test_field['type'])
|
||||
|
||||
self.assertEqual(explicitly_set, test_field['explicitly_set'])
|
||||
self.assertEqual(inheritable, test_field['inheritable'])
|
||||
self.assertEqual(value, test_field['value'])
|
||||
self.assertEqual(default_value, test_field['default_value'])
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
generate and view word cloud.
|
||||
|
||||
On the client side we show:
|
||||
If student does not yet anwered - `num_inputs` numbers of text inputs.
|
||||
If student does not yet answered - `num_inputs` numbers of text inputs.
|
||||
If student have answered - words he entered and cloud.
|
||||
"""
|
||||
|
||||
@@ -14,7 +14,8 @@ from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.editing_module import MetadataOnlyEditingDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
|
||||
from xblock.core import Scope, String, Object, Boolean, List, Integer
|
||||
from xblock.core import Scope, Object, Boolean, List
|
||||
from fields import StringyBoolean, StringyInteger
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,22 +32,23 @@ def pretty_bool(value):
|
||||
|
||||
class WordCloudFields(object):
|
||||
"""XFields for word cloud."""
|
||||
display_name = String(
|
||||
help="Display name for this module",
|
||||
scope=Scope.settings
|
||||
)
|
||||
num_inputs = Integer(
|
||||
help="Number of inputs.",
|
||||
num_inputs = StringyInteger(
|
||||
display_name="Inputs",
|
||||
help="Number of text boxes available for students to input words/sentences.",
|
||||
scope=Scope.settings,
|
||||
default=5
|
||||
default=5,
|
||||
values={"min": 1}
|
||||
)
|
||||
num_top_words = Integer(
|
||||
help="Number of max words, which will be displayed.",
|
||||
num_top_words = StringyInteger(
|
||||
display_name="Maximum Words",
|
||||
help="Maximum number of words to be displayed in generated word cloud.",
|
||||
scope=Scope.settings,
|
||||
default=250
|
||||
default=250,
|
||||
values={"min": 1}
|
||||
)
|
||||
display_student_percents = Boolean(
|
||||
help="Display usage percents for each word?",
|
||||
display_student_percents = StringyBoolean(
|
||||
display_name="Show Percents",
|
||||
help="Statistics are shown for entered words near that word.",
|
||||
scope=Scope.settings,
|
||||
default=True
|
||||
)
|
||||
@@ -205,7 +207,7 @@ class WordCloudModule(WordCloudFields, XModule):
|
||||
# Update top_words.
|
||||
self.top_words = self.top_dict(
|
||||
temp_all_words,
|
||||
int(self.num_top_words)
|
||||
self.num_top_words
|
||||
)
|
||||
|
||||
# Save all_words in database.
|
||||
@@ -226,7 +228,7 @@ class WordCloudModule(WordCloudFields, XModule):
|
||||
'element_id': self.location.html_id(),
|
||||
'element_class': self.location.category,
|
||||
'ajax_url': self.system.ajax_url,
|
||||
'num_inputs': int(self.num_inputs),
|
||||
'num_inputs': self.num_inputs,
|
||||
'submitted': self.submitted
|
||||
}
|
||||
self.content = self.system.render_template('word_cloud.html', context)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import copy
|
||||
import yaml
|
||||
import os
|
||||
|
||||
@@ -9,7 +10,7 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from xblock.core import XBlock, Scope, String
|
||||
from xblock.core import XBlock, Scope, String, Integer, Float
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -75,12 +76,13 @@ class HTMLSnippet(object):
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"get_html() must be provided by specific modules - not present in {0}"
|
||||
.format(self.__class__))
|
||||
.format(self.__class__))
|
||||
|
||||
|
||||
class XModuleFields(object):
|
||||
display_name = String(
|
||||
help="Display name for this module",
|
||||
display_name="Display Name",
|
||||
help="This name appears in the horizontal navigation at the top of the page.",
|
||||
scope=Scope.settings,
|
||||
default=None
|
||||
)
|
||||
@@ -356,7 +358,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
metadata_translations = {
|
||||
'slug': 'url_name',
|
||||
'name': 'display_name',
|
||||
}
|
||||
}
|
||||
|
||||
# ============================= STRUCTURAL MANIPULATION ===================
|
||||
def __init__(self,
|
||||
@@ -458,7 +460,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
# ================================= JSON PARSING ===========================
|
||||
@staticmethod
|
||||
def load_from_json(json_data, system, default_class=None):
|
||||
@@ -523,10 +524,10 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
# ================================= XML PARSING ============================
|
||||
@staticmethod
|
||||
def load_from_xml(xml_data,
|
||||
system,
|
||||
org=None,
|
||||
course=None,
|
||||
default_class=None):
|
||||
system,
|
||||
org=None,
|
||||
course=None,
|
||||
default_class=None):
|
||||
"""
|
||||
This method instantiates the correct subclass of XModuleDescriptor based
|
||||
on the contents of xml_data.
|
||||
@@ -541,7 +542,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
class_ = XModuleDescriptor.load_class(
|
||||
etree.fromstring(xml_data).tag,
|
||||
default_class
|
||||
)
|
||||
)
|
||||
# leave next line, commented out - useful for low-level debugging
|
||||
# log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (
|
||||
# etree.fromstring(xml_data).tag,class_))
|
||||
@@ -625,7 +626,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
"""
|
||||
inherited_metadata = getattr(self, '_inherited_metadata', {})
|
||||
inheritable_metadata = getattr(self, '_inheritable_metadata', {})
|
||||
metadata = {}
|
||||
metadata_fields = {}
|
||||
for field in self.fields:
|
||||
|
||||
if field.scope != Scope.settings or field in self.non_editable_metadata_fields:
|
||||
@@ -641,13 +642,39 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
if field.name in inherited_metadata:
|
||||
explicitly_set = False
|
||||
|
||||
metadata[field.name] = {'field': field,
|
||||
'value': value,
|
||||
'default_value': default_value,
|
||||
'inheritable': inheritable,
|
||||
'explicitly_set': explicitly_set }
|
||||
# We support the following editors:
|
||||
# 1. A select editor for fields with a list of possible values (includes Booleans).
|
||||
# 2. Number editors for integers and floats.
|
||||
# 3. A generic string editor for anything else (editing JSON representation of the value).
|
||||
type = "Generic"
|
||||
values = [] if field.values is None else copy.deepcopy(field.values)
|
||||
if isinstance(values, tuple):
|
||||
values = list(values)
|
||||
if isinstance(values, list):
|
||||
if len(values) > 0:
|
||||
type = "Select"
|
||||
for index, choice in enumerate(values):
|
||||
json_choice = copy.deepcopy(choice)
|
||||
if isinstance(json_choice, dict) and 'value' in json_choice:
|
||||
json_choice['value'] = field.to_json(json_choice['value'])
|
||||
else:
|
||||
json_choice = field.to_json(json_choice)
|
||||
values[index] = json_choice
|
||||
elif isinstance(field, Integer):
|
||||
type = "Integer"
|
||||
elif isinstance(field, Float):
|
||||
type = "Float"
|
||||
metadata_fields[field.name] = {'field_name': field.name,
|
||||
'type': type,
|
||||
'display_name': field.display_name,
|
||||
'value': field.to_json(value),
|
||||
'options': values,
|
||||
'default_value': field.to_json(default_value),
|
||||
'inheritable': inheritable,
|
||||
'explicitly_set': explicitly_set,
|
||||
'help': field.help}
|
||||
|
||||
return metadata
|
||||
return metadata_fields
|
||||
|
||||
|
||||
class DescriptorSystem(object):
|
||||
@@ -740,7 +767,7 @@ class ModuleSystem(object):
|
||||
s3_interface=None,
|
||||
cache=None,
|
||||
can_execute_unsafe_code=None,
|
||||
):
|
||||
):
|
||||
'''
|
||||
Create a closure around the system environment.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user