diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index a11a6cb869..b2941ac7a5 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -2,6 +2,7 @@ Feature: Advanced (manual) course policy In order to specify course policy settings for which no custom user interface exists I want to be able to manually enter JSON key /value pairs + Scenario: A course author sees default advanced settings Given I have opened a new course in Studio When I select the Advanced Settings @@ -11,6 +12,8 @@ Feature: Advanced (manual) course policy Given I am on the Advanced Course Settings page in Studio Then the settings are alphabetized + # Sauce labs does not play nicely with CodeMirror + @skip_sauce Scenario: Test cancel editing key value Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key @@ -19,6 +22,8 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is unchanged + # Sauce labs does not play nicely with CodeMirror + @skip_sauce Scenario: Test editing key value Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key and save @@ -26,6 +31,8 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is changed + # Sauce labs does not play nicely with CodeMirror + @skip_sauce Scenario: Test how multi-line input appears Given I am on the Advanced Course Settings page in Studio When I create a JSON object as a value for "discussion_topics" @@ -33,6 +40,8 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as formatted + # Sauce labs does not play nicely with CodeMirror + @skip_sauce Scenario: Test error if value supplied is of the wrong type Given I am on the Advanced Course Settings page in Studio When I create a JSON object as a value for "display_name" @@ -41,6 +50,8 @@ Feature: Advanced (manual) course policy Then the policy key value is unchanged # This feature will work in Firefox only when Firefox is the active window + # Sauce labs does not play nicely with CodeMirror + @skip_sauce Scenario: Test automatic quoting of non-JSON values Given I am on the Advanced Course Settings page in Studio When I create a non-JSON value not in quotes @@ -48,6 +59,8 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as a string + # Sauce labs does not play nicely with CodeMirror + @skip_sauce Scenario: Confirmation is shown on save Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature index f13ce53fc2..6289df9cfc 100644 --- a/cms/djangoapps/contentstore/features/checklists.feature +++ b/cms/djangoapps/contentstore/features/checklists.feature @@ -10,7 +10,10 @@ Feature: Course checklists Then I can check and uncheck tasks in a checklist And They are correctly selected after reloading the page - # CHROME ONLY, due to issues getting link to be active in firefox + # There are issues getting link to be active in browsers other than chrome + @skip_firefox + @skip_internetexplorer + @skip_safari Scenario: A task can link to a location within Studio Given I have opened Checklists When I select a link to the course outline @@ -18,7 +21,10 @@ Feature: Course checklists And I press the browser back button Then I am brought back to the course outline in the correct state - # CHROME ONLY, due to issues getting link to be active in firefox + # There are issues getting link to be active in browsers other than chrome + @skip_firefox + @skip_internetexplorer + @skip_safari Scenario: A task can link to a location outside Studio Given I have opened Checklists When I select a link to help page diff --git a/cms/djangoapps/contentstore/features/course-overview.feature b/cms/djangoapps/contentstore/features/course-overview.feature index a9aed5d982..2cbb22ddd7 100644 --- a/cms/djangoapps/contentstore/features/course-overview.feature +++ b/cms/djangoapps/contentstore/features/course-overview.feature @@ -64,6 +64,10 @@ Feature: Course Overview And I change an assignment's grading status Then I am shown a notification + # Notification is not shown on reorder for IE + # Safari does not have moveMouseTo implemented + @skip_internetexplorer + @skip_safari Scenario: Notification is shown on subsection reorder Given I have opened a new course section in Studio And I have added a new subsection diff --git a/cms/djangoapps/contentstore/features/course-settings.feature b/cms/djangoapps/contentstore/features/course-settings.feature index 8f00452efe..9976179b68 100644 --- a/cms/djangoapps/contentstore/features/course-settings.feature +++ b/cms/djangoapps/contentstore/features/course-settings.feature @@ -1,6 +1,8 @@ Feature: Course Settings As a course author, I want to be able to configure my course settings. + # Safari has trouble keeps dates on refresh + @skip_safari Scenario: User can set course dates Given I have opened a new course in Studio When I select Schedule and Details @@ -8,12 +10,16 @@ Feature: Course Settings And I press the "Save" notification button Then I see the set dates on refresh + # IE has trouble with saving information + @skip_internetexplorer Scenario: User can clear previously set course dates (except start date) Given I have set course dates And I clear all the dates except start And I press the "Save" notification button Then I see cleared dates on refresh + # IE has trouble with saving information + @skip_internetexplorer Scenario: User cannot clear the course start date Given I have set course dates And I press the "Save" notification button @@ -21,6 +27,10 @@ Feature: Course Settings Then I receive a warning about course start date And The previously set start date is shown on refresh + # IE has trouble with saving information + # Safari gets CSRF token errors + @skip_internetexplorer + @skip_safari Scenario: User can correct the course start date warning Given I have tried to clear the course start And I have entered a new course start date @@ -28,12 +38,16 @@ Feature: Course Settings Then The warning about course start date goes away And My new course start date is shown on refresh + # Safari does not save + refresh properly through sauce labs + @skip_safari Scenario: Settings are only persisted when saved Given I have set course dates And I press the "Save" notification button When I change fields Then I do not see the new changes persisted on refresh + # Safari does not save + refresh properly through sauce labs + @skip_safari Scenario: Settings are reset on cancel Given I have set course dates And I press the "Save" notification button @@ -41,6 +55,8 @@ Feature: Course Settings And I press the "Cancel" notification button Then I do not see the changes + # Safari gets CSRF token errors + @skip_safari Scenario: Confirmation is shown on save Given I have opened a new course in Studio When I select Schedule and Details diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index db7b4d81f9..8b31d325e5 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -91,7 +91,7 @@ def remove_course_team_admin(_step, outer_capture, name): @step(u'"([^"]*)" logs in$') def other_user_login(_step, name): - world.browser.cookies.delete() + world.visit('logout') world.visit('/') signin_css = 'a.action-signin' diff --git a/cms/djangoapps/contentstore/features/course-updates.feature b/cms/djangoapps/contentstore/features/course-updates.feature index fb18e51f2d..41ee785db5 100644 --- a/cms/djangoapps/contentstore/features/course-updates.feature +++ b/cms/djangoapps/contentstore/features/course-updates.feature @@ -1,6 +1,8 @@ Feature: Course updates As a course author, I want to be able to provide updates to my students + # Internet explorer can't select all so the update appears weirdly + @skip_internetexplorer Scenario: Users can add updates Given I have opened a new course in Studio And I go to the course updates page @@ -8,6 +10,8 @@ Feature: Course updates Then I should see the update "Hello" And I see a "saving" notification + # Internet explorer can't select all so the update appears weirdly + @skip_internetexplorer Scenario: Users can edit updates Given I have opened a new course in Studio And I go to the course updates page @@ -33,6 +37,8 @@ Feature: Course updates Then I should see the date "June 1, 2013" And I see a "saving" notification + # Internet explorer can't select all so the update appears weirdly + @skip_internetexplorer Scenario: Users can change handouts Given I have opened a new course in Studio And I go to the course updates page diff --git a/cms/djangoapps/contentstore/features/discussion-editor.feature b/cms/djangoapps/contentstore/features/discussion-editor.feature index 8fb14c3205..e4b1f5450b 100644 --- a/cms/djangoapps/contentstore/features/discussion-editor.feature +++ b/cms/djangoapps/contentstore/features/discussion-editor.feature @@ -6,6 +6,8 @@ Feature: Discussion Component Editor And I edit and select Settings Then I see three alphabetized settings and their expected values + # Safari doesn't save the name properly + @skip_safari Scenario: User can modify display name Given I have created a Discussion Tag And I edit and select Settings diff --git a/cms/djangoapps/contentstore/features/grading.feature b/cms/djangoapps/contentstore/features/grading.feature index 5cf5a72866..0b34feb7aa 100644 --- a/cms/djangoapps/contentstore/features/grading.feature +++ b/cms/djangoapps/contentstore/features/grading.feature @@ -13,7 +13,7 @@ Feature: Course Grading When I add "6" new grades Then I see I now have "5" grades - #Cannot reliably make the delete button appear so using javascript instead + # Cannot reliably make the delete button appear so using javascript instead Scenario: Users can delete grading ranges Given I have opened a new course in Studio And I am viewing the grading settings @@ -21,6 +21,9 @@ Feature: Course Grading And I delete a grade Then I see I now have "2" grades + # IE and Safari cannot reliably drag and drop through selenium + @skip_internetexplorer + @skip_safari Scenario: Users can move grading ranges Given I have opened a new course in Studio And I am viewing the grading settings @@ -85,6 +88,9 @@ Feature: Course Grading When I change assignment type "Homework" to "" Then the save button is disabled + # IE and Safari cannot type in grade range name + @skip_internetexplorer + @skip_safari Scenario: User can edit grading range names Given I have opened a new course in Studio And I have populated the course diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py index 719b3f7f7c..93e44b3893 100644 --- a/cms/djangoapps/contentstore/features/grading.py +++ b/cms/djangoapps/contentstore/features/grading.py @@ -112,10 +112,10 @@ def changes_not_persisted(step): @step(u'I see the assignment type "(.*)"$') def i_see_the_assignment_type(_step, name): - assignment_css = '#course-grading-assignment-name' - assignments = world.css_find(assignment_css) - types = [ele['value'] for ele in assignments] - assert name in types + assignment_css = '#course-grading-assignment-name' + assignments = world.css_find(assignment_css) + types = [ele['value'] for ele in assignments] + assert name in types @step(u'I change the highest grade range to "(.*)"$') @@ -144,6 +144,7 @@ def cannot_edit_fail(_step): pass # We should get this exception on failing to edit the element + @step(u'I change the grace period to "(.*)"$') def i_change_grace_period(_step, grace_period): grace_period_css = '#course-grading-graceperiod' diff --git a/cms/djangoapps/contentstore/features/html-editor.feature b/cms/djangoapps/contentstore/features/html-editor.feature index 4cd5e1c1b9..4419d6018b 100644 --- a/cms/djangoapps/contentstore/features/html-editor.feature +++ b/cms/djangoapps/contentstore/features/html-editor.feature @@ -6,6 +6,8 @@ Feature: HTML Editor And I edit and select Settings Then I see only the HTML display name setting + # Safari doesn't save the name properly + @skip_safari Scenario: User can modify display name Given I have created a Blank HTML Page And I edit and select Settings diff --git a/cms/djangoapps/contentstore/features/problem-editor.feature b/cms/djangoapps/contentstore/features/problem-editor.feature index 50c49a1896..1296acec1c 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.feature +++ b/cms/djangoapps/contentstore/features/problem-editor.feature @@ -7,12 +7,16 @@ Feature: Problem Editor Then I see five alphabetized settings and their expected values And Edit High Level Source is not visible + # Safari is having trouble saving the values on sauce + @skip_safari Scenario: User can modify String values Given I have created a Blank Common Problem When I edit and select Settings Then I can modify the display name And my display name change is persisted on save + # Safari is having trouble saving the values on sauce + @skip_safari Scenario: User can specify special characters in String values Given I have created a Blank Common Problem When I edit and select Settings @@ -25,6 +29,8 @@ Feature: Problem Editor Then I can revert the display name to unset And my display name is unset on save + # IE will not click the revert button properly + @skip_internetexplorer Scenario: User can select values in a Select Given I have created a Blank Common Problem When I edit and select Settings @@ -32,6 +38,8 @@ Feature: Problem Editor And my change to randomization is persisted And I can revert to the default value for randomization + # Safari will input it as 35. + @skip_safari Scenario: User can modify float input values Given I have created a Blank Common Problem When I edit and select Settings @@ -44,16 +52,22 @@ Feature: Problem Editor When I edit and select Settings Then if I set the weight to "abc", it remains unset + # Safari will input it as 234. + @skip_safari Scenario: User cannot type decimal values integer number field Given I have created a Blank Common Problem When I edit and select Settings Then if I set the max attempts to "2.34", it will persist as a valid integer + # Safari will input it incorrectly + @skip_safari Scenario: User cannot type out of range values in an integer number field Given I have created a Blank Common Problem When I edit and select Settings Then if I set the max attempts to "-3", it will persist as a valid integer + # Safari will input it as 35. + @skip_safari Scenario: Settings changes are not saved on Cancel Given I have created a Blank Common Problem When I edit and select Settings @@ -67,6 +81,8 @@ Feature: Problem Editor Then Edit High Level Source is visible # This feature will work in Firefox only when Firefox is the active window + # IE will not interact with the high level source in sauce labs + @skip_internetexplorer Scenario: High Level source is persisted for LaTeX problem (bug STUD-280) Given I have created a LaTeX Problem When I edit and compile the High Level Source diff --git a/cms/djangoapps/contentstore/features/static-pages.feature b/cms/djangoapps/contentstore/features/static-pages.feature index 9997df69f0..c1a8ec91fc 100644 --- a/cms/djangoapps/contentstore/features/static-pages.feature +++ b/cms/djangoapps/contentstore/features/static-pages.feature @@ -15,6 +15,8 @@ Feature: Static Pages And I "delete" the "Empty" page Then I should not see a "Empty" static page + # Safari won't update the name properly + @skip_safari Scenario: Users can edit static pages Given I have opened a new course in Studio And I go to the static pages page diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index 84755b3644..6703c60c3b 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -25,6 +25,8 @@ Feature: Create Subsection And I reload the page Then I see it marked as Homework + # Safari has trouble saving the date in Sauce + @skip_safari Scenario: Set a due date in a different year (bug #256) Given I have opened a new subsection in Studio And I set the subsection release date to 12/25/2011 03:00 diff --git a/cms/djangoapps/contentstore/features/textbooks.feature b/cms/djangoapps/contentstore/features/textbooks.feature index 0758a0b57b..36de10daa1 100644 --- a/cms/djangoapps/contentstore/features/textbooks.feature +++ b/cms/djangoapps/contentstore/features/textbooks.feature @@ -5,6 +5,9 @@ Feature: Textbooks When I go to the textbooks page Then I should see a message telling me to create a new textbook + # IE and Safari on sauce labs will not upload the textbook correctly resulting in an error + @skip_internetexplorer + @skip_safari Scenario: Create a textbook Given I have opened a new course in Studio And I go to the textbooks page diff --git a/cms/djangoapps/contentstore/features/upload.feature b/cms/djangoapps/contentstore/features/upload.feature index 8d40163685..441de597ea 100644 --- a/cms/djangoapps/contentstore/features/upload.feature +++ b/cms/djangoapps/contentstore/features/upload.feature @@ -1,6 +1,8 @@ Feature: Upload Files As a course author, I want to be able to upload files for my students + # Uploading isn't working on safari with sauce labs + @skip_safari Scenario: Users can upload files Given I have opened a new course in Studio And I go to the files and uploads page @@ -8,6 +10,8 @@ Feature: Upload Files Then I should see the file "test" was uploaded And The url for the file "test" is valid + # Uploading isn't working on safari with sauce labs + @skip_safari Scenario: Users can update files Given I have opened a new course in studio And I go to the files and uploads page @@ -15,6 +19,8 @@ Feature: Upload Files And I upload the file "test" Then I should see only one "test" + # Uploading isn't working on safari with sauce labs + @skip_safari Scenario: Users can delete uploaded files Given I have opened a new course in studio And I go to the files and uploads page @@ -23,12 +29,16 @@ Feature: Upload Files Then I should not see the file "test" was uploaded And I see a confirmation that the file was deleted + # Uploading isn't working on safari with sauce labs + @skip_safari Scenario: Users can download files Given I have opened a new course in studio And I go to the files and uploads page When I upload the file "test" Then I can download the correct "test" file + # Uploading isn't working on safari with sauce labs + @skip_safari Scenario: Users can download updated files Given I have opened a new course in studio And I go to the files and uploads page diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index a989d6c07f..882b36e6b2 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -10,6 +10,7 @@ import os TEST_ROOT = settings.COMMON_TEST_DATA_ROOT + @step(u'I go to the files and uploads page') def go_to_uploads(_step): menu_css = 'li.nav-course-courseware' @@ -106,8 +107,8 @@ def get_index(file_name): def get_file(file_name): index = get_index(file_name) assert index != -1 - url_css = 'a.filename' + def get_url(): return world.css_find(url_css)[index]._element.get_attribute('href') url = world.retry_on_exception(get_url) diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature index a53183e37c..d238a7e523 100644 --- a/cms/djangoapps/contentstore/features/video-editor.feature +++ b/cms/djangoapps/contentstore/features/video-editor.feature @@ -6,17 +6,23 @@ Feature: Video Component Editor And I edit the component Then I see the correct video settings and default values + # Safari has trouble saving values on Sauce + @skip_safari Scenario: User can modify Video display name Given I have created a Video component And I edit the component Then I can modify the display name And my video display name change is persisted on save + # Sauce Labs cannot delete cookies + @skip_sauce Scenario: Captions are hidden when "show captions" is false Given I have created a Video component And I have set "show captions" to False Then when I view the video it does not show the captions + # Sauce Labs cannot delete cookies + @skip_sauce Scenario: Captions are shown when "show captions" is true Given I have created a Video component And I have set "show captions" to True diff --git a/cms/djangoapps/contentstore/features/video.feature b/cms/djangoapps/contentstore/features/video.feature index 50c06fde63..d2f9915f55 100644 --- a/cms/djangoapps/contentstore/features/video.feature +++ b/cms/djangoapps/contentstore/features/video.feature @@ -10,15 +10,21 @@ Feature: Video Component Given I have clicked the new unit button Then creating a video takes a single click + # Sauce Labs cannot delete cookies + @skip_sauce Scenario: Captions are hidden correctly Given I have created a Video component And I have hidden captions Then when I view the video it does not show the captions + # Sauce Labs cannot delete cookies + @skip_sauce Scenario: Captions are shown correctly Given I have created a Video component Then when I view the video it does show the captions + # Sauce Labs cannot delete cookies + @skip_sauce Scenario: Captions are toggled correctly Given I have created a Video component And I have toggled captions diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 46f439b055..e0d58b32f0 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -2,7 +2,7 @@ Script for importing courseware from XML format """ -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand, CommandError, make_option from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.django import modulestore from xmodule.contentstore.django import contentstore @@ -14,18 +14,26 @@ class Command(BaseCommand): """ help = 'Import the specified data directory into the default ModuleStore' + option_list = BaseCommand.option_list + ( + make_option('--nostatic', + action='store_true', + help='Skip import of static content'), + ) + def handle(self, *args, **options): "Execute the command" if len(args) == 0: - raise CommandError("import requires at least one argument: [...]") + raise CommandError("import requires at least one argument: [--nostatic] [...]") data_dir = args[0] + do_import_static = not (options.get('nostatic', False)) if len(args) > 1: course_dirs = args[1:] else: course_dirs = None print("Importing. Data_dir={data}, course_dirs={courses}".format( data=data_dir, - courses=course_dirs)) + courses=course_dirs, + dis=do_import_static)) import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False, - static_content_store=contentstore(), verbose=True) + static_content_store=contentstore(), verbose=True, do_import_static=do_import_static) diff --git a/cms/djangoapps/contentstore/tests/test_assets.py b/cms/djangoapps/contentstore/tests/test_assets.py index b627237729..2f158cfda6 100644 --- a/cms/djangoapps/contentstore/tests/test_assets.py +++ b/cms/djangoapps/contentstore/tests/test_assets.py @@ -60,11 +60,11 @@ class UploadTestCase(CourseTestCase): f = BytesIO("sample content") f.name = "sample.txt" resp = self.client.post(self.url, {"name": "my-name", "file": f}) - self.assert2XX(resp.status_code) + self.assertEquals(resp.status_code, 200) def test_no_file(self): resp = self.client.post(self.url, {"name": "file.txt"}) - self.assert4XX(resp.status_code) + self.assertEquals(resp.status_code, 400) def test_get(self): resp = self.client.get(self.url) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 2afc26707a..7d5cd3cbcb 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -486,7 +486,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): content_store = contentstore() module_store = modulestore('direct') - import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store) + import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store, verbose=True) course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall') course = module_store.get_item(course_location) @@ -962,8 +962,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): 'vertical', 'vertical_test', None]), depth=1) self.assertTrue(getattr(vertical, 'is_draft', False)) + self.assertNotIn('index_in_children_list', child.xml_attributes) + self.assertNotIn('parent_sequential_url', vertical.xml_attributes) + for child in vertical.get_children(): self.assertTrue(getattr(child, 'is_draft', False)) + self.assertNotIn('index_in_children_list', child.xml_attributes) + if hasattr(child, 'data'): + self.assertNotIn('index_in_children_list', child.data) + self.assertNotIn('parent_sequential_url', child.xml_attributes) + if hasattr(child, 'data'): + self.assertNotIn('parent_sequential_url', child.data) # make sure that we don't have a sequential that is in draft mode sequential = draft_store.get_item(Location(['i4x', 'edX', 'toy', @@ -1074,6 +1083,38 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # It should now contain empty data self.assertEquals(imported_word_cloud.data, '') + def test_html_export_roundtrip(self): + """ + Test that a course which has HTML that has style formatting is preserved in export/import + """ + module_store = modulestore('direct') + content_store = contentstore() + + import_from_xml(module_store, 'common/test/data/', ['toy']) + + location = CourseDescriptor.id_to_location('edX/toy/2012_Fall') + + # Export the course + root_dir = path(mkdtemp_clean()) + export_to_xml(module_store, content_store, location, root_dir, 'test_roundtrip') + + # Reimport and get the video back + import_from_xml(module_store, root_dir) + + # get the sample HTML with styling information + html_module = module_store.get_instance( + 'edX/toy/2012_Fall', + Location(['i4x', 'edX', 'toy', 'html', 'with_styling']) + ) + self.assertIn('

