diff --git a/.ruby-gemset b/.ruby-gemset index 93a8706d3e..77266c35f0 100644 --- a/.ruby-gemset +++ b/.ruby-gemset @@ -1 +1 @@ -mitx +edx-platform diff --git a/AUTHORS b/AUTHORS index 67279f9053..091e054a45 100644 --- a/AUTHORS +++ b/AUTHORS @@ -73,3 +73,4 @@ David Baumgold Jason Bau Frances Botsford Jonah Stanley +Slater Victoroff diff --git a/README.md b/README.md index ed52c21fb2..ba3c99e6e2 100644 --- a/README.md +++ b/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. 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..4c674dc34c --- /dev/null +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -0,0 +1,85 @@ +# 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/features/video.feature b/cms/djangoapps/contentstore/features/video.feature new file mode 100644 index 0000000000..a4cf84d978 --- /dev/null +++ b/cms/djangoapps/contentstore/features/video.feature @@ -0,0 +1,6 @@ +Feature: Video Component + As a course author, I want to be able to view my created videos in Studio. + + Scenario: Autoplay is disabled in Studio + Given I have created a Video component + Then when I view the video it does not have autoplay enabled diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py new file mode 100644 index 0000000000..f25b8d6d7e --- /dev/null +++ b/cms/djangoapps/contentstore/features/video.py @@ -0,0 +1,11 @@ +#pylint: disable=C0111 + +from lettuce import world, step + +############### ACTIONS #################### + + +@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') diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index a36ed76d11..0b4535bb70 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -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]) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 34e5da4b4d..f769652493 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -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 login.' + self.assertIn(expected, resp.content) + def test_private_pages_auth(self): """Make sure pages that do require login work.""" auth_pages = ( diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 30005d4524..8120e08107 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -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 = [ diff --git a/cms/envs/common.py b/cms/envs/common.py index 90e15186d7..ed90572715 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -40,7 +40,10 @@ MITX_FEATURES = { 'SEGMENT_IO': True, # Enable URL that shows information about the status of various services - 'ENABLE_SERVICE_STATUS': False + 'ENABLE_SERVICE_STATUS': False, + + # Don't autoplay videos for course authors + 'AUTOPLAY_VIDEOS': False } ENABLE_JASMINE = False @@ -223,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 }, 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($(" + + + +
-
- ${editor} -
-
- Save - Cancel -
-
+
+ + +
+ +
+
+ ${editor} +
+
+ +
- + ${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 @@ +
    + <% _.each(_.range(numEntries), function() { %> + + <% }) %> +
diff --git a/cms/templates/js/metadata-number-entry.underscore b/cms/templates/js/metadata-number-entry.underscore new file mode 100644 index 0000000000..333233ef4e --- /dev/null +++ b/cms/templates/js/metadata-number-entry.underscore @@ -0,0 +1,8 @@ +
+ + + +
+<%= model.get('help') %> diff --git a/cms/templates/js/metadata-option-entry.underscore b/cms/templates/js/metadata-option-entry.underscore new file mode 100644 index 0000000000..4cb107e882 --- /dev/null +++ b/cms/templates/js/metadata-option-entry.underscore @@ -0,0 +1,16 @@ +
+ + + +
+<%= model.get('help') %> diff --git a/cms/templates/js/metadata-string-entry.underscore b/cms/templates/js/metadata-string-entry.underscore new file mode 100644 index 0000000000..759e3ad826 --- /dev/null +++ b/cms/templates/js/metadata-string-entry.underscore @@ -0,0 +1,8 @@ +
+ + + +
+<%= model.get('help') %> diff --git a/cms/templates/registration/activation_complete.html b/cms/templates/registration/activation_complete.html index 8cc3dc8c56..a4d028ef5b 100644 --- a/cms/templates/registration/activation_complete.html +++ b/cms/templates/registration/activation_complete.html @@ -3,6 +3,12 @@ <%namespace name='static' file='../static_content.html'/> +%if not user_logged_in: +<%block name="bodyclass"> + not-signedin + +%endif + <%block name="content">
@@ -18,7 +24,7 @@ %if user_logged_in: Visit your dashboard to see your courses. %else: - You can now login. + You can now login. %endif

diff --git a/cms/templates/unit.html b/cms/templates/unit.html index cb34f42a09..851e3da260 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -78,22 +78,13 @@ % endif
    - % 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: -
  • - - ${name} - -
  • - - % else: -
  • - - ${name} - -
  • - % endif +
  • + + ${name} + +
  • % endif %endfor @@ -102,23 +93,13 @@ % if type == "problem":
      - % for name, location, has_markdown, is_empty in templates: + % for name, location, has_markdown in templates: % if not has_markdown: - % if is_empty: -
    • - - ${name} - -
    • - - % else: -
    • - - ${name} - - -
    • - % endif +
    • + + ${name} + +
    • % endif % endfor
    diff --git a/cms/templates/widgets/html-edit.html b/cms/templates/widgets/html-edit.html index 7a9d563a57..879ae43e07 100644 --- a/cms/templates/widgets/html-edit.html +++ b/cms/templates/widgets/html-edit.html @@ -1,12 +1,16 @@ +<%! from django.utils.translation import ugettext as _ %> + +
    +
    + + +
    + + +
    +
    +
    <%include file="metadata-edit.html" /> -
    - - -
    - - -
    -
    diff --git a/cms/templates/widgets/metadata-edit.html b/cms/templates/widgets/metadata-edit.html index aada438f38..db50a1d877 100644 --- a/cms/templates/widgets/metadata-edit.html +++ b/cms/templates/widgets/metadata-edit.html @@ -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() %> - +
    -
    Numerical Input
    +
    ${_("Numerical Input")}
    @@ -85,7 +87,7 @@
    -
    Dropdown
    +
    ${_("Dropdown")}
    @@ -94,7 +96,7 @@
-
Explanation
+
${_("Explanation")}
@@ -105,3 +107,5 @@
+ +<%include file="metadata-edit.html" /> diff --git a/cms/templates/widgets/raw-edit.html b/cms/templates/widgets/raw-edit.html index 9488552be5..2b1b0f9ef3 100644 --- a/cms/templates/widgets/raw-edit.html +++ b/cms/templates/widgets/raw-edit.html @@ -1,4 +1,6 @@ +
+
+ +
+
<%include file="metadata-edit.html" /> -
- -
diff --git a/cms/templates/widgets/sequence-edit.html b/cms/templates/widgets/sequence-edit.html index c70f2568fa..7956fbfbaf 100644 --- a/cms/templates/widgets/sequence-edit.html +++ b/cms/templates/widgets/sequence-edit.html @@ -29,7 +29,6 @@ - <%include file="metadata-edit.html" />
    @@ -50,5 +49,6 @@
+ <%include file="metadata-edit.html" /> diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py index 1b509a14f4..4857fe68ca 100644 --- a/cms/xmodule_namespace.py +++ b/cms/xmodule_namespace.py @@ -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) + diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index 768c51b25e..decce42368 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -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 diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 221a80c66c..ecd43eb719 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -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 @@ -69,7 +69,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() @@ -85,6 +85,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): assert is_css_present(css_selector) @@ -101,7 +109,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 "" diff --git a/common/djangoapps/util/views.py b/common/djangoapps/util/views.py index 991d6e2e75..d0aa0dc680 100644 --- a/common/djangoapps/util/views.py +++ b/common/djangoapps/util/views.py @@ -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(""" -

request:

-
{0}
- -

request.GET

: - -
{1}
- -

request.POST

: -
{2}
- -

request.REQUEST

: -
{3}
- - - - -""".format( - pprint.pformat(request), - pprint.pformat(dict(request.GET)), - pprint.pformat(dict(request.POST)), - pprint.pformat(dict(request.REQUEST)), - )) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 2f33b66bfd..2ee82e2fb4 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -182,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 diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py index 58d0860af6..cfa1b7525d 100644 --- a/common/lib/calc/tests/test_calc.py +++ b/common/lib/calc/tests/test_calc.py @@ -5,6 +5,7 @@ Unit tests for calc.py import unittest import numpy import calc +from pyparsing import ParseException class EvaluatorTest(unittest.TestCase): @@ -20,6 +21,11 @@ class EvaluatorTest(unittest.TestCase): 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) @@ -30,7 +36,22 @@ class EvaluatorTest(unittest.TestCase): self.assertEqual(easy_eval("-13"), -13) self.assertEqual(easy_eval("-3.14"), -3.14) self.assertEqual(easy_eval("-.618033989"), -0.618033989) - # See also test_exponential_answer and test_si_suffix + + 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): """ diff --git a/common/lib/capa/setup.py b/common/lib/capa/setup.py index 2e73701060..dcb631e376 100644 --- a/common/lib/capa/setup.py +++ b/common/lib/capa/setup.py @@ -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"], ) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index eb6bdc18c9..9ac540138e 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -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): diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 0e32770afc..b3f0e19109 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -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 diff --git a/common/lib/xmodule/xmodule/css/editor/edit.scss b/common/lib/xmodule/xmodule/css/editor/edit.scss index ac53bb5a70..d30f69bcd2 100644 --- a/common/lib/xmodule/xmodule/css/editor/edit.scss +++ b/common/lib/xmodule/xmodule/css/editor/edit.scss @@ -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; diff --git a/common/lib/xmodule/xmodule/css/problem/edit.scss b/common/lib/xmodule/xmodule/css/problem/edit.scss index be5455e901..249b767e5e 100644 --- a/common/lib/xmodule/xmodule/css/problem/edit.scss +++ b/common/lib/xmodule/xmodule/css/problem/edit.scss @@ -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; diff --git a/common/lib/xmodule/xmodule/css/sequence/display.scss b/common/lib/xmodule/xmodule/css/sequence/display.scss index e006e02773..ed09d5cf0f 100644 --- a/common/lib/xmodule/xmodule/css/sequence/display.scss +++ b/common/lib/xmodule/xmodule/css/sequence/display.scss @@ -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 { diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss index bf575e74a3..f3f76dc0d6 100644 --- a/common/lib/xmodule/xmodule/css/video/display.scss +++ b/common/lib/xmodule/xmodule/css/video/display.scss @@ -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 { diff --git a/common/lib/xmodule/xmodule/css/videoalpha/display.scss b/common/lib/xmodule/xmodule/css/videoalpha/display.scss index bf575e74a3..f3f76dc0d6 100644 --- a/common/lib/xmodule/xmodule/css/videoalpha/display.scss +++ b/common/lib/xmodule/xmodule/css/videoalpha/display.scss @@ -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 { diff --git a/common/lib/xmodule/xmodule/discussion_module.py b/common/lib/xmodule/xmodule/discussion_module.py index 98082ddea2..aef4821839 100644 --- a/common/lib/xmodule/xmodule/discussion_module.py +++ b/common/lib/xmodule/xmodule/discussion_module.py @@ -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) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index d4c2ff00ae..5939fbcdd8 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -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("

