Heading 1
+${_("Heading 1")}
Multiple Choice
+${_("Multiple Choice")}
diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index afb38c3f9e..96b840ae96 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -5,8 +5,6 @@ from lettuce import world, step from nose.tools import assert_true from nose.tools import assert_equal -from xmodule.modulestore.django import _MODULESTORES, modulestore -from xmodule.templates import update_templates from auth.authz import get_user_by_email from selenium.webdriver.common.keys import Keys @@ -50,31 +48,31 @@ def i_press_the_category_delete_icon(step, category): @step('I have opened a new course in Studio$') def i_have_opened_a_new_course(step): + open_new_course() + + +####### HELPER FUNCTIONS ############## +def open_new_course(): world.clear_courses() log_into_studio() create_a_course() -####### HELPER FUNCTIONS ############## def create_studio_user( uname='robot', email='robot+studio@edx.org', password='test', is_staff=False): - studio_user = world.UserFactory.build( + studio_user = world.UserFactory( username=uname, email=email, password=password, is_staff=is_staff) - studio_user.set_password(password) - studio_user.save() registration = world.RegistrationFactory(user=studio_user) registration.register(studio_user) registration.activate() - user_profile = world.UserProfileFactory(user=studio_user) - def fill_in_course_info( name='Robot Super Course', @@ -153,4 +151,13 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time): world.css_fill(time_css, desired_time) e = world.css_find(time_css).first e._element.send_keys(Keys.TAB) - time.sleep(float(1)) + time.sleep(float(1)) + + +@step('I have created a Video component$') +def i_created_a_video_component(step): + world.create_component_instance( + step, '.large-video-icon', + 'i4x://edx/templates/video/default', + '.xmodule_VideoModule' + ) diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py new file mode 100644 index 0000000000..ee684e53dc --- /dev/null +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -0,0 +1,86 @@ +# disable missing docstring +#pylint: disable=C0111 + +from lettuce import world +from nose.tools import assert_equal +from terrain.steps import reload_the_page + + +@world.absorb +def create_component_instance(step, component_button_css, instance_id, expected_css): + click_new_component_button(step, component_button_css) + click_component_from_menu(instance_id, expected_css) + + +@world.absorb +def click_new_component_button(step, component_button_css): + step.given('I have opened a new course section in Studio') + step.given('I have added a new subsection') + step.given('I expand the first section') + world.css_click('a.new-unit-item') + world.css_click(component_button_css) + + +@world.absorb +def click_component_from_menu(instance_id, expected_css): + elem_css = "a[data-location='%s']" % instance_id + 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 diff --git a/cms/djangoapps/contentstore/features/discussion-editor.feature b/cms/djangoapps/contentstore/features/discussion-editor.feature new file mode 100644 index 0000000000..24683c3297 --- /dev/null +++ b/cms/djangoapps/contentstore/features/discussion-editor.feature @@ -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 diff --git a/cms/djangoapps/contentstore/features/discussion-editor.py b/cms/djangoapps/contentstore/features/discussion-editor.py new file mode 100644 index 0000000000..aced4c2c88 --- /dev/null +++ b/cms/djangoapps/contentstore/features/discussion-editor.py @@ -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] + ]) diff --git a/cms/djangoapps/contentstore/features/html-editor.feature b/cms/djangoapps/contentstore/features/html-editor.feature new file mode 100644 index 0000000000..6cd455d681 --- /dev/null +++ b/cms/djangoapps/contentstore/features/html-editor.feature @@ -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 diff --git a/cms/djangoapps/contentstore/features/html-editor.py b/cms/djangoapps/contentstore/features/html-editor.py new file mode 100644 index 0000000000..054c0ea642 --- /dev/null +++ b/cms/djangoapps/contentstore/features/html-editor.py @@ -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]]) diff --git a/cms/djangoapps/contentstore/features/problem-editor.feature b/cms/djangoapps/contentstore/features/problem-editor.feature new file mode 100644 index 0000000000..6ed8c1619b --- /dev/null +++ b/cms/djangoapps/contentstore/features/problem-editor.feature @@ -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 diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py new file mode 100644 index 0000000000..5dfcf55046 --- /dev/null +++ b/cms/djangoapps/contentstore/features/problem-editor.py @@ -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) diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index edc8b17168..1134e53280 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -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() diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature new file mode 100644 index 0000000000..4c2a460042 --- /dev/null +++ b/cms/djangoapps/contentstore/features/video-editor.feature @@ -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 diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video-editor.py new file mode 100644 index 0000000000..27423575c3 --- /dev/null +++ b/cms/djangoapps/contentstore/features/video-editor.py @@ -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]]) diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 30005d4524..a06e55fe8f 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -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 = [ diff --git a/cms/envs/common.py b/cms/envs/common.py index 90e15186d7..e60d337731 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -223,7 +223,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 }, diff --git a/cms/static/coffee/fixtures/metadata-editor.underscore b/cms/static/coffee/fixtures/metadata-editor.underscore new file mode 120000 index 0000000000..9696774d0a --- /dev/null +++ b/cms/static/coffee/fixtures/metadata-editor.underscore @@ -0,0 +1 @@ +../../../templates/js/metadata-editor.underscore \ No newline at end of file diff --git a/cms/static/coffee/fixtures/metadata-number-entry.underscore b/cms/static/coffee/fixtures/metadata-number-entry.underscore new file mode 120000 index 0000000000..99138aa9c1 --- /dev/null +++ b/cms/static/coffee/fixtures/metadata-number-entry.underscore @@ -0,0 +1 @@ +../../../templates/js/metadata-number-entry.underscore \ No newline at end of file diff --git a/cms/static/coffee/fixtures/metadata-option-entry.underscore b/cms/static/coffee/fixtures/metadata-option-entry.underscore new file mode 120000 index 0000000000..c6cd499801 --- /dev/null +++ b/cms/static/coffee/fixtures/metadata-option-entry.underscore @@ -0,0 +1 @@ +../../../templates/js/metadata-option-entry.underscore \ No newline at end of file diff --git a/cms/static/coffee/fixtures/metadata-string-entry.underscore b/cms/static/coffee/fixtures/metadata-string-entry.underscore new file mode 120000 index 0000000000..f713ab5387 --- /dev/null +++ b/cms/static/coffee/fixtures/metadata-string-entry.underscore @@ -0,0 +1 @@ +../../../templates/js/metadata-string-entry.underscore \ No newline at end of file diff --git a/cms/static/coffee/spec/models/metadata_spec.coffee b/cms/static/coffee/spec/models/metadata_spec.coffee new file mode 100644 index 0000000000..5ff65e1bfa --- /dev/null +++ b/cms/static/coffee/spec/models/metadata_spec.coffee @@ -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) + diff --git a/cms/static/coffee/spec/views/metadata_edit_spec.coffee b/cms/static/coffee/spec/views/metadata_edit_spec.coffee new file mode 100644 index 0000000000..0c2069cf00 --- /dev/null +++ b/cms/static/coffee/spec/views/metadata_edit_spec.coffee @@ -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($(" + + + +
- + ${preview} diff --git a/cms/templates/js/metadata-editor.underscore b/cms/templates/js/metadata-editor.underscore new file mode 100644 index 0000000000..03fdd28996 --- /dev/null +++ b/cms/templates/js/metadata-editor.underscore @@ -0,0 +1,6 @@ +
diff --git a/common/lib/xmodule/xmodule/templates/problem/empty.yaml b/common/lib/xmodule/xmodule/templates/problem/empty.yaml
index 39c9e7671c..97a2aef423 100644
--- a/common/lib/xmodule/xmodule/templates/problem/empty.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/empty.yaml
@@ -2,11 +2,8 @@
metadata:
display_name: Blank Common Problem
rerandomize: never
- showanswer: always
+ showanswer: finished
markdown: ""
- weight: ""
- empty: True
- attempts: ""
data: |
diff --git a/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml b/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml
index 3ef619d54b..ab1f22e3b2 100644
--- a/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml
@@ -2,9 +2,7 @@
metadata:
display_name: Image Mapped Input
rerandomize: never
- showanswer: always
- weight: ""
- attempts: ""
+ showanswer: finished
data: |
diff --git a/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml b/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml
index 3a35a35199..10d51de280 100644
--- a/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml
@@ -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
diff --git a/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml b/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml
index 1dc46f5f51..548fd94fab 100644
--- a/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml
@@ -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
diff --git a/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml b/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml
index f523c7fdc5..c2edfb1cbc 100644
--- a/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml
@@ -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
diff --git a/common/lib/xmodule/xmodule/templates/problem/string_response.yaml b/common/lib/xmodule/xmodule/templates/problem/string_response.yaml
index c018d3f6cf..64e3dc062f 100644
--- a/common/lib/xmodule/xmodule/templates/problem/string_response.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/string_response.yaml
@@ -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
diff --git a/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml b/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml
index 7f1c838ca9..53e9eeaae4 100644
--- a/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml
+++ b/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml
@@ -1,9 +1,5 @@
---
metadata:
display_name: Word cloud
- version: 1
- num_inputs: 5
- num_top_words: 250
- display_student_percents: True
data: {}
children: []
diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py
index 5fe57892be..a75dfc8d20 100644
--- a/common/lib/xmodule/xmodule/tests/test_import.py
+++ b/common/lib/xmodule/xmodule/tests/test_import.py
@@ -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):
"""
diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py
index e41bcdd73a..dd59ca2b48 100644
--- a/common/lib/xmodule/xmodule/tests/test_xml_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py
@@ -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="