diff --git a/AUTHORS b/AUTHORS index 138fff37fb..aa74e4be65 100644 --- a/AUTHORS +++ b/AUTHORS @@ -143,3 +143,6 @@ Jonas Jelten Christine Lytwynec John Cox Ben Weeks +David Bodor +Sébastien Hinderer + diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 79de8c09f4..6f004b9657 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,10 @@ Studio: Add drag-and-drop support to the container page. STUD-1309. Common: Add extensible third-party auth module. +LMS: Switch default instructor dashboard to the new (formerly "beta") + instructor dashboard. Puts the old (now "legacy") dash behind a feature flag. + LMS-1296 + Blades: Handle situation if no response were sent from XQueue to LMS in Matlab problem after Run Code button press. BLD-994. diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_import.py b/cms/djangoapps/contentstore/management/commands/tests/test_import.py index 09cb87043e..d22da16854 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_import.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_import.py @@ -23,8 +23,26 @@ class TestImport(ModuleStoreTestCase): Unit tests for importing a course from command line """ +<<<<<<< HEAD COURSE_KEY = SlashSeparatedCourseKey(u'edX', u'test_import_course', u'2013_Spring') DIFF_KEY = SlashSeparatedCourseKey(u'edX', u'test_import_course', u'2014_Spring') +======= + BASE_COURSE_ID = ['EDx', '0.00x', '2013_Spring', ] + DIFF_RUN = ['EDx', '0.00x', '2014_Spring', ] + TRUNCATED_COURSE = ['EDx', '0.00', '2014_Spring', ] + + def create_course_xml(self, content_dir, course_id): + directory = tempfile.mkdtemp(dir=content_dir) + os.makedirs(os.path.join(directory, "course")) + with open(os.path.join(directory, "course.xml"), "w+") as f: + f.write(''.format(course_id)) + + with open(os.path.join(directory, "course", "{0[2]}.xml".format(course_id)), "w+") as f: + f.write('') + + return directory +>>>>>>> edx/master def setUp(self): """ @@ -35,6 +53,7 @@ class TestImport(ModuleStoreTestCase): self.addCleanup(shutil.rmtree, self.content_dir) # Create good course xml +<<<<<<< HEAD self.good_dir = tempfile.mkdtemp(dir=self.content_dir) os.makedirs(os.path.join(self.good_dir, "course")) with open(os.path.join(self.good_dir, "course.xml"), "w+") as f: @@ -53,14 +72,30 @@ class TestImport(ModuleStoreTestCase): with open(os.path.join(self.dupe_dir, "course", "{0.run}.xml".format(self.DIFF_KEY)), "w+") as f: f.write('') +======= + self.good_dir = self.create_course_xml(self.content_dir, self.BASE_COURSE_ID) + + # Create run changed course xml + self.dupe_dir = self.create_course_xml(self.content_dir, self.DIFF_RUN) + + # Create course XML where TRUNCATED_COURSE.org == BASE_COURSE_ID.org + # and BASE_COURSE_ID.startswith(TRUNCATED_COURSE.course) + self.course_dir = self.create_course_xml(self.content_dir, self.TRUNCATED_COURSE) +>>>>>>> edx/master def test_forum_seed(self): """ Tests that forum roles were created with import. """ +<<<<<<< HEAD self.assertFalse(are_permissions_roles_seeded(self.COURSE_KEY)) call_command('import', self.content_dir, self.good_dir) self.assertTrue(are_permissions_roles_seeded(self.COURSE_KEY)) +======= + self.assertFalse(are_permissions_roles_seeded('/'.join(self.BASE_COURSE_ID))) + call_command('import', self.content_dir, self.good_dir) + self.assertTrue(are_permissions_roles_seeded('/'.join(self.BASE_COURSE_ID))) +>>>>>>> edx/master def test_duplicate_with_url(self): """ @@ -71,9 +106,33 @@ class TestImport(ModuleStoreTestCase): # Load up base course and verify it is available call_command('import', self.content_dir, self.good_dir) store = modulestore() +<<<<<<< HEAD self.assertIsNotNone(store.get_course(self.COURSE_KEY)) # Now load up duped course and verify it doesn't load call_command('import', self.content_dir, self.dupe_dir) self.assertIsNone(store.get_course(self.DIFF_KEY)) self.assertTrue(are_permissions_roles_seeded(self.COURSE_KEY)) +======= + self.assertIsNotNone(store.get_course('/'.join(self.BASE_COURSE_ID))) + + # Now load up duped course and verify it doesn't load + call_command('import', self.content_dir, self.dupe_dir) + self.assertIsNone(store.get_course('/'.join(self.DIFF_RUN))) + + def test_truncated_course_with_url(self): + """ + Check to make sure an import only blocks true duplicates: new + courses with similar but not unique org/course combinations aren't + blocked if the original course's course starts with the new course's + org/course combinations (i.e. EDx/0.00x/Spring -> EDx/0.00/Spring) + """ + # Load up base course and verify it is available + call_command('import', self.content_dir, self.good_dir) + store = modulestore() + self.assertIsNotNone(store.get_course('/'.join(self.BASE_COURSE_ID))) + + # Now load up the course with a similar course_id and verify it loads + call_command('import', self.content_dir, self.course_dir) + self.assertIsNotNone(store.get_course('/'.join(self.TRUNCATED_COURSE))) +>>>>>>> edx/master diff --git a/cms/djangoapps/contentstore/tests/test_import.py b/cms/djangoapps/contentstore/tests/test_import.py index a82e456c57..9894115578 100644 --- a/cms/djangoapps/contentstore/tests/test_import.py +++ b/cms/djangoapps/contentstore/tests/test_import.py @@ -65,17 +65,47 @@ class ContentStoreImportTest(ModuleStoreTestCase): def load_test_import_course(self): ''' - Load the standard course used to test imports (for do_import_static=False behavior). + Load the standard course used to test imports + (for do_import_static=False behavior). ''' content_store = contentstore() module_store = modulestore('direct') +<<<<<<< HEAD import_from_xml(module_store, 'common/test/data/', ['test_import_course'], static_content_store=content_store, do_import_static=False, verbose=True) course_id = SlashSeparatedCourseKey('edX', 'test_import_course', '2012_Fall') course = module_store.get_course(course_id) +======= + 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) +>>>>>>> edx/master self.assertIsNotNone(course) return module_store, content_store, course + def test_import_course_into_similar_namespace(self): + # Checks to make sure that a course with an org/course like + # edx/course can be imported into a namespace with an org/course + # like edx/course_name + module_store, __, __, course_location = self.load_test_import_course() + __, course_items = import_from_xml( + module_store, + 'common/test/data', + ['test_import_course_2'], + target_location_namespace=course_location, + verbose=True, + ) + self.assertEqual(len(course_items), 1) + def test_unicode_chars_in_course_name_import(self): """ # Test that importing course with unicode 'id' and 'display name' doesn't give UnicodeEncodeError diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index 046fc9bfee..6118eb3018 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -59,7 +59,8 @@ DEBUG_TOOLBAR_PANELS = ( ) DEBUG_TOOLBAR_CONFIG = { - 'INTERCEPT_REDIRECTS': False + 'INTERCEPT_REDIRECTS': False, + 'SHOW_TOOLBAR_CALLBACK': lambda _: True, } # To see stacktraces for MongoDB queries, set this to True. diff --git a/cms/static/sass/elements/_modal-window.scss b/cms/static/sass/elements/_modal-window.scss index bf991bcecd..8df685f5f0 100644 --- a/cms/static/sass/elements/_modal-window.scss +++ b/cms/static/sass/elements/_modal-window.scss @@ -179,10 +179,6 @@ height: 365px; } - &.modal-type-problem .CodeMirror { - height: 435px; - } - .wrapper-comp-settings { .list-input { diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index a21ce346b1..00d3badd5e 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -429,16 +429,16 @@ class LoncapaProblem(object): def do_targeted_feedback(self, tree): """ - Implements the targeted-feedback=N in-place on -- + Implements targeted-feedback in-place on -- choice-level explanations shown to a student after submission. Does nothing if there is no targeted-feedback attribute. """ - for mult_choice_response in tree.xpath('//multiplechoiceresponse[@targeted-feedback]'): - # Note that the modifications has been done, avoiding problems if called twice. - if hasattr(self, 'has_targeted'): - continue - self.has_targeted = True # pylint: disable=W0201 + # Note that the modifications has been done, avoiding problems if called twice. + if hasattr(self, 'has_targeted'): + return + self.has_targeted = True # pylint: disable=W0201 + for mult_choice_response in tree.xpath('//multiplechoiceresponse[@targeted-feedback]'): show_explanation = mult_choice_response.get('targeted-feedback') == 'alwaysShowCorrectChoiceExplanation' # Grab the first choicegroup (there should only be one within each tag) diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index 2b28f4cacf..63187f214b 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -39,7 +39,7 @@
${msg|n}
-
+
${queue_msg|n}
@@ -55,13 +55,11 @@ if($(parent_elt).find('.capa_alert').length) { $(parent_elt).find('.capa_alert').remove(); } - var alert_elem = "
" + msg + "
"; - alert_elem = $(alert_elem).addClass('capa_alert'); + var alert_elem = $("
" + msg + "
"); + alert_elem.addClass('capa_alert').addClass('is-fading-in'); $(parent_elt).find('.action').after(alert_elem); - $(parent_elt).find('.capa_alert').css({opacity: 0}).animate({opacity: 1}, 700); } - // hook up the plot button var plot = function(event) { var problem_elt = $(event.target).closest('.problems-wrapper'); @@ -72,7 +70,7 @@ // since there could be multiple codemirror instances on the page, // save all of them. $('.CodeMirror').each(function(i, el){ - el.CodeMirror.save(); + el.CodeMirror.save(); }); var input = $("#input_${id}"); @@ -81,33 +79,39 @@ answer = input.serialize(); - // setup callback for after we send information to plot + // a chain of callbacks, each querying the server on success of the previous one + + var get_callback = function(response) { + var new_result_elem = $(response.html).find(".ungraded-matlab-result"); + new_result_elem.addClass("is-fading-in"); + result_elem = $(problem_elt).find(".ungraded-matlab-result"); + result_elem.replaceWith(new_result_elem); + console.log(response.html); + } + var plot_callback = function(response) { if(response.success) { - window.location.reload(); + $.postWithPrefix(url + "/problem_get", get_callback); + } else { + gentle_alert(problem_elt, response.message); + } + } + + var save_callback = function(response) { + if(response.success) { + // send information to the problem's plot functionality + Problem.inputAjax(url, input_id, 'plot', + {'submission': submission}, plot_callback); } else { gentle_alert(problem_elt, response.message); } } - var save_callback = function(response) { - if(response.success) { - // send information to the problem's plot functionality - Problem.inputAjax(url, input_id, 'plot', - {'submission': submission}, plot_callback); - } - else { - gentle_alert(problem_elt, response.message); - } - } - // save the answer $.postWithPrefix(url + '/problem_save', answer, save_callback); - } $('#plot_${id}').click(plot); - }); diff --git a/common/lib/capa/capa/tests/__init__.py b/common/lib/capa/capa/tests/__init__.py index cec2399222..bbcef25bb4 100644 --- a/common/lib/capa/capa/tests/__init__.py +++ b/common/lib/capa/capa/tests/__init__.py @@ -57,3 +57,15 @@ def test_capa_system(): def new_loncapa_problem(xml, capa_system=None, seed=723): """Construct a `LoncapaProblem` suitable for unit tests.""" return LoncapaProblem(xml, id='1', seed=seed, capa_system=capa_system or test_capa_system()) + + +def load_fixture(relpath): + """ + Return a `unicode` object representing the contents + of the fixture file at the given path within a test_files directory + in the same directory as the test file. + """ + abspath = os.path.join(os.path.dirname(__file__), 'test_files', relpath) + with open(abspath) as fixture_file: + contents = fixture_file.read() + return contents.decode('utf8') diff --git a/common/lib/capa/capa/tests/test_files/targeted_feedback.xml b/common/lib/capa/capa/tests/test_files/targeted_feedback.xml new file mode 100644 index 0000000000..e762f19c65 --- /dev/null +++ b/common/lib/capa/capa/tests/test_files/targeted_feedback.xml @@ -0,0 +1,50 @@ + +