Response submitted for scoring.

") 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() diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee index 22308a5568..561ca07c8a 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee @@ -66,7 +66,7 @@ class @VideoPlayer extends Subview at: 'top center' onReady: (event) => - unless onTouchBasedDevice() + unless onTouchBasedDevice() or $('.video:first').data('autoplay') == 'False' $('.video-load-complete:first').data('video').player.play() onStateChange: (event) => diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/controller_query_service.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/controller_query_service.py index b807e05160..3668cd6cc9 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/controller_query_service.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/controller_query_service.py @@ -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: diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index b0d666621f..ccc3e31f51 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -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 + diff --git a/common/lib/xmodule/xmodule/templates/combinedopenended/default.yaml b/common/lib/xmodule/xmodule/templates/combinedopenended/default.yaml index d74517a3c3..f7d639ebfb 100644 --- a/common/lib/xmodule/xmodule/templates/combinedopenended/default.yaml +++ b/common/lib/xmodule/xmodule/templates/combinedopenended/default.yaml @@ -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: | diff --git a/common/lib/xmodule/xmodule/templates/html/empty.yaml b/common/lib/xmodule/xmodule/templates/html/empty.yaml index b6d867d7d6..40b005af28 100644 --- a/common/lib/xmodule/xmodule/templates/html/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/html/empty.yaml @@ -1,7 +1,6 @@ --- metadata: display_name: Blank HTML Page - empty: True data: | diff --git a/common/lib/xmodule/xmodule/templates/peer_grading/default.yaml b/common/lib/xmodule/xmodule/templates/peer_grading/default.yaml index 23d41d616f..5d88a18ad8 100644 --- a/common/lib/xmodule/xmodule/templates/peer_grading/default.yaml +++ b/common/lib/xmodule/xmodule/templates/peer_grading/default.yaml @@ -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: | diff --git a/common/lib/xmodule/xmodule/templates/problem/circuitschematic.yaml b/common/lib/xmodule/xmodule/templates/problem/circuitschematic.yaml index a94b824cfb..56f802a6a3 100644 --- a/common/lib/xmodule/xmodule/templates/problem/circuitschematic.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/circuitschematic.yaml @@ -3,9 +3,7 @@ metadata: display_name: Circuit Schematic Builder rerandomize: never - showanswer: always - weight: "" - attempts: "" + showanswer: finished data: | Please make a voltage divider that splits the provided voltage evenly. diff --git a/common/lib/xmodule/xmodule/templates/problem/customgrader.yaml b/common/lib/xmodule/xmodule/templates/problem/customgrader.yaml index aadbe4075a..b5b0d71f4d 100644 --- a/common/lib/xmodule/xmodule/templates/problem/customgrader.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/customgrader.yaml @@ -2,9 +2,7 @@ metadata: display_name: Custom Python-Evaluated Input rerandomize: never - showanswer: always - weight: "" - attempts: "" + showanswer: finished data: |

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/emptyadvanced.yaml b/common/lib/xmodule/xmodule/templates/problem/emptyadvanced.yaml index bba7b3a8ac..3d696ec2fd 100644 --- a/common/lib/xmodule/xmodule/templates/problem/emptyadvanced.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/emptyadvanced.yaml @@ -2,10 +2,7 @@ metadata: display_name: Blank Advanced Problem rerandomize: never - showanswer: always - weight: "" - attempts: "" - empty: True + showanswer: finished data: | diff --git a/common/lib/xmodule/xmodule/templates/problem/forumularesponse.yaml b/common/lib/xmodule/xmodule/templates/problem/forumularesponse.yaml index b4c53a107b..0401a01c31 100644 --- a/common/lib/xmodule/xmodule/templates/problem/forumularesponse.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/forumularesponse.yaml @@ -2,9 +2,7 @@ metadata: display_name: Math Expression Input rerandomize: never - showanswer: always - weight: "" - attempts: "" + showanswer: finished 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="

