From 2ca1d0ef78825673ff8b956d53c3ae68acafabf7 Mon Sep 17 00:00:00 2001 From: Syed Hassan Raza Date: Fri, 12 Jun 2015 20:29:49 +0500 Subject: [PATCH 1/2] fix the source_version xblock after discard changes formatting changes --- .../xmodule/modulestore/split_mongo/split.py | 12 +++- .../tests/test_mixed_modulestore.py | 69 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py index 1dedc2f394..b8e425eb27 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py @@ -1875,6 +1875,11 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): block_data.fields = settings new_id = new_structure['_id'] + + # source_version records which revision a block was copied from. In this method, we're updating + # the block, so it's no longer a direct copy, and we can remove the source_version reference. + block_data.edit_info.source_version = None + self.version_block(block_data, user_id, new_id) self.update_structure(course_key, new_structure) # update the index entry if appropriate @@ -2969,8 +2974,11 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): if getattr(destination_block.edit_info, key) is None: setattr(destination_block.edit_info, key, val) - # introduce new edit info field for tracing where copied/published blocks came - destination_block.edit_info.source_version = new_block.edit_info.update_version + # If the block we are copying from was itself a copy, then just + # reference the original source, rather than the copy. + destination_block.edit_info.source_version = ( + new_block.edit_info.source_version or new_block.edit_info.update_version + ) if blacklist != EXCLUDE_ALL: for child in destination_block.fields.get('children', []): diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py index e83c9be516..c490b0f614 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py @@ -523,6 +523,75 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup): component = self.store.publish(component.location, self.user_id) self.assertFalse(self.store.has_changes(component)) + @ddt.data('draft', 'split') + def test_unit_stuck_in_draft_mode(self, default_ms): + """ + After revert_to_published() the has_changes() should return false if draft has no changes + """ + self.initdb(default_ms) + + test_course = self.store.create_course('testx', 'GreekHero', 'test_run', self.user_id) + + # Create a dummy component to test against + xblock = self.store.create_item( + self.user_id, + test_course.id, + 'vertical', + block_id='test_vertical' + ) + + # Not yet published, so changes are present + self.assertTrue(self.store.has_changes(xblock)) + + # Publish and verify that there are no unpublished changes + component = self.store.publish(xblock.location, self.user_id) + self.assertFalse(self.store.has_changes(component)) + + self.store.revert_to_published(component.location, self.user_id) + component = self.store.get_item(component.location) + self.assertFalse(self.store.has_changes(component)) + + # Publish and verify again + component = self.store.publish(component.location, self.user_id) + self.assertFalse(self.store.has_changes(component)) + + @ddt.data('draft', 'split') + def test_unit_stuck_in_published_mode(self, default_ms): + """ + After revert_to_published() the has_changes() should return true if draft has changes + """ + self.initdb(default_ms) + + test_course = self.store.create_course('testx', 'GreekHero', 'test_run', self.user_id) + + # Create a dummy component to test against + xblock = self.store.create_item( + self.user_id, + test_course.id, + 'vertical', + block_id='test_vertical' + ) + + # Not yet published, so changes are present + self.assertTrue(self.store.has_changes(xblock)) + + # Publish and verify that there are no unpublished changes + component = self.store.publish(xblock.location, self.user_id) + self.assertFalse(self.store.has_changes(component)) + + # Discard changes and verify that there are no changes + self.store.revert_to_published(component.location, self.user_id) + component = self.store.get_item(component.location) + self.assertFalse(self.store.has_changes(component)) + + # Change the component, then check that there now are changes + component = self.store.get_item(component.location) + component.display_name = 'Changed Display Name' + self.store.update_item(component, self.user_id) + + # Verify that changes are present + self.assertTrue(self.store.has_changes(component)) + def setup_has_changes(self, default_ms): """ Common set up for has_changes tests below. From 61431015c2d6f5cba6ee9087f5fba3ac0ba077f5 Mon Sep 17 00:00:00 2001 From: Adam Palay Date: Mon, 22 Jun 2015 17:19:53 -0400 Subject: [PATCH 2/2] update xblock only to mark field values as dirty if they've changed (TNL-2475) force save "download_video" field if not set set timezone to UTC explicitly --- .../contentstore/tests/test_utils.py | 2 +- .../xmodule/modulestore/xml_importer.py | 8 ++--- .../tests/test_delay_between_attempts.py | 36 +++++++++---------- .../xmodule/video_module/video_module.py | 3 +- .../tests/test_submitting_problems.py | 28 ++++++++++++++- requirements/edx/github.txt | 2 +- 6 files changed, 52 insertions(+), 27 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index c29231b2ea..194e5859d2 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -154,7 +154,7 @@ class XBlockVisibilityTestCase(ModuleStoreTestCase): super(XBlockVisibilityTestCase, self).setUp() self.dummy_user = ModuleStoreEnum.UserID.test - self.past = datetime(1970, 1, 1) + self.past = datetime(1970, 1, 1, tzinfo=UTC) self.future = datetime.now(UTC) + timedelta(days=1) self.course = CourseFactory.create() diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index dd9fb95cf1..9aa0e08432 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -1182,9 +1182,7 @@ def _update_module_location(module, new_location): # in which one component of the key is the XBlock's location (equivalent to "scope_ids"). # Since we've changed the XBlock's location, we need to re-save # all the XBlock's fields so they will be stored using the new location in the key. - # However, since XBlocks only save "dirty" fields, we need to first - # explicitly set each field to its current value before triggering the save. + # However, since XBlocks only save "dirty" fields, we need to call + # XBlock's `force_save_fields_method` if len(rekey_fields) > 0: - for rekey_field_name in rekey_fields: - setattr(module, rekey_field_name, getattr(module, rekey_field_name)) - module.save() + module.force_save_fields(rekey_fields) diff --git a/common/lib/xmodule/xmodule/tests/test_delay_between_attempts.py b/common/lib/xmodule/xmodule/tests/test_delay_between_attempts.py index 9a239f0860..7deabf8756 100644 --- a/common/lib/xmodule/xmodule/tests/test_delay_between_attempts.py +++ b/common/lib/xmodule/xmodule/tests/test_delay_between_attempts.py @@ -188,9 +188,9 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase): num_attempts = 1 (module, result) = self.create_and_check( num_attempts=num_attempts, - last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36), + last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC), submission_wait_seconds=180, - considered_now=datetime.datetime(2013, 12, 6, 0, 18, 36) + considered_now=datetime.datetime(2013, 12, 6, 0, 18, 36, tzinfo=UTC) ) # You should get a dialog that tells you to wait 2 minutes # Also, the number of attempts should not be incremented @@ -202,9 +202,9 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase): num_attempts = 1 (module, result) = self.create_and_check( num_attempts=num_attempts, - last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36), + last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC), submission_wait_seconds=180, - considered_now=datetime.datetime(2013, 12, 6, 0, 20, 35) + considered_now=datetime.datetime(2013, 12, 6, 0, 20, 35, tzinfo=UTC) ) # You should get a dialog that tells you to wait 2 minutes # Also, the number of attempts should not be incremented @@ -216,9 +216,9 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase): num_attempts = 1 (module, result) = self.create_and_check( num_attempts=num_attempts, - last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36), + last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC), submission_wait_seconds=180, - considered_now=datetime.datetime(2013, 12, 6, 0, 20, 36) + considered_now=datetime.datetime(2013, 12, 6, 0, 20, 36, tzinfo=UTC) ) # Successfully submitted and answered # Also, the number of attempts should increment by 1 @@ -230,9 +230,9 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase): num_attempts = 1 (module, result) = self.create_and_check( num_attempts=num_attempts, - last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36), + last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC), submission_wait_seconds=180, - considered_now=datetime.datetime(2013, 12, 6, 0, 24, 0) + considered_now=datetime.datetime(2013, 12, 6, 0, 24, 0, tzinfo=UTC) ) # Successfully submitted and answered # Also, the number of attempts should increment by 1 @@ -246,17 +246,17 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase): with self.assertRaises(xmodule.exceptions.NotFoundError): (module, unused_result) = self.create_and_check( num_attempts=num_attempts, - last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36), + last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC), submission_wait_seconds=180, - considered_now=datetime.datetime(2013, 12, 6, 0, 24, 0) + considered_now=datetime.datetime(2013, 12, 6, 0, 24, 0, tzinfo=UTC) ) # Now try it without the check_problem (module, unused_result) = self.create_and_check( num_attempts=num_attempts, - last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36), + last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC), submission_wait_seconds=180, - considered_now=datetime.datetime(2013, 12, 6, 0, 24, 0), + considered_now=datetime.datetime(2013, 12, 6, 0, 24, 0, tzinfo=UTC), skip_check_problem=True ) # Expect that number of attempts NOT incremented @@ -267,9 +267,9 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase): num_attempts = 1 (module, result) = self.create_and_check( num_attempts=num_attempts, - last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36), + last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC), submission_wait_seconds=60 * 60 * 2, - considered_now=datetime.datetime(2013, 12, 6, 2, 15, 35) + considered_now=datetime.datetime(2013, 12, 6, 2, 15, 35, tzinfo=UTC) ) # You should get a dialog that tells you to wait 2 minutes # Also, the number of attempts should not be incremented @@ -281,9 +281,9 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase): num_attempts = 1 (module, result) = self.create_and_check( num_attempts=num_attempts, - last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36), + last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC), submission_wait_seconds=60 * 60 * 2 + 63, - considered_now=datetime.datetime(2013, 12, 6, 1, 15, 40) + considered_now=datetime.datetime(2013, 12, 6, 1, 15, 40, tzinfo=UTC) ) # You should get a dialog that tells you to wait 2 minutes # Also, the number of attempts should not be incremented @@ -295,9 +295,9 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase): num_attempts = 1 (module, result) = self.create_and_check( num_attempts=num_attempts, - last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36), + last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC), submission_wait_seconds=60, - considered_now=datetime.datetime(2013, 12, 6, 0, 17, 36) + considered_now=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC) ) # You should get a dialog that tells you to wait 2 minutes # Also, the number of attempts should not be incremented diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index 72c8c82aba..fc76bb6c0c 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -381,9 +381,10 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler if not self.fields['download_video'].is_set_on(self): self.download_video = True - # Set download_video field to default value if its not explicitly set for backward compatibility. + # Force download_video field to default value if it's not explicitly set for backward compatibility. if not self.fields['download_video'].is_set_on(self): self.download_video = self.download_video + self.force_save_fields(['download_video']) # for backward compatibility. # If course was existed and was not re-imported by the moment of adding `download_track` field, diff --git a/lms/djangoapps/courseware/tests/test_submitting_problems.py b/lms/djangoapps/courseware/tests/test_submitting_problems.py index 34fa3d8161..dab16c4657 100644 --- a/lms/djangoapps/courseware/tests/test_submitting_problems.py +++ b/lms/djangoapps/courseware/tests/test_submitting_problems.py @@ -18,7 +18,7 @@ from capa.tests.response_xml_factory import ( CodeResponseXMLFactory, ) from courseware import grades -from courseware.models import StudentModule +from courseware.models import StudentModule, StudentModuleHistory from courseware.tests.helpers import LoginEnrollmentTestCase from lms.djangoapps.lms_xblock.runtime import quote_slashes from student.tests.factories import UserFactory @@ -426,6 +426,32 @@ class TestCourseGrader(TestSubmittingProblems): ) self.assertEqual(json.loads(resp.content).get("success"), err_msg) + def test_show_answer_doesnt_write_to_csm(self): + self.basic_setup() + self.submit_question_answer('p1', {'2_1': u'Correct'}) + + # Now fetch the state entry for that problem. + student_module = StudentModule.objects.get( + course_id=self.course.id, + student=self.student_user + ) + # count how many state history entries there are + baseline = StudentModuleHistory.objects.filter( + student_module=student_module + ) + baseline_count = baseline.count() + self.assertEqual(baseline_count, 3) + + # now click "show answer" + self.show_question_answer('p1') + + # check that we don't have more state history entries + csmh = StudentModuleHistory.objects.filter( + student_module=student_module + ) + current_count = csmh.count() + self.assertEqual(current_count, 3) + def test_none_grade(self): """ Check grade is 0 to begin with. diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 8926e31496..82f790cdad 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -32,7 +32,7 @@ git+https://github.com/hmarr/django-debug-toolbar-mongo.git@b0686a76f1ce3532088c -e git+https://github.com/jazkarta/ccx-keys.git@e6b03704b1bb97c1d2f31301ecb4e3a687c536ea#egg=ccx-keys # Our libraries: --e git+https://github.com/edx/XBlock.git@e1831fa86bff778ffe1308e00d8ed51b26f7c047#egg=XBlock +-e git+https://github.com/edx/XBlock.git@9fc3367b64a7baa4986f5681ea588d0ffe724a0f#egg=XBlock -e git+https://github.com/edx/codejail.git@6b17c33a89bef0ac510926b1d7fea2748b73aadd#egg=codejail -e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool -e git+https://github.com/edx/event-tracking.git@0.2.0#egg=event-tracking