def _create_course(self): """ * some detached items * some attached children * some orphans """ date_proxy = Date() metadata = { 'start': date_proxy.to_json(datetime.datetime(2000, 3, 13, 4)), 'display_name': 'Migration test course', } data = { 'wiki_slug': 'test_course_slug' } fields = metadata.copy() fields.update(data) # split requires the course to be created separately from creating items self.split_mongo.create_course( self.split_package_id, 'test_org', self.userid, fields=fields, root_block_id='runid' ) self.course_location = Location('i4x', 'test_org', 'test_course', 'course', 'runid') self.old_mongo.create_and_save_xmodule(self.course_location, data, metadata) runtime = self.old_mongo.get_item(self.course_location).runtime self._create_item('chapter', 'Chapter1', {}, {'display_name': 'Chapter 1'}, 'course', 'runid', runtime) self._create_item('chapter', 'Chapter2', {}, {'display_name': 'Chapter 2'}, 'course', 'runid', runtime) self._create_item('chapter', 'OrphanChapter', {}, {'display_name': 'Orphan Chapter'}, None, None, runtime) self._create_item('vertical', 'Vert1', {}, {'display_name': 'Vertical 1'}, 'chapter', 'Chapter1', runtime) self._create_item('vertical', 'OrphanVert', {}, {'display_name': 'Orphan Vertical'}, None, None, runtime) self._create_item('html', 'Html1', "<p>Goodbye</p>", {'display_name': 'Parented Html'}, 'vertical', 'Vert1', runtime) self._create_item('html', 'OrphanHtml', "<p>Hello</p>", {'display_name': 'Orphan html'}, None, None, runtime) self._create_item('static_tab', 'staticuno', "<p>tab</p>", {'display_name': 'Tab uno'}, None, None, runtime) self._create_item('about', 'overview', "<p>overview</p>", {}, None, None, runtime) self._create_item('course_info', 'updates', "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, None, None, runtime)
def compare_date_fields(self, details, encoded, context, field): if details[field] is not None: date = Date() if field in encoded and encoded[field] is not None: dt1 = date.from_json(encoded[field]) dt2 = details[field] self.assertEqual(dt1, dt2, msg="{} != {} at {}".format(dt1, dt2, context)) else: self.fail(field + " missing from encoded but in details at " + context) elif field in encoded and encoded[field] is not None: self.fail(field + " included in encoding but missing from details at " + context)
def compare_date_fields(self, details, encoded, context, field): if details[field] is not None: date = Date() if field in encoded and encoded[field] is not None: dt1 = date.from_json(encoded[field]) dt2 = details[field] expected_delta = datetime.timedelta(0) self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context) else: self.fail(field + " missing from encoded but in details at " + context) elif field in encoded and encoded[field] is not None: self.fail(field + " included in encoding but missing from details at " + context)
def _create_course(self): """ Create the course, publish all verticals * some detached items """ date_proxy = Date() metadata = { 'start': date_proxy.to_json(datetime.datetime(2000, 3, 13, 4)), 'display_name': 'Migration test course', } data = { 'wiki_slug': 'test_course_slug' } fields = metadata.copy() fields.update(data) self.course_location = Location('i4x', 'test_org', 'test_course', 'course', 'runid') self.old_mongo.create_and_save_xmodule(self.course_location, data, metadata) runtime = self.draft_mongo.get_item(self.course_location).runtime self._create_item('chapter', 'Chapter1', {}, {'display_name': 'Chapter 1'}, 'course', 'runid', runtime) self._create_item('chapter', 'Chapter2', {}, {'display_name': 'Chapter 2'}, 'course', 'runid', runtime) self._create_item('vertical', 'Vert1', {}, {'display_name': 'Vertical 1'}, 'chapter', 'Chapter1', runtime) self._create_item('vertical', 'Vert2', {}, {'display_name': 'Vertical 2'}, 'chapter', 'Chapter1', runtime) self._create_item('html', 'Html1', "<p>Goodbye</p>", {'display_name': 'Parented Html'}, 'vertical', 'Vert1', runtime) self._create_item( 'discussion', 'Discussion1', "discussion discussion_category=\"Lecture 1\" discussion_id=\"a08bfd89b2aa40fa81f2c650a9332846\" discussion_target=\"Lecture 1\"/>\n", { "discussion_category": "Lecture 1", "discussion_target": "Lecture 1", "display_name": "Lecture 1 Discussion", "discussion_id": "a08bfd89b2aa40fa81f2c650a9332846" }, 'vertical', 'Vert1', runtime ) self._create_item('html', 'Html2', "<p>Hellow</p>", {'display_name': 'Hollow Html'}, 'vertical', 'Vert1', runtime) self._create_item( 'discussion', 'Discussion2', "discussion discussion_category=\"Lecture 2\" discussion_id=\"b08bfd89b2aa40fa81f2c650a9332846\" discussion_target=\"Lecture 2\"/>\n", { "discussion_category": "Lecture 2", "discussion_target": "Lecture 2", "display_name": "Lecture 2 Discussion", "discussion_id": "b08bfd89b2aa40fa81f2c650a9332846" }, 'vertical', 'Vert2', runtime ) self._create_item('static_tab', 'staticuno', "<p>tab</p>", {'display_name': 'Tab uno'}, None, None, runtime) self._create_item('about', 'overview', "<p>overview</p>", {}, None, None, runtime) self._create_item('course_info', 'updates', "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, None, None, runtime)
def compare_date_fields(self, details, encoded, context, field): """ Compare the given date fields between the before and after doing json deserialization """ if details[field] is not None: date = Date() if field in encoded and encoded[field] is not None: dt1 = date.from_json(encoded[field]) dt2 = details[field] self.assertEqual(dt1, dt2, msg="{} != {} at {}".format(dt1, dt2, context)) else: self.fail(field + " missing from encoded but in details at " + context) elif field in encoded and encoded[field] is not None: self.fail(field + " included in encoding but missing from details at " + context)
def move_overrides_to_edx_when(apps, schema_editor): from xmodule.fields import Date from edx_when import api date_field = Date() StudentFieldOverride = apps.get_model('courseware', 'StudentFieldOverride') log = logging.getLogger(__name__) for override in StudentFieldOverride.objects.filter(field='due'): try: abs_date = date_field.from_json(json.loads(override.value)) api.set_date_for_block( override.course_id, override.location, 'due', abs_date, user=override.student) except Exception: # pylint: disable=broad-except log.exception("migrating %d %r: %r", override.id, override.location, override.value)
def compare_date_fields(self, details, encoded, context, field): if details[field] is not None: date = Date() if field in encoded and encoded[field] is not None: encoded_encoded = date.from_json(encoded[field]) dt1 = CourseDetailsViewTest.struct_to_datetime(encoded_encoded) if isinstance(details[field], datetime.datetime): dt2 = details[field] else: details_encoded = date.from_json(details[field]) dt2 = CourseDetailsViewTest.struct_to_datetime(details_encoded) expected_delta = datetime.timedelta(0) self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context) else: self.fail(field + " missing from encoded but in details at " + context) elif field in encoded and encoded[field] is not None: self.fail(field + " included in encoding but missing from details at " + context)
def update_from_json(cls, course_locator, jsondict): """ Decode the json into CourseDetails and save any changed attrs to the db """ course_old_location = loc_mapper().translate_locator_to_location(course_locator) descriptor = get_modulestore(course_old_location).get_item(course_old_location) dirty = False # In the descriptor's setter, the date is converted to JSON using Date's to_json method. # Calling to_json on something that is already JSON doesn't work. Since reaching directly # into the model is nasty, convert the JSON Date to a Python date, which is what the # setter expects as input. date = Date() if 'start_date' in jsondict: converted = date.from_json(jsondict['start_date']) else: converted = None if converted != descriptor.start: dirty = True descriptor.start = converted if 'end_date' in jsondict: converted = date.from_json(jsondict['end_date']) else: converted = None if converted != descriptor.end: dirty = True descriptor.end = converted if 'enrollment_start' in jsondict: converted = date.from_json(jsondict['enrollment_start']) else: converted = None if converted != descriptor.enrollment_start: dirty = True descriptor.enrollment_start = converted if 'enrollment_end' in jsondict: converted = date.from_json(jsondict['enrollment_end']) else: converted = None if converted != descriptor.enrollment_end: dirty = True descriptor.enrollment_end = converted if 'course_image_name' in jsondict and jsondict['course_image_name'] != descriptor.course_image: descriptor.course_image = jsondict['course_image_name'] dirty = True if dirty: # Save the data that we've just changed to the underlying # MongoKeyValueStore before we update the mongo datastore. descriptor.save() get_modulestore(course_old_location).update_metadata(course_old_location, own_metadata(descriptor)) # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # to make faster, could compare against db or could have client send over a list of which fields changed. temploc = Location(course_old_location).replace(category='about', name='syllabus') update_item(temploc, jsondict['syllabus']) temploc = temploc.replace(name='overview') update_item(temploc, jsondict['overview']) temploc = temploc.replace(name='tags') update_item(temploc, jsondict['tags']) temploc = temploc.replace(name='effort') update_item(temploc, jsondict['effort']) temploc = temploc.replace(name='video') recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video']) update_item(temploc, recomposed_video_tag) # Could just return jsondict w/o doing any db reads, but I put the reads in as a means to confirm # it persisted correctly return CourseDetails.fetch(course_locator)
def update_from_json(cls, course_key, jsondict, user): """ Decode the json into CourseDetails and save any changed attrs to the db """ module_store = modulestore() descriptor = module_store.get_course(course_key) dirty = False # In the descriptor's setter, the date is converted to JSON using Date's to_json method. # Calling to_json on something that is already JSON doesn't work. Since reaching directly # into the model is nasty, convert the JSON Date to a Python date, which is what the # setter expects as input. date = Date() if "start_date" in jsondict: converted = date.from_json(jsondict["start_date"]) else: converted = None if converted != descriptor.start: dirty = True descriptor.start = converted if "end_date" in jsondict: converted = date.from_json(jsondict["end_date"]) else: converted = None if converted != descriptor.end: dirty = True descriptor.end = converted if "enrollment_start" in jsondict: converted = date.from_json(jsondict["enrollment_start"]) else: converted = None if converted != descriptor.enrollment_start: dirty = True descriptor.enrollment_start = converted if "enrollment_end" in jsondict: converted = date.from_json(jsondict["enrollment_end"]) else: converted = None if converted != descriptor.enrollment_end: dirty = True descriptor.enrollment_end = converted if "course_image_name" in jsondict and jsondict["course_image_name"] != descriptor.course_image: descriptor.course_image = jsondict["course_image_name"] dirty = True if "pre_requisite_courses" in jsondict and sorted(jsondict["pre_requisite_courses"]) != sorted( descriptor.pre_requisite_courses ): descriptor.pre_requisite_courses = jsondict["pre_requisite_courses"] dirty = True if "license" in jsondict: descriptor.license = jsondict["license"] dirty = True if "language" in jsondict and jsondict["language"] != descriptor.language: descriptor.language = jsondict["language"] dirty = True if dirty: module_store.update_item(descriptor, user.id) # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # to make faster, could compare against db or could have client send over a list of which fields changed. for attribute in ABOUT_ATTRIBUTES: if attribute in jsondict: cls.update_about_item(course_key, attribute, jsondict[attribute], descriptor, user) recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict["intro_video"]) cls.update_about_item(course_key, "video", recomposed_video_tag, descriptor, user) # Could just return jsondict w/o doing any db reads, but I put the reads in as a means to confirm # it persisted correctly return CourseDetails.fetch(course_key)
def _create_course(self): """ Create the course, publish all verticals * some detached items """ date_proxy = Date() metadata = { "start": date_proxy.to_json(datetime.datetime(2000, 3, 13, 4)), "display_name": "Migration test course", } data = {"wiki_slug": "test_course_slug"} fields = metadata.copy() fields.update(data) self.course_location = Location("i4x", "test_org", "test_course", "course", "runid") self.old_mongo.create_and_save_xmodule(self.course_location, data, metadata) runtime = self.draft_mongo.get_item(self.course_location).runtime self._create_item("chapter", "Chapter1", {}, {"display_name": "Chapter 1"}, "course", "runid", runtime) self._create_item("chapter", "Chapter2", {}, {"display_name": "Chapter 2"}, "course", "runid", runtime) self._create_item("vertical", "Vert1", {}, {"display_name": "Vertical 1"}, "chapter", "Chapter1", runtime) self._create_item("vertical", "Vert2", {}, {"display_name": "Vertical 2"}, "chapter", "Chapter1", runtime) self._create_item( "html", "Html1", "<p>Goodbye</p>", {"display_name": "Parented Html"}, "vertical", "Vert1", runtime ) self._create_item( "discussion", "Discussion1", 'discussion discussion_category="Lecture 1" discussion_id="a08bfd89b2aa40fa81f2c650a9332846" discussion_target="Lecture 1"/>\n', { "discussion_category": "Lecture 1", "discussion_target": "Lecture 1", "display_name": "Lecture 1 Discussion", "discussion_id": "a08bfd89b2aa40fa81f2c650a9332846", }, "vertical", "Vert1", runtime, ) self._create_item( "html", "Html2", "<p>Hellow</p>", {"display_name": "Hollow Html"}, "vertical", "Vert1", runtime ) self._create_item( "discussion", "Discussion2", 'discussion discussion_category="Lecture 2" discussion_id="b08bfd89b2aa40fa81f2c650a9332846" discussion_target="Lecture 2"/>\n', { "discussion_category": "Lecture 2", "discussion_target": "Lecture 2", "display_name": "Lecture 2 Discussion", "discussion_id": "b08bfd89b2aa40fa81f2c650a9332846", }, "vertical", "Vert2", runtime, ) self._create_item("static_tab", "staticuno", "<p>tab</p>", {"display_name": "Tab uno"}, None, None, runtime) self._create_item("about", "overview", "<p>overview</p>", {}, None, None, runtime) self._create_item( "course_info", "updates", "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, None, None, runtime )
def update_from_json(cls, course_key, jsondict, user): # pylint: disable=too-many-statements """ Decode the json into CourseDetails and save any changed attrs to the db """ module_store = modulestore() descriptor = module_store.get_course(course_key) dirty = False # In the descriptor's setter, the date is converted to JSON # using Date's to_json method. Calling to_json on something that # is already JSON doesn't work. Since reaching directly into the # model is nasty, convert the JSON Date to a Python date, which # is what the setter expects as input. date = Date() if 'start_date' in jsondict: converted = date.from_json(jsondict['start_date']) else: converted = None if converted != descriptor.start: dirty = True descriptor.start = converted if 'end_date' in jsondict: converted = date.from_json(jsondict['end_date']) else: converted = None if converted != descriptor.end: dirty = True descriptor.end = converted if 'enrollment_start' in jsondict: converted = date.from_json(jsondict['enrollment_start']) else: converted = None if converted != descriptor.enrollment_start: dirty = True descriptor.enrollment_start = converted if 'enrollment_end' in jsondict: converted = date.from_json(jsondict['enrollment_end']) else: converted = None if converted != descriptor.enrollment_end: dirty = True descriptor.enrollment_end = converted if 'course_image_name' in jsondict and jsondict[ 'course_image_name'] != descriptor.course_image: descriptor.course_image = jsondict['course_image_name'] dirty = True if 'banner_image_name' in jsondict and jsondict[ 'banner_image_name'] != descriptor.banner_image: descriptor.banner_image = jsondict['banner_image_name'] dirty = True if 'video_thumbnail_image_name' in jsondict \ and jsondict['video_thumbnail_image_name'] != descriptor.video_thumbnail_image: descriptor.video_thumbnail_image = jsondict[ 'video_thumbnail_image_name'] dirty = True if 'pre_requisite_courses' in jsondict \ and sorted(jsondict['pre_requisite_courses']) != sorted(descriptor.pre_requisite_courses): descriptor.pre_requisite_courses = jsondict[ 'pre_requisite_courses'] dirty = True if 'license' in jsondict: descriptor.license = jsondict['license'] dirty = True if 'learning_info' in jsondict: descriptor.learning_info = jsondict['learning_info'] dirty = True if 'instructor_info' in jsondict: descriptor.instructor_info = jsondict['instructor_info'] dirty = True if 'language' in jsondict and jsondict[ 'language'] != descriptor.language: descriptor.language = jsondict['language'] dirty = True if (SelfPacedConfiguration.current().enabled and descriptor.can_toggle_course_pacing and 'self_paced' in jsondict and jsondict['self_paced'] != descriptor.self_paced): descriptor.self_paced = jsondict['self_paced'] dirty = True if dirty: module_store.update_item(descriptor, user.id) # NOTE: below auto writes to the db w/o verifying that any of # the fields actually changed to make faster, could compare # against db or could have client send over a list of which # fields changed. for attribute in ABOUT_ATTRIBUTES: if attribute in jsondict: cls.update_about_item(descriptor, attribute, jsondict[attribute], user.id) cls.update_about_video(descriptor, jsondict['intro_video'], user.id) # Could just return jsondict w/o doing any db reads, but I put # the reads in as a means to confirm it persisted correctly return CourseDetails.fetch(course_key)
class DateTest(unittest.TestCase): # lint-amnesty, pylint: disable=missing-class-docstring date = Date() def compare_dates(self, dt1, dt2, expected_delta): assert (dt1 - dt2) == expected_delta, ((( (str(dt1) + '-') + str(dt2)) + '!=') + str(expected_delta)) def test_from_json(self): """Test conversion from iso compatible date strings to struct_time""" self.compare_dates(DateTest.date.from_json("2013-01-01"), DateTest.date.from_json("2012-12-31"), datetime.timedelta(days=1)) self.compare_dates(DateTest.date.from_json("2013-01-01T00"), DateTest.date.from_json("2012-12-31T23"), datetime.timedelta(hours=1)) self.compare_dates(DateTest.date.from_json("2013-01-01T00:00"), DateTest.date.from_json("2012-12-31T23:59"), datetime.timedelta(minutes=1)) self.compare_dates(DateTest.date.from_json("2013-01-01T00:00:00"), DateTest.date.from_json("2012-12-31T23:59:59"), datetime.timedelta(seconds=1)) self.compare_dates(DateTest.date.from_json("2013-01-01T00:00:00Z"), DateTest.date.from_json("2012-12-31T23:59:59Z"), datetime.timedelta(seconds=1)) self.compare_dates( DateTest.date.from_json("2012-12-31T23:00:01-01:00"), DateTest.date.from_json("2013-01-01T00:00:00+01:00"), datetime.timedelta(hours=1, seconds=1)) def test_enforce_type(self): assert DateTest.date.enforce_type(None) is None assert DateTest.date.enforce_type('') is None assert DateTest.date.enforce_type('2012-12-31T23:00:01') ==\ datetime.datetime(2012, 12, 31, 23, 0, 1, tzinfo=UTC) assert DateTest.date.enforce_type(1234567890000) == datetime.datetime( 2009, 2, 13, 23, 31, 30, tzinfo=UTC) assert DateTest.date.enforce_type(datetime.datetime(2014, 5, 9, 21, 1, 27, tzinfo=UTC)) ==\ datetime.datetime(2014, 5, 9, 21, 1, 27, tzinfo=UTC) with pytest.raises(TypeError): DateTest.date.enforce_type([1]) def test_return_None(self): assert DateTest.date.from_json('') is None assert DateTest.date.from_json(None) is None with pytest.raises(TypeError): DateTest.date.from_json(['unknown value']) def test_old_due_date_format(self): current = datetime.datetime.today() assert datetime.datetime( current.year, 3, 12, 12, tzinfo=UTC) == DateTest.date.from_json('March 12 12:00') assert datetime.datetime( current.year, 12, 4, 16, 30, tzinfo=UTC) == DateTest.date.from_json('December 4 16:30') assert DateTest.date.from_json('12 12:00') is None def test_non_std_from_json(self): """ Test the non-standard args being passed to from_json """ now = datetime.datetime.now(UTC) delta = now - datetime.datetime.fromtimestamp(0, UTC) assert DateTest.date.from_json(delta.total_seconds() * 1000) == now yesterday = datetime.datetime.now(UTC) - datetime.timedelta(days=-1) assert DateTest.date.from_json(yesterday) == yesterday def test_to_json(self): """ Test converting time reprs to iso dates """ assert DateTest.date.to_json(datetime.datetime.strptime('2012-12-31T23:59:59Z', '%Y-%m-%dT%H:%M:%SZ')) ==\ '2012-12-31T23:59:59Z' assert DateTest.date.to_json( DateTest.date.from_json( '2012-12-31T23:59:59Z')) == '2012-12-31T23:59:59Z' assert DateTest.date.to_json(DateTest.date.from_json('2012-12-31T23:00:01-01:00')) ==\ '2012-12-31T23:00:01-01:00' with pytest.raises(TypeError): DateTest.date.to_json('2012-12-31T23:00:01-01:00')
def update_from_json(cls, course_key, jsondict, user): # pylint: disable=too-many-statements """ Decode the json into CourseDetails and save any changed attrs to the db """ module_store = modulestore() descriptor = module_store.get_course(course_key) dirty = False # In the descriptor's setter, the date is converted to JSON # using Date's to_json method. Calling to_json on something that # is already JSON doesn't work. Since reaching directly into the # model is nasty, convert the JSON Date to a Python date, which # is what the setter expects as input. date = Date() if 'start_date' in jsondict: converted = date.from_json(jsondict['start_date']) else: converted = None if converted != descriptor.start: dirty = True descriptor.start = converted if 'end_date' in jsondict: converted = date.from_json(jsondict['end_date']) else: converted = None if converted != descriptor.end: dirty = True descriptor.end = converted if 'enrollment_start' in jsondict: converted = date.from_json(jsondict['enrollment_start']) else: converted = None if converted != descriptor.enrollment_start: dirty = True descriptor.enrollment_start = converted if 'enrollment_end' in jsondict: converted = date.from_json(jsondict['enrollment_end']) else: converted = None if converted != descriptor.enrollment_end: dirty = True descriptor.enrollment_end = converted if 'course_image_name' in jsondict and jsondict['course_image_name'] != descriptor.course_image: descriptor.course_image = jsondict['course_image_name'] dirty = True if 'pre_requisite_courses' in jsondict \ and sorted(jsondict['pre_requisite_courses']) != sorted(descriptor.pre_requisite_courses): descriptor.pre_requisite_courses = jsondict['pre_requisite_courses'] dirty = True if 'license' in jsondict: descriptor.license = jsondict['license'] dirty = True if 'language' in jsondict and jsondict['language'] != descriptor.language: descriptor.language = jsondict['language'] dirty = True if (SelfPacedConfiguration.current().enabled and descriptor.can_toggle_course_pacing and 'self_paced' in jsondict and jsondict['self_paced'] != descriptor.self_paced): descriptor.self_paced = jsondict['self_paced'] dirty = True if dirty: module_store.update_item(descriptor, user.id) # NOTE: below auto writes to the db w/o verifying that any of # the fields actually changed to make faster, could compare # against db or could have client send over a list of which # fields changed. for attribute in ABOUT_ATTRIBUTES: if attribute in jsondict: cls.update_about_item(descriptor, attribute, jsondict[attribute], user.id) cls.update_about_video(descriptor, jsondict['intro_video'], user.id) # Could just return jsondict w/o doing any db reads, but I put # the reads in as a means to confirm it persisted correctly return CourseDetails.fetch(course_key)
def update_from_json(cls, course_locator, jsondict, user): """ Decode the json into CourseDetails and save any changed attrs to the db """ course_old_location = loc_mapper().translate_locator_to_location(course_locator) descriptor = get_modulestore(course_old_location).get_item(course_old_location) dirty = False # In the descriptor's setter, the date is converted to JSON using Date's to_json method. # Calling to_json on something that is already JSON doesn't work. Since reaching directly # into the model is nasty, convert the JSON Date to a Python date, which is what the # setter expects as input. date = Date() if 'start_date' in jsondict: converted = date.from_json(jsondict['start_date']) else: converted = None if converted != descriptor.start: dirty = True descriptor.start = converted if 'end_date' in jsondict: converted = date.from_json(jsondict['end_date']) else: converted = None if converted != descriptor.end: dirty = True descriptor.end = converted if 'enrollment_start' in jsondict: converted = date.from_json(jsondict['enrollment_start']) else: converted = None if converted != descriptor.enrollment_start: dirty = True descriptor.enrollment_start = converted if 'enrollment_end' in jsondict: converted = date.from_json(jsondict['enrollment_end']) else: converted = None if converted != descriptor.enrollment_end: dirty = True descriptor.enrollment_end = converted if 'course_image_name' in jsondict and jsondict['course_image_name'] != descriptor.course_image: descriptor.course_image = jsondict['course_image_name'] dirty = True if dirty: get_modulestore(course_old_location).update_item(descriptor, user.id) # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # to make faster, could compare against db or could have client send over a list of which fields changed. for about_type in ['syllabus', 'overview', 'effort', 'short_description']: cls.update_about_item(course_old_location, about_type, jsondict[about_type], descriptor, user) recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video']) cls.update_about_item(course_old_location, 'video', recomposed_video_tag, descriptor, user) # Could just return jsondict w/o doing any db reads, but I put the reads in as a means to confirm # it persisted correctly return CourseDetails.fetch(course_locator)
class InheritanceMixin(XBlockMixin): """Field definitions for inheritable fields.""" graded = Boolean( help="Whether this module contributes to the final course grade", scope=Scope.settings, default=False, ) start = Date(help="Start time when this module is visible", default=DEFAULT_START_DATE, scope=Scope.settings) due = Date( display_name=_("Due Date"), help=_("Enter the default date by which problems are due."), scope=Scope.settings, ) visible_to_staff_only = Boolean( help= _("If true, can be seen only by course staff, regardless of start date." ), default=False, scope=Scope.settings, ) course_edit_method = String( display_name=_("Course Editor"), help= _("Enter the method by which this course is edited (\"XML\" or \"Studio\")." ), default="Studio", scope=Scope.settings, deprecated= True # Deprecated because user would not change away from Studio within Studio. ) giturl = String( display_name=_("GIT URL"), help=_("Enter the URL for the course data GIT repository."), scope=Scope.settings) xqa_key = String(display_name=_("XQA Key"), help=_("This setting is not currently supported."), scope=Scope.settings, deprecated=True) graceperiod = Timedelta( help= "Amount of time after the due date that submissions will be accepted", scope=Scope.settings, ) group_access = Dict( help=_( "Enter the ids for the content groups this problem belongs to."), scope=Scope.settings, ) showanswer = String( display_name=_("Show Answer"), help=_( # Translators: DO NOT translate the words in quotes here, they are # specific words for the acceptable values. 'Specify when the Show Answer button appears for each problem. ' 'Valid values are "always", "answered", "attempted", "closed", ' '"finished", "past_due", "correct_or_past_due", and "never".'), scope=Scope.settings, default="finished", ) show_correctness = String( display_name=_("Show Results"), help=_( # Translators: DO NOT translate the words in quotes here, they are # specific words for the acceptable values. 'Specify when to show answer correctness and score to learners. ' 'Valid values are "always", "never", and "past_due".'), scope=Scope.settings, default="always", ) rerandomize = String( display_name=_("Randomization"), help=_( # Translators: DO NOT translate the words in quotes here, they are # specific words for the acceptable values. 'Specify the default for how often variable values in a problem are randomized. ' 'This setting should be set to "never" unless you plan to provide a Python ' 'script to identify and randomize values in most of the problems in your course. ' 'Valid values are "always", "onreset", "never", and "per_student".' ), scope=Scope.settings, default="never", ) days_early_for_beta = Float( display_name=_("Days Early for Beta Users"), help= _("Enter the number of days before the start date that beta users can access the course." ), scope=Scope.settings, default=None, ) static_asset_path = String( display_name=_("Static Asset Path"), help= _("Enter the path to use for files on the Files & Uploads page. This value overrides the Studio default, c4x://." ), scope=Scope.settings, default='', ) use_latex_compiler = Boolean( display_name=_("Enable LaTeX Compiler"), help= _("Enter true or false. If true, you can use the LaTeX templates for HTML components and advanced Problem components." ), default=False, scope=Scope.settings) max_attempts = Integer( display_name=_("Maximum Attempts"), help= _("Enter the maximum number of times a student can try to answer problems. By default, Maximum Attempts is set to null, meaning that students have an unlimited number of attempts for problems. You can override this course-wide setting for individual problems. However, if the course-wide setting is a specific number, you cannot set the Maximum Attempts for individual problems to unlimited." ), values={"min": 0}, scope=Scope.settings) matlab_api_key = String( display_name=_("Matlab API key"), help= _("Enter the API key provided by MathWorks for accessing the MATLAB Hosted Service. " "This key is granted for exclusive use in this course for the specified duration. " "Do not share the API key with other courses. Notify MathWorks immediately " "if you believe the key is exposed or compromised. To obtain a key for your course, " "or to report an issue, please contact [email protected]"), scope=Scope.settings) # This is should be scoped to content, but since it's defined in the policy # file, it is currently scoped to settings. user_partitions = UserPartitionList( display_name=_("Group Configurations"), help= _("Enter the configurations that govern how students are grouped together." ), default=[], scope=Scope.settings) video_speed_optimizations = Boolean( display_name=_("Enable video caching system"), help= _("Enter true or false. If true, video caching will be used for HTML5 videos." ), default=True, scope=Scope.settings) video_auto_advance = Boolean( display_name=_("Enable video auto-advance"), help= _("Specify whether to show an auto-advance button in videos. If the student clicks it, when the last video in a unit finishes it will automatically move to the next unit and autoplay the first video." ), scope=Scope.settings, default=False) video_bumper = Dict( display_name=_("Video Pre-Roll"), help= _("Identify a video, 5-10 seconds in length, to play before course videos. Enter the video ID from " "the Video Uploads page and one or more transcript files in the following format: {format}. " "For example, an entry for a video with two transcripts looks like this: {example}" ), help_format_args=dict( format= '{"video_id": "ID", "transcripts": {"language": "/static/filename.srt"}}', example= ('{' '"video_id": "77cef264-d6f5-4cf2-ad9d-0178ab8c77be", ' '"transcripts": {"en": "/static/DemoX-D01_1.srt", "uk": "/static/DemoX-D01_1_uk.srt"}' '}'), ), scope=Scope.settings) show_reset_button = Boolean( display_name=_("Show Reset Button for Problems"), help= _("Enter true or false. If true, problems in the course default to always displaying a 'Reset' button. " "You can override this in each problem's settings. All existing problems are affected when " "this course-wide setting is changed."), scope=Scope.settings, default=False) edxnotes = Boolean( display_name=_("Enable Student Notes"), help= _("Enter true or false. If true, students can use the Student Notes feature." ), default=False, scope=Scope.settings) edxnotes_visibility = Boolean( display_name="Student Notes Visibility", help=_( "Indicates whether Student Notes are visible in the course. " "Students can also show or hide their notes in the courseware."), default=True, scope=Scope.user_info) in_entrance_exam = Boolean( display_name=_("Tag this module as part of an Entrance Exam section"), help=_( "Enter true or false. If true, answer submissions for problem modules will be " "considered in the Entrance Exam scoring/gating algorithm."), scope=Scope.settings, default=False) self_paced = Boolean( display_name=_('Self Paced'), help= _('Set this to "true" to mark this course as self-paced. Self-paced courses do not have ' 'due dates for assignments, and students can progress through the course at any rate before ' 'the course ends.'), default=False, scope=Scope.settings) @property def close_date(self): """ Return the date submissions should be closed from. If graceperiod is present for the course, all the submissions can be submitted till due date and the graceperiod. If no graceperiod, then the close date is same as the due date. """ due_date = self.due if self.graceperiod is not None and due_date: return due_date + self.graceperiod return due_date def is_past_due(self): """ Returns the boolean identifying if the submission due date has passed. """ return self.close_date is not None and timezone.now() > self.close_date def has_deadline_passed(self): """ Returns a boolean indicating if the submission is past its deadline. If the course is self-paced or no due date has been specified, then the submission can be made. If none of these cases exists, check if the submission due date has passed or not. """ if self.self_paced or self.close_date is None: return False return self.is_past_due()
def test_course_metadata_utils(self): """ Test every single function in course_metadata_utils. """ def mock_strftime_localized(date_time, format_string): """ Mock version of strftime_localized used for testing purposes. Because we don't have a real implementation of strftime_localized to work with (strftime_localized is provided by the XBlock runtime, which we don't have access to for this test case), we must declare this dummy implementation. This does NOT behave like a real strftime_localized should. It purposely returns a really dumb value that's only useful for testing purposes. Arguments: date_time (datetime): datetime to be formatted. format_string (str): format specifier. Valid values include: - 'DATE_TIME' - 'TIME' - 'SHORT_DATE' - 'LONG_DATE' Returns (str): format_string + " " + str(date_time) """ if format_string in ['DATE_TIME', 'TIME', 'SHORT_DATE', 'LONG_DATE']: return format_string + " " + date_time.strftime("%Y-%m-%d %H:%M:%S") else: raise ValueError("Invalid format string :" + format_string) def nop_gettext(text): """Dummy implementation of gettext, so we don't need Django.""" return text test_datetime = datetime(1945, 02, 06, 04, 20, 00, tzinfo=UTC()) advertised_start_parsable = "2038-01-19 03:14:07" advertised_start_bad_date = "215-01-01 10:10:10" advertised_start_unparsable = "This coming fall" FunctionTest = namedtuple('FunctionTest', 'function scenarios') # pylint: disable=invalid-name TestScenario = namedtuple('TestScenario', 'arguments expected_return') # pylint: disable=invalid-name function_tests = [ FunctionTest(clean_course_key, [ # Test with a Mongo course and '=' as padding. TestScenario( (self.demo_course.id, '='), "course_MVSFQL2EMVWW6WBOGEXUMYLMNRPTEMBRGQ======" ), # Test with a Split course and '~' as padding. TestScenario( (self.html_course.id, '~'), "course_MNXXK4TTMUWXMMJ2KVXGS5TFOJZWS5DZLAVUGUZNGIYDGK2ZGIYDSNQ~" ), ]), FunctionTest(url_name_for_course_location, [ TestScenario((self.demo_course.location,), self.demo_course.location.name), TestScenario((self.html_course.location,), self.html_course.location.name), ]), FunctionTest(display_name_with_default_escaped, [ # Test course with no display name. TestScenario((self.demo_course,), "Empty"), # Test course with a display name that contains characters that need escaping. TestScenario((self.html_course,), "Intro to <html>"), ]), FunctionTest(display_name_with_default, [ # Test course with no display name. TestScenario((self.demo_course,), "Empty"), # Test course with a display name that contains characters that need escaping. TestScenario((self.html_course,), "Intro to <html>"), ]), FunctionTest(number_for_course_location, [ TestScenario((self.demo_course.location,), "DemoX.1"), TestScenario((self.html_course.location,), "CS-203"), ]), FunctionTest(has_course_started, [ TestScenario((self.demo_course.start,), True), TestScenario((self.html_course.start,), False), ]), FunctionTest(has_course_ended, [ TestScenario((self.demo_course.end,), True), TestScenario((self.html_course.end,), False), ]), FunctionTest(course_start_date_is_default, [ TestScenario((test_datetime, advertised_start_parsable), False), TestScenario((test_datetime, None), False), TestScenario((DEFAULT_START_DATE, advertised_start_parsable), False), TestScenario((DEFAULT_START_DATE, None), True), ]), FunctionTest(course_start_datetime_text, [ # Test parsable advertised start date. # Expect start datetime to be parsed and formatted back into a string. TestScenario( (DEFAULT_START_DATE, advertised_start_parsable, 'DATE_TIME', nop_gettext, mock_strftime_localized), mock_strftime_localized(Date().from_json(advertised_start_parsable), 'DATE_TIME') + " UTC" ), # Test un-parsable advertised start date. # Expect date parsing to throw a ValueError, and the advertised # start to be returned in Title Case. TestScenario( (test_datetime, advertised_start_unparsable, 'DATE_TIME', nop_gettext, mock_strftime_localized), advertised_start_unparsable.title() ), # Test parsable advertised start date from before January 1, 1900. # Expect mock_strftime_localized to throw a ValueError, and the # advertised start to be returned in Title Case. TestScenario( (test_datetime, advertised_start_bad_date, 'DATE_TIME', nop_gettext, mock_strftime_localized), advertised_start_bad_date.title() ), # Test without advertised start date, but with a set start datetime. # Expect formatted datetime to be returned. TestScenario( (test_datetime, None, 'SHORT_DATE', nop_gettext, mock_strftime_localized), mock_strftime_localized(test_datetime, 'SHORT_DATE') ), # Test without advertised start date and with default start datetime. # Expect TBD to be returned. TestScenario( (DEFAULT_START_DATE, None, 'SHORT_DATE', nop_gettext, mock_strftime_localized), 'TBD' ) ]), FunctionTest(course_end_datetime_text, [ # Test with a set end datetime. # Expect formatted datetime to be returned. TestScenario( (test_datetime, 'TIME', mock_strftime_localized), mock_strftime_localized(test_datetime, 'TIME') + " UTC" ), # Test with default end datetime. # Expect empty string to be returned. TestScenario( (None, 'TIME', mock_strftime_localized), "" ) ]), FunctionTest(may_certify_for_course, [ TestScenario(('early_with_info', True, True), True), TestScenario(('early_no_info', False, False), True), TestScenario(('end', True, False), True), TestScenario(('end', False, True), True), TestScenario(('end', False, False), False), ]), ] for function_test in function_tests: for scenario in function_test.scenarios: actual_return = function_test.function(*scenario.arguments) self.assertEqual(actual_return, scenario.expected_return) # Even though we don't care about testing mock_strftime_localized, # we still need to test it with a bad format string in order to # satisfy the coverage checker. with self.assertRaises(ValueError): mock_strftime_localized(test_datetime, 'BAD_FORMAT_SPECIFIER')
def update_from_json(cls, course_key, jsondict, user): """ Decode the json into CourseDetails and save any changed attrs to the db """ module_store = modulestore() descriptor = module_store.get_course(course_key) dirty = False # In the descriptor's setter, the date is converted to JSON using Date's to_json method. # Calling to_json on something that is already JSON doesn't work. Since reaching directly # into the model is nasty, convert the JSON Date to a Python date, which is what the # setter expects as input. date = Date() if 'start_date' in jsondict: converted = date.from_json(jsondict['start_date']) else: converted = None if converted != descriptor.start: dirty = True descriptor.start = converted if 'end_date' in jsondict: converted = date.from_json(jsondict['end_date']) else: converted = None if converted != descriptor.end: dirty = True descriptor.end = converted if 'enrollment_start' in jsondict: converted = date.from_json(jsondict['enrollment_start']) else: converted = None if converted != descriptor.enrollment_start: dirty = True descriptor.enrollment_start = converted if 'enrollment_end' in jsondict: converted = date.from_json(jsondict['enrollment_end']) else: converted = None if converted != descriptor.enrollment_end: dirty = True descriptor.enrollment_end = converted if 'course_image_name' in jsondict and jsondict[ 'course_image_name'] != descriptor.course_image: descriptor.course_image = jsondict['course_image_name'] dirty = True if 'pre_requisite_courses' in jsondict \ and sorted(jsondict['pre_requisite_courses']) != sorted(descriptor.pre_requisite_courses): descriptor.pre_requisite_courses = jsondict[ 'pre_requisite_courses'] dirty = True if 'license' in jsondict: descriptor.license = jsondict['license'] dirty = True if 'language' in jsondict and jsondict[ 'language'] != descriptor.language: descriptor.language = jsondict['language'] dirty = True if dirty: module_store.update_item(descriptor, user.id) # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # to make faster, could compare against db or could have client send over a list of which fields changed. for attribute in ABOUT_ATTRIBUTES: if attribute in jsondict: cls.update_about_item(course_key, attribute, jsondict[attribute], descriptor, user) recomposed_video_tag = CourseDetails.recompose_video_tag( jsondict['intro_video']) cls.update_about_item(course_key, 'video', recomposed_video_tag, descriptor, user) # Could just return jsondict w/o doing any db reads, but I put the reads in as a means to confirm # it persisted correctly return CourseDetails.fetch(course_key)
class ImportTestCase(BaseCourseTestCase): date = Date() def test_fallback(self): '''Check that malformed xml loads as an ErrorDescriptor.''' # Use an exotic character to also flush out Unicode issues. bad_xml = u'''<sequential display_name="oops\N{SNOWMAN}"><video url="hi"></sequential>''' system = self.get_system() descriptor = system.process_xml(bad_xml) self.assertEqual(descriptor.__class__.__name__, 'ErrorDescriptorWithMixins') def test_unique_url_names(self): '''Check that each error gets its very own url_name''' bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>''' bad_xml2 = '''<sequential url_name="oops"><video url="hi"></sequential>''' system = self.get_system() descriptor1 = system.process_xml(bad_xml) descriptor2 = system.process_xml(bad_xml2) self.assertNotEqual(descriptor1.location, descriptor2.location) # Check that each vertical gets its very own url_name bad_xml = '''<vertical display_name="abc"><problem url_name="exam1:2013_Spring:abc"/></vertical>''' bad_xml2 = '''<vertical display_name="abc"><problem url_name="exam2:2013_Spring:abc"/></vertical>''' descriptor1 = system.process_xml(bad_xml) descriptor2 = system.process_xml(bad_xml2) self.assertNotEqual(descriptor1.location, descriptor2.location) def test_reimport(self): '''Make sure an already-exported error xml tag loads properly''' self.maxDiff = None bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>''' system = self.get_system() descriptor = system.process_xml(bad_xml) resource_fs = None tag_xml = descriptor.export_to_xml(resource_fs) re_import_descriptor = system.process_xml(tag_xml) self.assertEqual(re_import_descriptor.__class__.__name__, 'ErrorDescriptorWithMixins') self.assertEqual(descriptor.contents, re_import_descriptor.contents) self.assertEqual(descriptor.error_msg, re_import_descriptor.error_msg) def test_fixed_xml_tag(self): """Make sure a tag that's been fixed exports as the original tag type""" # create a error tag with valid xml contents root = etree.Element('error') good_xml = '''<sequential display_name="fixed"><video url="hi"/></sequential>''' root.text = good_xml xml_str_in = etree.tostring(root) # load it system = self.get_system() descriptor = system.process_xml(xml_str_in) # export it resource_fs = None xml_str_out = descriptor.export_to_xml(resource_fs) # Now make sure the exported xml is a sequential xml_out = etree.fromstring(xml_str_out) self.assertEqual(xml_out.tag, 'sequential') def test_metadata_import_export(self): """Two checks: - unknown metadata is preserved across import-export - inherited metadata doesn't leak to children. """ system = self.get_system() v = 'March 20 17:00' url_name = 'test1' start_xml = ''' <course org="{org}" course="{course}" due="{due}" url_name="{url_name}" unicorn="purple"> <chapter url="hi" url_name="ch" display_name="CH"> <html url_name="h" display_name="H">Two houses, ...</html> </chapter> </course>'''.format(due=v, org=ORG, course=COURSE, url_name=url_name) descriptor = system.process_xml(start_xml) compute_inherited_metadata(descriptor) # pylint: disable=W0212 print(descriptor, descriptor._field_data) self.assertEqual(descriptor.due, ImportTestCase.date.from_json(v)) # Check that the child inherits due correctly child = descriptor.get_children()[0] self.assertEqual(child.due, ImportTestCase.date.from_json(v)) # need to convert v to canonical json b4 comparing self.assertEqual( ImportTestCase.date.to_json(ImportTestCase.date.from_json(v)), child.xblock_kvs.inherited_settings['due'] ) # Now export and check things resource_fs = MemoryFS() exported_xml = descriptor.export_to_xml(resource_fs) # Check that the exported xml is just a pointer print("Exported xml:", exported_xml) pointer = etree.fromstring(exported_xml) self.assertTrue(is_pointer_tag(pointer)) # but it's a special case course pointer self.assertEqual(pointer.attrib['course'], COURSE) self.assertEqual(pointer.attrib['org'], ORG) # Does the course still have unicorns? with resource_fs.open('course/{url_name}.xml'.format(url_name=url_name)) as f: course_xml = etree.fromstring(f.read()) self.assertEqual(course_xml.attrib['unicorn'], 'purple') # the course and org tags should be _only_ in the pointer self.assertTrue('course' not in course_xml.attrib) self.assertTrue('org' not in course_xml.attrib) # did we successfully strip the url_name from the definition contents? self.assertTrue('url_name' not in course_xml.attrib) # Does the chapter tag now have a due attribute? # hardcoded path to child with resource_fs.open('chapter/ch.xml') as f: chapter_xml = etree.fromstring(f.read()) self.assertEqual(chapter_xml.tag, 'chapter') self.assertFalse('due' in chapter_xml.attrib) def test_metadata_no_inheritance(self): """ Checks that default value of None (for due) does not get marked as inherited. """ system = self.get_system() url_name = 'test1' start_xml = ''' <course org="{org}" course="{course}" url_name="{url_name}" unicorn="purple"> <chapter url="hi" url_name="ch" display_name="CH"> <html url_name="h" display_name="H">Two houses, ...</html> </chapter> </course>'''.format(org=ORG, course=COURSE, url_name=url_name) descriptor = system.process_xml(start_xml) compute_inherited_metadata(descriptor) self.assertEqual(descriptor.due, None) # Check that the child does not inherit a value for due child = descriptor.get_children()[0] self.assertEqual(child.due, None) # Check that the child hasn't started yet self.assertLessEqual( datetime.datetime.now(UTC()), child.start ) def test_metadata_override_default(self): """ Checks that due date can be overriden at child level. """ system = self.get_system() course_due = 'March 20 17:00' child_due = 'April 10 00:00' url_name = 'test1' start_xml = ''' <course org="{org}" course="{course}" due="{due}" url_name="{url_name}" unicorn="purple"> <chapter url="hi" url_name="ch" display_name="CH"> <html url_name="h" display_name="H">Two houses, ...</html> </chapter> </course>'''.format(due=course_due, org=ORG, course=COURSE, url_name=url_name) descriptor = system.process_xml(start_xml) child = descriptor.get_children()[0] # pylint: disable=W0212 child._field_data.set(child, 'due', child_due) compute_inherited_metadata(descriptor) self.assertEqual(descriptor.due, ImportTestCase.date.from_json(course_due)) self.assertEqual(child.due, ImportTestCase.date.from_json(child_due)) # Test inherited metadata. Due does not appear here (because explicitly set on child). self.assertEqual( ImportTestCase.date.to_json(ImportTestCase.date.from_json(course_due)), child.xblock_kvs.inherited_settings['due'] ) def test_is_pointer_tag(self): """ Check that is_pointer_tag works properly. """ yes = ["""<html url_name="blah"/>""", """<html url_name="blah"></html>""", """<html url_name="blah"> </html>""", """<problem url_name="blah"/>""", """<course org="HogwartsX" course="Mathemagics" url_name="3.14159"/>"""] no = ["""<html url_name="blah" also="this"/>""", """<html url_name="blah">some text</html>""", """<problem url_name="blah"><sub>tree</sub></problem>""", """<course org="HogwartsX" course="Mathemagics" url_name="3.14159"> <chapter>3</chapter> </course> """] for xml_str in yes: print("should be True for {0}".format(xml_str)) self.assertTrue(is_pointer_tag(etree.fromstring(xml_str))) for xml_str in no: print("should be False for {0}".format(xml_str)) self.assertFalse(is_pointer_tag(etree.fromstring(xml_str))) def test_metadata_inherit(self): """Make sure that metadata is inherited properly""" print("Starting import") course = self.get_course('toy') def check_for_key(key, node, value): "recursive check for presence of key" print("Checking {0}".format(node.location.url())) self.assertEqual(getattr(node, key), value) for c in node.get_children(): check_for_key(key, c, value) check_for_key('graceperiod', course, course.graceperiod) def test_policy_loading(self): """Make sure that when two courses share content with the same org and course names, policy applies to the right one.""" toy = self.get_course('toy') two_toys = self.get_course('two_toys') self.assertEqual(toy.url_name, "2012_Fall") self.assertEqual(two_toys.url_name, "TT_2012_Fall") toy_ch = toy.get_children()[0] two_toys_ch = two_toys.get_children()[0] self.assertEqual(toy_ch.display_name, "Overview") self.assertEqual(two_toys_ch.display_name, "Two Toy Overview") # Also check that the grading policy loaded self.assertEqual(two_toys.grade_cutoffs['C'], 0.5999) # Also check that keys from policy are run through the # appropriate attribute maps -- 'graded' should be True, not 'true' self.assertEqual(toy.graded, True) def test_definition_loading(self): """When two courses share the same org and course name and both have a module with the same url_name, the definitions shouldn't clash. TODO (vshnayder): once we have a CMS, this shouldn't happen--locations should uniquely name definitions. But in our imperfect XML world, it can (and likely will) happen.""" modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy', 'two_toys']) toy_id = "edX/toy/2012_Fall" two_toy_id = "edX/toy/TT_2012_Fall" location = Location(["i4x", "edX", "toy", "video", "Welcome"]) toy_video = modulestore.get_instance(toy_id, location) two_toy_video = modulestore.get_instance(two_toy_id, location) self.assertEqual(toy_video.youtube_id_1_0, "p2Q6BrNhdh8") self.assertEqual(two_toy_video.youtube_id_1_0, "p2Q6BrNhdh9") def test_colon_in_url_name(self): """Ensure that colons in url_names convert to file paths properly""" print("Starting import") # Not using get_courses because we need the modulestore object too afterward modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy']) courses = modulestore.get_courses() self.assertEquals(len(courses), 1) course = courses[0] course_id = course.id print("course errors:") for (msg, err) in modulestore.get_item_errors(course.location): print(msg) print(err) chapters = course.get_children() self.assertEquals(len(chapters), 5) ch2 = chapters[1] self.assertEquals(ch2.url_name, "secret:magic") print("Ch2 location: ", ch2.location) also_ch2 = modulestore.get_instance(course_id, ch2.location) self.assertEquals(ch2, also_ch2) print("making sure html loaded") cloc = course.location loc = Location(cloc.tag, cloc.org, cloc.course, 'html', 'secret:toylab') html = modulestore.get_instance(course_id, loc) self.assertEquals(html.display_name, "Toy lab") def test_unicode(self): """Check that courses with unicode characters in filenames and in org/course/name import properly. Currently, this means: (a) Having files with unicode names does not prevent import; (b) if files are not loaded because of unicode filenames, there are appropriate exceptions/errors to that effect.""" print("Starting import") modulestore = XMLModuleStore(DATA_DIR, course_dirs=['test_unicode']) courses = modulestore.get_courses() self.assertEquals(len(courses), 1) course = courses[0] print("course errors:") # Expect to find an error/exception about characters in "®esources" expect = "Invalid characters" errors = [(msg.encode("utf-8"), err.encode("utf-8")) for msg, err in modulestore.get_item_errors(course.location)] self.assertTrue(any(expect in msg or expect in err for msg, err in errors)) chapters = course.get_children() self.assertEqual(len(chapters), 4) def test_url_name_mangling(self): """ Make sure that url_names are only mangled once. """ modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy']) toy_id = "edX/toy/2012_Fall" course = modulestore.get_course(toy_id) chapters = course.get_children() ch1 = chapters[0] sections = ch1.get_children() self.assertEqual(len(sections), 4) for i in (2, 3): video = sections[i] # Name should be 'video_{hash}' print("video {0} url_name: {1}".format(i, video.url_name)) self.assertEqual(len(video.url_name), len('video_') + 12) def test_poll_and_conditional_import(self): modulestore = XMLModuleStore(DATA_DIR, course_dirs=['conditional_and_poll']) course = modulestore.get_courses()[0] chapters = course.get_children() ch1 = chapters[0] sections = ch1.get_children() self.assertEqual(len(sections), 1) location = course.location conditional_location = Location( location.tag, location.org, location.course, 'conditional', 'condone' ) module = modulestore.get_instance(course.id, conditional_location) self.assertEqual(len(module.children), 1) poll_location = Location( location.tag, location.org, location.course, 'poll_question', 'first_poll' ) module = modulestore.get_instance(course.id, poll_location) self.assertEqual(len(module.get_children()), 0) self.assertEqual(module.voted, False) self.assertEqual(module.poll_answer, '') self.assertEqual(module.poll_answers, {}) self.assertEqual( module.answers, [ {'text': u'Yes', 'id': 'Yes'}, {'text': u'No', 'id': 'No'}, {'text': u"Don't know", 'id': 'Dont_know'} ] ) def test_error_on_import(self): '''Check that when load_error_module is false, an exception is raised, rather than returning an ErrorModule''' bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>''' system = self.get_system(False) self.assertRaises(etree.XMLSyntaxError, system.process_xml, bad_xml) def test_graphicslidertool_import(self): ''' Check to see if definition_from_xml in gst_module.py works properly. Pulls data from the graphic_slider_tool directory in the test data directory. ''' modulestore = XMLModuleStore(DATA_DIR, course_dirs=['graphic_slider_tool']) sa_id = "edX/gst_test/2012_Fall" location = Location(["i4x", "edX", "gst_test", "graphical_slider_tool", "sample_gst"]) gst_sample = modulestore.get_instance(sa_id, location) render_string_from_sample_gst_xml = """ <slider var="a" style="width:400px;float:left;"/>\ <plot style="margin-top:15px;margin-bottom:15px;"/>""".strip() self.assertIn(render_string_from_sample_gst_xml, gst_sample.data) def test_word_cloud_import(self): modulestore = XMLModuleStore(DATA_DIR, course_dirs=['word_cloud']) course = modulestore.get_courses()[0] chapters = course.get_children() ch1 = chapters[0] sections = ch1.get_children() self.assertEqual(len(sections), 1) location = course.location location = Location( location.tag, location.org, location.course, 'word_cloud', 'cloud1' ) module = modulestore.get_instance(course.id, location) self.assertEqual(len(module.get_children()), 0) self.assertEqual(module.num_inputs, 5) self.assertEqual(module.num_top_words, 250) def test_cohort_config(self): """ Check that cohort config parsing works right. """ modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy']) toy_id = "edX/toy/2012_Fall" course = modulestore.get_course(toy_id) # No config -> False self.assertFalse(course.is_cohorted) # empty config -> False course.cohort_config = {} self.assertFalse(course.is_cohorted) # false config -> False course.cohort_config = {'cohorted': False} self.assertFalse(course.is_cohorted) # and finally... course.cohort_config = {'cohorted': True} self.assertTrue(course.is_cohorted)
def update_from_json(cls, jsondict): """ Decode the json into CourseDetails and save any changed attrs to the db """ # TODO make it an error for this to be undefined & for it to not be retrievable from modulestore course_location = Location(jsondict['course_location']) # Will probably want to cache the inflight courses because every blur generates an update descriptor = get_modulestore(course_location).get_item(course_location) dirty = False # In the descriptor's setter, the date is converted to JSON using Date's to_json method. # Calling to_json on something that is already JSON doesn't work. Since reaching directly # into the model is nasty, convert the JSON Date to a Python date, which is what the # setter expects as input. date = Date() if 'start_date' in jsondict: converted = date.from_json(jsondict['start_date']) else: converted = None if converted != descriptor.start: dirty = True descriptor.start = converted if 'end_date' in jsondict: converted = date.from_json(jsondict['end_date']) else: converted = None if converted != descriptor.end: dirty = True descriptor.end = converted if 'enrollment_start' in jsondict: converted = date.from_json(jsondict['enrollment_start']) else: converted = None if converted != descriptor.enrollment_start: dirty = True descriptor.enrollment_start = converted if 'enrollment_end' in jsondict: converted = date.from_json(jsondict['enrollment_end']) else: converted = None if converted != descriptor.enrollment_end: dirty = True descriptor.enrollment_end = converted if dirty: # Save the data that we've just changed to the underlying # MongoKeyValueStore before we update the mongo datastore. descriptor.save() get_modulestore(course_location).update_metadata( course_location, own_metadata(descriptor)) # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # to make faster, could compare against db or could have client send over a list of which fields changed. temploc = Location(course_location).replace(category='about', name='syllabus') update_item(temploc, jsondict['syllabus']) temploc = temploc.replace(name='overview') update_item(temploc, jsondict['overview']) temploc = temploc.replace(name='effort') update_item(temploc, jsondict['effort']) temploc = temploc.replace(name='video') recomposed_video_tag = CourseDetails.recompose_video_tag( jsondict['intro_video']) update_item(temploc, recomposed_video_tag) # Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm # it persisted correctly return CourseDetails.fetch(course_location)
class DateTest(unittest.TestCase): date = Date() def compare_dates(self, dt1, dt2, expected_delta): self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "-" + str(dt2) + "!=" + str(expected_delta)) def test_from_json(self): '''Test conversion from iso compatible date strings to struct_time''' self.compare_dates( DateTest.date.from_json("2013-01-01"), DateTest.date.from_json("2012-12-31"), datetime.timedelta(days=1)) self.compare_dates( DateTest.date.from_json("2013-01-01T00"), DateTest.date.from_json("2012-12-31T23"), datetime.timedelta(hours=1)) self.compare_dates( DateTest.date.from_json("2013-01-01T00:00"), DateTest.date.from_json("2012-12-31T23:59"), datetime.timedelta(minutes=1)) self.compare_dates( DateTest.date.from_json("2013-01-01T00:00:00"), DateTest.date.from_json("2012-12-31T23:59:59"), datetime.timedelta(seconds=1)) self.compare_dates( DateTest.date.from_json("2013-01-01T00:00:00Z"), DateTest.date.from_json("2012-12-31T23:59:59Z"), datetime.timedelta(seconds=1)) self.compare_dates( DateTest.date.from_json("2012-12-31T23:00:01-01:00"), DateTest.date.from_json("2013-01-01T00:00:00+01:00"), datetime.timedelta(hours=1, seconds=1)) def test_return_None(self): self.assertIsNone(DateTest.date.from_json("")) self.assertIsNone(DateTest.date.from_json(None)) with self.assertRaises(TypeError): DateTest.date.from_json(['unknown value']) def test_old_due_date_format(self): current = datetime.datetime.today() self.assertEqual( datetime.datetime(current.year, 3, 12, 12, tzinfo=UTC()), DateTest.date.from_json("March 12 12:00")) self.assertEqual( datetime.datetime(current.year, 12, 4, 16, 30, tzinfo=UTC()), DateTest.date.from_json("December 4 16:30")) self.assertIsNone(DateTest.date.from_json("12 12:00")) def test_non_std_from_json(self): """ Test the non-standard args being passed to from_json """ now = datetime.datetime.now(UTC()) delta = now - datetime.datetime.fromtimestamp(0, UTC()) self.assertEqual(DateTest.date.from_json(delta.total_seconds() * 1000), now) yesterday = datetime.datetime.now(UTC()) - datetime.timedelta(days=-1) self.assertEqual(DateTest.date.from_json(yesterday), yesterday) def test_to_json(self): ''' Test converting time reprs to iso dates ''' self.assertEqual( DateTest.date.to_json( datetime.datetime.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")), "2012-12-31T23:59:59Z") self.assertEqual( DateTest.date.to_json( DateTest.date.from_json("2012-12-31T23:59:59Z")), "2012-12-31T23:59:59Z") self.assertEqual( DateTest.date.to_json( DateTest.date.from_json("2012-12-31T23:00:01-01:00")), "2012-12-31T23:00:01-01:00") with self.assertRaises(TypeError): DateTest.date.to_json('2012-12-31T23:00:01-01:00')
class ImportTestCase(BaseCourseTestCase): # lint-amnesty, pylint: disable=missing-class-docstring date = Date() def test_fallback(self): '''Check that malformed xml loads as an ErrorBlock.''' # Use an exotic character to also flush out Unicode issues. bad_xml = u'''<sequential display_name="oops\N{SNOWMAN}"><video url="hi"></sequential>''' system = self.get_system() descriptor = system.process_xml(bad_xml) self.assertEqual(descriptor.__class__.__name__, 'ErrorBlockWithMixins') def test_unique_url_names(self): '''Check that each error gets its very own url_name''' bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>''' bad_xml2 = '''<sequential url_name="oops"><video url="hi"></sequential>''' system = self.get_system() descriptor1 = system.process_xml(bad_xml) descriptor2 = system.process_xml(bad_xml2) self.assertNotEqual(descriptor1.location, descriptor2.location) # Check that each vertical gets its very own url_name bad_xml = '''<vertical display_name="abc"><problem url_name="exam1:2013_Spring:abc"/></vertical>''' bad_xml2 = '''<vertical display_name="abc"><problem url_name="exam2:2013_Spring:abc"/></vertical>''' descriptor1 = system.process_xml(bad_xml) descriptor2 = system.process_xml(bad_xml2) self.assertNotEqual(descriptor1.location, descriptor2.location) def test_reimport(self): '''Make sure an already-exported error xml tag loads properly''' self.maxDiff = None bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>''' system = self.get_system() descriptor = system.process_xml(bad_xml) node = etree.Element('unknown') descriptor.add_xml_to_node(node) re_import_descriptor = system.process_xml(etree.tostring(node)) self.assertEqual(re_import_descriptor.__class__.__name__, 'ErrorBlockWithMixins') self.assertEqual(descriptor.contents, re_import_descriptor.contents) self.assertEqual(descriptor.error_msg, re_import_descriptor.error_msg) def test_fixed_xml_tag(self): """Make sure a tag that's been fixed exports as the original tag type""" # create a error tag with valid xml contents root = etree.Element('error') good_xml = '''<sequential display_name="fixed"><video url="hi"/></sequential>''' root.text = good_xml xml_str_in = etree.tostring(root) # load it system = self.get_system() descriptor = system.process_xml(xml_str_in) # export it node = etree.Element('unknown') descriptor.add_xml_to_node(node) # Now make sure the exported xml is a sequential self.assertEqual(node.tag, 'sequential') def course_descriptor_inheritance_check(self, descriptor, from_date_string, unicorn_color, course_run=RUN): """ Checks to make sure that metadata inheritance on a course descriptor is respected. """ # pylint: disable=protected-access print((descriptor, descriptor._field_data)) self.assertEqual(descriptor.due, ImportTestCase.date.from_json(from_date_string)) # Check that the child inherits due correctly child = descriptor.get_children()[0] self.assertEqual(child.due, ImportTestCase.date.from_json(from_date_string)) # need to convert v to canonical json b4 comparing self.assertEqual( ImportTestCase.date.to_json( ImportTestCase.date.from_json(from_date_string)), child.xblock_kvs.inherited_settings['due']) # Now export and check things file_system = OSFS(mkdtemp()) descriptor.runtime.export_fs = file_system.makedir(u'course', recreate=True) node = etree.Element('unknown') descriptor.add_xml_to_node(node) # Check that the exported xml is just a pointer print(("Exported xml:", etree.tostring(node))) self.assertTrue(is_pointer_tag(node)) # but it's a special case course pointer self.assertEqual(node.attrib['course'], COURSE) self.assertEqual(node.attrib['org'], ORG) # Does the course still have unicorns? with descriptor.runtime.export_fs.open( u'course/{course_run}.xml'.format(course_run=course_run)) as f: course_xml = etree.fromstring(f.read()) self.assertEqual(course_xml.attrib['unicorn'], unicorn_color) # the course and org tags should be _only_ in the pointer self.assertNotIn('course', course_xml.attrib) self.assertNotIn('org', course_xml.attrib) # did we successfully strip the url_name from the definition contents? self.assertNotIn('url_name', course_xml.attrib) # Does the chapter tag now have a due attribute? # hardcoded path to child with descriptor.runtime.export_fs.open(u'chapter/ch.xml') as f: chapter_xml = etree.fromstring(f.read()) self.assertEqual(chapter_xml.tag, 'chapter') self.assertNotIn('due', chapter_xml.attrib) def test_metadata_import_export(self): """Two checks: - unknown metadata is preserved across import-export - inherited metadata doesn't leak to children. """ system = self.get_system() from_date_string = 'March 20 17:00' url_name = 'test1' unicorn_color = 'purple' start_xml = ''' <course org="{org}" course="{course}" due="{due}" url_name="{url_name}" unicorn="{unicorn_color}"> <chapter url="hi" url_name="ch" display_name="CH"> <html url_name="h" display_name="H">Two houses, ...</html> </chapter> </course>'''.format(due=from_date_string, org=ORG, course=COURSE, url_name=url_name, unicorn_color=unicorn_color) descriptor = system.process_xml(start_xml) compute_inherited_metadata(descriptor) self.course_descriptor_inheritance_check(descriptor, from_date_string, unicorn_color) def test_library_metadata_import_export(self): """Two checks: - unknown metadata is preserved across import-export - inherited metadata doesn't leak to children. """ system = self.get_system(library=True) from_date_string = 'March 26 17:00' url_name = 'test2' unicorn_color = 'rainbow' start_xml = ''' <library org="TestOrg" library="TestLib" display_name="stuff"> <course org="{org}" course="{course}" due="{due}" url_name="{url_name}" unicorn="{unicorn_color}"> <chapter url="hi" url_name="ch" display_name="CH"> <html url_name="h" display_name="H">Two houses, ...</html> </chapter> </course> </library>'''.format(due=from_date_string, org=ORG, course=COURSE, url_name=url_name, unicorn_color=unicorn_color) descriptor = system.process_xml(start_xml) # pylint: disable=protected-access original_unwrapped = descriptor._unwrapped_field_data LibraryXMLModuleStore.patch_descriptor_kvs(descriptor) # '_unwrapped_field_data' is reset in `patch_descriptor_kvs` # pylint: disable=protected-access self.assertIsNot(original_unwrapped, descriptor._unwrapped_field_data) compute_inherited_metadata(descriptor) # Check the course module, since it has inheritance descriptor = descriptor.get_children()[0] self.course_descriptor_inheritance_check(descriptor, from_date_string, unicorn_color) def test_metadata_no_inheritance(self): """ Checks that default value of None (for due) does not get marked as inherited when a course is the root block. """ system = self.get_system() url_name = 'test1' start_xml = ''' <course org="{org}" course="{course}" url_name="{url_name}" unicorn="purple"> <chapter url="hi" url_name="ch" display_name="CH"> <html url_name="h" display_name="H">Two houses, ...</html> </chapter> </course>'''.format(org=ORG, course=COURSE, url_name=url_name) descriptor = system.process_xml(start_xml) compute_inherited_metadata(descriptor) self.course_descriptor_no_inheritance_check(descriptor) def test_library_metadata_no_inheritance(self): """ Checks that the default value of None (for due) does not get marked as inherited when a library is the root block. """ system = self.get_system() url_name = 'test1' start_xml = ''' <library org="TestOrg" library="TestLib" display_name="stuff"> <course org="{org}" course="{course}" url_name="{url_name}" unicorn="purple"> <chapter url="hi" url_name="ch" display_name="CH"> <html url_name="h" display_name="H">Two houses, ...</html> </chapter> </course> </library>'''.format(org=ORG, course=COURSE, url_name=url_name) descriptor = system.process_xml(start_xml) LibraryXMLModuleStore.patch_descriptor_kvs(descriptor) compute_inherited_metadata(descriptor) # Run the checks on the course node instead. descriptor = descriptor.get_children()[0] self.course_descriptor_no_inheritance_check(descriptor) def course_descriptor_no_inheritance_check(self, descriptor): """ Verifies that a default value of None (for due) does not get marked as inherited. """ self.assertEqual(descriptor.due, None) # Check that the child does not inherit a value for due child = descriptor.get_children()[0] self.assertEqual(child.due, None) # Check that the child hasn't started yet self.assertLessEqual(datetime.datetime.now(UTC), child.start) def override_metadata_check(self, descriptor, child, course_due, child_due): """ Verifies that due date can be overriden at child level. """ self.assertEqual(descriptor.due, ImportTestCase.date.from_json(course_due)) self.assertEqual(child.due, ImportTestCase.date.from_json(child_due)) # Test inherited metadata. Due does not appear here (because explicitly set on child). self.assertEqual( ImportTestCase.date.to_json( ImportTestCase.date.from_json(course_due)), child.xblock_kvs.inherited_settings['due']) def test_metadata_override_default(self): """ Checks that due date can be overriden at child level when a course is the root. """ system = self.get_system() course_due = 'March 20 17:00' child_due = 'April 10 00:00' url_name = 'test1' start_xml = ''' <course org="{org}" course="{course}" due="{due}" url_name="{url_name}" unicorn="purple"> <chapter url="hi" url_name="ch" display_name="CH"> <html url_name="h" display_name="H">Two houses, ...</html> </chapter> </course>'''.format(due=course_due, org=ORG, course=COURSE, url_name=url_name) descriptor = system.process_xml(start_xml) child = descriptor.get_children()[0] # pylint: disable=protected-access child._field_data.set(child, 'due', child_due) compute_inherited_metadata(descriptor) self.override_metadata_check(descriptor, child, course_due, child_due) def test_library_metadata_override_default(self): """ Checks that due date can be overriden at child level when a library is the root. """ system = self.get_system() course_due = 'March 20 17:00' child_due = 'April 10 00:00' url_name = 'test1' start_xml = ''' <library org="TestOrg" library="TestLib" display_name="stuff"> <course org="{org}" course="{course}" due="{due}" url_name="{url_name}" unicorn="purple"> <chapter url="hi" url_name="ch" display_name="CH"> <html url_name="h" display_name="H">Two houses, ...</html> </chapter> </course> </library>'''.format(due=course_due, org=ORG, course=COURSE, url_name=url_name) descriptor = system.process_xml(start_xml) LibraryXMLModuleStore.patch_descriptor_kvs(descriptor) # Chapter is two levels down here. child = descriptor.get_children()[0].get_children()[0] # pylint: disable=protected-access child._field_data.set(child, 'due', child_due) compute_inherited_metadata(descriptor) descriptor = descriptor.get_children()[0] self.override_metadata_check(descriptor, child, course_due, child_due) def test_is_pointer_tag(self): """ Check that is_pointer_tag works properly. """ yes = [ """<html url_name="blah"/>""", """<html url_name="blah"></html>""", """<html url_name="blah"> </html>""", """<problem url_name="blah"/>""", """<course org="HogwartsX" course="Mathemagics" url_name="3.14159"/>""" ] no = [ """<html url_name="blah" also="this"/>""", """<html url_name="blah">some text</html>""", """<problem url_name="blah"><sub>tree</sub></problem>""", """<course org="HogwartsX" course="Mathemagics" url_name="3.14159"> <chapter>3</chapter> </course> """ ] for xml_str in yes: print("should be True for {0}".format(xml_str)) self.assertTrue(is_pointer_tag(etree.fromstring(xml_str))) for xml_str in no: print("should be False for {0}".format(xml_str)) self.assertFalse(is_pointer_tag(etree.fromstring(xml_str))) def test_metadata_inherit(self): """Make sure that metadata is inherited properly""" print("Starting import") course = self.get_course('toy') def check_for_key(key, node, value): "recursive check for presence of key" print("Checking {0}".format(text_type(node.location))) self.assertEqual(getattr(node, key), value) for c in node.get_children(): check_for_key(key, c, value) check_for_key('graceperiod', course, course.graceperiod) def test_policy_loading(self): """Make sure that when two courses share content with the same org and course names, policy applies to the right one.""" toy = self.get_course('toy') two_toys = self.get_course('two_toys') self.assertEqual(toy.url_name, "2012_Fall") self.assertEqual(two_toys.url_name, "TT_2012_Fall") toy_ch = toy.get_children()[0] two_toys_ch = two_toys.get_children()[0] self.assertEqual(toy_ch.display_name, "Overview") self.assertEqual(two_toys_ch.display_name, "Two Toy Overview") # Also check that the grading policy loaded self.assertEqual(two_toys.grade_cutoffs['C'], 0.5999) # Also check that keys from policy are run through the # appropriate attribute maps -- 'graded' should be True, not 'true' self.assertEqual(toy.graded, True) def test_static_tabs_import(self): """Make sure that the static tabs are imported correctly""" modulestore = XMLModuleStore(DATA_DIR, source_dirs=['toy']) location_tab_syllabus = BlockUsageLocator(CourseLocator( "edX", "toy", "2012_Fall", deprecated=True), "static_tab", "syllabus", deprecated=True) toy_tab_syllabus = modulestore.get_item(location_tab_syllabus) self.assertEqual(toy_tab_syllabus.display_name, 'Syllabus') self.assertEqual(toy_tab_syllabus.course_staff_only, False) location_tab_resources = BlockUsageLocator(CourseLocator( "edX", "toy", "2012_Fall", deprecated=True), "static_tab", "resources", deprecated=True) toy_tab_resources = modulestore.get_item(location_tab_resources) self.assertEqual(toy_tab_resources.display_name, 'Resources') self.assertEqual(toy_tab_resources.course_staff_only, True) def test_definition_loading(self): """When two courses share the same org and course name and both have a module with the same url_name, the definitions shouldn't clash. TODO (vshnayder): once we have a CMS, this shouldn't happen--locations should uniquely name definitions. But in our imperfect XML world, it can (and likely will) happen.""" modulestore = XMLModuleStore(DATA_DIR, source_dirs=['toy', 'two_toys']) location = BlockUsageLocator(CourseLocator("edX", "toy", "2012_Fall", deprecated=True), "video", "Welcome", deprecated=True) toy_video = modulestore.get_item(location) location_two = BlockUsageLocator(CourseLocator("edX", "toy", "TT_2012_Fall", deprecated=True), "video", "Welcome", deprecated=True) two_toy_video = modulestore.get_item(location_two) self.assertEqual(toy_video.youtube_id_1_0, "p2Q6BrNhdh8") self.assertEqual(two_toy_video.youtube_id_1_0, "p2Q6BrNhdh9") def test_colon_in_url_name(self): """Ensure that colons in url_names convert to file paths properly""" print("Starting import") # Not using get_courses because we need the modulestore object too afterward modulestore = XMLModuleStore(DATA_DIR, source_dirs=['toy']) courses = modulestore.get_courses() self.assertEqual(len(courses), 1) course = courses[0] print("course errors:") for (msg, err) in modulestore.get_course_errors(course.id): print(msg) print(err) chapters = course.get_children() self.assertEqual(len(chapters), 5) ch2 = chapters[1] self.assertEqual(ch2.url_name, "secret:magic") print("Ch2 location: ", ch2.location) also_ch2 = modulestore.get_item(ch2.location) self.assertEqual(ch2, also_ch2) print("making sure html loaded") loc = course.id.make_usage_key('html', 'secret:toylab') html = modulestore.get_item(loc) self.assertEqual(html.display_name, "Toy lab") def test_unicode(self): """Check that courses with unicode characters in filenames and in org/course/name import properly. Currently, this means: (a) Having files with unicode names does not prevent import; (b) if files are not loaded because of unicode filenames, there are appropriate exceptions/errors to that effect.""" print("Starting import") modulestore = XMLModuleStore(DATA_DIR, source_dirs=['test_unicode']) courses = modulestore.get_courses() self.assertEqual(len(courses), 1) course = courses[0] print("course errors:") # Expect to find an error/exception about characters in "®esources" expect = "InvalidKeyError" errors = [ (msg, err) for msg, err # lint-amnesty, pylint: disable=unnecessary-comprehension in modulestore.get_course_errors(course.id) ] self.assertTrue( any(expect in msg or expect in err for msg, err in errors)) chapters = course.get_children() self.assertEqual(len(chapters), 4) def test_url_name_mangling(self): """ Make sure that url_names are only mangled once. """ modulestore = XMLModuleStore(DATA_DIR, source_dirs=['toy']) toy_id = CourseKey.from_string('edX/toy/2012_Fall') course = modulestore.get_course(toy_id) chapters = course.get_children() ch1 = chapters[0] sections = ch1.get_children() self.assertEqual(len(sections), 4) for i in (2, 3): video = sections[i] # Name should be 'video_{hash}' print("video {0} url_name: {1}".format(i, video.url_name)) self.assertEqual(len(video.url_name), len('video_') + 12) def test_poll_and_conditional_import(self): modulestore = XMLModuleStore(DATA_DIR, source_dirs=['conditional_and_poll']) course = modulestore.get_courses()[0] chapters = course.get_children() ch1 = chapters[0] sections = ch1.get_children() self.assertEqual(len(sections), 1) conditional_location = course.id.make_usage_key( 'conditional', 'condone') module = modulestore.get_item(conditional_location) self.assertEqual(len(module.children), 1) poll_location = course.id.make_usage_key('poll_question', 'first_poll') module = modulestore.get_item(poll_location) self.assertEqual(len(module.get_children()), 0) self.assertEqual(module.voted, False) self.assertEqual(module.poll_answer, '') self.assertEqual(module.poll_answers, {}) self.assertEqual(module.answers, [{ 'text': u'Yes', 'id': 'Yes' }, { 'text': u'No', 'id': 'No' }, { 'text': u"Don't know", 'id': 'Dont_know' }]) def test_error_on_import(self): '''Check that when load_error_module is false, an exception is raised, rather than returning an ErrorBlock''' bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>''' system = self.get_system(False) self.assertRaises(etree.XMLSyntaxError, system.process_xml, bad_xml) def test_word_cloud_import(self): modulestore = XMLModuleStore(DATA_DIR, source_dirs=['word_cloud']) course = modulestore.get_courses()[0] chapters = course.get_children() ch1 = chapters[0] sections = ch1.get_children() self.assertEqual(len(sections), 1) location = course.id.make_usage_key('word_cloud', 'cloud1') module = modulestore.get_item(location) self.assertEqual(len(module.get_children()), 0) self.assertEqual(module.num_inputs, 5) self.assertEqual(module.num_top_words, 250) def test_cohort_config(self): """ Check that cohort config parsing works right. Note: The cohort config on the CourseModule is no longer used. See openedx.core.djangoapps.course_groups.models.CourseCohortSettings. """ modulestore = XMLModuleStore(DATA_DIR, source_dirs=['toy']) toy_id = CourseKey.from_string('edX/toy/2012_Fall') course = modulestore.get_course(toy_id) # No config -> False self.assertFalse(course.is_cohorted) # empty config -> False course.cohort_config = {} self.assertFalse(course.is_cohorted) # false config -> False course.cohort_config = {'cohorted': False} self.assertFalse(course.is_cohorted) # and finally... course.cohort_config = {'cohorted': True} self.assertTrue(course.is_cohorted)
def create_source_course(self): """ A course testing all of the conversion mechanisms: * some inheritable settings * sequences w/ draft and live intermixed children to ensure all get to the draft but only the live ones get to published. Some are only draft, some are both, some are only live. * about, static_tab, and conditional documents """ location = Location('i4x', 'test_org', 'test_course', 'course', 'runid') self.course_location = location date_proxy = Date() metadata = { 'start': date_proxy.to_json(datetime.datetime(2000, 3, 13, 4)), 'display_name': 'Migration test course', } data = { 'wiki_slug': 'test_course_slug' } course_root = self._create_and_get_item(self.old_mongo, location, data, metadata) runtime = course_root.runtime # chapters location = location.replace(category='chapter', name=uuid.uuid4().hex) chapter1 = self._create_and_get_item(self.old_mongo, location, {}, {'display_name': 'Chapter 1'}, runtime) course_root.children.append(chapter1.location.url()) location = location.replace(category='chapter', name=uuid.uuid4().hex) chapter2 = self._create_and_get_item(self.old_mongo, location, {}, {'display_name': 'Chapter 2'}, runtime) course_root.children.append(chapter2.location.url()) self.old_mongo.update_children(course_root.location, course_root.children) # vertical in live only location = location.replace(category='vertical', name=uuid.uuid4().hex) live_vert = self._create_and_get_item(self.old_mongo, location, {}, {'display_name': 'Live vertical'}, runtime) chapter1.children.append(live_vert.location.url()) self.create_random_units(self.old_mongo, live_vert) # vertical in both live and draft location = location.replace(category='vertical', name=uuid.uuid4().hex) both_vert = self._create_and_get_item( self.old_mongo, location, {}, {'display_name': 'Both vertical'}, runtime ) draft_both = self._create_and_get_item( self.draft_mongo, location, {}, {'display_name': 'Both vertical renamed'}, runtime ) chapter1.children.append(both_vert.location.url()) self.create_random_units(self.old_mongo, both_vert, self.draft_mongo, draft_both) # vertical in draft only (x2) location = location.replace(category='vertical', name=uuid.uuid4().hex) draft_vert = self._create_and_get_item( self.draft_mongo, location, {}, {'display_name': 'Draft vertical'}, runtime) chapter1.children.append(draft_vert.location.url()) self.create_random_units(self.draft_mongo, draft_vert) location = location.replace(category='vertical', name=uuid.uuid4().hex) draft_vert = self._create_and_get_item( self.draft_mongo, location, {}, {'display_name': 'Draft vertical2'}, runtime) chapter1.children.append(draft_vert.location.url()) self.create_random_units(self.draft_mongo, draft_vert) # and finally one in live only (so published has to skip 2) location = location.replace(category='vertical', name=uuid.uuid4().hex) live_vert = self._create_and_get_item( self.old_mongo, location, {}, {'display_name': 'Live vertical end'}, runtime) chapter1.children.append(live_vert.location.url()) self.create_random_units(self.old_mongo, live_vert) # update the chapter self.old_mongo.update_children(chapter1.location, chapter1.children) # now the other one w/ the conditional # first create some show children indirect1 = self._create_and_get_item( self.old_mongo, location.replace(category='discussion', name=uuid.uuid4().hex), "", {'display_name': 'conditional show 1'}, runtime ) indirect2 = self._create_and_get_item( self.old_mongo, location.replace(category='html', name=uuid.uuid4().hex), "", {'display_name': 'conditional show 2'}, runtime ) location = location.replace(category='conditional', name=uuid.uuid4().hex) metadata = { 'xml_attributes': { 'sources': [live_vert.location.url(), ], 'completed': True, }, } data = { 'show_tag_list': [indirect1.location.url(), indirect2.location.url()] } conditional = self._create_and_get_item(self.old_mongo, location, data, metadata, runtime) conditional.children = [indirect1.location.url(), indirect2.location.url()] # add direct children self.create_random_units(self.old_mongo, conditional) chapter2.children.append(conditional.location.url()) self.old_mongo.update_children(chapter2.location, chapter2.children) # and the ancillary docs (not children) location = location.replace(category='static_tab', name=uuid.uuid4().hex) # the below automatically adds the tab to the course _tab = self._create_and_get_item(self.old_mongo, location, "", {'display_name': 'Tab uno'}, runtime) location = location.replace(category='about', name='overview') _overview = self._create_and_get_item(self.old_mongo, location, "<p>test</p>", {}, runtime) location = location.replace(category='course_info', name='updates') _overview = self._create_and_get_item( self.old_mongo, location, "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, runtime )
def create_source_course(self): """ A course testing all of the conversion mechanisms: * some inheritable settings * sequences w/ draft and live intermixed children to ensure all get to the draft but only the live ones get to published. Some are only draft, some are both, some are only live. * about, static_tab, and conditional documents """ location = Location('i4x', 'test_org', 'test_course', 'course', 'runid') self.course_location = location date_proxy = Date() metadata = { 'start': date_proxy.to_json(datetime.datetime(2000, 3, 13, 4)), 'display_name': 'Migration test course', } data = {'wiki_slug': 'test_course_slug'} course_root = self._create_and_get_item(self.old_mongo, location, data, metadata) runtime = course_root.runtime # chapters location = location.replace(category='chapter', name=uuid.uuid4().hex) chapter1 = self._create_and_get_item(self.old_mongo, location, {}, {'display_name': 'Chapter 1'}, runtime) course_root.children.append(chapter1.location.url()) location = location.replace(category='chapter', name=uuid.uuid4().hex) chapter2 = self._create_and_get_item(self.old_mongo, location, {}, {'display_name': 'Chapter 2'}, runtime) course_root.children.append(chapter2.location.url()) self.old_mongo.update_children(course_root.location, course_root.children) # vertical in live only location = location.replace(category='vertical', name=uuid.uuid4().hex) live_vert = self._create_and_get_item( self.old_mongo, location, {}, {'display_name': 'Live vertical'}, runtime) chapter1.children.append(live_vert.location.url()) self.create_random_units(self.old_mongo, live_vert) # vertical in both live and draft location = location.replace(category='vertical', name=uuid.uuid4().hex) both_vert = self._create_and_get_item( self.old_mongo, location, {}, {'display_name': 'Both vertical'}, runtime) draft_both = self._create_and_get_item( self.draft_mongo, location, {}, {'display_name': 'Both vertical renamed'}, runtime) chapter1.children.append(both_vert.location.url()) self.create_random_units(self.old_mongo, both_vert, self.draft_mongo, draft_both) # vertical in draft only (x2) location = location.replace(category='vertical', name=uuid.uuid4().hex) draft_vert = self._create_and_get_item( self.draft_mongo, location, {}, {'display_name': 'Draft vertical'}, runtime) chapter1.children.append(draft_vert.location.url()) self.create_random_units(self.draft_mongo, draft_vert) location = location.replace(category='vertical', name=uuid.uuid4().hex) draft_vert = self._create_and_get_item( self.draft_mongo, location, {}, {'display_name': 'Draft vertical2'}, runtime) chapter1.children.append(draft_vert.location.url()) self.create_random_units(self.draft_mongo, draft_vert) # and finally one in live only (so published has to skip 2) location = location.replace(category='vertical', name=uuid.uuid4().hex) live_vert = self._create_and_get_item( self.old_mongo, location, {}, {'display_name': 'Live vertical end'}, runtime) chapter1.children.append(live_vert.location.url()) self.create_random_units(self.old_mongo, live_vert) # update the chapter self.old_mongo.update_children(chapter1.location, chapter1.children) # now the other one w/ the conditional # first create some show children indirect1 = self._create_and_get_item( self.old_mongo, location.replace(category='discussion', name=uuid.uuid4().hex), "", {'display_name': 'conditional show 1'}, runtime) indirect2 = self._create_and_get_item( self.old_mongo, location.replace(category='html', name=uuid.uuid4().hex), "", {'display_name': 'conditional show 2'}, runtime) location = location.replace(category='conditional', name=uuid.uuid4().hex) metadata = { 'xml_attributes': { 'sources': [ live_vert.location.url(), ], 'completed': True, }, } data = { 'show_tag_list': [indirect1.location.url(), indirect2.location.url()] } conditional = self._create_and_get_item(self.old_mongo, location, data, metadata, runtime) conditional.children = [ indirect1.location.url(), indirect2.location.url() ] # add direct children self.create_random_units(self.old_mongo, conditional) chapter2.children.append(conditional.location.url()) self.old_mongo.update_children(chapter2.location, chapter2.children) # and the ancillary docs (not children) location = location.replace(category='static_tab', name=uuid.uuid4().hex) # the below automatically adds the tab to the course _tab = self._create_and_get_item(self.old_mongo, location, "", {'display_name': 'Tab uno'}, runtime) location = location.replace(category='about', name='overview') _overview = self._create_and_get_item(self.old_mongo, location, "<p>test</p>", {}, runtime) location = location.replace(category='course_info', name='updates') _overview = self._create_and_get_item( self.old_mongo, location, "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, runtime)
def update_from_json(cls, course_key, jsondict, user): """ Decode the json into CourseDetails and save any changed attrs to the db """ module_store = modulestore('direct') descriptor = module_store.get_course(course_key) dirty = False # In the descriptor's setter, the date is converted to JSON using Date's to_json method. # Calling to_json on something that is already JSON doesn't work. Since reaching directly # into the model is nasty, convert the JSON Date to a Python date, which is what the # setter expects as input. date = Date() if 'start_date' in jsondict: converted = date.from_json(jsondict['start_date']) else: converted = None if converted != descriptor.start: dirty = True descriptor.start = converted if 'end_date' in jsondict: converted = date.from_json(jsondict['end_date']) else: converted = None if converted != descriptor.end: dirty = True descriptor.end = converted if 'enrollment_start' in jsondict: converted = date.from_json(jsondict['enrollment_start']) else: converted = None if converted != descriptor.enrollment_start: dirty = True descriptor.enrollment_start = converted if 'enrollment_end' in jsondict: converted = date.from_json(jsondict['enrollment_end']) else: converted = None if converted != descriptor.enrollment_end: dirty = True descriptor.enrollment_end = converted if 'course_image_name' in jsondict and jsondict['course_image_name'] != descriptor.course_image: descriptor.course_image = jsondict['course_image_name'] dirty = True if dirty: module_store.update_item(descriptor, user.id) # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # to make faster, could compare against db or could have client send over a list of which fields changed. for about_type in ['syllabus', 'overview', 'effort', 'short_description']: cls.update_about_item(course_key, about_type, jsondict[about_type], descriptor, user) recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video']) cls.update_about_item(course_key, 'video', recomposed_video_tag, descriptor, user) # Could just return jsondict w/o doing any db reads, but I put the reads in as a means to confirm # it persisted correctly return CourseDetails.fetch(course_key)
def test_metadata_import_export(self): """Two checks: - unknown metadata is preserved across import-export - inherited metadata doesn't leak to children. """ system = self.get_system() v = 'March 20 17:00' url_name = 'test1' start_xml = ''' <course org="{org}" course="{course}" due="{due}" url_name="{url_name}" unicorn="purple"> <chapter url="hi" url_name="ch" display_name="CH"> <html url_name="h" display_name="H">Two houses, ...</html> </chapter> </course>'''.format(due=v, org=ORG, course=COURSE, url_name=url_name) descriptor = system.process_xml(start_xml) compute_inherited_metadata(descriptor) print(descriptor, descriptor._model_data) self.assertEqual(descriptor.lms.due, Date().from_json(v)) # Check that the child inherits due correctly child = descriptor.get_children()[0] self.assertEqual(child.lms.due, Date().from_json(v)) self.assertEqual(child._inheritable_metadata, child._inherited_metadata) self.assertEqual(2, len(child._inherited_metadata)) self.assertEqual('1970-01-01T00:00:00Z', child._inherited_metadata['start']) self.assertEqual(v, child._inherited_metadata['due']) # Now export and check things resource_fs = MemoryFS() exported_xml = descriptor.export_to_xml(resource_fs) # Check that the exported xml is just a pointer print("Exported xml:", exported_xml) pointer = etree.fromstring(exported_xml) self.assertTrue(is_pointer_tag(pointer)) # but it's a special case course pointer self.assertEqual(pointer.attrib['course'], COURSE) self.assertEqual(pointer.attrib['org'], ORG) # Does the course still have unicorns? with resource_fs.open( 'course/{url_name}.xml'.format(url_name=url_name)) as f: course_xml = etree.fromstring(f.read()) self.assertEqual(course_xml.attrib['unicorn'], 'purple') # the course and org tags should be _only_ in the pointer self.assertTrue('course' not in course_xml.attrib) self.assertTrue('org' not in course_xml.attrib) # did we successfully strip the url_name from the definition contents? self.assertTrue('url_name' not in course_xml.attrib) # Does the chapter tag now have a due attribute? # hardcoded path to child with resource_fs.open('chapter/ch.xml') as f: chapter_xml = etree.fromstring(f.read()) self.assertEqual(chapter_xml.tag, 'chapter') self.assertFalse('due' in chapter_xml.attrib)
def update_from_json(cls, jsondict): """ Decode the json into CourseDetails and save any changed attrs to the db """ # TODO make it an error for this to be undefined & for it to not be retrievable from modulestore course_location = jsondict['course_location'] # Will probably want to cache the inflight courses because every blur generates an update descriptor = get_modulestore(course_location).get_item(course_location) dirty = False # In the descriptor's setter, the date is converted to JSON using Date's to_json method. # Calling to_json on something that is already JSON doesn't work. Since reaching directly # into the model is nasty, convert the JSON Date to a Python date, which is what the # setter expects as input. date = Date() if 'start_date' in jsondict: converted = date.from_json(jsondict['start_date']) else: converted = None if converted != descriptor.start: dirty = True descriptor.start = converted if 'end_date' in jsondict: converted = date.from_json(jsondict['end_date']) else: converted = None if converted != descriptor.end: dirty = True descriptor.end = converted if 'enrollment_start' in jsondict: converted = date.from_json(jsondict['enrollment_start']) else: converted = None if converted != descriptor.enrollment_start: dirty = True descriptor.enrollment_start = converted if 'enrollment_end' in jsondict: converted = date.from_json(jsondict['enrollment_end']) else: converted = None if converted != descriptor.enrollment_end: dirty = True descriptor.enrollment_end = converted if dirty: get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor)) # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # to make faster, could compare against db or could have client send over a list of which fields changed. temploc = Location(course_location)._replace(category='about', name='syllabus') update_item(temploc, jsondict['syllabus']) temploc = temploc._replace(name='overview') update_item(temploc, jsondict['overview']) temploc = temploc._replace(name='effort') update_item(temploc, jsondict['effort']) temploc = temploc._replace(name='video') recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video']) update_item(temploc, recomposed_video_tag) # Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm # it persisted correctly return CourseDetails.fetch(course_location)
def create_source_course(self): """ A course testing all of the conversion mechanisms: * some inheritable settings * sequences w/ draft and live intermixed children to ensure all get to the draft but only the live ones get to published. Some are only draft, some are both, some are only live. * about, static_tab, and conditional documents """ location = Location("i4x", "test_org", "test_course", "course", "runid") self.course_location = location date_proxy = Date() metadata = { "start": date_proxy.to_json(datetime.datetime(2000, 3, 13, 4)), "display_name": "Migration test course", } data = {"wiki_slug": "test_course_slug"} course_root = self._create_and_get_item(self.old_mongo, location, data, metadata) runtime = course_root.runtime # chapters location = location.replace(category="chapter", name=uuid.uuid4().hex) chapter1 = self._create_and_get_item(self.old_mongo, location, {}, {"display_name": "Chapter 1"}, runtime) course_root.children.append(chapter1.location.url()) location = location.replace(category="chapter", name=uuid.uuid4().hex) chapter2 = self._create_and_get_item(self.old_mongo, location, {}, {"display_name": "Chapter 2"}, runtime) course_root.children.append(chapter2.location.url()) self.old_mongo.update_children(course_root.location, course_root.children) # vertical in live only location = location.replace(category="vertical", name=uuid.uuid4().hex) live_vert = self._create_and_get_item(self.old_mongo, location, {}, {"display_name": "Live vertical"}, runtime) chapter1.children.append(live_vert.location.url()) self.create_random_units(self.old_mongo, live_vert) # vertical in both live and draft location = location.replace(category="vertical", name=uuid.uuid4().hex) both_vert = self._create_and_get_item(self.old_mongo, location, {}, {"display_name": "Both vertical"}, runtime) draft_both = self._create_and_get_item( self.draft_mongo, location, {}, {"display_name": "Both vertical renamed"}, runtime ) chapter1.children.append(both_vert.location.url()) self.create_random_units(self.old_mongo, both_vert, self.draft_mongo, draft_both) # vertical in draft only (x2) location = location.replace(category="vertical", name=uuid.uuid4().hex) draft_vert = self._create_and_get_item( self.draft_mongo, location, {}, {"display_name": "Draft vertical"}, runtime ) chapter1.children.append(draft_vert.location.url()) self.create_random_units(self.draft_mongo, draft_vert) location = location.replace(category="vertical", name=uuid.uuid4().hex) draft_vert = self._create_and_get_item( self.draft_mongo, location, {}, {"display_name": "Draft vertical2"}, runtime ) chapter1.children.append(draft_vert.location.url()) self.create_random_units(self.draft_mongo, draft_vert) # and finally one in live only (so published has to skip 2) location = location.replace(category="vertical", name=uuid.uuid4().hex) live_vert = self._create_and_get_item( self.old_mongo, location, {}, {"display_name": "Live vertical end"}, runtime ) chapter1.children.append(live_vert.location.url()) self.create_random_units(self.old_mongo, live_vert) # update the chapter self.old_mongo.update_children(chapter1.location, chapter1.children) # now the other one w/ the conditional # first create some show children indirect1 = self._create_and_get_item( self.old_mongo, location.replace(category="discussion", name=uuid.uuid4().hex), "", {"display_name": "conditional show 1"}, runtime, ) indirect2 = self._create_and_get_item( self.old_mongo, location.replace(category="html", name=uuid.uuid4().hex), "", {"display_name": "conditional show 2"}, runtime, ) location = location.replace(category="conditional", name=uuid.uuid4().hex) metadata = {"xml_attributes": {"sources": [live_vert.location.url()], "completed": True}} data = {"show_tag_list": [indirect1.location.url(), indirect2.location.url()]} conditional = self._create_and_get_item(self.old_mongo, location, data, metadata, runtime) conditional.children = [indirect1.location.url(), indirect2.location.url()] # add direct children self.create_random_units(self.old_mongo, conditional) chapter2.children.append(conditional.location.url()) self.old_mongo.update_children(chapter2.location, chapter2.children) # and the ancillary docs (not children) location = location.replace(category="static_tab", name=uuid.uuid4().hex) # the below automatically adds the tab to the course _tab = self._create_and_get_item(self.old_mongo, location, "", {"display_name": "Tab uno"}, runtime) location = location.replace(category="about", name="overview") _overview = self._create_and_get_item(self.old_mongo, location, "<p>test</p>", {}, runtime) location = location.replace(category="course_info", name="updates") _overview = self._create_and_get_item( self.old_mongo, location, "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, runtime )
def convert_datetime_to_iso(dt): return Date().to_json(dt)
def convert_datetime_to_iso(datetime_obj): """ Use the xblock serializer to convert the datetime """ return Date().to_json(datetime_obj)
import unittest from django.test.utils import override_settings from django.utils.timezone import utc from courseware.models import StudentModule from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from student.tests.factories import UserFactory from xmodule.fields import Date from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from opaque_keys.edx.keys import CourseKey from ..views import tools DATE_FIELD = Date() class TestDashboardError(unittest.TestCase): """ Test DashboardError exceptions. """ def test_response(self): error = tools.DashboardError(u'Oh noes!') response = json.loads(error.response().content) self.assertEqual(response, {'error': 'Oh noes!'}) class TestHandleDashboardError(unittest.TestCase): """ Test handle_dashboard_error decorator.
class InheritanceMixin(XBlockMixin): """Field definitions for inheritable fields.""" graded = Boolean( help="Whether this module contributes to the final course grade", scope=Scope.settings, default=False, ) start = Date(help="Start time when this module is visible", default=DEFAULT_START_DATE, scope=Scope.settings) due = Date( display_name=_("Due Date"), help=_("Enter the default date by which problems are due."), scope=Scope.settings, ) visible_to_staff_only = Boolean( help= _("If true, can be seen only by course staff, regardless of start date." ), default=False, scope=Scope.settings, ) course_edit_method = String( display_name=_("Course Editor"), help= _("Enter the method by which this course is edited (\"XML\" or \"Studio\")." ), default="Studio", scope=Scope.settings, deprecated= True # Deprecated because user would not change away from Studio within Studio. ) giturl = String( display_name=_("GIT URL"), help=_("Enter the URL for the course data GIT repository."), scope=Scope.settings) xqa_key = String(display_name=_("XQA Key"), help=_("This setting is not currently supported."), scope=Scope.settings, deprecated=True) annotation_storage_url = String(help=_( "Enter the location of the annotation storage server. The textannotation, videoannotation, and imageannotation advanced modules require this setting." ), scope=Scope.settings, default= "http://your_annotation_storage.com", display_name=_( "URL for Annotation Storage")) annotation_token_secret = String(help=_( "Enter the secret string for annotation storage. The textannotation, videoannotation, and imageannotation advanced modules require this string." ), scope=Scope.settings, default= "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name=_( "Secret Token String for Annotation")) graceperiod = Timedelta( help= "Amount of time after the due date that submissions will be accepted", scope=Scope.settings, ) group_access = Dict( help=_( "Enter the ids for the content groups this problem belongs to."), scope=Scope.settings, ) showanswer = String( display_name=_("Show Answer"), help=_('Specify when the Show Answer button appears for each problem. ' 'Valid values are "always", "answered", "attempted", "closed", ' '"finished", "past_due", "correct_or_past_due", and "never".'), scope=Scope.settings, default="finished", ) rerandomize = String( display_name=_("Randomization"), help= _('Specify the default for how often variable values in a problem are randomized. ' 'This setting should be set to \"never\" unless you plan to provide a Python ' 'script to identify and randomize values in most of the problems in your course. ' 'Valid values are \"always\", \"onreset\", \"never\", and \"per_student\".' ), scope=Scope.settings, default="never", ) days_early_for_beta = Float( display_name=_("Days Early for Beta Users"), help= _("Enter the number of days before the start date that beta users can access the course." ), scope=Scope.settings, default=None, ) static_asset_path = String( display_name=_("Static Asset Path"), help= _("Enter the path to use for files on the Files & Uploads page. This value overrides the Studio default, c4x://." ), scope=Scope.settings, default='', ) text_customization = Dict( display_name=_("Text Customization"), help=_( "Enter string customization substitutions for particular locations." ), scope=Scope.settings, ) use_latex_compiler = Boolean( display_name=_("Enable LaTeX Compiler"), help= _("Enter true or false. If true, you can use the LaTeX templates for HTML components and advanced Problem components." ), default=False, scope=Scope.settings) max_attempts = Integer( display_name=_("Maximum Attempts"), help= _("Enter the maximum number of times a student can try to answer problems. By default, Maximum Attempts is set to null, meaning that students have an unlimited number of attempts for problems. You can override this course-wide setting for individual problems. However, if the course-wide setting is a specific number, you cannot set the Maximum Attempts for individual problems to unlimited." ), values={"min": 0}, scope=Scope.settings) matlab_api_key = String( display_name=_("Matlab API key"), help= _("Enter the API key provided by MathWorks for accessing the MATLAB Hosted Service. " "This key is granted for exclusive use in this course for the specified duration. " "Do not share the API key with other courses. Notify MathWorks immediately " "if you believe the key is exposed or compromised. To obtain a key for your course, " "or to report an issue, please contact [email protected]"), scope=Scope.settings) # This is should be scoped to content, but since it's defined in the policy # file, it is currently scoped to settings. user_partitions = UserPartitionList( display_name=_("Group Configurations"), help= _("Enter the configurations that govern how students are grouped together." ), default=[], scope=Scope.settings) video_speed_optimizations = Boolean( display_name=_("Enable video caching system"), help= _("Enter true or false. If true, video caching will be used for HTML5 videos." ), default=True, scope=Scope.settings) video_bumper = Dict( display_name=_("Video Pre-Roll"), help= _("""Identify a video, 5-10 seconds in length, to play before course videos. Enter the video ID from""" """ the Video Uploads page and one or more transcript files in the following format:""" """ {"video_id": "ID", "transcripts": {"language": "/static/filename.srt"}}.""" """ For example, an entry for a video with two transcripts looks like this:""" """ {"video_id": "77cef264-d6f5-4cf2-ad9d-0178ab8c77be",""" """ "transcripts": {"en": "/static/DemoX-D01_1.srt", "uk": "/static/DemoX-D01_1_uk.srt"}}""" ), scope=Scope.settings) reset_key = "DEFAULT_SHOW_RESET_BUTTON" default_reset_button = getattr(settings, reset_key) if hasattr( settings, reset_key) else False show_reset_button = Boolean( display_name=_("Show Reset Button for Problems"), help= _("Enter true or false. If true, problems in the course default to always displaying a 'Reset' button. You can " "override this in each problem's settings. All existing problems are affected when this course-wide setting is changed." ), scope=Scope.settings, default=default_reset_button) edxnotes = Boolean( display_name=_("Enable Student Notes"), help= _("Enter true or false. If true, students can use the Student Notes feature." ), default=False, scope=Scope.settings) edxnotes_visibility = Boolean( display_name="Student Notes Visibility", help=_( "Indicates whether Student Notes are visible in the course. " "Students can also show or hide their notes in the courseware."), default=True, scope=Scope.user_info) in_entrance_exam = Boolean( display_name=_("Tag this module as part of an Entrance Exam section"), help=_( "Enter true or false. If true, answer submissions for problem modules will be " "considered in the Entrance Exam scoring/gating algorithm."), scope=Scope.settings, default=False)