Test Template HTML
") 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']) diff --git a/common/lib/xmodule/xmodule/word_cloud_module.py b/common/lib/xmodule/xmodule/word_cloud_module.py index 440da8b887..e38b8cf195 100644 --- a/common/lib/xmodule/xmodule/word_cloud_module.py +++ b/common/lib/xmodule/xmodule/word_cloud_module.py @@ -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) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 76ac6a1ff6..3ae70543cb 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -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. diff --git a/common/static/css/vendor/html5-input-polyfills/number-polyfill.css b/common/static/css/vendor/html5-input-polyfills/number-polyfill.css new file mode 100644 index 0000000000..f3d8805739 --- /dev/null +++ b/common/static/css/vendor/html5-input-polyfills/number-polyfill.css @@ -0,0 +1,87 @@ +/* HTML5 Number polyfill | Jonathan Stipe | https://github.com/jonstipe/number-polyfill*/ +div.number-spin-btn-container { + display: inline-block; + position: absolute; + vertical-align: middle; + margin: 0 0 0 3px; + padding: 0; + left: 74%; + top: 6px; +} + +div.number-spin-btn { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + border-width: 2px; + border-color: #ededed #777777 #777777 #ededed; + border-style: solid; + background-color: #eeeeee; + width: 1em; + font-size: 14px; } + div.number-spin-btn:hover { + /* added blue hover color */ + background-color: rgb(85, 151, 221); + cursor: pointer; } + div.number-spin-btn:active { + border-width: 2px; + border-color: #5e5e5e #d8d8d8 #d8d8d8 #5e5e5e; + border-style: solid; + background-color: #999999; } + +div.number-spin-btn-up { + border-bottom-width: 1px; + -moz-border-radius: 0px; + -webkit-border-radius: 0px; + border-radius: 0px; + font-size: 14px; } + div.number-spin-btn-up:before { + border-width: 0 0.3em 0.3em 0.3em; + border-color: transparent transparent black transparent; + top: 25%; } + div.number-spin-btn-up:active { + border-bottom-width: 1px; } + div.number-spin-btn-up:active:before { + border-bottom-color: white; + top: 26%; + left: 51%; } + +div.number-spin-btn-down { + border-top-width: 1px; + -moz-border-radius: 0px 0px 3px 3px; + -webkit-border-radius: 0px 0px 3px 3px; + border-radius: 0px 0px 3px 3px; } + div.number-spin-btn-down:before { + border-width: 0.3em 0.3em 0 0.3em; + border-color: black transparent transparent transparent; + top: 75%; } + div.number-spin-btn-down:active { + border-top-width: 1px; } + div.number-spin-btn-down:active:before { + border-top-color: white; + top: 76%; + left: 51%; } + +div.number-spin-btn-up:before, +div.number-spin-btn-down:before { + content: ""; + width: 0; + height: 0; + border-style: solid; + position: absolute; + left: 50%; + margin: -0.15em 0 0 -0.3em; + padding: 0; } + +input:disabled + div.number-spin-btn-container > div.number-spin-btn-up:active, input:disabled + div.number-spin-btn-container > div.number-spin-btn-down:active { + border-color: #ededed #777777 #777777 #ededed; + border-style: solid; + background-color: #cccccc; } +input:disabled + div.number-spin-btn-container > div.number-spin-btn-up:before, input:disabled + div.number-spin-btn-container > div.number-spin-btn-up:active:before { + border-bottom-color: #999999; + top: 25%; + left: 50%; } +input:disabled + div.number-spin-btn-container > div.number-spin-btn-down:before, input:disabled + div.number-spin-btn-container > div.number-spin-btn-down:active:before { + border-top-color: #999999; + top: 75%; + left: 50%; } diff --git a/common/static/js/vendor/html5-input-polyfills/number-polyfill.js b/common/static/js/vendor/html5-input-polyfills/number-polyfill.js new file mode 100644 index 0000000000..9e95972c00 --- /dev/null +++ b/common/static/js/vendor/html5-input-polyfills/number-polyfill.js @@ -0,0 +1,298 @@ +// Generated by CoffeeScript 1.4.0 + +/* +HTML5 Number polyfill | Jonathan Stipe | https://github.com/jonstipe/number-polyfill +*/ + +(function() { + + (function($) { + var i; + i = document.createElement("input"); + i.setAttribute("type", "number"); + if (i.type === "text") { + $.fn.inputNumber = function() { + var clipValues, decrement, domMouseScrollHandler, extractNumDecimalDigits, getParams, increment, matchStep, mouseWheelHandler; + getParams = function(elem) { + var $elem, max, min, step, val; + $elem = $(elem); + step = $elem.attr('step'); + min = $elem.attr('min'); + max = $elem.attr('max'); + val = parseFloat($elem.val()); + step = /^-?\d+(?:\.\d+)?$/.test(step) ? parseFloat(step) : null; + min = /^-?\d+(?:\.\d+)?$/.test(min) ? parseFloat(min) : null; + max = /^-?\d+(?:\.\d+)?$/.test(max) ? parseFloat(max) : null; + if (isNaN(val)) { + val = min || 0; + } + return { + min: min, + max: max, + step: step, + val: val + }; + }; + clipValues = function(value, min, max) { + if ((max != null) && value > max) { + return max; + } else if ((min != null) && value < min) { + return min; + } else { + return value; + } + }; + extractNumDecimalDigits = function(input) { + var num, raisedNum; + if (input != null) { + num = 0; + raisedNum = input; + while (raisedNum !== Math.round(raisedNum)) { + num += 1; + raisedNum = input * Math.pow(10, num); + } + return num; + } else { + return 0; + } + }; + matchStep = function(value, min, max, step) { + var mod, raiseTo, raisedMod, raisedStep, raisedStepDown, raisedStepUp, raisedValue, stepDecimalDigits, stepDown, stepUp; + stepDecimalDigits = extractNumDecimalDigits(step); + if (step == null) { + return value; + } else if (stepDecimalDigits === 0) { + mod = (value - (min || 0)) % step; + if (mod === 0) { + return value; + } else { + stepDown = value - mod; + stepUp = stepDown + step; + if ((stepUp > max) || ((value - stepDown) < (stepUp - value))) { + return stepDown; + } else { + return stepUp; + } + } + } else { + raiseTo = Math.pow(10, stepDecimalDigits); + raisedStep = step * raiseTo; + raisedMod = (value - (min || 0)) * raiseTo % raisedStep; + if (raisedMod === 0) { + return value; + } else { + raisedValue = value * raiseTo; + raisedStepDown = raisedValue - raisedMod; + raisedStepUp = raisedStepDown + raisedStep; + if (((raisedStepUp / raiseTo) > max) || ((raisedValue - raisedStepDown) < (raisedStepUp - raisedValue))) { + return raisedStepDown / raiseTo; + } else { + return raisedStepUp / raiseTo; + } + } + } + }; + increment = function(elem) { + var newVal, params, raiseTo; + if (!$(elem).is(":disabled")) { + params = getParams(elem); + raiseTo = Math.pow(10, Math.max(extractNumDecimalDigits(params['val']), extractNumDecimalDigits(params['step']))); + newVal = (Math.round(params['val'] * raiseTo) + Math.round((params['step'] || 1) * raiseTo)) / raiseTo; + if ((params['max'] != null) && newVal > params['max']) { + newVal = params['max']; + } + newVal = matchStep(newVal, params['min'], params['max'], params['step']); + $(elem).val(newVal).change(); + } + return null; + }; + decrement = function(elem) { + var newVal, params, raiseTo; + if (!$(elem).is(":disabled")) { + params = getParams(elem); + raiseTo = Math.pow(10, Math.max(extractNumDecimalDigits(params['val']), extractNumDecimalDigits(params['step']))); + newVal = (Math.round(params['val'] * raiseTo) - Math.round((params['step'] || 1) * raiseTo)) / raiseTo; + if ((params['min'] != null) && newVal < params['min']) { + newVal = params['min']; + } + newVal = matchStep(newVal, params['min'], params['max'], params['step']); + $(elem).val(newVal).change(); + } + return null; + }; + domMouseScrollHandler = function(e) { + e.preventDefault(); + if (e.originalEvent.detail < 0) { + increment(this); + } else { + decrement(this); + } + return null; + }; + mouseWheelHandler = function(e) { + e.preventDefault(); + if (e.originalEvent.wheelDelta > 0) { + increment(this); + } else { + decrement(this); + } + return null; + }; + $(this).filter('input[type="number"]').each(function() { + var $downBtn, $elem, $upBtn, attrMutationCallback, attrObserver, btnContainer, downBtn, elem, halfHeight, upBtn; + elem = this; + $elem = $(elem); + halfHeight = ($elem.outerHeight() / 6) + 'px'; + upBtn = document.createElement('div'); + downBtn = document.createElement('div'); + $upBtn = $(upBtn); + $downBtn = $(downBtn); + btnContainer = document.createElement('div'); + $upBtn.addClass('number-spin-btn number-spin-btn-up').css('height', halfHeight); + $downBtn.addClass('number-spin-btn number-spin-btn-down').css('height', halfHeight); + btnContainer.appendChild(upBtn); + btnContainer.appendChild(downBtn); + $(btnContainer).addClass('number-spin-btn-container').insertAfter(elem); + $elem.on({ + focus: function(e) { + $elem.on({ + DOMMouseScroll: domMouseScrollHandler, + mousewheel: mouseWheelHandler + }); + return null; + }, + blur: function(e) { + $elem.off({ + DOMMouseScroll: domMouseScrollHandler, + mousewheel: mouseWheelHandler + }); + return null; + }, + keypress: function(e) { + var _ref, _ref1; + if (e.keyCode === 38) { + increment(this); + } else if (e.keyCode === 40) { + decrement(this); + } else 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(); + } + return null; + }, + change: function(e) { + var newVal, params; + if (e.originalEvent != null) { + params = getParams(this); + newVal = clipValues(params['val'], params['min'], params['max']); + newVal = matchStep(newVal, params['min'], params['max'], params['step'], params['stepDecimal']); + $(this).val(newVal); + } + return null; + } + }); + $upBtn.on("mousedown", function(e) { + var releaseFunc, timeoutFunc; + increment(elem); + timeoutFunc = function(elem, incFunc) { + incFunc(elem); + $elem.data("timeoutID", window.setTimeout(timeoutFunc, 10, elem, incFunc)); + return null; + }; + releaseFunc = function(e) { + window.clearTimeout($elem.data("timeoutID")); + $(document).off('mouseup', releaseFunc); + $upBtn.off('mouseleave', releaseFunc); + return null; + }; + $(document).on('mouseup', releaseFunc); + $upBtn.on('mouseleave', releaseFunc); + $elem.data("timeoutID", window.setTimeout(timeoutFunc, 700, elem, increment)); + return null; + }); + $downBtn.on("mousedown", function(e) { + var releaseFunc, timeoutFunc; + decrement(elem); + timeoutFunc = function(elem, decFunc) { + decFunc(elem); + $elem.data("timeoutID", window.setTimeout(timeoutFunc, 10, elem, decFunc)); + return null; + }; + releaseFunc = function(e) { + window.clearTimeout($elem.data("timeoutID")); + $(document).off('mouseup', releaseFunc); + $downBtn.off('mouseleave', releaseFunc); + return null; + }; + $(document).on('mouseup', releaseFunc); + $downBtn.on('mouseleave', releaseFunc); + $elem.data("timeoutID", window.setTimeout(timeoutFunc, 700, elem, decrement)); + return null; + }); + $elem.css("textAlign", 'left'); + if ($elem.css("opacity") !== "1") { + $(btnContainer).css("opacity", $elem.css("opacity")); + } + if ($elem.css("visibility") !== "visible") { + $(btnContainer).css("visibility", $elem.css("visibility")); + } + if (elem.style.display !== "") { + $(btnContainer).css("display", $elem.css("display")); + } + if ((typeof WebKitMutationObserver !== "undefined" && WebKitMutationObserver !== null) || (typeof MutationObserver !== "undefined" && MutationObserver !== null)) { + attrMutationCallback = function(mutations, observer) { + var mutation, _i, _len; + for (_i = 0, _len = mutations.length; _i < _len; _i++) { + mutation = mutations[_i]; + if (mutation.type === "attributes") { + if (mutation.attributeName === "class") { + $(btnContainer).removeClass(mutation.oldValue).addClass(elem.className); + } else if (mutation.attributeName === "style") { + $(btnContainer).css({ + "opacity": elem.style.opacity, + "visibility": elem.style.visibility, + "display": elem.style.display + }); + } + } + } + return null; + }; + attrObserver = (typeof WebKitMutationObserver !== "undefined" && WebKitMutationObserver !== null) ? new WebKitMutationObserver(attrMutationCallback) : ((typeof MutationObserver !== "undefined" && MutationObserver !== null) ? new MutationObserver(attrMutationCallback) : null); + attrObserver.observe(elem, { + attributes: true, + attributeOldValue: true, + attributeFilter: ["class", "style"] + }); + } else if (typeof MutationEvent !== "undefined" && MutationEvent !== null) { + $elem.on("DOMAttrModified", function(evt) { + if (evt.originalEvent.attrName === "class") { + $(btnContainer).removeClass(evt.originalEvent.prevValue).addClass(evt.originalEvent.newValue); + } else if (evt.originalEvent.attrName === "style") { + $(btnContainer).css({ + "display": elem.style.display, + "visibility": elem.style.visibility, + "opacity": elem.style.opacity + }); + } + return null; + }); + } + return null; + }); + return $(this); + }; + $(function() { + $('input[type="number"]').inputNumber(); + return null; + }); + null; + } else { + $.fn.inputNumber = function() { + return $(this); + }; + null; + } + return null; + })(jQuery); + +}).call(this); diff --git a/common/static/js/vendor/tiny_mce/themes/advanced/skins/studio/ui.css b/common/static/js/vendor/tiny_mce/themes/advanced/skins/studio/ui.css index b2d3e494aa..fc97eb6450 100644 --- a/common/static/js/vendor/tiny_mce/themes/advanced/skins/studio/ui.css +++ b/common/static/js/vendor/tiny_mce/themes/advanced/skins/studio/ui.css @@ -25,8 +25,8 @@ /* Layout */ .studioSkin table.mceLayout {border:0;} -.studioSkin table.mceLayout tr.mceFirst td {border-top:1px solid #3c3c3c;} -.studioSkin table.mceLayout tr.mceLast td {border-bottom:1px solid #3c3c3c;} +.studioSkin table.mceLayout tr.mceFirst td {border-top: 1px solid #D1DCE6; border-left: none; border-right:none;} +.studioSkin table.mceLayout tr.mceLast td {border-bottom:none;} .studioSkin table.mceToolbar, .studioSkin tr.mceFirst .mceToolbar tr td, .studioSkin tr.mceLast .mceToolbar tr td {border:0; margin:0; padding:0;} .studioSkin td.mceToolbar { background: -webkit-linear-gradient(top, #d4dee8, #c9d5e2); @@ -36,11 +36,11 @@ background: linear-gradient(top, #d4dee8, #c9d5e2); border: 1px solid #3c3c3c; border-bottom-color: #a5aaaf; - border-radius: 3px 3px 0 0; + border-radius: 0; padding: 10px 10px 9px; vertical-align: top; } -.studioSkin .mceIframeContainer {border: 1px solid #3c3c3c; border-top: none;} +.studioSkin .mceIframeContainer {border: 1px solid white; border-top: none;} .studioSkin .mceStatusbar {background:#F0F0EE; font-size:9pt; line-height:16px; overflow:visible; color:#000; display:block; height:20px} .studioSkin .mceStatusbar div {float:left; margin:2px} .studioSkin .mceStatusbar a.mceResize {display:block; float:right; background:url(../../img/studio-icons.png) -800px 0; width:20px; height:20px; cursor:se-resize; outline:0} diff --git a/doc/README b/doc/README index d40f5d988d..395fc07dc5 100644 --- a/doc/README +++ b/doc/README @@ -1,3 +1,3 @@ -This directory contains some high level documentation for the code. We should strive to keep it up-to-date, but don't take it as the absolute truth. +This directory contains some high level documentation for the code. -A good place to start is 'overview.md' +WARNING: much of this is out-of-date. It stil may be helpful, though. diff --git a/doc/overview.md b/doc/overview.md index f64d12920d..4d074dfaf3 100644 --- a/doc/overview.md +++ b/doc/overview.md @@ -1,4 +1,4 @@ -# Documentation for edX code (mitx repo) +# Documentation for edX code (edx-platform repo) This document explains the general structure of the edX platform, and defines some of the acronyms and terms you'll see flying around in the code. diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index e81568ae4b..874ba0142a 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -20,7 +20,7 @@ logger = getLogger(__name__) TEST_COURSE_ORG = 'edx' TEST_COURSE_NAME = 'Test Course' -TEST_SECTION_NAME = "Problem" +TEST_SECTION_NAME = 'Test Section' @step(u'The course "([^"]*)" exists$') diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature new file mode 100644 index 0000000000..c4d96f93f7 --- /dev/null +++ b/lms/djangoapps/courseware/features/video.feature @@ -0,0 +1,6 @@ +Feature: Video component + As a student, I want to view course videos in LMS. + + Scenario: Autoplay is enabled in LMS + Given the course has a Video component + Then when I view the video it has autoplay enabled diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py new file mode 100644 index 0000000000..9930489d4b --- /dev/null +++ b/lms/djangoapps/courseware/features/video.py @@ -0,0 +1,34 @@ +#pylint: disable=C0111 + +from lettuce import world, step +from lettuce.django import django_url +from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_course, section_location + +############### ACTIONS #################### + + +@step('when I view the video it has autoplay enabled') +def does_autoplay(step): + assert(world.css_find('.video')[0]['data-autoplay'] == 'True') + + +@step('the course has a Video component') +def view_video(step): + coursename = TEST_COURSE_NAME.replace(' ', '_') + i_am_registered_for_the_course(step, coursename) + + # Make sure we have a video + add_video_to_course(coursename) + chapter_name = TEST_SECTION_NAME.replace(" ", "_") + section_name = chapter_name + url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' % + (chapter_name, section_name)) + + world.browser.visit(url) + + +def add_video_to_course(course): + template_name = 'i4x://edx/templates/video/default' + world.ItemFactory.create(parent_location=section_location(course), + template=template_name, + display_name='Video') diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 18ba863d69..13d780df12 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -10,6 +10,7 @@ from mock import MagicMock, patch, Mock from django.core.urlresolvers import reverse from django.contrib.auth.models import Group from django.http import HttpResponse +from django.conf import settings from mitxmako.shortcuts import render_to_string from xmodule.open_ended_grading_classes import peer_grading_service, controller_query_service @@ -31,7 +32,6 @@ from xmodule.tests import test_util_open_ended from courseware.tests import factories - @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class TestStaffGradingService(LoginEnrollmentTestCase): ''' @@ -310,8 +310,7 @@ class TestPanel(LoginEnrollmentTestCase): found_module, peer_grading_module = views.find_peer_grading_module(self.course) self.assertTrue(found_module) - @patch('xmodule.open_ended_grading_classes.controller_query_service.ControllerQueryService', - controller_query_service.MockControllerQueryService) + @patch('open_ended_grading.views.controller_qs', controller_query_service.MockControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, views.system)) def test_problem_list(self): """ Ensure that the problem list from the grading controller server can be rendered properly locally @@ -319,4 +318,4 @@ class TestPanel(LoginEnrollmentTestCase): """ request = Mock(user=self.user) response = views.student_problem_list(request, self.course.id) - self.assertTrue(isinstance(response, HttpResponse)) + self.assertRegexpMatches(response.content, "Here are a list of open ended problems for this course.") diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index 2bb6f61491..a914e434a9 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -36,6 +36,7 @@ system = ModuleSystem( replace_urls=None, xblock_model_data={} ) + controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system) """ diff --git a/lms/envs/common.py b/lms/envs/common.py index e7bc9519d9..f75dcf8804 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -113,7 +113,10 @@ MITX_FEATURES = { 'ENABLE_SERVICE_STATUS': False, # Toggle to indicate use of a custom theme - 'USE_CUSTOM_THEME': False + 'USE_CUSTOM_THEME': False, + + # Do autoplay videos for students + 'AUTOPLAY_VIDEOS': True } # Used for A/B testing diff --git a/lms/envs/test_ike.py b/lms/envs/test_ike.py deleted file mode 100644 index 46f7df211c..0000000000 --- a/lms/envs/test_ike.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -This config file runs the simplest dev environment using sqlite, and db-based -sessions. Assumes structure: - -/envroot/ - /db # This is where it'll write the database file - /mitx # The location of this repo - /log # Where we're going to write log files -""" - -# We intentionally define lots of variables that aren't used, and -# want to import all variables from base settings files -# pylint: disable=W0401, W0614 - -from .common import * -from logsettings import get_logger_config -import os - -DEBUG = True - -INSTALLED_APPS = [ - app - for app - in INSTALLED_APPS -] - -# Nose Test Runner -INSTALLED_APPS += ['django_nose'] -#NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html', '--cover-inclusive'] -NOSE_ARGS = ['--cover-erase', '--with-xunit', '--cover-html', '--cover-inclusive'] -for app in os.listdir(PROJECT_ROOT / 'djangoapps'): - NOSE_ARGS += ['--cover-package', app] -TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' - -# Local Directories -TEST_ROOT = path("test_root") -COURSES_ROOT = TEST_ROOT / "data" -DATA_DIR = COURSES_ROOT -MAKO_TEMPLATES['course'] = [DATA_DIR] -MAKO_TEMPLATES['sections'] = [DATA_DIR / 'sections'] -MAKO_TEMPLATES['custom_tags'] = [DATA_DIR / 'custom_tags'] -MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates', - DATA_DIR / 'info', - DATA_DIR / 'problems'] - -LOGGING = get_logger_config(TEST_ROOT / "log", - logging_env="dev", - tracking_filename="tracking.log", - debug=True) - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': PROJECT_ROOT / "db" / "mitx.db", - } -} - -CACHES = { - # This is the cache used for most things. - # In staging/prod envs, the sessions also live here. - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'mitx_loc_mem_cache', - 'KEY_FUNCTION': 'util.memcache.safe_key', - }, - - # The general cache is what you get if you use our util.cache. It's used for - # things like caching the course.xml file for different A/B test groups. - # We set it to be a DummyCache to force reloading of course.xml in dev. - # In staging environments, we would grab VERSION from data uploaded by the - # push process. - 'general': { - 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', - 'KEY_PREFIX': 'general', - 'VERSION': 4, - 'KEY_FUNCTION': 'util.memcache.safe_key', - } -} - -# Dummy secret key for dev -SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' - -############################ FILE UPLOADS (for discussion forums) ############################# -DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' -MEDIA_ROOT = PROJECT_ROOT / "uploads" -MEDIA_URL = "/static/uploads/" -STATICFILES_DIRS.append(("uploads", MEDIA_ROOT)) -FILE_UPLOAD_TEMP_DIR = PROJECT_ROOT / "uploads" -FILE_UPLOAD_HANDLERS = ( - 'django.core.files.uploadhandler.MemoryFileUploadHandler', - 'django.core.files.uploadhandler.TemporaryFileUploadHandler', -) diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index 88d3fd88a3..ddfe4a88f1 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -85,8 +85,8 @@ } @-webkit-keyframes fadeIn { - 0% { opacity: 0; } - 100% { opacity: 1; } + 0% { opacity: 0.0; } + 100% { opacity: 1.0; } } @@ -736,11 +736,11 @@ body.discussion { &.is-open { .browse-topic-drop-btn span { - opacity: 1; + opacity: 1.0; } .browse-topic-drop-icon { - opacity: 0; + opacity: 0.0; } &.is-dropped { @@ -788,7 +788,7 @@ body.discussion { &::-webkit-input-placeholder, &:-moz-placeholder, &:-ms-input-placeholder { - opacity: 1; + opacity: 1.0; } } } @@ -818,7 +818,7 @@ body.discussion { line-height: 58px; color: #333; text-shadow: 0 1px 0 rgba(255, 255, 255, .8); - opacity: 0; + opacity: 0.0; @include transition(opacity .2s); } } @@ -833,7 +833,7 @@ body.discussion { height: 16px; margin-left: -12px; background: url(../images/browse-icon.png) no-repeat; - opacity: 1; + opacity: 1.0; @include transition(none); } @@ -967,7 +967,7 @@ body.discussion { &::-webkit-input-placeholder, &:-moz-placeholder, &:-ms-input-placeholder { - opacity: 0; + opacity: 0.0; @include transition(opacity .2s); } @@ -2454,7 +2454,7 @@ body.discussion { font-style: italic; cursor:pointer; margin-right: 10px; - opacity:.8; + opacity: 0.8; span { cursor: pointer; @@ -2462,7 +2462,7 @@ body.discussion { &:hover { @include transition(opacity .2s); - opacity: 1; + opacity: 1.0; } } @@ -2475,7 +2475,7 @@ body.discussion { top:-13px; margin-right:35px; margin-top:13px; - opacity: 1; + opacity: 1.0; } .notpinned .icon { @@ -2523,11 +2523,11 @@ display:none; padding-right: 5px; font-style: italic; cursor:pointer; - opacity:.8; + opacity: 0.8; &:hover { @include transition(opacity .2s); - opacity: 1; + opacity: 1.0; } } diff --git a/lms/static/sass/base/_animations.scss b/lms/static/sass/base/_animations.scss index 0c95e6b5af..4c4620ca27 100644 --- a/lms/static/sass/base/_animations.scss +++ b/lms/static/sass/base/_animations.scss @@ -9,12 +9,12 @@ @mixin home-header-pop-up-keyframes { 0% { - opacity: 0; + opacity: 0.0; top: 300px; //@include transform(scale(0.9)); } 45% { - opacity: 1; + opacity: 1.0; } 65% { top: -40px; @@ -43,19 +43,19 @@ @mixin title-appear-keyframes { 0% { - opacity: 0; + opacity: 0.0; top: 60px; @include transform(scale(0.9)); } 20% { - opacity: 1; + opacity: 1.0; } 27% { // this % of total-time should be ~ 1.25s top: 40px; @include transform(scale(1)); } 90% { // this % of total-time is when 2nd half of animation starts - opacity: 1; + opacity: 1.0; top: 40px; @include transform(scale(1)); } @@ -79,24 +79,24 @@ @mixin home-appear-keyframes { 0% { - opacity: 0; + opacity: 0.0; top: 60px; @include transform(scale(0.9)); } 20% { - opacity: 1; + opacity: 1.0; } 30% { // this % of total-time should be ~ 1.25s top: 40px; @include transform(scale(1)); } 80% { // this % of total-time is when 2nd half of animation starts - opacity: 1; + opacity: 1.0; top: 40px; @include transform(scale(1)); } 100% { - opacity: 0; + opacity: 0.0; top: 60px; @include transform(scale(0.7)); } @@ -117,10 +117,10 @@ @mixin edx-appear-keyframes { 0% { - opacity: 0; + opacity: 0.0; } 100% { - opacity: 1; + opacity: 1.0; } } @@ -231,7 +231,7 @@ opacity: 0.9; } 80% { - opacity: 1; + opacity: 1.0; } 100% { bottom: 0px; diff --git a/lms/static/sass/course/_textbook.scss b/lms/static/sass/course/_textbook.scss index 1d72ae3199..83aca09ab6 100644 --- a/lms/static/sass/course/_textbook.scss +++ b/lms/static/sass/course/_textbook.scss @@ -38,7 +38,7 @@ div.book-wrapper { line-height: 2.1em; text-align: right; color: #9a9a9a; - opacity: 0; + opacity: 0.0; @include transition(opacity .15s); } @@ -55,7 +55,7 @@ div.book-wrapper { background-color: transparent; .page-number { - opacity: 1; + opacity: 1.0; } } } @@ -119,7 +119,7 @@ div.book-wrapper { @include box-sizing(border-box); display: table; height: 100%; - opacity: 0; + opacity: 0.0; filter: alpha(opacity=0); text-indent: -9999px; @include transition; @@ -127,7 +127,7 @@ div.book-wrapper { width: 100%; &:hover { - opacity: 1; + opacity: 1.0; filter: alpha(opacity=100); } } diff --git a/lms/static/sass/course/courseware/_sidebar.scss b/lms/static/sass/course/courseware/_sidebar.scss index 6cf6f6a602..24bda451a7 100644 --- a/lms/static/sass/course/courseware/_sidebar.scss +++ b/lms/static/sass/course/courseware/_sidebar.scss @@ -61,7 +61,7 @@ section.course-index { span.ui-icon { left: 0; background-image: url("/static/images/ui-icons_222222_256x240.png"); - opacity: .3; + opacity: 0.3; } } } @@ -146,7 +146,7 @@ section.course-index { @include box-shadow(inset 0 1px 14px 0 rgba(0,0,0, 0.1)); &:after { - opacity: 1; + opacity: 1.0; right: 15px; } } @@ -174,7 +174,7 @@ section.course-index { background: $sidebar-active-image; &:after { - opacity: 1; + opacity: 1.0; right: 15px; } diff --git a/lms/static/sass/course/layout/_calculator.scss b/lms/static/sass/course/layout/_calculator.scss index 2819546f9f..c0a8764a8c 100644 --- a/lms/static/sass/course/layout/_calculator.scss +++ b/lms/static/sass/course/layout/_calculator.scss @@ -27,7 +27,7 @@ div.calc-main { width: 16px; &:hover { - opacity: .8; + opacity: 0.8; } &.closed { @@ -136,7 +136,7 @@ div.calc-main { &.shown { display: block; - opacity: 1; + opacity: 1.0; } dt { diff --git a/lms/static/sass/ie.scss b/lms/static/sass/ie.scss index 4b0f5aa3c0..e03b711bae 100644 --- a/lms/static/sass/ie.scss +++ b/lms/static/sass/ie.scss @@ -26,7 +26,7 @@ header.global { } h2 { - opacity: 1; + opacity: 1.0; } } @@ -51,7 +51,7 @@ header.global { text-decoration: none; &::before { - opacity: 1; + opacity: 1.0; } .name { diff --git a/lms/static/sass/multicourse/_course_about.scss b/lms/static/sass/multicourse/_course_about.scss index 9eab7c0a4f..0008bf1efe 100644 --- a/lms/static/sass/multicourse/_course_about.scss +++ b/lms/static/sass/multicourse/_course_about.scss @@ -429,7 +429,7 @@ &:hover { .sharing-message { - opacity: 1; + opacity: 1.0; top: 56px; } } @@ -470,7 +470,7 @@ width: 44px; &:hover { - opacity: 1; + opacity: 1.0; } img { @@ -514,7 +514,7 @@ &:hover { .icon { - opacity: 1; + opacity: 1.0; } } diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index b173647550..c0dac89199 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -74,7 +74,7 @@ &:hover { .title .icon { - opacity: 1; + opacity: 1.0; } } diff --git a/lms/static/sass/multicourse/_home.scss b/lms/static/sass/multicourse/_home.scss index ea8ddaf654..d4def21f5c 100644 --- a/lms/static/sass/multicourse/_home.scss +++ b/lms/static/sass/multicourse/_home.scss @@ -53,7 +53,7 @@ @include box-sizing(border-box); @include inline-block; left: 0px; - opacity: 1; + opacity: 1.0; padding: 20px 30px; top: 0px; @include transition(all, 0.2s, linear); @@ -312,7 +312,7 @@ text-decoration: none; &::before { - opacity: 1; + opacity: 1.0; } .name { diff --git a/lms/static/sass/shared/_course_object.scss b/lms/static/sass/shared/_course_object.scss index f78c483925..bd4a8dc049 100644 --- a/lms/static/sass/shared/_course_object.scss +++ b/lms/static/sass/shared/_course_object.scss @@ -117,7 +117,7 @@ .info-link { color: $link-color; - opacity: 1; + opacity: 1.0; } h2 { diff --git a/lms/static/sass/shared/_modal.scss b/lms/static/sass/shared/_modal.scss index 7a51213dee..9777c582da 100644 --- a/lms/static/sass/shared/_modal.scss +++ b/lms/static/sass/shared/_modal.scss @@ -300,8 +300,29 @@ } } } + + #help_wrapper { + padding: 0 ($baseline*1.5) ($baseline*1.5) ($baseline*1.5); + + header { + margin-bottom: $baseline; + padding-right: 0; + padding-left: 0; + } + + + } + + .tip { + font-size: 12px; + display: block; + color: $dark-gray; + } + + } .leanModal_box { @extend .modal; } + diff --git a/lms/templates/help_modal.html b/lms/templates/help_modal.html index deebd391d2..350f858334 100644 --- a/lms/templates/help_modal.html +++ b/lms/templates/help_modal.html @@ -20,17 +20,15 @@ <% discussion_link = get_discussion_link(course) if course else None %> -% if discussion_link: -

- Have a course-specific question? - - Post it on the course forums. - -

-
-% endif -

Have a general question about edX? Check the FAQ.

+

For questions on course lectures, homework, tools, or materials for this course, post in the + course discussion forum. +

+

Have general questions about edX? You can find lots of helpful information in the edX + FAQ.

+ +

Have a question about something specific? You can contact the edX general + support team directly:


@@ -58,9 +56,10 @@ discussion_link = get_discussion_link(course) if course else None % endif - + - +
diff --git a/lms/templates/video.html b/lms/templates/video.html index 24785abf72..267372176a 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -3,8 +3,24 @@ % endif %if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: -
- +
+
+
+
+
+
    +
  • +
  • +
    0:00 / 0:00
    +
  • +
+ +
+
+
+
%elif settings.MITX_FEATURES.get('USE_YOUTUBE_OBJECT_API') and normal_speed_video_id: %else: -
+
diff --git a/lms/urls.py b/lms/urls.py index 98c36df9b0..851731e6ec 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -330,13 +330,6 @@ if settings.DEBUG or settings.MITX_FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'): ## Jasmine and admin urlpatterns += (url(r'^admin/', include(admin.site.urls)),) -if settings.DEBUG: - # Originally added to allow debugging issues when prod is - # mysteriously different from staging (specifically missing get - # parameters in certain cases), but removing from prod because - # it's a security risk. - urlpatterns += (url(r'^debug_request$', 'util.views.debug_request'),) - if settings.MITX_FEATURES.get('AUTH_USE_OPENID'): urlpatterns += ( url(r'^openid/login/$', 'django_openid_auth.views.login_begin', name='openid-login'), diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 01768bcac9..88d93be05c 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -4,7 +4,7 @@ beautifulsoup4==4.1.3 beautifulsoup==3.2.1 boto==2.6.0 celery==3.0.19 -distribute==0.6.28 +distribute>=0.6.28 django-celery==3.0.17 django-countries==1.5 django-followit==0.0.3 @@ -19,7 +19,7 @@ django-sekizai==0.6.1 django-ses==0.4.1 django-storages==1.1.5 django-threaded-multihost==1.4-1 -django==1.4.3 +django==1.4.5 feedparser==5.1.3 fs==0.4.0 GitPython==0.3.2.RC1 diff --git a/requirements/system/ubuntu/apt-packages.txt b/requirements/system/ubuntu/apt-packages.txt index 2635388757..c61c658111 100644 --- a/requirements/system/ubuntu/apt-packages.txt +++ b/requirements/system/ubuntu/apt-packages.txt @@ -1,12 +1,18 @@ python-software-properties pkg-config +gfortran +libatlas-dev +libblas-dev +liblapack-dev +liblapack3gf curl git python-virtualenv +python-scipy +python-numpy build-essential python-dev gfortran -liblapack-dev libfreetype6-dev libpng12-dev libjpeg-dev @@ -14,6 +20,7 @@ libxml2-dev libxslt-dev yui-compressor graphviz +libgraphviz-dev graphviz-dev mysql-server libmysqlclient-dev @@ -23,3 +30,7 @@ libreadline6-dev mongodb nodejs coffeescript +mysql +libmysqlclient-dev +virtualenvwrapper +libgeos-ruby1.8 diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh index d387465c49..520ce05b5c 100755 --- a/scripts/create-dev-env.sh +++ b/scripts/create-dev-env.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash + +#Exit if any commands return a non-zero status set -e # posix compliant sanity check @@ -27,10 +29,17 @@ EOL } +#Setting error color to red before reset error() { printf '\E[31m'; echo "$@"; printf '\E[0m' } +#Setting warning color to magenta before reset +warning() { + printf '\E[35m'; echo "$@"; printf '\E[0m' +} + +#Setting output color to cyan before reset output() { printf '\E[36m'; echo "$@"; printf '\E[0m' } @@ -51,7 +60,7 @@ EO info() { cat<1.7 is # --no-site-packages - mkvirtualenv -a "$BASE/mitx" mitx || { + mkvirtualenv -a "$HOME/.virtualenvs" edx-platform || { error "mkvirtualenv exited with a non-zero error" return 1 } @@ -380,10 +442,30 @@ if [[ -n $compile ]]; then rm -rf numpy-${NUMPY_VER} scipy-${SCIPY_VER} fi +# building correct version of distribute from source +DISTRIBUTE_VER="0.6.28" +output "Building Distribute" +SITE_PACKAGES="$HOME/.virtualenvs/edx-platform/lib/python2.7/site-packages" +cd "$SITE_PACKAGES" +curl -O http://pypi.python.org/packages/source/d/distribute/distribute-${DISTRIBUTE_VER}.tar.gz +tar -xzvf distribute-${DISTRIBUTE_VER}.tar.gz +cd distribute-${DISTRIBUTE_VER} +python setup.py install +cd .. +rm distribute-${DISTRIBUTE_VER}.tar.gz + +DISTRIBUTE_VERSION=`pip freeze | grep distribute` + +if [[ "$DISTRIBUTE_VERSION" == "distribute==0.6.28" ]]; then + output "Distribute successfully installed" +else + error "Distribute failed to build correctly. This script requires a working version of Distribute 0.6.28 in your virtualenv's python installation" + exit 1 +fi + case `uname -s` in Darwin) # on mac os x get the latest distribute and pip - curl http://python-distribute.org/distribute_setup.py | python pip install -U pip # need latest pytz before compiling numpy and scipy pip install -U pytz @@ -395,18 +477,29 @@ case `uname -s` in ;; esac -output "Installing MITx pre-requirements" -pip install -r $BASE/mitx/pre-requirements.txt +output "Installing edX pre-requirements" +pip install -r $BASE/edx-platform/requirements/edx/pre.txt -output "Installing MITx requirements" -# Need to be in the mitx dir to get the paths to local modules right -cd $BASE/mitx -pip install -r requirements.txt +output "Installing edX requirements" +# Install prereqs +cd $BASE/edx-platform +rvm use $RUBY_VER +rake install_prereqs + +# Final dependecy +output "Finishing Touches" +cd $BASE +pip install argcomplete +cd $BASE/edx-platform +bundle install mkdir "$BASE/log" || true mkdir "$BASE/db" || true +mkdir "$BASE/data" || true - +rake django-admin[syncdb] +rake django-admin[migrate] +rake django-admin[update-templates] # Configure Git output "Fixing your git default settings" diff --git a/scripts/install-system-req.sh b/scripts/install-system-req.sh index 37bc6d1716..43e405524d 100755 --- a/scripts/install-system-req.sh +++ b/scripts/install-system-req.sh @@ -16,10 +16,11 @@ output() { ### START -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -BREW_FILE=$DIR/"brew-formulas.txt" -APT_REPOS_FILE=$DIR/"apt-repos.txt" -APT_PKGS_FILE=$DIR/"apt-packages.txt" +SELF_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +REQUIREMENTS_DIR="$SELF_DIR/../requirements/system" +BREW_FILE=$REQUIREMENTS_DIR/"mac_os_x/brew-formulas.txt" +APT_REPOS_FILE=$REQUIREMENTS_DIR/"ubuntu/apt-repos.txt" +APT_PKGS_FILE=$REQUIREMENTS_DIR/"ubuntu/apt-packages.txt" case `uname -s` in [Ll]inux) @@ -30,8 +31,9 @@ case `uname -s` in distro=`lsb_release -cs` case $distro in - maya|lisa|natty|oneiric|precise|quantal) - output "Installing Ubuntu requirements" + #Tries to install the same + squeeze|wheezy|jessie|maya|lisa|olivia|nadia|natty|oneiric|precise|quantal|raring) + output "Installing Debian family requirements" # DEBIAN_FRONTEND=noninteractive is required for silent mysql-server installation export DEBIAN_FRONTEND=noninteractive @@ -39,7 +41,10 @@ case `uname -s` in # add repositories cat $APT_REPOS_FILE | xargs -n 1 sudo add-apt-repository -y sudo apt-get -y update - + sudo apt-get -y install gfortran + sudo apt-get -y install graphviz libgraphviz-dev graphviz-dev + sudo apt-get -y install libatlas-dev libblas-dev + sudo apt-get -y install ruby-rvm # install packages listed in APT_PKGS_FILE cat $APT_PKGS_FILE | xargs sudo apt-get -y install ;; @@ -70,10 +75,13 @@ EO output "Installing OSX requirements" if [[ ! -r $BREW_FILE ]]; then - error "$BREW_FILE does not exist, needed to install brew" + error "$BREW_FILE does not exist, please include the brew formulas file in the requirements/system/mac_os_x directory" exit 1 fi + # for some reason openssl likes to be installed by itself first + brew install openssl + # brew errors if the package is already installed for pkg in $(cat $BREW_FILE); do grep $pkg <(brew list) &>/dev/null || {