예제 #1
0
    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)
예제 #3
0
    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)
예제 #4
0
    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)
예제 #7
0
    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)
예제 #8
0
    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)
예제 #10
0
    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
        )
예제 #11
0
    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')
예제 #13
0
    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)
예제 #14
0
    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)
예제 #15
0
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 &lt;html&gt;"),
            ]),
            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')
예제 #17
0
    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)
예제 #18
0
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)
예제 #19
0
    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)
예제 #20
0
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')
예제 #21
0
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
        )
예제 #23
0
    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)
예제 #24
0
    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)
예제 #25
0
    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)
예제 #26
0
    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)
예제 #29
0
 def convert_datetime_to_iso(datetime_obj):
     """
     Use the xblock serializer to convert the datetime
     """
     return Date().to_json(datetime_obj)
예제 #30
0
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.
예제 #31
0
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)