', html_module.data) + + # get the sample HTML with just a simple tag information + html_module = module_store.get_instance( + 'edX/toy/2012_Fall', + Location(['i4x', 'edX', 'toy', 'html', 'just_img']) + ) + self.assertIn('', html_module.data) + def test_course_handouts_rewrites(self): module_store = modulestore('direct') @@ -1384,7 +1425,7 @@ class ContentStoreTest(ModuleStoreTestCase): 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) self.assertContains(resp, 'Chapter 2') # go to various pages @@ -1394,92 +1435,92 @@ class ContentStoreTest(ModuleStoreTestCase): kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # export page resp = self.client.get(reverse('export_course', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # manage users resp = self.client.get(reverse('manage_users', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # course info resp = self.client.get(reverse('course_info', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # settings_details resp = self.client.get(reverse('settings_details', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # settings_details resp = self.client.get(reverse('settings_grading', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # static_pages resp = self.client.get(reverse('static_pages', kwargs={'org': loc.org, 'course': loc.course, 'coursename': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # static_pages resp = self.client.get(reverse('asset_index', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # go look at a subsection page subsection_location = loc.replace(category='sequential', name='test_sequence') resp = self.client.get(reverse('edit_subsection', kwargs={'location': subsection_location.url()})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # go look at the Edit page unit_location = loc.replace(category='vertical', name='test_vertical') resp = self.client.get(reverse('edit_unit', kwargs={'location': unit_location.url()})) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # delete a component del_loc = loc.replace(category='html', name='test_html') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # delete a unit del_loc = loc.replace(category='vertical', name='test_vertical') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # delete a unit del_loc = loc.replace(category='sequential', name='test_sequence') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # delete a chapter del_loc = loc.replace(category='chapter', name='chapter_2') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) def test_import_into_new_course_id(self): module_store = modulestore('direct') diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index dbdf8b3f6e..524dde07e5 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -439,12 +439,12 @@ class CourseGraderUpdatesTest(CourseTestCase): def test_get(self): resp = self.client.get(self.url) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) obj = json.loads(resp.content) def test_delete(self): resp = self.client.delete(self.url) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) def test_post(self): grader = { @@ -455,5 +455,5 @@ class CourseGraderUpdatesTest(CourseTestCase): "weight": 17.3, } resp = self.client.post(self.url, grader) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) obj = json.loads(resp.content) diff --git a/cms/djangoapps/contentstore/tests/test_import_nostatic.py b/cms/djangoapps/contentstore/tests/test_import_nostatic.py new file mode 100644 index 0000000000..67f2202011 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_import_nostatic.py @@ -0,0 +1,128 @@ +#pylint: disable=E1101 +''' +Tests for importing with no static +''' + +from django.test.client import Client +from django.test.utils import override_settings +from django.conf import settings +from path import path +import copy + +from django.contrib.auth.models import User + +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase + +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from xmodule.contentstore.django import contentstore +from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.contentstore.content import StaticContent +from xmodule.contentstore.django import _CONTENTSTORE + +from xmodule.course_module import CourseDescriptor + +from xmodule.exceptions import NotFoundError +from uuid import uuid4 +from pymongo import MongoClient + +TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) +TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex + + +@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) +class ContentStoreImportNoStaticTest(ModuleStoreTestCase): + """ + Tests that rely on the toy and test_import_course courses. + NOTE: refactor using CourseFactory so they do not. + """ + def setUp(self): + + settings.MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') + settings.MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') + uname = 'testuser' + email = 'test+courses@edx.org' + password = 'foo' + + # Create the use so we can log them in. + self.user = User.objects.create_user(uname, email, password) + + # Note that we do not actually need to do anything + # for registration if we directly mark them active. + self.user.is_active = True + # Staff has access to view all courses + self.user.is_staff = True + + # Save the data that we've just changed to the db. + self.user.save() + + self.client = Client() + self.client.login(username=uname, password=password) + + def tearDown(self): + MongoClient().drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) + _CONTENTSTORE.clear() + + def load_test_import_course(self): + ''' + Load the standard course used to test imports (for do_import_static=False behavior). + ''' + content_store = contentstore() + module_store = modulestore('direct') + import_from_xml(module_store, 'common/test/data/', ['test_import_course'], static_content_store=content_store, do_import_static=False, verbose=True) + course_location = CourseDescriptor.id_to_location('edX/test_import_course/2012_Fall') + course = module_store.get_item(course_location) + self.assertIsNotNone(course) + + return module_store, content_store, course, course_location + + def test_static_import(self): + ''' + Stuff in static_import should always be imported into contentstore + ''' + _, content_store, course, course_location = self.load_test_import_course() + + # make sure we have ONE asset in our contentstore ("should_be_imported.html") + all_assets = content_store.get_all_content_for_course(course_location) + print "len(all_assets)=%d" % len(all_assets) + self.assertEqual(len(all_assets), 1) + + content = None + try: + location = StaticContent.get_location_from_path('/c4x/edX/test_import_course/asset/should_be_imported.html') + content = content_store.find(location) + except NotFoundError: + pass + + self.assertIsNotNone(content) + + # make sure course.lms.static_asset_path is correct + print "static_asset_path = {0}".format(course.lms.static_asset_path) + self.assertEqual(course.lms.static_asset_path, 'test_import_course') + + def test_asset_import_nostatic(self): + ''' + This test validates that an image asset is NOT imported when do_import_static=False + ''' + content_store = contentstore() + + module_store = modulestore('direct') + import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store, do_import_static=False, verbose=True) + + course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall') + module_store.get_item(course_location) + + # make sure we have NO assets in our contentstore + all_assets = content_store.get_all_content_for_course(course_location) + print "len(all_assets)=%d" % len(all_assets) + self.assertEqual(len(all_assets), 0) + + def test_no_static_link_rewrites_on_import(self): + module_store = modulestore('direct') + import_from_xml(module_store, 'common/test/data/', ['toy'], do_import_static=False, verbose=True) + + handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'course_info', 'handouts', None])) + self.assertIn('/static/', handouts.data) + + handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None])) + self.assertIn('/static/', handouts.data) diff --git a/cms/djangoapps/contentstore/tests/test_item.py b/cms/djangoapps/contentstore/tests/test_item.py index 260444a8f7..e5ff992cb8 100644 --- a/cms/djangoapps/contentstore/tests/test_item.py +++ b/cms/djangoapps/contentstore/tests/test_item.py @@ -34,7 +34,7 @@ class DeleteItem(CourseTestCase): resp.content, "application/json" ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) class TestCreateItem(CourseTestCase): diff --git a/cms/djangoapps/contentstore/tests/test_textbooks.py b/cms/djangoapps/contentstore/tests/test_textbooks.py index a21a1b1023..950d0f780e 100644 --- a/cms/djangoapps/contentstore/tests/test_textbooks.py +++ b/cms/djangoapps/contentstore/tests/test_textbooks.py @@ -23,7 +23,7 @@ class TextbookIndexTestCase(CourseTestCase): def test_view_index(self): "Basic check that the textbook index page responds correctly" resp = self.client.get(self.url) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # we don't have resp.context right now, # due to bugs in our testing harness :( if resp.context: @@ -36,7 +36,7 @@ class TextbookIndexTestCase(CourseTestCase): HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) obj = json.loads(resp.content) self.assertEqual(self.course.pdf_textbooks, obj) @@ -73,7 +73,7 @@ class TextbookIndexTestCase(CourseTestCase): HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) obj = json.loads(resp.content) self.assertEqual(content, obj) @@ -90,7 +90,7 @@ class TextbookIndexTestCase(CourseTestCase): HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) # reload course store = get_modulestore(self.course.location) @@ -111,7 +111,7 @@ class TextbookIndexTestCase(CourseTestCase): HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) obj = json.loads(resp.content) self.assertIn("error", obj) @@ -184,7 +184,7 @@ class TextbookCreateTestCase(CourseTestCase): HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) self.assertNotIn("Location", resp) @@ -238,14 +238,14 @@ class TextbookByIdTestCase(CourseTestCase): def test_get_1(self): "Get the first textbook" resp = self.client.get(self.url1) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) compare = json.loads(resp.content) self.assertEqual(compare, self.textbook1) def test_get_2(self): "Get the second textbook" resp = self.client.get(self.url2) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) compare = json.loads(resp.content) self.assertEqual(compare, self.textbook2) @@ -257,7 +257,7 @@ class TextbookByIdTestCase(CourseTestCase): def test_delete(self): "Delete a textbook by ID" resp = self.client.delete(self.url1) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) course = self.store.get_item(self.course.location) self.assertEqual(course.pdf_textbooks, [self.textbook2]) @@ -288,7 +288,7 @@ class TextbookByIdTestCase(CourseTestCase): ) self.assertEqual(resp.status_code, 201) resp2 = self.client.get(url) - self.assert2XX(resp2.status_code) + self.assertEqual(resp2.status_code, 200) compare = json.loads(resp2.content) self.assertEqual(compare, textbook) course = self.store.get_item(self.course.location) @@ -311,7 +311,7 @@ class TextbookByIdTestCase(CourseTestCase): ) self.assertEqual(resp.status_code, 201) resp2 = self.client.get(self.url2) - self.assert2XX(resp2.status_code) + self.assertEqual(resp2.status_code, 200) compare = json.loads(resp2.content) self.assertEqual(compare, replacement) course = self.store.get_item(self.course.location) diff --git a/cms/djangoapps/contentstore/tests/test_users.py b/cms/djangoapps/contentstore/tests/test_users.py index cbb8aa8b01..80b2364c43 100644 --- a/cms/djangoapps/contentstore/tests/test_users.py +++ b/cms/djangoapps/contentstore/tests/test_users.py @@ -72,13 +72,13 @@ class UsersTestCase(CourseTestCase): def test_detail_inactive(self): resp = self.client.get(self.inactive_detail_url) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 200) result = json.loads(resp.content) self.assertFalse(result["active"]) def test_detail_invalid(self): resp = self.client.get(self.invalid_detail_url) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 404) result = json.loads(resp.content) self.assertIn("error", result) @@ -87,7 +87,7 @@ class UsersTestCase(CourseTestCase): self.detail_url, data={"role": None}, ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -103,7 +103,7 @@ class UsersTestCase(CourseTestCase): content_type="application/json", HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -122,7 +122,7 @@ class UsersTestCase(CourseTestCase): content_type="application/json", HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -142,7 +142,7 @@ class UsersTestCase(CourseTestCase): content_type="application/json", HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -157,7 +157,7 @@ class UsersTestCase(CourseTestCase): content_type="application/json", HTTP_ACCEPT="application/json", ) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) result = json.loads(resp.content) self.assertIn("error", result) self.assert_not_enrolled() @@ -169,7 +169,7 @@ class UsersTestCase(CourseTestCase): content_type="application/json", HTTP_ACCEPT="application/json", ) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) result = json.loads(resp.content) self.assertIn("error", result) self.assert_not_enrolled() @@ -180,7 +180,7 @@ class UsersTestCase(CourseTestCase): data={"role": "staff"}, HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -197,7 +197,7 @@ class UsersTestCase(CourseTestCase): self.detail_url, HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -214,7 +214,7 @@ class UsersTestCase(CourseTestCase): self.detail_url, HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) groups = [g.name for g in ext_user.groups.all()] @@ -273,7 +273,7 @@ class UsersTestCase(CourseTestCase): data={"role": "instructor"}, HTTP_ACCEPT="application/json", ) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) result = json.loads(resp.content) self.assertIn("error", result) @@ -288,7 +288,7 @@ class UsersTestCase(CourseTestCase): data={"role": "instructor"}, HTTP_ACCEPT="application/json", ) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) result = json.loads(resp.content) self.assertIn("error", result) @@ -306,7 +306,7 @@ class UsersTestCase(CourseTestCase): }) resp = self.client.delete(self_url) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) # reload user from DB user = User.objects.get(email=self.user.email) groups = [g.name for g in user.groups.all()] @@ -321,7 +321,7 @@ class UsersTestCase(CourseTestCase): self.ext_user.save() resp = self.client.delete(self.detail_url) - self.assert4XX(resp.status_code) + self.assertEqual(resp.status_code, 400) result = json.loads(resp.content) self.assertIn("error", result) # reload user from DB @@ -347,7 +347,7 @@ class UsersTestCase(CourseTestCase): self.detail_url, HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) self.assert_enrolled() def test_staff_to_instructor_still_enrolled(self): @@ -366,7 +366,7 @@ class UsersTestCase(CourseTestCase): content_type="application/json", HTTP_ACCEPT="application/json", ) - self.assert2XX(resp.status_code) + self.assertEqual(resp.status_code, 204) self.assert_enrolled() def assert_not_enrolled(self): diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 7debfe18d1..3b89e2e988 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -8,6 +8,7 @@ so that we can run the lettuce acceptance tests. # pylint: disable=W0401, W0614 from .test import * +from lms.envs.sauce import * # You need to start the server in debug mode, # otherwise the browser will not render the pages correctly @@ -17,7 +18,7 @@ DEBUG = True import logging logging.disable(logging.ERROR) import os -import random +from random import choice, randint def seed(): @@ -75,7 +76,6 @@ DATABASES = { # Use the auto_auth workflow for creating users and logging them in MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True - # HACK # Setting this flag to false causes imports to not load correctly in the lettuce python files # We do not yet understand why this occurs. Setting this to true is a stopgap measure @@ -84,5 +84,5 @@ USE_I18N = True # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) -LETTUCE_SERVER_PORT = random.randint(1024, 65535) -LETTUCE_BROWSER = 'chrome' +LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535) +LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome') diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py index 2e519edb30..a17df56a71 100644 --- a/common/djangoapps/course_groups/tests/tests.py +++ b/common/djangoapps/course_groups/tests/tests.py @@ -8,19 +8,20 @@ from course_groups.models import CourseUserGroup from course_groups.cohorts import (get_cohort, get_course_cohorts, is_commentable_cohorted, get_cohort_by_name) -from xmodule.modulestore.django import modulestore, _MODULESTORES +from xmodule.modulestore.django import modulestore, clear_existing_modulestores -from xmodule.modulestore.tests.django_utils import xml_store_config +from xmodule.modulestore.tests.django_utils import mixed_store_config # NOTE: running this with the lms.envs.test config works without # manually overriding the modulestore. However, running with # cms.envs.test doesn't. TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) +TEST_MAPPING = {'edX/toy/2012_Fall': 'xml'} +TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, TEST_MAPPING) -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestCohorts(django.test.TestCase): @staticmethod @@ -82,9 +83,7 @@ class TestCohorts(django.test.TestCase): """ Make sure that course is reloaded every time--clear out the modulestore. """ - # don't like this, but don't know a better way to undo all changes made - # to course. We don't have a course.clone() method. - _MODULESTORES.clear() + clear_existing_modulestores() def test_get_cohort(self): """ diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index 561c078b3b..7a5e711f44 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -33,6 +33,7 @@ class CourseMode(models.Model): currency = models.CharField(default="usd", max_length=8) DEFAULT_MODE = Mode('honor', _('Honor Code Certificate'), 0, '', 'usd') + DEFAULT_MODE_SLUG = 'honor' class Meta: """ meta attributes of this model """ @@ -51,3 +52,18 @@ class CourseMode(models.Model): if not modes: modes = [cls.DEFAULT_MODE] return modes + + @classmethod + def mode_for_course(cls, course_id, mode_slug): + """ + Returns the mode for the course corresponding to mode_slug. + + If this particular mode is not set for the course, returns None + """ + modes = cls.modes_for_course(course_id) + + matched = [m for m in modes if m.slug == mode_slug] + if matched: + return matched[0] + else: + return None diff --git a/common/djangoapps/course_modes/tests.py b/common/djangoapps/course_modes/tests.py index 907797bf17..1fba5ca197 100644 --- a/common/djangoapps/course_modes/tests.py +++ b/common/djangoapps/course_modes/tests.py @@ -60,3 +60,6 @@ class CourseModeModelTest(TestCase): modes = CourseMode.modes_for_course(self.course_id) self.assertEqual(modes, set_modes) + self.assertEqual(mode1, CourseMode.mode_for_course(self.course_id, u'honor')) + self.assertEqual(mode2, CourseMode.mode_for_course(self.course_id, u'verified')) + self.assertIsNone(CourseMode.mode_for_course(self.course_id, 'DNE')) diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py index 6bb9c38e6f..0355730256 100644 --- a/common/djangoapps/external_auth/tests/test_shib.py +++ b/common/djangoapps/external_auth/tests/test_shib.py @@ -14,11 +14,9 @@ from django.contrib.auth.models import AnonymousUser, User from django.utils.importlib import import_module from xmodule.modulestore.tests.factories import CourseFactory -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config from xmodule.modulestore.inheritance import own_metadata -from xmodule.modulestore.django import modulestore - -from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from xmodule.modulestore.django import editable_modulestore from external_auth.models import ExternalAuthMap from external_auth.views import shib_login, course_specific_login, course_specific_register @@ -27,6 +25,8 @@ from student.views import create_account, change_enrollment from student.models import UserProfile, Registration, CourseEnrollment from student.tests.factories import UserFactory +TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}) + # Shib is supposed to provide 'REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider' # attributes via request.META. We can count on 'Shib-Identity-Provider', and 'REMOTE_USER' being present # b/c of how mod_shib works but should test the behavior with the rest of the attributes present/missing @@ -64,7 +64,7 @@ def gen_all_identities(): yield _build_identity_dict(mail, given_name, surname) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache') +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache') class ShibSPTest(ModuleStoreTestCase): """ Tests for the Shibboleth SP, which communicates via request.META @@ -73,7 +73,7 @@ class ShibSPTest(ModuleStoreTestCase): request_factory = RequestFactory() def setUp(self): - self.store = modulestore() + self.store = editable_modulestore() @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True) def test_exception_shib_login(self): diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py index d7f2df8322..712664bf39 100644 --- a/common/djangoapps/static_replace/__init__.py +++ b/common/djangoapps/static_replace/__init__.py @@ -90,7 +90,7 @@ def replace_course_urls(text, course_id): return re.sub(_url_replace_regex('/course/'), replace_course_url, text) -def replace_static_urls(text, data_directory, course_id=None): +def replace_static_urls(text, data_directory, course_id=None, static_asset_path=''): """ Replace /static/$stuff urls either with their correct url as generated by collectstatic, (/static/$md5_hashed_stuff) or by the course-specific content static url @@ -100,6 +100,7 @@ def replace_static_urls(text, data_directory, course_id=None): text: The source text to do the substitution in data_directory: The directory in which course data is stored course_id: The course identifier used to distinguish static content for this course in studio + static_asset_path: Path for static assets, which overrides data_directory and course_namespace, if nonempty """ def replace_static_url(match): @@ -116,7 +117,7 @@ def replace_static_urls(text, data_directory, course_id=None): if settings.DEBUG and finders.find(rest, True): return original # if we're running with a MongoBacked store course_namespace is not None, then use studio style urls - elif course_id and modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE: + elif (not static_asset_path) and course_id and modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE: # first look in the static file pipeline and see if we are trying to reference # a piece of static content which is in the mitx repo (e.g. JS associated with an xmodule) @@ -135,7 +136,7 @@ def replace_static_urls(text, data_directory, course_id=None): url = StaticContent.convert_legacy_static_url_with_course_id(rest, course_id) # Otherwise, look the file up in staticfiles_storage, and append the data directory if needed else: - course_path = "/".join((data_directory, rest)) + course_path = "/".join((static_asset_path or data_directory, rest)) try: if staticfiles_storage.exists(rest): @@ -152,7 +153,7 @@ def replace_static_urls(text, data_directory, course_id=None): return re.sub( - _url_replace_regex('/static/(?!{data_dir})'.format(data_dir=data_directory)), + _url_replace_regex('/static/(?!{data_dir})'.format(data_dir=static_asset_path or data_directory)), replace_static_url, text ) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 6b5897e97d..3d977b28c9 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -827,9 +827,6 @@ class CourseEnrollment(models.Model): @classmethod def is_enrolled(cls, user, course_id): """ - Remove the user from a given course. If the relevant `CourseEnrollment` - object doesn't exist, we log an error but don't throw an exception. - Returns True if the user is enrolled in the course (the entry must exist and it must have `is_active=True`). Otherwise, returns False. diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index c2bf2bbbf3..4bf1aea40c 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -11,6 +11,10 @@ from logging import getLogger from django.core.management import call_command from django.conf import settings from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from requests import put +from base64 import encodestring +from json import dumps # Let the LMS and CMS do their one-time setup # For example, setting up mongo caches @@ -42,43 +46,93 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...") MAX_VALID_BROWSER_ATTEMPTS = 20 +def get_username_and_key(): + """ + Returns the Sauce Labs username and access ID as set by environment variables + """ + return {"username": settings.SAUCE.get('USERNAME'), "access-key": settings.SAUCE.get('ACCESS_ID')} + + +def set_job_status(jobid, passed=True): + """ + Sets the job status on sauce labs + """ + body_content = dumps({"passed": passed}) + config = get_username_and_key() + base64string = encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] + result = put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid), + data=body_content, + headers={"Authorization": "Basic {}".format(base64string)}) + return result.status_code == 200 + + +def make_desired_capabilities(): + """ + Returns a DesiredCapabilities object corresponding to the environment sauce parameters + """ + desired_capabilities = settings.SAUCE.get('BROWSER', DesiredCapabilities.CHROME) + desired_capabilities['platform'] = settings.SAUCE.get('PLATFORM') + desired_capabilities['version'] = settings.SAUCE.get('VERSION') + desired_capabilities['device-type'] = settings.SAUCE.get('DEVICE') + desired_capabilities['name'] = settings.SAUCE.get('SESSION') + desired_capabilities['build'] = settings.SAUCE.get('BUILD') + desired_capabilities['video-upload-on-pass'] = False + desired_capabilities['sauce-advisor'] = False + desired_capabilities['record-screenshots'] = False + desired_capabilities['selenium-version'] = "2.34.0" + desired_capabilities['max-duration'] = 3600 + desired_capabilities['public'] = 'public restricted' + return desired_capabilities + + @before.harvest def initial_setup(server): """ Launch the browser once before executing the tests. """ - browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') + world.absorb(settings.SAUCE.get('SAUCE_ENABLED'), 'SAUCE_ENABLED') - # There is an issue with ChromeDriver2 r195627 on Ubuntu - # in which we sometimes get an invalid browser session. - # This is a work-around to ensure that we get a valid session. - success = False - num_attempts = 0 - while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: + if not world.SAUCE_ENABLED: + browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') - # Get a browser session - world.browser = Browser(browser_driver) + # There is an issue with ChromeDriver2 r195627 on Ubuntu + # in which we sometimes get an invalid browser session. + # This is a work-around to ensure that we get a valid session. + success = False + num_attempts = 0 + while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: + world.browser = Browser(browser_driver) - # Try to visit the main page - # If the browser session is invalid, this will - # raise a WebDriverException - try: - world.visit('/') + # Try to visit the main page + # If the browser session is invalid, this will + # raise a WebDriverException + try: + world.visit('/') - except WebDriverException: - world.browser.quit() - num_attempts += 1 + except WebDriverException: + world.browser.quit() + num_attempts += 1 - else: - success = True + else: + success = True - # If we were unable to get a valid session within the limit of attempts, - # then we cannot run the tests. - if not success: - raise IOError("Could not acquire valid {driver} browser session.".format(driver=browser_driver)) + # If we were unable to get a valid session within the limit of attempts, + # then we cannot run the tests. + if not success: + raise IOError("Could not acquire valid {driver} browser session.".format(driver=browser_driver)) - # Set the browser size to 1280x1024 - world.browser.driver.set_window_size(1280, 1024) + world.browser.driver.set_window_size(1280, 1024) + + else: + config = get_username_and_key() + world.browser = Browser( + 'remote', + url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']), + **make_desired_capabilities() + ) + world.browser.driver.implicitly_wait(30) + + world.absorb(world.browser.driver.session_id, 'jobid') @before.each_scenario @@ -97,7 +151,6 @@ def clear_data(scenario): world.spew('scenario_dict') - @after.each_scenario def reset_databases(scenario): ''' @@ -108,9 +161,10 @@ def reset_databases(scenario): mongo = MongoClient() mongo.drop_database(settings.CONTENTSTORE['OPTIONS']['db']) _CONTENTSTORE.clear() - modulestore = xmodule.modulestore.django.modulestore() + + modulestore = xmodule.modulestore.django.editable_modulestore() modulestore.collection.drop() - xmodule.modulestore.django._MODULESTORES.clear() + xmodule.modulestore.django.clear_existing_modulestores() # Uncomment below to trigger a screenshot on error @@ -128,4 +182,6 @@ def teardown_browser(total): """ Quit the browser after executing the tests. """ + if world.SAUCE_ENABLED: + set_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed) world.browser.quit() diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index eca3290080..fc01d25d66 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -10,7 +10,7 @@ from django.contrib.auth import authenticate, login from django.contrib.auth.middleware import AuthenticationMiddleware from django.contrib.sessions.middleware import SessionMiddleware from student.models import CourseEnrollment -from xmodule.modulestore.django import modulestore +from xmodule.modulestore.django import editable_modulestore from xmodule.contentstore.django import contentstore from urllib import quote_plus @@ -60,11 +60,9 @@ def register_by_course_id(course_id, is_staff=False): @world.absorb def clear_courses(): # Flush and initialize the module store - # It needs the templates because it creates new records - # by cloning from the template. # Note that if your test module gets in some weird state # (though it shouldn't), do this manually # from the bash shell to drop it: # $ mongo test_xmodule --eval "db.dropDatabase()" - modulestore().collection.drop() + editable_modulestore().collection.drop() contentstore().fs_files.drop() diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 9cf2aeda49..f13b3ff932 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -99,7 +99,7 @@ def i_am_logged_in_user(step): @step('I am not logged in$') def i_am_not_logged_in(step): - world.browser.cookies.delete() + world.visit('logout') @step('I am staff for course "([^"]*)"$') @@ -138,10 +138,13 @@ def should_have_link_with_path_and_text(step, path, text): @step(r'should( not)? see "(.*)" (?:somewhere|anywhere) (?:in|on) (?:the|this) page') def should_see_in_the_page(step, doesnt_appear, text): + multiplier = 1 + if world.SAUCE_ENABLED: + multiplier = 2 if doesnt_appear: - assert world.browser.is_text_not_present(text, wait_time=5) + assert world.browser.is_text_not_present(text, wait_time=5*multiplier) else: - assert world.browser.is_text_present(text, wait_time=5) + assert world.browser.is_text_present(text, wait_time=5*multiplier) @step('I am logged in$') @@ -150,7 +153,7 @@ def i_am_logged_in(step): world.log_in(username='robot', password='test') world.browser.visit(django_url('/')) # You should not see the login link - assert_equals(world.browser.find_by_css('a#login'), []) + assert world.is_css_not_present('a#login') @step(u'I am an edX user$') diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index 6b73395599..16fe12371b 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -76,7 +76,7 @@ def replace_course_urls(get_html, course_id): return _get_html -def replace_static_urls(get_html, data_dir, course_id=None): +def replace_static_urls(get_html, data_dir, course_id=None, static_asset_path=''): """ Updates the supplied module with a new get_html function that wraps the old get_html function and substitutes urls of the form /static/... @@ -85,7 +85,7 @@ def replace_static_urls(get_html, data_dir, course_id=None): @wraps(get_html) def _get_html(): - return static_replace.replace_static_urls(get_html(), data_dir, course_id) + return static_replace.replace_static_urls(get_html(), data_dir, course_id, static_asset_path=static_asset_path) return _get_html diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 7cbb3b2b67..f7960b13b1 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -213,7 +213,7 @@ class CombinedOpenEndedFields(object): help="The number of times the student can try to answer this problem.", default=1, scope=Scope.settings, - values={"min" : 1 } + values={"min": 1 } ) accept_file_upload = Boolean( display_name="Allow File Uploads", @@ -229,12 +229,10 @@ class CombinedOpenEndedFields(object): ) due = Date( help="Date that this problem is due by", - default=None, scope=Scope.settings ) graceperiod = Timedelta( 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) @@ -244,7 +242,7 @@ class CombinedOpenEndedFields(object): 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"}, + values={"min": 0, "step": ".1"}, default=1 ) min_to_calibrate = Integer( @@ -252,28 +250,28 @@ class CombinedOpenEndedFields(object): help="The minimum number of calibration essays each student will need to complete for peer grading.", default=3, scope=Scope.settings, - values={"min" : 1, "max" : 20, "step" : "1"} + values={"min": 1, "max": 20, "step": "1"} ) max_to_calibrate = Integer( display_name="Maximum Peer Grading Calibrations", help="The maximum number of calibration essays each student will need to complete for peer grading.", default=6, scope=Scope.settings, - values={"min" : 1, "max" : 20, "step" : "1"} + values={"min": 1, "max": 20, "step": "1"} ) peer_grader_count = Integer( display_name="Peer Graders per Response", help="The number of peers who will grade each submission.", default=3, scope=Scope.settings, - values={"min" : 1, "step" : "1", "max" : 5} + values={"min": 1, "step": "1", "max": 5} ) required_peer_grading = Integer( display_name="Required Peer Grading", help="The number of other students each student making a submission will have to grade.", default=3, scope=Scope.settings, - values={"min" : 1, "step" : "1", "max" : 5} + values={"min": 1, "step": "1", "max": 5} ) markdown = String( help="Markdown source of this module", diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 4555395fef..10bcf4a4e3 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -147,51 +147,51 @@ class TextbookList(List): class CourseFields(object): textbooks = TextbookList(help="List of pairs of (title, url) for textbooks used in this course", - default=[], scope=Scope.content) + default=[], scope=Scope.content) wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content) enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings) enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings) start = Date(help="Start time when this module is visible", - # using now(UTC()) resulted in fractional seconds which screwed up comparisons and anyway w/b the - # time of first invocation of this stmt on the server - default=datetime.fromtimestamp(0, UTC()), - scope=Scope.settings) + # using now(UTC()) resulted in fractional seconds which screwed up comparisons and anyway w/b the + # time of first invocation of this stmt on the server + default=datetime.fromtimestamp(0, UTC()), + scope=Scope.settings) end = Date(help="Date that this class ends", scope=Scope.settings) advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings) grading_policy = Dict(help="Grading policy definition for this class", - default={"GRADER": [ - { - "type": "Homework", - "min_count": 12, - "drop_count": 2, - "short_label": "HW", - "weight": 0.15 - }, - { - "type": "Lab", - "min_count": 12, - "drop_count": 2, - "weight": 0.15 - }, - { - "type": "Midterm Exam", - "short_label": "Midterm", - "min_count": 1, - "drop_count": 0, - "weight": 0.3 - }, - { - "type": "Final Exam", - "short_label": "Final", - "min_count": 1, - "drop_count": 0, - "weight": 0.4 - } - ], - "GRADE_CUTOFFS": { - "Pass": 0.5 - }}, - scope=Scope.content) + default={"GRADER": [ + { + "type": "Homework", + "min_count": 12, + "drop_count": 2, + "short_label": "HW", + "weight": 0.15 + }, + { + "type": "Lab", + "min_count": 12, + "drop_count": 2, + "weight": 0.15 + }, + { + "type": "Midterm Exam", + "short_label": "Midterm", + "min_count": 1, + "drop_count": 0, + "weight": 0.3 + }, + { + "type": "Final Exam", + "short_label": "Final", + "min_count": 1, + "drop_count": 0, + "weight": 0.4 + } + ], + "GRADE_CUTOFFS": { + "Pass": 0.5 + }}, + scope=Scope.content) show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings) display_name = String(help="Display name for this module", default="Empty", display_name="Display Name", scope=Scope.settings) show_chat = Boolean(help="Whether to show the chat widget in this course", default=False, scope=Scope.settings) @@ -201,7 +201,7 @@ class CourseFields(object): discussion_topics = Dict( help="Map of topics names to ids", scope=Scope.settings - ) + ) testcenter_info = Dict(help="Dictionary of Test Center info", scope=Scope.settings) announcement = Date(help="Date this course is announced", scope=Scope.settings) cohort_config = Dict(help="Dictionary defining cohort configuration", scope=Scope.settings) @@ -216,128 +216,124 @@ class CourseFields(object): advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings) has_children = True checklists = List(scope=Scope.settings, - default=[ - {"short_description" : "Getting Started With Studio", - "items" : [{"short_description": "Add Course Team Members", - "long_description": "Grant your collaborators permission to edit your course so you can work together.", - "is_checked": False, - "action_url": "ManageUsers", - "action_text": "Edit Course Team", - "action_external": False}, - {"short_description": "Set Important Dates for Your Course", - "long_description": "Establish your course's student enrollment and launch dates on the Schedule and Details page.", - "is_checked": False, - "action_url": "SettingsDetails", - "action_text": "Edit Course Details & Schedule", - "action_external": False}, - {"short_description": "Draft Your Course's Grading Policy", - "long_description": "Set up your assignment types and grading policy even if you haven't created all your assignments.", - "is_checked": False, - "action_url": "SettingsGrading", - "action_text": "Edit Grading Settings", - "action_external": False}, - {"short_description": "Explore the Other Studio Checklists", - "long_description": "Discover other available course authoring tools, and find help when you need it.", - "is_checked": False, - "action_url": "", - "action_text": "", - "action_external": False}] - }, - {"short_description" : "Draft a Rough Course Outline", - "items" : [{"short_description": "Create Your First Section and Subsection", - "long_description": "Use your course outline to build your first Section and Subsection.", - "is_checked": False, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": False}, - {"short_description": "Set Section Release Dates", - "long_description": "Specify the release dates for each Section in your course. Sections become visible to students on their release dates.", - "is_checked": False, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": False}, - {"short_description": "Designate a Subsection as Graded", - "long_description": "Set a Subsection to be graded as a specific assignment type. Assignments within graded Subsections count toward a student's final grade.", - "is_checked": False, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": False}, - {"short_description": "Reordering Course Content", - "long_description": "Use drag and drop to reorder the content in your course.", - "is_checked": False, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": False}, - {"short_description": "Renaming Sections", - "long_description": "Rename Sections by clicking the Section name from the Course Outline.", - "is_checked": False, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": False}, - {"short_description": "Deleting Course Content", - "long_description": "Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is no Undo function.", - "is_checked": False, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": False}, - {"short_description": "Add an Instructor-Only Section to Your Outline", - "long_description": "Some course authors find using a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future.", - "is_checked": False, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": False}] - }, - {"short_description" : "Explore edX's Support Tools", - "items" : [{"short_description": "Explore the Studio Help Forum", - "long_description": "Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio.", - "is_checked": False, - "action_url": "http://help.edge.edx.org/", - "action_text": "Visit Studio Help", - "action_external": True}, - {"short_description": "Enroll in edX 101", - "long_description": "Register for edX 101, edX's primer for course creation.", - "is_checked": False, - "action_url": "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about", - "action_text": "Register for edX 101", - "action_external": True}, - {"short_description": "Download the Studio Documentation", - "long_description": "Download the searchable Studio reference documentation in PDF form.", - "is_checked": False, - "action_url": "http://files.edx.org/Getting_Started_with_Studio.pdf", - "action_text": "Download Documentation", - "action_external": True}] - }, - {"short_description" : "Draft Your Course About Page", - "items" : [{"short_description": "Draft a Course Description", - "long_description": "Courses on edX have an About page that includes a course video, description, and more. Draft the text students will read before deciding to enroll in your course.", - "is_checked": False, - "action_url": "SettingsDetails", - "action_text": "Edit Course Schedule & Details", - "action_external": False}, - {"short_description": "Add Staff Bios", - "long_description": "Showing prospective students who their instructor will be is helpful. Include staff bios on the course About page.", - "is_checked": False, - "action_url": "SettingsDetails", - "action_text": "Edit Course Schedule & Details", - "action_external": False}, - {"short_description": "Add Course FAQs", - "long_description": "Include a short list of frequently asked questions about your course.", - "is_checked": False, - "action_url": "SettingsDetails", - "action_text": "Edit Course Schedule & Details", - "action_external": False}, - {"short_description": "Add Course Prerequisites", - "long_description": "Let students know what knowledge and/or skills they should have before they enroll in your course.", - "is_checked": False, - "action_url": "SettingsDetails", - "action_text": "Edit Course Schedule & Details", - "action_external": False}] - } + default=[ + {"short_description": "Getting Started With Studio", + "items": [{"short_description": "Add Course Team Members", + "long_description": "Grant your collaborators permission to edit your course so you can work together.", + "is_checked": False, + "action_url": "ManageUsers", + "action_text": "Edit Course Team", + "action_external": False}, + {"short_description": "Set Important Dates for Your Course", + "long_description": "Establish your course's student enrollment and launch dates on the Schedule and Details page.", + "is_checked": False, + "action_url": "SettingsDetails", + "action_text": "Edit Course Details & Schedule", + "action_external": False}, + {"short_description": "Draft Your Course's Grading Policy", + "long_description": "Set up your assignment types and grading policy even if you haven't created all your assignments.", + "is_checked": False, + "action_url": "SettingsGrading", + "action_text": "Edit Grading Settings", + "action_external": False}, + {"short_description": "Explore the Other Studio Checklists", + "long_description": "Discover other available course authoring tools, and find help when you need it.", + "is_checked": False, + "action_url": "", + "action_text": "", + "action_external": False}]}, + {"short_description": "Draft a Rough Course Outline", + "items": [{"short_description": "Create Your First Section and Subsection", + "long_description": "Use your course outline to build your first Section and Subsection.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}, + {"short_description": "Set Section Release Dates", + "long_description": "Specify the release dates for each Section in your course. Sections become visible to students on their release dates.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}, + {"short_description": "Designate a Subsection as Graded", + "long_description": "Set a Subsection to be graded as a specific assignment type. Assignments within graded Subsections count toward a student's final grade.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}, + {"short_description": "Reordering Course Content", + "long_description": "Use drag and drop to reorder the content in your course.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}, + {"short_description": "Renaming Sections", + "long_description": "Rename Sections by clicking the Section name from the Course Outline.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}, + {"short_description": "Deleting Course Content", + "long_description": "Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is no Undo function.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}, + {"short_description": "Add an Instructor-Only Section to Your Outline", + "long_description": "Some course authors find using a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}]}, + {"short_description": "Explore edX's Support Tools", + "items": [{"short_description": "Explore the Studio Help Forum", + "long_description": "Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio.", + "is_checked": False, + "action_url": "http://help.edge.edx.org/", + "action_text": "Visit Studio Help", + "action_external": True}, + {"short_description": "Enroll in edX 101", + "long_description": "Register for edX 101, edX's primer for course creation.", + "is_checked": False, + "action_url": "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about", + "action_text": "Register for edX 101", + "action_external": True}, + {"short_description": "Download the Studio Documentation", + "long_description": "Download the searchable Studio reference documentation in PDF form.", + "is_checked": False, + "action_url": "http://files.edx.org/Getting_Started_with_Studio.pdf", + "action_text": "Download Documentation", + "action_external": True}]}, + {"short_description": "Draft Your Course About Page", + "items": [{"short_description": "Draft a Course Description", + "long_description": "Courses on edX have an About page that includes a course video, description, and more. Draft the text students will read before deciding to enroll in your course.", + "is_checked": False, + "action_url": "SettingsDetails", + "action_text": "Edit Course Schedule & Details", + "action_external": False}, + {"short_description": "Add Staff Bios", + "long_description": "Showing prospective students who their instructor will be is helpful. Include staff bios on the course About page.", + "is_checked": False, + "action_url": "SettingsDetails", + "action_text": "Edit Course Schedule & Details", + "action_external": False}, + {"short_description": "Add Course FAQs", + "long_description": "Include a short list of frequently asked questions about your course.", + "is_checked": False, + "action_url": "SettingsDetails", + "action_text": "Edit Course Schedule & Details", + "action_external": False}, + {"short_description": "Add Course Prerequisites", + "long_description": "Let students know what knowledge and/or skills they should have before they enroll in your course.", + "is_checked": False, + "action_url": "SettingsDetails", + "action_text": "Edit Course Schedule & Details", + "action_external": False}]} ]) info_sidebar_name = String(scope=Scope.settings, default='Course Handouts') show_timezone = Boolean(help="True if timezones should be shown on dates in the courseware", scope=Scope.settings, default=True) enrollment_domain = String(help="External login method associated with user accounts allowed to register in course", - scope=Scope.settings) + scope=Scope.settings) course_image = String( help="Filename of the course image", scope=Scope.settings, diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 2d8fb765d9..13bdc52538 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -19,10 +19,10 @@ h2 { iframe[seamless]{ - background-color: transparent; - border: 0px none transparent; - padding: 0px; - overflow: hidden; + overflow: hidden; + padding: 0px; + border: 0px none transparent; + background-color: transparent; } .inline-error { @@ -31,17 +31,17 @@ iframe[seamless]{ section.problem-progress { display: inline-block; - color: #999; - font-size: em(16); - font-weight: 100; padding-left: 5px; + color: #999; + font-weight: 100; + font-size: em(16); } section.problem { @media print { display: block; - width: auto; padding: 0; + width: auto; canvas, img { page-break-inside: avoid; @@ -49,30 +49,29 @@ section.problem { } .inline { - display: inline; + display: inline; } .choicegroup { @include clearfix; - - label.choicegroup_correct{ - &:after{ - content: url('../images/correct-icon.png'); - margin-left:15px - } - } - - label.choicegroup_incorrect{ - &:after{ - content: url('../images/incorrect-icon.png'); - margin-left:15px; - } - } - - min-width:100px; + min-width: 100px; width: auto !important; width: 100px; + label.choicegroup_correct { + &:after { + margin-left: 15px; + content: url('../images/correct-icon.png'); + } + } + + label.choicegroup_incorrect { + &:after { + margin-left: 15px; + content: url('../images/incorrect-icon.png'); + } + } + .indicator_container { float: left; width: 25px; @@ -82,9 +81,9 @@ section.problem { fieldset { @include box-sizing(border-box); + margin: 0px 0px $baseline; + padding-left: $baseline; border-left: 1px solid #ddd; - padding-left: 20px; - margin: 0px 0px 20px; } input[type="radio"], @@ -102,21 +101,21 @@ section.problem { ol.enumerate { li { &:before { - content: " "; display: block; - height: 0; visibility: hidden; + height: 0; + content: " "; } } } .solution-span { > span { - margin: 20px 0; + margin: $baseline 0; display: block; border: 1px solid #ddd; - padding: 9px 15px 20px; - background: #FFF; + padding: 9px 15px $baseline; + background: #fff; position: relative; box-shadow: inset 0 0 0 1px #eee; border-radius: 3px; @@ -133,26 +132,26 @@ section.problem { margin-top: -2px; } &.status { + margin: 8px 0 0 $baseline/2; text-indent: -9999px; - margin: 8px 0 0 10px; } } &.unanswered { p.status { @include inline-block(); - background: url('../images/unanswered-icon.png') center center no-repeat; - height: 14px; width: 14px; + height: 14px; + background: url('../images/unanswered-icon.png') center center no-repeat; } } &.correct, &.ui-icon-check { p.status { @include inline-block(); - background: url('../images/correct-icon.png') center center no-repeat; - height: 20px; width: 25px; + height: 20px; + background: url('../images/correct-icon.png') center center no-repeat; } input { @@ -163,9 +162,9 @@ section.problem { &.processing { p.status { @include inline-block(); - background: url('../images/spinner.gif') center center no-repeat; - height: 20px; width: 20px; + height: 20px; + background: url('../images/spinner.gif') center center no-repeat; } input { @@ -176,9 +175,9 @@ section.problem { &.incorrect, &.incomplete, &.ui-icon-close { p.status { @include inline-block(); - background: url('../images/incorrect-icon.png') center center no-repeat; - height: 20px; width: 20px; + height: 20px; + background: url('../images/incorrect-icon.png') center center no-repeat; text-indent: -9999px; } @@ -195,12 +194,12 @@ section.problem { p.answer { @include inline-block(); margin-bottom: 0; - margin-left: 10px; + margin-left: $baseline/2; &:before { + display: inline; content: "Answer: "; font-weight: bold; - display: inline; } &:empty { @@ -228,12 +227,12 @@ section.problem { margin-bottom: 0; &.math { - padding: 6px; - background: #f1f1f1; - border: 1px solid #e3e3e3; @include inline-block; - border-radius: 4px; + padding: 6px; min-width: 30px; + border: 1px solid #e3e3e3; + border-radius: 4px; + background: #f1f1f1; } } } @@ -241,98 +240,91 @@ section.problem { span { &.unanswered, &.ui-icon-bullet { @include inline-block(); - background: url('../images/unanswered-icon.png') center center no-repeat; - height: 14px; position: relative; top: 4px; width: 14px; + height: 14px; + background: url('../images/unanswered-icon.png') center center no-repeat; } &.processing, &.ui-icon-processing { @include inline-block(); - background: url('../images/spinner.gif') center center no-repeat; - height: 20px; position: relative; top: 6px; width: 25px; + height: 20px; + background: url('../images/spinner.gif') center center no-repeat; } &.correct, &.ui-icon-check { @include inline-block(); - background: url('../images/correct-icon.png') center center no-repeat; - height: 20px; position: relative; top: 3px; width: 25px; + height: 20px; + background: url('../images/correct-icon.png') center center no-repeat; } &.partially-correct { @include inline-block(); - background: url('../images/partially-correct-icon.png') center center no-repeat; - height: 20px; position: relative; top: 6px; width: 25px; + height: 20px; + background: url('../images/partially-correct-icon.png') center center no-repeat; } &.incorrect, &.incomplete, &.ui-icon-close { @include inline-block(); - background: url('../images/incorrect-icon.png') center center no-repeat; - height: 20px; - width: 20px; position: relative; top: 3px; + width: 20px; + height: 20px; + background: url('../images/incorrect-icon.png') center center no-repeat; } } - .reload - { + .reload { float:right; - margin: 10px; + margin: $baseline/2; } .grader-status { - padding: 9px; - background: #F6F6F6; - border: 1px solid #ddd; - border-top: 0; - margin-bottom: 20px; @include clearfix; + margin: $baseline/2 0; + padding: $baseline/2; + border-radius: 5px; + background: #F6F6F6; span { - text-indent: -9999px; - overflow: hidden; display: block; float: left; + overflow: hidden; margin: -7px 7px 0 0; + text-indent: -9999px; } .grading { - background: url('../images/info-icon.png') left center no-repeat; - padding-left: 25px; - text-indent: 0px; margin: 0px 7px 0 0; + padding-left: 25px; + background: url('../images/info-icon.png') left center no-repeat; + text-indent: 0px; } p { - line-height: 20px; - text-transform: capitalize; - margin-bottom: 0; float: left; + margin-bottom: 0; + text-transform: capitalize; + line-height: 20px; } &.file { - background: #FFF; - margin-top: 20px; - padding: 20px 0 0 0; - - border: { - top: 1px solid #eee; - right: 0; - bottom: 0; - left: 0; - } + margin-top: $baseline; + padding: $baseline 0 0 0; + border: 0; + border-top: 1px solid #eee; + background: #fff; p.debug { display: none; @@ -345,54 +337,54 @@ section.problem { } .evaluation { - p { - margin-bottom: 4px; - } + p { + margin-bottom: 4px; + } } .feedback-on-feedback { - height: 100px; - margin-right: 20px; + margin-right: $baseline; + height: 100px; } .evaluation-response { - header { - text-align: right; - a { - font-size: .85em; - } + header { + text-align: right; + a { + font-size: .85em; } + } } .evaluation-scoring { - .scoring-list { - list-style-type: none; - margin-left: 3px; + .scoring-list { + margin-left: 3px; + list-style-type: none; - li { - &:first-child { - margin-left: 0px; - } - display:inline; - margin-left: 50px; + li { + display:inline; + margin-left: 50px; + + &:first-child { + margin-left: 0px; + } - label { - font-size: .9em; - } - - } + label { + font-size: .9em; + } } - + } } + .submit-message-container { - margin: 10px 0px ; + margin: $baseline 0px ; } } form.option-input { - margin: -10px 0 20px; - padding-bottom: 20px; + margin: -$baseline/2 0 $baseline; + padding-bottom: $baseline; select { margin-right: flex-gutter(); @@ -400,17 +392,17 @@ section.problem { } ul { - list-style: disc outside none; margin-bottom: lh(); margin-left: .75em; margin-left: .75rem; + list-style: disc outside none; } ol { - list-style: decimal outside none; margin-bottom: lh(); margin-left: .75em; margin-left: .75rem; + list-style: decimal outside none; } dl { @@ -431,8 +423,8 @@ section.problem { } li { - line-height: 1.4em; margin-bottom: lh(.5); + line-height: 1.4em; &:last-child { margin-bottom: 0; @@ -449,8 +441,8 @@ section.problem { table-layout: auto; th { - font-weight: bold; text-align: left; + font-weight: bold; } td { @@ -463,44 +455,43 @@ section.problem { } caption { - background: #f1f1f1; margin-bottom: .75em; margin-bottom: .75rem; padding: .75em 0; padding: .75rem 0; + background: #f1f1f1; } tr, td, th { vertical-align: middle; } - } code { margin: 0 2px; padding: 0px 5px; - white-space: nowrap; - border: 1px solid #EAEAEA; - background-color: #F8F8F8; + border: 1px solid #eaeaea; border-radius: 3px; + background-color: #f8f8f8; + white-space: nowrap; font-size: .9em; } pre { - background-color: #F8F8F8; - border: 1px solid #CCC; + overflow: auto; + padding: 6px $baseline/2; + border: 1px solid #ccc; + border-radius: 3px; + background-color: #f8f8f8; font-size: .9em; line-height: 1.4; - overflow: auto; - padding: 6px 10px; - border-radius: 3px; > code { margin: 0; padding: 0; - white-space: pre; border: none; background: transparent; + white-space: pre; } } @@ -517,26 +508,25 @@ section.problem { } pre { - border-radius: 0; - border-radius: 0; - border-width: 0; + overflow: hidden; margin: 0; padding: 0; + border-width: 0; + border-radius: 0; background: transparent; - font-family: inherit; - font-size: inherit; white-space: pre; word-wrap: normal; - overflow: hidden; + font-size: inherit; + font-family: inherit; resize: none; &.CodeMirror-cursor { - z-index: 10; position: absolute; + z-index: 10; visibility: hidden; - border-left: 1px solid black; - border-right: none; width: 0; + border-right: none; + border-left: 1px solid black; } } } @@ -546,14 +536,14 @@ section.problem { } hr { - background: #ddd; - border: none; - clear: both; - color: #ddd; float: none; - height: 1px; + clear: both; margin: 0 0 .75rem; width: 100%; + height: 1px; + border: none; + background: #ddd; + color: #ddd; } .hidden { @@ -570,17 +560,17 @@ section.problem { center { display: block; margin: lh() 0; - border: 1px solid #ccc; padding: lh(); + border: 1px solid #ccc; } section.action { - margin-top: 20px; + margin-top: $baseline; .save, .check, .show, .reset { height: ($baseline*2); - font-weight: 600; vertical-align: middle; + font-weight: 600; } .save { @@ -590,8 +580,8 @@ section.problem { .show { .show-label { - font-size: 1.0em; font-weight: 600; + font-size: 1.0em; } } @@ -602,20 +592,20 @@ section.problem { // padding: 8px 12px; // margin-top: 10px; @include inline-block; - font-style: italic; - margin: 8px 0 0 10px; + margin: 8px 0 0 $baseline/2; color: #777; + font-style: italic; -webkit-font-smoothing: antialiased; } } .detailed-solution { > p:first-child { - font-size: 0.9em; + color: #aaa; + text-transform: uppercase; font-weight: bold; font-style: normal; - text-transform: uppercase; - color: #AAA; + font-size: 0.9em; } p:last-child { @@ -624,12 +614,12 @@ section.problem { } div.capa_alert { + margin-top: $baseline; padding: 8px 12px; - border: 1px solid #EBE8BF; + border: 1px solid #ebe8bf; border-radius: 3px; - background: #FFFCDD; + background: #fffcdd; font-size: 0.9em; - margin-top: 10px; } div.capa_reset { @@ -638,12 +628,14 @@ section.problem { background-color: lighten($error-red, 25%); border-radius: 3px; font-size: 1em; - margin-top: 10px; - margin-bottom: 10px; + margin-top: $baseline/2; + margin-bottom: $baseline/2; } + .capa_reset>h2 { - color: #AA0000; + color: #aa0000; } + .capa_reset li { font-size: 0.9em; } @@ -652,10 +644,10 @@ section.problem { border: 1px solid #ccc; h3 { - border-bottom: 1px solid #e3e3e3; - text-shadow: 0 1px 0 #fff; padding: 9px; + border-bottom: 1px solid #e3e3e3; background: #eee; + text-shadow: 0 1px 0 #fff; font-weight: bold; font-size: em(16); } @@ -675,7 +667,7 @@ section.problem { a { display: block; padding: 9px; - background: #F6F6F6; + background: #f6f6f6; box-shadow: inset 0 0 0 1px #fff; } } @@ -693,22 +685,22 @@ section.problem { margin-bottom: 12px; h3 { - font-size: 0.9em; + color: #aaa; + text-transform: uppercase; font-weight: bold; font-style: normal; - text-transform: uppercase; - color: #AAA; + font-size: 0.9em; } } > section { - border: 1px solid #ddd; - padding: 9px 9px 20px; - margin-bottom: 10px; - background: #FFF; position: relative; - box-shadow: inset 0 0 0 1px #eee; + margin-bottom: $baseline/2; + padding: 9px 9px $baseline; + border: 1px solid #ddd; border-radius: 3px; + background: #fff; + box-shadow: inset 0 0 0 1px #eee; p:last-of-type { margin-bottom: 0; @@ -719,28 +711,29 @@ section.problem { } a.full { - @include position(absolute, 0 0 1px 0px); - font-size: .8em; - padding: 4px; - text-align: right; - width: 100%; - display: block; - background: #F3F3F3; + @include position(absolute, 0 0 1px 0); @include box-sizing(border-box); + display: block; + padding: 4px; + width: 100%; + background: #f3f3f3; + text-align: right; + font-size: .8em; } } } .external-grader-message { section { - padding-left: 20px; - background-color: #FAFAFA; - color: #2C2C2C; - font-family: monospace; + padding-top: $baseline/2; + padding-left: $baseline; + background-color: #fafafa; + color: #2c2c2c; font-size: 1em; - padding-top: 10px; + font-family: monospace; + header { - font-size: 1.4em; + font-size: 1.4em; } .shortform { @@ -748,35 +741,36 @@ section.problem { } .longform { - padding: 0px; - margin: 0px; + margin: 0; + padding: 0; .result-errors { - margin: 5px; - padding: 10px 10px 10px 40px; + margin: $baseline/4; + padding: $baseline/2 $baseline/2 $baseline/2 $baseline*2; background: url('../images/incorrect-icon.png') center left no-repeat; + li { - color: #B00; - } + color: #b00; + } } .result-output { - margin: 5px; - padding: 20px 0px 15px 50px; - border-top: 1px solid #DDD; - border-left: 20px solid #FAFAFA; + margin: $baseline/4; + padding: $baseline 0 15px 50px; + border-top: 1px solid #ddd; + border-left: $baseline solid #fafafa; h4 { - font-family: monospace; font-size: 1em; + font-family: monospace; } dl { - margin: 0px; + margin: 0; } dt { - margin-top: 20px; + margin-top: $baseline; } dd { @@ -786,6 +780,7 @@ section.problem { .result-correct { background: url('../images/correct-icon.png') left 20px no-repeat; + .result-actual-output { color: #090; } @@ -793,6 +788,7 @@ section.problem { .result-incorrect { background: url('../images/incorrect-icon.png') left 20px no-repeat; + .result-actual-output { color: #B00; } @@ -800,16 +796,16 @@ section.problem { .markup-text{ margin: 5px; - padding: 20px 0px 15px 50px; - border-top: 1px solid #DDD; - border-left: 20px solid #FAFAFA; + padding: $baseline 0 15px 50px; + border-top: 1px solid #ddd; + border-left: 20px solid #fafafa; bs { - color: #BB0000; + color: #bb0000; } bg { - color: #BDA046; + color: #bda046; } } } @@ -817,96 +813,111 @@ section.problem { } .rubric { - tr { - margin:10px 0px; - height: 100%; - } - td { - padding: 20px 0px; - margin: 10px 0px; - height: 100%; - } - th { - padding: 5px; - margin: 5px; - } - label, - .view-only { - margin:3px; - position: relative; - padding: 15px; - width: 150px; - height:100%; - display: inline-block; - min-height: 50px; - min-width: 50px; - background-color: #CCC; - font-size: .9em; - } - .grade { - position: absolute; - bottom:0px; - right:0px; - margin:10px; - } - .selected-grade { - background: #666; - color: white; - } - input[type=radio]:checked + label { - background: #666; - color: white; } - input[class='score-selection'] { - display: none; - } + tr { + margin: $baseline/2 0; + height: 100%; + } + + td { + margin: $baseline/2 0; + padding: $baseline 0; + height: 100%; + } + + th { + margin: $baseline/4; + padding: $baseline/4; + } + + label, + .view-only { + position: relative; + display: inline-block; + margin: 3px; + padding: 15px; + min-width: 50px; + min-height: 50px; + width: 150px; + height: 100%; + background-color: #ccc; + font-size: .9em; + } + + .grade { + position: absolute; + right: 0; + bottom: 0; + margin: $baseline/2; + } + + .selected-grade { + background: #666; + color: white; + } + + input[type=radio]:checked + label { + background: #666; + color: white; + } + + input[class='score-selection'] { + display: none; + } } .annotation-input { $yellow: rgba(255,255,10,0.3); - + margin: 0 0 1em 0; border: 1px solid #ccc; border-radius: 1em; - margin: 0 0 1em 0; .annotation-header { - font-weight: bold; - border-bottom: 1px solid #ccc; padding: .5em 1em; + border-bottom: 1px solid #ccc; + font-weight: bold; } + .annotation-body { padding: .5em 1em; } + a.annotation-return { float: right; font: inherit; font-weight: normal; } + a.annotation-return:after { content: " \2191" } .block, ul.tags { margin: .5em 0; padding: 0; } + .block-highlight { padding: .5em; + border: 1px solid darken($yellow, 10%); + background-color: $yellow; color: #333; font-style: normal; - background-color: $yellow; - border: 1px solid darken($yellow, 10%); } + .block-comment { font-style: italic; } ul.tags { display: block; - list-style-type: none; margin-left: 1em; + list-style-type: none; + li { + position: relative; display: block; margin: 1em 0 0 0; - position: relative; + .tag { display: inline-block; - cursor: pointer; + margin-left: $baseline*2; border: 1px solid rgb(102,102,102); - margin-left: 40px; + cursor: pointer; + &.selected { background-color: $yellow; } @@ -918,42 +929,49 @@ section.problem { .tag-status, .tag { padding: .25em .5em; } } } + textarea.comment { $num-lines-to-show: 5; $line-height: 1.4em; $padding: .2em; - width: 100%; padding: $padding (2 * $padding); - line-height: $line-height; + width: 100%; height: ($num-lines-to-show * $line-height) + (2*$padding) - (($line-height - 1)/2); + line-height: $line-height; } + .answer-annotation { display: block; margin: 0; } /* for debugging the input value field. enable the debug flag on the inputtype */ .debug-value { - color: #fff; - padding: 1em; - margin: 1em 0; - background-color: #999; - border: 1px solid #000; - input[type="text"] { width: 100%; } - pre { background-color: #CCC; color: #000; } - &:before { - display: block; - content: "debug input value"; - text-transform: uppercase; - font-weight: bold; - font-size: 1.5em; - } + margin: 1em 0; + padding: 1em; + border: 1px solid #000; + background-color: #999; + color: #fff; + + input[type="text"] { width: 100%; } + + pre { background-color: #CCC; color: #000; } + + &:before { + display: block; + content: "debug input value"; + text-transform: uppercase; + font-weight: bold; + font-size: 1.5em; + } } } + .choicetextgroup{ + @extend .choicegroup; + input[type="text"]{ margin-bottom: 0.5em; } - @extend .choicegroup; - label.choicetextgroup_correct, section.choicetextgroup_correct{ + label.choicetextgroup_correct, section.choicetextgroup_correct { @extend label.choicegroup_correct; input[type="text"] { @@ -961,17 +979,18 @@ section.problem { } } - label.choicetextgroup_incorrect, section.choicetextgroup_incorrect{ + label.choicetextgroup_incorrect, section.choicetextgroup_incorrect { @extend label.choicegroup_incorrect; } - label.choicetextgroup_show_correct, section.choicetextgroup_show_correct{ - &:after{ - content: url('../images/correct-icon.png'); + label.choicetextgroup_show_correct, section.choicetextgroup_show_correct { + &:after { margin-left:15px; + content: url('../images/correct-icon.png'); } } - span.mock_label{ + + span.mock_label { cursor : default; } } diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 78f0213c8d..7ca99be5c4 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -1,3 +1,6 @@ +// lms - xmodule - combinedopenended +// ==================== + h2 { margin-top: 0; margin-bottom: 15px; @@ -16,244 +19,470 @@ h2 { } } + // Problem Header +div.name{ + padding-bottom: 15px; + + h2 { + display: inline; + } + + .progress-container { + display: inline; + float: right; + padding-top: 3px; + } +} + .inline-error { color: darken($error-red, 10%); } section.combined-open-ended { @include clearfix; - .status-container - { - padding-bottom: 5px; +} + + +div.problemwrapper { + border: 1px solid lightgray; + border-radius: $baseline/2; + + .status-bar { + background-color: #eee; + border-radius: $baseline/2 $baseline/2 0 0; + border-bottom: 1px solid lightgray; + + .statustable { + width: 100%; + padding: $baseline; + } + + .status-container { + display: table-cell; + text-align: center; + + .status-elements { + border-radius: $baseline/4; + border: 1px solid lightgray; + } + } + + .problemtype-container { + padding: $baseline/2; + width: 60%; + } + + .problemtype{ + padding: $baseline/2; + } + + .assessments-container { + float: right; + padding: $baseline/2 $baseline $baseline/2 $baseline/2; + + .assessment-text { + display: inline-block; + display: table-cell; + padding-right: $baseline/2; + } + } } - .item-container - { - padding-bottom: 10px; + .item-container { + padding-bottom: $baseline/2; + margin: 15px; } - .result-container - { - float:left; - width: 100%; - position:relative; - } - h4 - { - margin-bottom:10px; + .result-container { + float: left; + width: 100%; + position: relative; } } section.legend-container { + margin: 15px; + border-radius: $baseline/4; + .legenditem { - background-color : #d4d4d4; - font-size: .9em; - padding: 2px; display: inline; + padding: $baseline/2; width: 20%; + background-color: #eee; + font-size: .9em; } - margin-bottom: 5px; } section.combined-open-ended-status { + vertical-align: center; - .statusitem { - color: #2C2C2C; - background-color : #d4d4d4; - font-size: .9em; - padding: 2px; - display: inline; - width: 20%; - .show-results { - margin-top: .3em; - text-align:right; - } - .show-results-button { - font: 1em monospace; - } + .statusitem { + display: table-cell; + padding: $baseline/2; + width: 30px; + border-right: 1px solid lightgray; + background-color: #eee; + color: #2c2c2c; + font-size: .9em; + + &:first-child { + border-radius: $baseline/4 0 0 $baseline/4; } - .statusitem-current { - background-color: #B2B2B2; - color: #222; - } - - span { - &.unanswered { - @include inline-block(); - background: url('../images/unanswered-icon.png') center center no-repeat; - height: 14px; - position: relative; - width: 14px; - float: right; - } - - &.correct { - @include inline-block(); - background: url('../images/correct-icon.png') center center no-repeat; - height: 20px; - position: relative; - width: 25px; - float: right; - } - - &.incorrect { - @include inline-block(); - background: url('../images/incorrect-icon.png') center center no-repeat; - height: 20px; - width: 20px; - position: relative; - float: right; - } + &:last-child { + border-right: 0; + border-radius: 0 $baseline/4 $baseline/4 0; } + + &:only-child { + border-radius: $baseline/4; + } + + .show-results { + margin-top: .3em; + text-align:right; + } + + .show-results-button { + font: 1em monospace; + } + } + + .statusitem-current { + background-color: #fff; + color: #222; + } + + span { + &.unanswered { + @include inline-block(); + position: relative; + float: right; + width: 14px; + height: 14px; + background: url('../images/unanswered-icon.png') center center no-repeat; + } + + &.correct { + @include inline-block(); + position: relative; + float: right; + width: 25px; + height: 20px; + background: url('../images/correct-icon.png') center center no-repeat; + } + + &.incorrect { + @include inline-block(); + position: relative; + float: right; + width: 20px; + height: 20px; + background: url('../images/incorrect-icon.png') center center no-repeat; + } + } + + .icon-caret-right { + display: inline-block; + margin-right: ($baseline/4); + vertical-align: baseline; + } +} + +// Problem Section Controls + +.visibility-control, .visibility-control-prompt { + display: block; + width: 100%; + height: 40px; + + .inner { + float: left; + margin-top: $baseline; + width: 85%; + height: 5px; + border-top: 1px dotted #ddd; + } +} + +.section-header { + display: block; + float: right; + padding-top: $baseline/2; + width: 15%; + text-align: center; + font-size: .9em; +} + +// Rubric Styling + +.wrapper-score-selection { + display: table-cell; + padding: 0 $baseline/2; + width: 20px; + vertical-align: middle; +} + +.wrappable { + display: table-cell; + padding: $baseline/4; +} + +.rubric-list-item { + margin-bottom: 2px; + padding: $baseline/2; + + &:hover { + background-color: #eee; + } + .rubric-label-selected{ + border-radius: $baseline/4; + background-color: #eee; + } +} + +span.rubric-category { + display: block; + margin-bottom: $baseline/2; + padding-top: $baseline/2; + width: 100%; + border-bottom: 1px solid lightgray; + font-size: 1.1em; } div.combined-rubric-container { - ul.rubric-list{ + margin: 15px; + padding-top: $baseline/2; + padding-bottom: $baseline/4; + + ul.rubric-list { + margin: 0 $baseline $baseline/2 $baseline; + padding: 0; list-style-type: none; - padding:0; - margin:0; + li { - &.rubric-list-item{ + + &.rubric-list-item { margin-bottom: 2px; - padding: 0px; + padding: $baseline/2; } } } + h4 { + padding-top: $baseline/2; + } + span.rubric-category { - font-size: .9em; + display: block; + width: 100%; + border-bottom: 1px solid lightgray; font-weight: bold; + font-size: .9em; } - padding-bottom: 5px; - padding-top: 10px; + + label.choicegroup_correct { + &:before { + margin-right: 15px; + content: url('../images/correct-icon.png'); + } + } + + label.choicegroup_partialcorrect { + &:before { + margin-right: 15px; + content: url('../images/partially-correct-icon.png'); + } + } + + label.choicegroup_incorrect { + &:before { + margin-right: 15px; + content: url('../images/incorrect-icon.png'); + } + } + + div.written-feedback { + background: #f6f6f6; + padding: 15px; + } } - + div.result-container { - padding-top: 10px; - padding-bottom: 5px; - .evaluation { + padding-top: $baseline/2; + padding-bottom: $baseline/4; - p { - margin-bottom: 1px; + .evaluation { + p { + margin-bottom: 1px; + } + } + + .feedback-on-feedback { + height: 100px; + margin-right: 0; + } + + .evaluation-response { + margin-bottom: 2px; + + header { + a { + font-size: .85em; + } + } + } + + .evaluation-scoring { + .scoring-list { + margin-left: 3px; + list-style-type: none; + + li { + display:inline; + margin-left: 0; + + &:first-child { + margin-left: 0; } - } - .feedback-on-feedback { - height: 100px; - margin-right: 0px; - } - - .evaluation-response { - margin-bottom: 2px; - header { - a { - font-size: .85em; - } + label { + font-size: .9em; } + } } - .evaluation-scoring { - .scoring-list { - list-style-type: none; - margin-left: 3px; + } - li { - &:first-child { - margin-left: 0px; - } - display:inline; - margin-left: 0px; + .submit-message-container { + margin: $baseline/2 0; + } - label { - font-size: .9em; - } - } + .external-grader-message { + margin-bottom: $baseline/4; + + section { + padding-left: $baseline; + background-color: #fafafa; + color: #2c2c2c; + font-family: monospace; + font-size: 1em; + padding-top: $baseline/2; + padding-bottom: 30px; + + header { + font-size: 1.4em; + } + + .shortform { + font-weight: bold; + } + + .longform { + padding: 0; + margin: 0; + + .result-errors { + margin: $baseline/4; + padding: $baseline/2 $baseline/2 $baseline/2 $baseline*2; + background: url('../images/incorrect-icon.png') center left no-repeat; + + li { + color: #B00; + } } - } - .submit-message-container { - margin: 10px 0px ; - } - .external-grader-message { - margin-bottom: 5px; - section { - padding-left: 20px; - background-color: #FAFAFA; - color: #2C2C2C; - font-family: monospace; - font-size: 1em; - padding-top: 10px; - padding-bottom:30px; - header { - font-size: 1.4em; + .result-output { + margin: $baseline/4; + padding: $baseline 0 15px 50px; + border-top: 1px solid #ddd; + border-left: 20px solid #fafafa; + + h4 { + font-size: 1em; + font-family: monospace; } - .shortform { - font-weight: bold; + dl { + margin: 0; } - .longform { - padding: 0px; - margin: 0px; + dt { + margin-top: $baseline; + } - .result-errors { - margin: 5px; - padding: 10px 10px 10px 40px; - background: url('../images/incorrect-icon.png') center left no-repeat; - li { - color: #B00; - } - } + dd { + margin-left: 24pt; + } + } - .result-output { - margin: 5px; - padding: 20px 0px 15px 50px; - border-top: 1px solid #DDD; - border-left: 20px solid #FAFAFA; + .markup-text{ + margin: $baseline/4; + padding: $baseline 0 15px 50px; + border-top: 1px solid #ddd; + border-left: 20px solid #fafafa; - h4 { - font-family: monospace; - font-size: 1em; - } + bs { + color: #bb0000; + } - dl { - margin: 0px; - } - - dt { - margin-top: 20px; - } - - dd { - margin-left: 24pt; - } - } - - .markup-text{ - margin: 5px; - padding: 20px 0px 15px 50px; - border-top: 1px solid #DDD; - border-left: 20px solid #FAFAFA; - - bs { - color: #BB0000; - } - - bg { - color: #BDA046; - } - } + bg { + color: #bda046; } } } + } + } + .rubric-result-container { + padding: 2px; + margin: 0; + display: inline; + .rubric-result { font-size: .9em; padding: 2px; display: inline-table; } - padding: 2px; - margin: 0px; - display : inline; + } +} + +div.rubric { + ul.rubric-list{ + margin: 0 $baseline $baseline/2 $baseline; + padding: 0; + list-style: none; + list-style-type: none; + + li { + &.rubric-list-item { + margin-bottom: 2px; + padding: $baseline/2; + border-radius: $baseline/4; + + &:hover { + background-color: #eee; + } + + .wrapper-score-selection { + display: table-cell; + padding: 0 $baseline/2; + width: 20px; + vertical-align: middle; + } + + .wrappable { + display: table-cell; + padding: $baseline/4; + } + } + } + } + + span.rubric-category { + display: block; + width: 100%; + border-bottom: 1px solid lightgray; + font-weight: bold; + font-size: .9em; } } @@ -261,8 +490,8 @@ div.result-container { section.open-ended-child { @media print { display: block; - width: auto; padding: 0; + width: auto; canvas, img { page-break-inside: avoid; @@ -270,30 +499,30 @@ section.open-ended-child { } .inline { - display: inline; + display: inline; } ol.enumerate { li { &:before { - content: " "; display: block; - height: 0; visibility: hidden; + height: 0; + content: " "; } } } .solution-span { > span { - margin: 20px 0; - display: block; - border: 1px solid #ddd; - padding: 9px 15px 20px; - background: #FFF; position: relative; - box-shadow: inset 0 0 0 1px #eee; + display: block; + margin: $baseline 0; + padding: 9px 15px $baseline; + border: 1px solid #ddd; border-radius: 3px; + background: #fff; + box-shadow: inset 0 0 0 1px #eee; &:empty { display: none; @@ -301,196 +530,190 @@ section.open-ended-child { } } - p { - &.answer { - margin-top: -2px; - } - &.status { - text-indent: -9999px; - margin: 8px 0 0 10px; - } + p { + &.answer { + margin-top: -2px; } - - div.unanswered { - p.status { - @include inline-block(); - background: url('../images/unanswered-icon.png') center center no-repeat; - height: 14px; - width: 14px; - } + &.status { + margin: 8px 0 0 $baseline/2; + text-indent: -9999px; } + } - div.correct, div.ui-icon-check { - p.status { - @include inline-block(); - background: url('../images/correct-icon.png') center center no-repeat; - height: 20px; - width: 25px; - } - - input { - border-color: green; - } - } - - div.processing { - p.status { - @include inline-block(); - background: url('../images/spinner.gif') center center no-repeat; - height: 20px; - width: 20px; - } - - input { - border-color: #aaa; - } - } - - div.incorrect, div.ui-icon-close { - p.status { - @include inline-block(); - background: url('../images/incorrect-icon.png') center center no-repeat; - height: 20px; - width: 20px; - text-indent: -9999px; - } - - input { - border-color: red; - } - } - - > span { - display: block; - margin-bottom: lh(.5); - } - - p.answer { + div.unanswered { + p.status { @include inline-block(); - margin-bottom: 0; - margin-left: 10px; + width: 14px; + height: 14px; + background: url('../images/unanswered-icon.png') center center no-repeat; + } + } + div.correct, div.ui-icon-check { + p.status { + @include inline-block(); + width: 25px; + height: 20px; + background: url('../images/correct-icon.png') center center no-repeat; + } + + input { + border-color: green; + } + } + + div.processing { + p.status { + @include inline-block(); + width: 20px; + height: 20px; + background: url('../images/spinner.gif') center center no-repeat; + } + + input { + border-color: #aaa; + } + } + + div.incorrect, div.ui-icon-close { + p.status { + @include inline-block(); + width: 20px; + height: 20px; + background: url('../images/incorrect-icon.png') center center no-repeat; + text-indent: -9999px; + } + + input { + border-color: red; + } + } + + > span { + display: block; + margin-bottom: lh(.5); + } + + p.answer { + @include inline-block(); + margin-bottom: 0; + margin-left: $baseline/2; + + &:before { + content: "Answer: "; + font-weight: bold; + display: inline; + + } + &:empty { &:before { - content: "Answer: "; - font-weight: bold; - display: inline; - - } - &:empty { - &:before { - display: none; - } + display: none; } } + } + + span { + &.unanswered, &.ui-icon-bullet { + @include inline-block(); + position: relative; + top: 4px; + width: 14px; + height: 14px; + background: url('../images/unanswered-icon.png') center center no-repeat; + } + + &.processing, &.ui-icon-processing { + @include inline-block(); + position: relative; + top: 6px; + width: 25px; + height: 20px; + background: url('../images/spinner.gif') center center no-repeat; + } + + &.correct, &.ui-icon-check { + @include inline-block(); + position: relative; + top: 6px; + width: 25px; + height: 20px; + background: url('../images/correct-icon.png') center center no-repeat; + } + + &.incorrect, &.ui-icon-close { + @include inline-block(); + position: relative; + top: 6px; + width: 20px; + height: 20px; + background: url('../images/incorrect-icon.png') center center no-repeat; + } + } + + .reload { + float:right; + margin: $baseline/2; + } + + div.short-form-response { + @include clearfix; + overflow-y: auto; + margin-bottom: 0; + padding: $baseline/2; + min-height: 20px; + height: auto; + border: 1px solid #ddd; + background: #f6f6f6; + } + + .grader-status { + @include clearfix; + margin: $baseline/2 0; + padding: $baseline/2; + border-radius: 5px; + background: #f6f6f6; span { - &.unanswered, &.ui-icon-bullet { - @include inline-block(); - background: url('../images/unanswered-icon.png') center center no-repeat; - height: 14px; - position: relative; - top: 4px; - width: 14px; - } - - &.processing, &.ui-icon-processing { - @include inline-block(); - background: url('../images/spinner.gif') center center no-repeat; - height: 20px; - position: relative; - top: 6px; - width: 25px; - } - - &.correct, &.ui-icon-check { - @include inline-block(); - background: url('../images/correct-icon.png') center center no-repeat; - height: 20px; - position: relative; - top: 6px; - width: 25px; - } - - &.incorrect, &.ui-icon-close { - @include inline-block(); - background: url('../images/incorrect-icon.png') center center no-repeat; - height: 20px; - width: 20px; - position: relative; - top: 6px; - } + display: block; + float: left; + overflow: hidden; + margin: -7px 7px 0 0; + text-indent: -9999px; } - .reload - { - float:right; - margin: 10px; + .grading { + margin: 0 7px 0 0; + padding-left: 25px; + background: url('../images/info-icon.png') left center no-repeat; + text-indent: 0; } - div.short-form-response { - background: #F6F6F6; - border: 1px solid #ddd; - margin-bottom: 0px; - overflow-y: auto; - height: 200px; - @include clearfix; - } + p { + float: left; + margin-bottom: 0; + line-height: 20px; + } - .grader-status { - padding: 9px; - background: #F6F6F6; - border: 1px solid #ddd; - border-top: 0; - margin-bottom: 20px; - @include clearfix; + &.file { + margin-top: $baseline; + padding: $baseline 0 0 0; + border: 0; + border-top: 1px solid #eee; + background: #fff; - span { - text-indent: -9999px; - overflow: hidden; - display: block; - float: left; - margin: -7px 7px 0 0; + p.debug { + display: none; } - .grading { - background: url('../images/info-icon.png') left center no-repeat; - padding-left: 25px; - text-indent: 0px; - margin: 0px 7px 0 0; - } - - p { - line-height: 20px; - margin-bottom: 0; + input { float: left; } - - &.file { - background: #FFF; - margin-top: 20px; - padding: 20px 0 0 0; - - border: { - top: 1px solid #eee; - right: 0; - bottom: 0; - left: 0; - } - - p.debug { - display: none; - } - - input { - float: left; - } - } - } + } form.option-input { - margin: -10px 0 20px; - padding-bottom: 20px; + margin: -$baseline/2 0 $baseline; + padding-bottom: $baseline; select { margin-right: flex-gutter(); @@ -498,29 +721,31 @@ section.open-ended-child { } ul { - list-style: disc outside none; margin-bottom: lh(); margin-left: .75em; margin-left: .75rem; } - ul.rubric-list{ - list-style-type: none; - padding:0; - margin:0; - li { - &.rubric-list-item{ - margin-bottom: 0px; - padding: 0px; - } - } + ul.rubric-list{ + margin: 0; + padding: 0; + list-style-type: none; + list-style: none; + + li { + &.rubric-list-item { + margin-bottom: 0; + padding: 0; + border-radius: $baseline/4; + } } + } ol { - list-style: decimal outside none; margin-bottom: lh(); margin-left: .75em; margin-left: .75rem; + list-style: decimal outside none; } dl { @@ -541,8 +766,9 @@ section.open-ended-child { } li { - margin-bottom: 0px; - padding: 0px; + margin-bottom: 0; + padding: 0; + &:last-child { margin-bottom: 0; } @@ -553,14 +779,14 @@ section.open-ended-child { } hr { - background: #ddd; - border: none; - clear: both; - color: #ddd; float: none; - height: 1px; + clear: both; margin: 0 0 .75rem; width: 100%; + height: 1px; + border: none; + background: #ddd; + color: #ddd; } .hidden { @@ -574,7 +800,7 @@ section.open-ended-child { } section.action { - margin-top: 20px; + margin-top: $baseline; input.save { @extend .blue-button !optional; @@ -582,20 +808,20 @@ section.open-ended-child { .submission_feedback { @include inline-block; - font-style: italic; - margin: 8px 0 0 10px; + margin: 8px 0 0 $baseline/2; color: #777; + font-style: italic; -webkit-font-smoothing: antialiased; } } .detailed-solution { > p:first-child { - font-size: 0.9em; + color: #aaa; + text-transform: uppercase; font-weight: bold; font-style: normal; - text-transform: uppercase; - color: #AAA; + font-size: 0.9em; } p:last-child { @@ -605,56 +831,136 @@ section.open-ended-child { div.open-ended-alert, .save_message { + margin-top: $baseline/2; + margin-bottom: $baseline/4; padding: 8px 12px; - border: 1px solid #EBE8BF; + border: 1px solid #ebe8bf; border-radius: 3px; - background: #FFFCDD; + background: #fffcdd; font-size: 0.9em; - margin-top: 10px; - margin-bottom:5px; } div.capa_reset { + margin-top: $baseline/2; + margin-bottom: $baseline/2; padding: 25px; border: 1px solid $error-red; - background-color: lighten($error-red, 25%); border-radius: 3px; + background-color: lighten($error-red, 25%); font-size: 1em; - margin-top: 10px; - margin-bottom: 10px; } - .capa_reset>h2 { - color: #AA0000; + + .capa_reset > h2 { + color: #aa0000; } + .capa_reset li { font-size: 0.9em; } .assessment-container { - margin: 40px 0px 30px 0px; - .scoring-container - { - p - { - margin-bottom: 1em; - } - label { - margin: 10px; - padding: 5px; - display: inline-block; - min-width: 50px; - background-color: #CCC; - text-size: 1.5em; - } - - input[type=radio]:checked + label { - background: #666; - color: white; - } - input[class='grade-selection'] { - display: none; - } + margin: $baseline*2 0px 30px 0px; + .scoring-container { + p { + margin-bottom: 1em; } + + label { + display: inline-block; + margin: $baseline/2; + padding: $baseline/4; + min-width: 50px; + background-color: #ccc; + text-size: 1.5em; + } + + input[type=radio]:checked + label { + background: #666; + color: white; + } + + input[class='grade-selection'] { + display: none; + } + } + } + + div.prompt { + background-color: white; + } + + h4 { + padding: $baseline/2 0; } } + +//OE Tool Area Styling + +.oe-tools { + display: inline-block; + width: 100%; + border-radius: 5px; + + .oe-tools-label, .oe-tools-scores-label { + display: inline-block; + padding: $baseline/2; + vertical-align: middle; + font-size: 0.8em; + } + + .rubric-button { + padding: 8px $baseline/4; + } + + .rubric-previous-button { + margin-right: $baseline/4; + } + + .rubric-next-button { + margin-left: $baseline/4; + } + + .next-step-button { + margin: $baseline/2; + } + .reset-button { + vertical-align: middle; + } +} + +// Staff Grading +.problem-list-container { + margin: $baseline/2; + + .instructions { + padding-bottom: $baseline/2; + } +} + +.staff-grading { + + .breadcrumbs { + padding: $baseline/10; + background-color: #f6f6f6; + border-radius: 5px; + margin-bottom: $baseline/2; + } + + .prompt-wrapper { + padding-top: $baseline/2; + + .meta-info-wrapper { + padding: $baseline/2; + border-radius: $baseline/4; + } + } +} + +section.peer-grading-container{ + div.peer-grading{ + section.calibration-feedback { + padding: 20px; + } + } +} diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 567f5c7eef..7a68c42ac9 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -33,11 +33,13 @@ class HtmlFields(object): class HtmlModule(HtmlFields, XModule): - js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'), - resource_string(__name__, 'js/src/collapsible.coffee'), - resource_string(__name__, 'js/src/html/display.coffee') - ] - } + js = { + 'coffee': [ + resource_string(__name__, 'js/src/javascript_loader.coffee'), + resource_string(__name__, 'js/src/collapsible.coffee'), + resource_string(__name__, 'js/src/html/display.coffee') + ] + } js_module_name = "HTMLModule" css = {'scss': [resource_string(__name__, 'css/html/display.scss')]} @@ -118,8 +120,10 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # from .html # 'filename' in html pointers is a relative path # (not same as 'html/blah.html' when the pointer is in a directory itself) - pointer_path = "{category}/{url_path}".format(category='html', - url_path=name_to_pathname(location.name)) + pointer_path = "{category}/{url_path}".format( + category='html', + url_path=name_to_pathname(location.name) + ) base = path(pointer_path).dirname() # log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename)) filepath = "{base}/{name}.html".format(base=base, name=filename) @@ -164,19 +168,16 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # TODO (vshnayder): make export put things in the right places. def definition_to_xml(self, resource_fs): - '''If the contents are valid xml, write them to filename.xml. Otherwise, - write just to filename.xml, and the html + ''' Write to filename.xml, and the html string to filename.html. ''' - try: - return etree.fromstring(self.data) - except etree.XMLSyntaxError: - pass - # Not proper format. Write html to file, return an empty tag + # Write html to file, return an empty tag pathname = name_to_pathname(self.url_name) - filepath = u'{category}/{pathname}.html'.format(category=self.category, - pathname=pathname) + filepath = u'{category}/{pathname}.html'.format( + category=self.category, + pathname=pathname + ) resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True) with resource_fs.open(filepath, 'w') as filestream: @@ -190,6 +191,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): elt.set("filename", relname) return elt + class AboutFields(object): display_name = String( help="Display name for this module", @@ -202,12 +204,14 @@ class AboutFields(object): scope=Scope.content ) + class AboutModule(AboutFields, HtmlModule): """ Overriding defaults but otherwise treated as HtmlModule. """ pass + class AboutDescriptor(AboutFields, HtmlDescriptor): """ These pieces of course content are treated as HtmlModules but we need to overload where the templates are located @@ -216,6 +220,7 @@ class AboutDescriptor(AboutFields, HtmlDescriptor): template_dir_name = "about" module_class = AboutModule + class StaticTabFields(object): """ The overrides for Static Tabs @@ -241,6 +246,7 @@ class StaticTabModule(StaticTabFields, HtmlModule): """ pass + class StaticTabDescriptor(StaticTabFields, HtmlDescriptor): """ These pieces of course content are treated as HtmlModules but we need to overload where the templates are located diff --git a/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html b/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html index abea783ae8..e5eb0858f7 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html +++ b/common/lib/xmodule/xmodule/js/fixtures/combined-open-ended.html @@ -1,75 +1,80 @@

-
+
- -

Problem 1

-
-

Status

-
-
- -
- - Step 1 (Problem complete) : 1 / 1 +

Problem 1

+
+

Status

+
+
+
+ Step 1 (Problem complete) : 1 / 1 - -
- -
- - Step 2 (Being scored) : None / 1 +
+
+ Step 2 (Being scored) : None / 1 - +
+
+
-
-
- -
- -
-

Problem

+
+

Problem

-
-
- - Some prompt. - -
-
-
- Submitted for grading. - -
- -
- - -
+
+
+
+
+
+ Some prompt. +
+ +
+
+ Submitted for grading. +
+ + +
+ +
- - +
+
+ - +
+
- -
-
-
- - -
- - + +
+ + +
+ + +
+ + + + Edit + / + QA -
-
Staff Debug Info
+ +
+ + Staff Debug Info + +