What is the correct answer?

+ + + wrong-1 + wrong-2 + correct-1 + wrong-3 + + + + + +
+

Targeted Feedback

+

This is the 1st WRONG solution

+
+
+ + +
+

Targeted Feedback

+

This is the 2nd WRONG solution

+
+
+ + +
+

Targeted Feedback

+

This is the 3rd WRONG solution

+
+
+ + +
+

Targeted Feedback

+

Feedback on your correct solution...

+
+
+ +
+ + +
+

Explanation

+

This is the solution explanation

+

Not much to explain here, sorry!

+
+
+
\ No newline at end of file diff --git a/common/lib/capa/capa/tests/test_files/targeted_feedback_multiple.xml b/common/lib/capa/capa/tests/test_files/targeted_feedback_multiple.xml new file mode 100644 index 0000000000..785df87c64 --- /dev/null +++ b/common/lib/capa/capa/tests/test_files/targeted_feedback_multiple.xml @@ -0,0 +1,91 @@ + +

Q1

+ + + wrong-1 + wrong-2 + correct-1 + wrong-3 + + + + + +
+

Targeted Feedback

+

This is the 1st WRONG solution

+
+
+ + +
+

Targeted Feedback

+

This is the 3rd WRONG solution

+
+
+ + +
+

Targeted Feedback

+

Feedback on your correct solution...

+
+
+ +
+ + + +
+

Explanation

+

This is the solution explanation

+

Not much to explain here, sorry!

+
+
+
+ +
+ +

Q2

+ + + wrong-1 + wrong-2 + correct-1 + wrong-3 + + + + + +
+

Targeted Feedback

+

This is the 1st WRONG solution

+
+
+ + +
+

Targeted Feedback

+

This is the 3rd WRONG solution

+
+
+ + +
+

Targeted Feedback

+

Feedback on your correct solution...

+
+
+ +
+ + + +
+

Explanation

+

This is the solution explanation

+

Not much to explain here, sorry!

+
+
+
+
\ No newline at end of file diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 770f6959ee..0057b90c3d 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -13,7 +13,7 @@ import textwrap import requests import mock -from . import new_loncapa_problem, test_capa_system +from . import new_loncapa_problem, test_capa_system, load_fixture import calc from capa.responsetypes import LoncapaProblemError, \ @@ -224,7 +224,7 @@ class SymbolicResponseTest(ResponseTest): for (input_str, input_mathml, server_fixture) in correct_inputs: print "Testing input: {0}".format(input_str) - server_resp = self._load_fixture(server_fixture) + server_resp = load_fixture(server_fixture) self._assert_symbolic_grade( problem, input_str, input_mathml, 'correct', snuggletex_resp=server_resp @@ -253,8 +253,8 @@ class SymbolicResponseTest(ResponseTest): options=["matrix", "imaginary"] ) - correct_snuggletex = self._load_fixture('snuggletex_correct.html') - dynamath_input = self._load_fixture('dynamath_input.txt') + correct_snuggletex = load_fixture('snuggletex_correct.html') + dynamath_input = load_fixture('dynamath_input.txt') student_response = "cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]" self._assert_symbolic_grade( @@ -269,7 +269,7 @@ class SymbolicResponseTest(ResponseTest): expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]", options=["matrix", "imaginary"]) - wrong_snuggletex = self._load_fixture('snuggletex_wrong.html') + wrong_snuggletex = load_fixture('snuggletex_wrong.html') dynamath_input = textwrap.dedent(""" 2 @@ -315,18 +315,6 @@ class SymbolicResponseTest(ResponseTest): correct_map.get_correctness('1_2_1'), expected_correctness ) - @staticmethod - def _load_fixture(relpath): - """ - Return a `unicode` object representing the contents - of the fixture file at `relpath` (relative to the test files dir) - """ - abspath = os.path.join(os.path.dirname(__file__), 'test_files', relpath) - with open(abspath) as fixture_file: - contents = fixture_file.read() - - return contents.decode('utf8') - class OptionResponseTest(ResponseTest): from capa.tests.response_xml_factory import OptionResponseXMLFactory diff --git a/common/lib/capa/capa/tests/test_targeted_feedback.py b/common/lib/capa/capa/tests/test_targeted_feedback.py index 6e0df87ff1..a9e34381a5 100644 --- a/common/lib/capa/capa/tests/test_targeted_feedback.py +++ b/common/lib/capa/capa/tests/test_targeted_feedback.py @@ -5,7 +5,7 @@ i.e. those with the element import unittest import textwrap -from . import test_capa_system, new_loncapa_problem +from . import test_capa_system, new_loncapa_problem, load_fixture class CapaTargetedFeedbackTest(unittest.TestCase): @@ -80,62 +80,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase): self.assertRegexpMatches(without_new_lines, r"
.*'wrong-1'.*'wrong-2'.*'correct-1'.*'wrong-3'.*
") self.assertRegexpMatches(without_new_lines, r"feedback1|feedback2|feedback3|feedbackC") - # A targeted-feedback problem shared for a few tests - common_targeted_xml = textwrap.dedent(""" - -

What is the correct answer?

- - - wrong-1 - wrong-2 - correct-1 - wrong-3 - - - - - -
-

Targeted Feedback

-

This is the 1st WRONG solution

-
-
- - -
-

Targeted Feedback

-

This is the 2nd WRONG solution

-
-
- - -
-

Targeted Feedback

-

This is the 3rd WRONG solution

-
-
- - -
-

Targeted Feedback

-

Feedback on your correct solution...

-
-
- -
- - -
-

Explanation

-

This is the solution explanation

-

Not much to explain here, sorry!

-
-
-
- """) - def test_targeted_feedback_not_finished(self): - problem = new_loncapa_problem(self.common_targeted_xml) + problem = new_loncapa_problem(load_fixture('targeted_feedback.xml')) the_html = problem.get_html() without_new_lines = the_html.replace("\n", "") @@ -144,7 +90,7 @@ class CapaTargetedFeedbackTest(unittest.TestCase): self.assertEquals(the_html, problem.get_html(), "Should be able to call get_html() twice") def test_targeted_feedback_student_answer1(self): - problem = new_loncapa_problem(self.common_targeted_xml) + problem = new_loncapa_problem(load_fixture('targeted_feedback.xml')) problem.done = True problem.student_answers = {'1_2_1': 'choice_3'} @@ -158,7 +104,7 @@ class CapaTargetedFeedbackTest(unittest.TestCase): self.assertEquals(the_html, the_html2) def test_targeted_feedback_student_answer2(self): - problem = new_loncapa_problem(self.common_targeted_xml) + problem = new_loncapa_problem(load_fixture('targeted_feedback.xml')) problem.done = True problem.student_answers = {'1_2_1': 'choice_0'} @@ -611,3 +557,41 @@ class CapaTargetedFeedbackTest(unittest.TestCase): self.assertNotRegexpMatches(without_new_lines, r"\{.*'1_solution_1'.*\}
") self.assertNotRegexpMatches(without_new_lines, r"feedback1|feedback3|feedbackC") + + def test_targeted_feedback_multiple_not_answered(self): + # Not answered -> empty targeted feedback + problem = new_loncapa_problem(load_fixture('targeted_feedback_multiple.xml')) + the_html = problem.get_html() + without_new_lines = the_html.replace("\n", "") + # Q1 and Q2 have no feedback + self.assertRegexpMatches( + without_new_lines, + r'\s*.*' + + r'\s*' + ) + + def test_targeted_feedback_multiple_answer_1(self): + problem = new_loncapa_problem(load_fixture('targeted_feedback_multiple.xml')) + problem.done = True + problem.student_answers = {'1_2_1': 'choice_0'} # feedback1 + the_html = problem.get_html() + without_new_lines = the_html.replace("\n", "") + # Q1 has feedback1 and Q2 has nothing + self.assertRegexpMatches( + without_new_lines, + r'.*?explanation-id="feedback1".*?.*' + + r'\s*' + ) + + def test_targeted_feedback_multiple_answer_2(self): + problem = new_loncapa_problem(load_fixture('targeted_feedback_multiple.xml')) + problem.done = True + problem.student_answers = {'1_2_1': 'choice_0', '1_3_1': 'mask_1'} # Q1 wrong, Q2 correct + the_html = problem.get_html() + without_new_lines = the_html.replace("\n", "") + # Q1 has feedback1 and Q2 has feedbackC + self.assertRegexpMatches( + without_new_lines, + r'.*?explanation-id="feedback1".*?.*' + + r'.*explanation-id="feedbackC".*?' + ) diff --git a/common/lib/xmodule/xmodule/js/spec/combinedopenended/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/combinedopenended/display_spec.coffee index ef2c3cf0f9..f90728728a 100644 --- a/common/lib/xmodule/xmodule/js/spec/combinedopenended/display_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/combinedopenended/display_spec.coffee @@ -81,7 +81,7 @@ describe 'CombinedOpenEnded', -> expect(window.setTimeout).toHaveBeenCalledWith(@combined.poll, 10000) expect(window.queuePollerID).toBe(5) - it 'polling stops properly', => + xit 'polling stops properly', => fakeResponseDone = state: "done" spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(fakeResponseDone) @combined.poll() diff --git a/common/lib/xmodule/xmodule/js/src/problem/edit.coffee b/common/lib/xmodule/xmodule/js/src/problem/edit.coffee index ee318ecdb7..30d448dd65 100644 --- a/common/lib/xmodule/xmodule/js/src/problem/edit.coffee +++ b/common/lib/xmodule/xmodule/js/src/problem/edit.coffee @@ -109,7 +109,7 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor $(".CodeMirror").css({"overflow": "visible"}) $(".modal-content").css({"overflow-y": "visible", "overflow-x": "visible"}) else - $(".CodeMirror").removeAttr("style") + $(".CodeMirror").css({"overflow": "none"}) $(".modal-content").removeAttr("style") ### diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py index ce5682665f..6555c94d29 100644 --- a/common/lib/xmodule/xmodule/modulestore/inheritance.py +++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py @@ -5,7 +5,7 @@ Support for inheritance of fields down an XBlock hierarchy. from datetime import datetime from pytz import UTC -from xblock.fields import Scope, Boolean, String, Float, XBlockMixin, Dict +from xblock.fields import Scope, Boolean, String, Float, XBlockMixin, Dict, Integer from xblock.runtime import KeyValueStore, KvsFieldData from xmodule.fields import Date, Timedelta @@ -78,7 +78,15 @@ class InheritanceMixin(XBlockMixin): use_latex_compiler = Boolean( help="Enable LaTeX templates?", default=False, - scope=Scope.settings) + scope=Scope.settings + ) + max_attempts = Integer( + display_name="Maximum Attempts", + help=("Defines the number of times a student can try to answer this problem. " + "If the value is not set, infinite attempts are allowed."), + values={"min": 0}, scope=Scope.settings + ) + def compute_inherited_metadata(descriptor): diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index 28c521d35f..c9d7577dee 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -1,9 +1,17 @@ # pylint: disable=E0611 from nose.tools import assert_equals, assert_raises, \ +<<<<<<< HEAD assert_not_equals, assert_false, assert_true, assert_greater, assert_is_instance +======= + assert_not_equals, assert_false, assert_true +from itertools import ifilter +>>>>>>> edx/master # pylint: enable=E0611 +from path import path import pymongo import logging +import shutil +from tempfile import mkdtemp from uuid import uuid4 import unittest import bson.son @@ -18,7 +26,11 @@ from xmodule.tests import DATA_DIR from xmodule.modulestore import Location, MONGO_MODULESTORE_TYPE from xmodule.modulestore.mongo import MongoModuleStore, MongoKeyValueStore from xmodule.modulestore.draft import DraftModuleStore +<<<<<<< HEAD from xmodule.modulestore.locations import SlashSeparatedCourseKey, AssetLocation +======= +from xmodule.modulestore.xml_exporter import export_to_xml +>>>>>>> edx/master from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint from xmodule.contentstore.mongo import MongoContentStore @@ -351,6 +363,7 @@ class TestMongoModuleStore(unittest.TestCase): } ) +<<<<<<< HEAD def check_xblock_fields(): def check_children(xblock): for child in xblock.children: @@ -390,6 +403,58 @@ class TestMongoModuleStore(unittest.TestCase): setup_test() check_xblock_fields() check_mongo_fields() +======= + def test_export_course_image(self): + """ + Test to make sure that we have a course image in the contentstore, + then export it to ensure it gets copied to both file locations. + """ + location = Location('c4x', 'edX', 'simple', 'asset', 'images_course_image.jpg') + course_location = Location('i4x', 'edX', 'simple', 'course', '2012_Fall') + + # This will raise if the course image is missing + self.content_store.find(location) + + root_dir = path(mkdtemp()) + try: + export_to_xml(self.store, self.content_store, course_location, root_dir, 'test_export') + assert_true(path(root_dir / 'test_export/static/images/course_image.jpg').isfile()) + assert_true(path(root_dir / 'test_export/static/images_course_image.jpg').isfile()) + finally: + shutil.rmtree(root_dir) + + def test_export_course_image_nondefault(self): + """ + Make sure that if a non-default image path is specified that we + don't export it to the static default location + """ + course = self.get_course_by_id('edX/toy/2012_Fall') + assert_true(course.course_image, 'just_a_test.jpg') + + root_dir = path(mkdtemp()) + try: + export_to_xml(self.store, self.content_store, course.location, root_dir, 'test_export') + assert_true(path(root_dir / 'test_export/static/just_a_test.jpg').isfile()) + assert_false(path(root_dir / 'test_export/static/images/course_image.jpg').isfile()) + finally: + shutil.rmtree(root_dir) + + def test_course_without_image(self): + """ + Make sure we elegantly passover our code when there isn't a static + image + """ + course = self.get_course_by_id('edX/simple_with_draft/2012_Fall') + root_dir = path(mkdtemp()) + try: + export_to_xml(self.store, self.content_store, course.location, root_dir, 'test_export') + assert_false(path(root_dir / 'test_export/static/images/course_image.jpg').isfile()) + assert_false(path(root_dir / 'test_export/static/images_course_image.jpg').isfile()) + finally: + shutil.rmtree(root_dir) + + +>>>>>>> edx/master class TestMongoKeyValueStore(object): """ diff --git a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py index c4ef90154f..f0710349e7 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py @@ -5,6 +5,8 @@ Methods for exporting course data to XML import logging import lxml.etree from xblock.fields import Scope +from xmodule.contentstore.content import StaticContent +from xmodule.exceptions import NotFoundError from xmodule.modulestore import Location from xmodule.modulestore.inheritance import own_metadata from fs.osfs import OSFS @@ -78,6 +80,26 @@ def export_to_xml(modulestore, contentstore, course_key, root_dir, course_dir, d root_dir + '/' + course_dir + '/policies/assets.json', ) + # If we are using the default course image, export it to the + # legacy location to support backwards compatibility. + if course.course_image == course.fields['course_image'].default: + try: + course_image = contentstore.find( + StaticContent.compute_location( + course.location.org, + course.location.course, + course.course_image + ), + ) + except NotFoundError: + pass + else: + output_dir = root_dir + '/' + course_dir + '/static/images/' + if not os.path.isdir(output_dir): + os.makedirs(output_dir) + with OSFS(output_dir).open('course_image.jpg', 'wb') as course_image_file: + course_image_file.write(course_image.data) + # export the static tabs export_extra_content(export_fs, modulestore, course_key, 'static_tab', 'tabs', '.html') diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index cd8fe0b120..16d9cbf772 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -188,6 +188,43 @@ def import_from_xml( for module in xml_module_store.modules[course_key].itervalues(): if module.scope_ids.block_type == 'course': course_data_path = path(data_dir) / module.data_dir +<<<<<<< HEAD +======= + course_location = module.location + course_org_lower = course_location.org.lower() + course_number_lower = course_location.course.lower() + + # Check to see if a course with the same + # pseudo_course_id, but different run exists in + # the passed store to avoid broken courses + courses = store.get_courses() + bad_run = False + if target_location_namespace is None: + for course in courses: + if course.location.org.lower() == course_org_lower and \ + course.location.course.lower() == course_number_lower: + log.debug('Import is overwriting existing course') + # Importing over existing course, check + # that runs match or fail + if course.location.name != module.location.name: + log.error( + 'A course with ID %s exists, and this ' + 'course has the same organization and ' + 'course number, but a different term that ' + 'is fully identified as %s.', + course.location.course_id, + module.location.course_id + ) + bad_run = True + break + if bad_run: + # Skip this course, but keep trying to import courses + continue + + log.debug('======> IMPORTING course to location {loc}'.format( + loc=course_location + )) +>>>>>>> edx/master log.debug(u'======> IMPORTING course {course_key}'.format( course_key=course_key, diff --git a/common/lib/xmodule/xmodule/templates/html/grade_me.yaml b/common/lib/xmodule/xmodule/templates/html/grade_me.yaml deleted file mode 100644 index ecb9a1fa4d..0000000000 --- a/common/lib/xmodule/xmodule/templates/html/grade_me.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -metadata: - display_name: (Grade Me!) Button -data: | -

By clicking the button below, you assert that you have completed the course in its entirety.

- - -

- - diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py index 75fc2e46a0..fdf232eb6e 100644 --- a/common/lib/xmodule/xmodule/tests/test_xml_module.py +++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py @@ -538,14 +538,14 @@ class TestXmlAttributes(XModuleXmlImportTest): # name) assert_in('attempts', seq.xml_attributes) - def test_inheritable_attribute(self): - # days_early_for_beta isn't a basic attribute of Sequence - assert_false(hasattr(SequenceDescriptor, 'days_early_for_beta')) + def check_inheritable_attribute(self, attribute, value): + # `attribute` isn't a basic attribute of Sequence + assert_false(hasattr(SequenceDescriptor, attribute)) - # days_early_for_beta is added by InheritanceMixin - assert_true(hasattr(InheritanceMixin, 'days_early_for_beta')) + # `attribute` is added by InheritanceMixin + assert_true(hasattr(InheritanceMixin, attribute)) - root = SequenceFactory.build(policy={'days_early_for_beta': '2'}) + root = SequenceFactory.build(policy={attribute: str(value)}) ProblemFactory.build(parent=root) # InheritanceMixin will be used when processing the XML @@ -556,10 +556,14 @@ class TestXmlAttributes(XModuleXmlImportTest): assert_equals(seq.unmixed_class, SequenceDescriptor) assert_not_equals(type(seq), SequenceDescriptor) - # days_early_for_beta is added to the constructed sequence, because + # `attribute` is added to the constructed sequence, because # it's in the InheritanceMixin - assert_equals(2, seq.days_early_for_beta) + assert_equals(value, getattr(seq, attribute)) - # days_early_for_beta is a known attribute, so we shouldn't include it + # `attribute` is a known attribute, so we shouldn't include it # in xml_attributes - assert_not_in('days_early_for_beta', seq.xml_attributes) + assert_not_in(attribute, seq.xml_attributes) + + def test_inheritable_attributes(self): + self.check_inheritable_attribute('days_early_for_beta', 2) + self.check_inheritable_attribute('max_attempts', 5) diff --git a/common/test/data/simple/static/images_course_image.jpg b/common/test/data/simple/static/images_course_image.jpg new file mode 100644 index 0000000000..6bb7f377a0 Binary files /dev/null and b/common/test/data/simple/static/images_course_image.jpg differ diff --git a/common/test/data/test_import_course_2/about/end_date.html b/common/test/data/test_import_course_2/about/end_date.html new file mode 100644 index 0000000000..a0990367ef --- /dev/null +++ b/common/test/data/test_import_course_2/about/end_date.html @@ -0,0 +1 @@ +TBD diff --git a/common/test/data/test_import_course_2/chapter/vertical_container.xml b/common/test/data/test_import_course_2/chapter/vertical_container.xml new file mode 100644 index 0000000000..886346704c --- /dev/null +++ b/common/test/data/test_import_course_2/chapter/vertical_container.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/common/test/data/test_import_course_2/course.xml b/common/test/data/test_import_course_2/course.xml new file mode 100644 index 0000000000..de9962da73 --- /dev/null +++ b/common/test/data/test_import_course_2/course.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/common/test/data/test_import_course_2/course/2014_Fall.xml b/common/test/data/test_import_course_2/course/2014_Fall.xml new file mode 100644 index 0000000000..9b14d49dcd --- /dev/null +++ b/common/test/data/test_import_course_2/course/2014_Fall.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/common/test/data/test_import_course_2/info/handouts.html b/common/test/data/test_import_course_2/info/handouts.html new file mode 100644 index 0000000000..85fa34d71d --- /dev/null +++ b/common/test/data/test_import_course_2/info/handouts.html @@ -0,0 +1 @@ +Sample \ No newline at end of file diff --git a/common/test/data/test_import_course_2/policies/2012_Fall.json b/common/test/data/test_import_course_2/policies/2012_Fall.json new file mode 100644 index 0000000000..464184fac8 --- /dev/null +++ b/common/test/data/test_import_course_2/policies/2012_Fall.json @@ -0,0 +1,33 @@ +{ + "course/2012_Fall": { + "graceperiod": "2 days 5 hours 59 minutes 59 seconds", + "start": "2015-07-17T12:00", + "display_name": "Toy Course", + "graded": "true", + "tabs": [ + {"type": "courseware"}, + {"type": "course_info", "name": "Course Info"}, + {"type": "static_tab", "url_slug": "syllabus", "name": "Syllabus"}, + {"type": "static_tab", "url_slug": "resources", "name": "Resources"}, + {"type": "discussion", "name": "Discussion"}, + {"type": "wiki", "name": "Wiki"}, + {"type": "progress", "name": "Progress"} + ] + }, + "chapter/Overview": { + "display_name": "Overview" + }, + "videosequence/Toy_Videos": { + "display_name": "Toy Videos", + "format": "Lecture Sequence" + }, + "html/secret:toylab": { + "display_name": "Toy lab" + }, + "video/Video_Resources": { + "display_name": "Video Resources" + }, + "video/Welcome": { + "display_name": "Welcome" + } +} diff --git a/common/test/data/test_import_course_2/sequential/vertical_sequential.xml b/common/test/data/test_import_course_2/sequential/vertical_sequential.xml new file mode 100644 index 0000000000..695e640243 --- /dev/null +++ b/common/test/data/test_import_course_2/sequential/vertical_sequential.xml @@ -0,0 +1,4 @@ + + + … + \ No newline at end of file diff --git a/common/test/data/test_import_course_2/vertical/vertical_test.xml b/common/test/data/test_import_course_2/vertical/vertical_test.xml new file mode 100644 index 0000000000..3dd75a08c8 --- /dev/null +++ b/common/test/data/test_import_course_2/vertical/vertical_test.xml @@ -0,0 +1,9 @@ + + diff --git a/common/test/data/test_import_course_2/video/separate_file_video.xml b/common/test/data/test_import_course_2/video/separate_file_video.xml new file mode 100644 index 0000000000..b90ea9d8c4 --- /dev/null +++ b/common/test/data/test_import_course_2/video/separate_file_video.xml @@ -0,0 +1 @@ +