示例#1
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))
        self.assertIsNone(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"))

    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")
示例#2
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=datetime(2030, 1, 1, tzinfo=UTC),
                 scope=Scope.settings)
    due = Date(
        display_name=_("Due Date"),
        help=_("Enter the default date by which problems are due."),
        scope=Scope.settings,
    )
    extended_due = Date(
        help="Date that this problem is due by for a particular student. This "
        "can be set by an instructor, and will override the global due "
        "date if it is set to a date that is later than the global due "
        "date.",
        default=None,
        scope=Scope.user_state,
    )
    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\", and \"never\"."
          ),
        scope=Scope.settings,
        default="finished",
    )
    rerandomize = String(
        display_name=_("Randomization"),
        help=
        _("Specify how often variable values in a problem are randomized when a student loads the problem. Valid values are \"always\", \"onreset\", \"never\", and \"per_student\". This setting only applies to problems that have randomly generated numeric values."
          ),
        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)

    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)
示例#3
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 'certificate_available_date' in jsondict:
            converted = date.from_json(jsondict['certificate_available_date'])
        else:
            converted = None

        if converted != descriptor.certificate_available_date:
            dirty = True
            descriptor.certificate_available_date = 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 (descriptor.can_toggle_course_pacing and 'self_paced' in jsondict
                and jsondict['self_paced'] != descriptor.self_paced):
            descriptor.self_paced = jsondict['self_paced']
            dirty = True

        if dirty:
            module_store.update_item(descriptor, user.id)

        # NOTE: below auto writes to the db w/o verifying that any of
        # the fields actually changed to make faster, could compare
        # against db or could have client send over a list of which
        # fields changed.
        for attribute in ABOUT_ATTRIBUTES:
            if attribute in jsondict:
                cls.update_about_item(descriptor, attribute,
                                      jsondict[attribute], user.id)

        cls.update_about_video(descriptor, jsondict['intro_video'], user.id)

        # Could just return jsondict w/o doing any db reads, but I put
        # the reads in as a means to confirm it persisted correctly
        return CourseDetails.fetch(course_key)
    def 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_item(course_root, '**replace_user**')
        # 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_item(chapter1, '**replace_user**')

        # 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_item(chapter2, '**replace_user**')

        # 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)
示例#5
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=datetime(2030, 1, 1, tzinfo=UTC),
                 scope=Scope.settings)
    due = Date(
        help="Date that this problem is due by",
        scope=Scope.settings,
    )
    extended_due = Date(
        help="Date that this problem is due by for a particular student. This "
        "can be set by an instructor, and will override the global due "
        "date if it is set to a date that is later than the global due "
        "date.",
        default=None,
        scope=Scope.user_state,
    )
    course_edit_method = String(
        help="Method with which this course is edited.",
        default="Studio",
        scope=Scope.settings)
    giturl = String(
        help="url root for course data git repository",
        scope=Scope.settings,
    )
    xqa_key = String(help="DO NOT USE", scope=Scope.settings)
    annotation_storage_url = String(
        help="Location of Annotation backend",
        scope=Scope.settings,
        default="http://your_annotation_storage.com",
        display_name="Url for Annotation Storage")
    annotation_token_secret = String(
        help="Secret string for annotation storage",
        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,
    )
    showanswer = String(
        help="When to show the problem answer to the student",
        scope=Scope.settings,
        default="finished",
    )
    rerandomize = String(
        help="When to rerandomize the problem",
        scope=Scope.settings,
        default="never",
    )
    days_early_for_beta = Float(
        help="Number of days early to show content to beta users",
        scope=Scope.settings,
        default=None,
    )
    static_asset_path = String(
        help="Path to use for static assets - overrides Studio c4x://",
        scope=Scope.settings,
        default='',
    )
    text_customization = Dict(
        help="String customization substitutions for particular locations",
        scope=Scope.settings,
    )
    use_latex_compiler = Boolean(help="Enable LaTeX templates?",
                                 default=False,
                                 scope=Scope.settings)
    max_attempts = Integer(
        display_name="Maximum Attempts",
        help=
        ("Defines the number of times a student can try to answer this problem. "
         "If the value is not set, infinite attempts are allowed."),
        values={"min": 0},
        scope=Scope.settings)
    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 by this course for the specified duration. "
        "Please do not share the API key with other courses and notify MathWorks immediately "
        "if you believe the key is exposed or compromised. To obtain a key for your course, "
        "or to report and 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(
        help=
        "The list of group configurations for partitioning students in content experiments.",
        default=[],
        scope=Scope.settings)
示例#6
0
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')
示例#7
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 = '''<sequential display_name="oops\N{SNOWMAN}"><video url="hi"></sequential>'''
        system = self.get_system()

        descriptor = system.process_xml(bad_xml)

        assert 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)

        assert 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)

        assert 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))

        assert re_import_descriptor.__class__.__name__ == 'ErrorBlockWithMixins'

        assert descriptor.contents == re_import_descriptor.contents
        assert 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
        assert 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))
        assert descriptor.due == ImportTestCase.date.from_json(
            from_date_string)

        # Check that the child inherits due correctly
        child = descriptor.get_children()[0]
        assert child.due == ImportTestCase.date.from_json(from_date_string)
        # need to convert v to canonical json b4 comparing
        assert 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('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)))
        assert is_pointer_tag(node)
        # but it's a special case course pointer
        assert node.attrib['course'] == COURSE
        assert node.attrib['org'] == ORG

        # Does the course still have unicorns?
        with descriptor.runtime.export_fs.open(
                f'course/{course_run}.xml') as f:
            course_xml = etree.fromstring(f.read())

        assert course_xml.attrib['unicorn'] == unicorn_color

        # the course and org tags should be _only_ in the pointer
        assert 'course' not in course_xml.attrib
        assert 'org' not in course_xml.attrib

        # did we successfully strip the url_name from the definition contents?
        assert 'url_name' not in course_xml.attrib

        # Does the chapter tag now have a due attribute?
        # hardcoded path to child
        with descriptor.runtime.export_fs.open('chapter/ch.xml') as f:
            chapter_xml = etree.fromstring(f.read())
        assert chapter_xml.tag == 'chapter'
        assert 'due' not in 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
        assert original_unwrapped is not 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.
        """
        assert descriptor.due is None

        # Check that the child does not inherit a value for due
        child = descriptor.get_children()[0]
        assert child.due is None

        # Check that the child hasn't started yet
        assert 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.
        """
        assert descriptor.due == ImportTestCase.date.from_json(course_due)
        assert child.due == ImportTestCase.date.from_json(child_due)
        # Test inherited metadata. Due does not appear here (because explicitly set on child).
        assert ImportTestCase.date.to_json(ImportTestCase.date.from_json(course_due)) == child.xblock_kvs.inherited_settings['due']  # pylint: disable=line-too-long

    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(f"should be True for {xml_str}")
            assert is_pointer_tag(etree.fromstring(xml_str))

        for xml_str in no:
            print(f"should be False for {xml_str}")
            assert not 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(f"Checking {str(node.location)}")
            assert 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')

        assert toy.url_name == '2012_Fall'
        assert two_toys.url_name == 'TT_2012_Fall'

        toy_ch = toy.get_children()[0]
        two_toys_ch = two_toys.get_children()[0]

        assert toy_ch.display_name == 'Overview'
        assert two_toys_ch.display_name == 'Two Toy Overview'

        # Also check that the grading policy loaded
        assert 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'
        assert toy.graded is 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)
        assert toy_tab_syllabus.display_name == 'Syllabus'
        assert toy_tab_syllabus.course_staff_only is 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)
        assert toy_tab_resources.display_name == 'Resources'
        assert toy_tab_resources.course_staff_only is 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)
        assert toy_video.youtube_id_1_0 == 'p2Q6BrNhdh8'
        assert 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()
        assert 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()
        assert len(chapters) == 5

        ch2 = chapters[1]
        assert ch2.url_name == 'secret:magic'

        print("Ch2 location: ", ch2.location)

        also_ch2 = modulestore.get_item(ch2.location)
        assert ch2 == also_ch2

        print("making sure html loaded")
        loc = course.id.make_usage_key('html', 'secret:toylab')
        html = modulestore.get_item(loc)
        assert 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()
        assert 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)
        ]

        assert any(
            ((expect in msg) or (expect in err)) for (msg, err) in errors)
        chapters = course.get_children()
        assert 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()

        assert len(sections) == 4

        for i in (2, 3):
            video = sections[i]
            # Name should be 'video_{hash}'
            print(f"video {i} url_name: {video.url_name}")
            assert 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()

        assert len(sections) == 1

        conditional_location = course.id.make_usage_key(
            'conditional', 'condone')
        module = modulestore.get_item(conditional_location)
        assert len(module.children) == 1

        poll_location = course.id.make_usage_key('poll_question', 'first_poll')
        module = modulestore.get_item(poll_location)
        assert len(module.get_children()) == 0
        assert module.voted is False
        assert module.poll_answer == ''
        assert module.poll_answers == {}
        assert module.answers ==\
               [{'text': 'Yes', 'id': 'Yes'}, {'text': 'No', 'id': 'No'}, {'text': "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()

        assert len(sections) == 1

        location = course.id.make_usage_key('word_cloud', 'cloud1')
        module = modulestore.get_item(location)
        assert len(module.get_children()) == 0
        assert module.num_inputs == 5
        assert module.num_top_words == 250

    def test_cohort_config(self):
        """
        Check that cohort config parsing works right.

        Note: The cohort config on the CourseBlock 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
        assert not course.is_cohorted

        # empty config -> False
        course.cohort_config = {}
        assert not course.is_cohorted

        # false config -> False
        course.cohort_config = {'cohorted': False}
        assert not course.is_cohorted

        # and finally...
        course.cohort_config = {'cohorted': True}
        assert course.is_cohorted
示例#8
0
 def convert_datetime_to_iso(datetime_obj):
     """
     Use the xblock serializer to convert the datetime
     """
     return Date().to_json(datetime_obj)
示例#9
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=datetime(2030, 1, 1, tzinfo=UTC),
                 scope=Scope.settings)
    due = Date(
        help="Date that this problem is due by",
        scope=Scope.settings,
    )
    extended_due = Date(
        help="Date that this problem is due by for a particular student. This "
        "can be set by an instructor, and will override the global due "
        "date if it is set to a date that is later than the global due "
        "date.",
        default=None,
        scope=Scope.user_state,
    )
    course_edit_method = String(
        help="Method with which this course is edited.",
        default="Studio",
        scope=Scope.settings)
    giturl = String(
        help="url root for course data git repository",
        scope=Scope.settings,
    )
    xqa_key = String(help="DO NOT USE", scope=Scope.settings)
    annotation_storage_url = String(
        help="Location of Annotation backend",
        scope=Scope.settings,
        default="http://your_annotation_storage.com",
        display_name="Url for Annotation Storage")
    annotation_token_secret = String(
        help="Secret string for annotation storage",
        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,
    )
    showanswer = String(
        help="When to show the problem answer to the student",
        scope=Scope.settings,
        default="finished",
    )
    rerandomize = String(
        help="When to rerandomize the problem",
        scope=Scope.settings,
        default="never",
    )
    days_early_for_beta = Float(
        help="Number of days early to show content to beta users",
        scope=Scope.settings,
        default=None,
    )
    static_asset_path = String(
        help="Path to use for static assets - overrides Studio c4x://",
        scope=Scope.settings,
        default='',
    )
    text_customization = Dict(
        help="String customization substitutions for particular locations",
        scope=Scope.settings,
    )
    use_latex_compiler = Boolean(help="Enable LaTeX templates?",
                                 default=False,
                                 scope=Scope.settings)
    max_attempts = Integer(
        display_name="Maximum Attempts",
        help=
        ("Defines the number of times a student can try to answer this problem. "
         "If the value is not set, infinite attempts are allowed."),
        values={"min": 0},
        scope=Scope.settings)
示例#10
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_enforce_type(self):
        self.assertEqual(DateTest.date.enforce_type(None), None)
        self.assertEqual(DateTest.date.enforce_type(""), None)
        self.assertEqual(
            DateTest.date.enforce_type("2012-12-31T23:00:01"),
            datetime.datetime(2012, 12, 31, 23, 0, 1, tzinfo=UTC)
        )
        self.assertEqual(
            DateTest.date.enforce_type(1234567890000),
            datetime.datetime(2009, 2, 13, 23, 31, 30, tzinfo=UTC)
        )
        self.assertEqual(
            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 self.assertRaises(TypeError):
            DateTest.date.enforce_type([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')
示例#11
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 (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(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)
示例#12
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 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_block, [
                TestScenario((self.demo_course,), self.demo_course.location.name),
                TestScenario((self.html_course,), 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')
 def convert_datetime_to_iso(datetime_obj):
     return Date().to_json(datetime_obj)
示例#15
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)
示例#16
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)

        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__,
                         '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
        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 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=protected-access
        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
        descriptor.runtime.export_fs = MemoryFS()
        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(
                '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 descriptor.runtime.export_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=protected-access
        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.to_deprecated_string()))
            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'])

        location = Location("edX", "toy", "2012_Fall", "video", "Welcome",
                            None)
        toy_video = modulestore.get_item(location)
        location_two = Location("edX", "toy", "TT_2012_Fall", "video",
                                "Welcome", None)
        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, course_dirs=['toy'])
        courses = modulestore.get_courses()
        self.assertEquals(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.assertEquals(len(chapters), 5)

        ch2 = chapters[1]
        self.assertEquals(ch2.url_name, "secret:magic")

        print("Ch2 location: ", ch2.location)

        also_ch2 = modulestore.get_item(ch2.location)
        self.assertEquals(ch2, also_ch2)

        print("making sure html loaded")
        loc = course.id.make_usage_key('html', 'secret:toylab')
        html = modulestore.get_item(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 = "InvalidKeyError"
        errors = [(msg.encode("utf-8"), err.encode("utf-8"))
                  for msg, err 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, course_dirs=['toy'])

        toy_id = SlashSeparatedCourseKey('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)

        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 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 = SlashSeparatedCourseKey("edX", "gst_test", "2012_Fall")
        location = sa_id.make_usage_key("graphical_slider_tool", "sample_gst")
        gst_sample = modulestore.get_item(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.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.
        """
        modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy'])

        toy_id = SlashSeparatedCourseKey('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)
示例#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('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)
示例#18
0
from django.contrib.auth.models import User
from django.core.exceptions import MultipleObjectsReturned
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey
from pytz import UTC

from edx_when import api, signals
from edx_when.field_data import DateLookupFieldData
from student.tests.factories import UserFactory
from xmodule.fields import Date
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory

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.
    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 '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_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 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=_(
            # 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",
    )
    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='',
    )
    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: {format}. "
          "For example, an entry for a video with two transcripts looks like this: {example}"
          ).
        format(
            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)

    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)

    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)
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,
    )
    # This attribute is for custom pacing in self paced courses for Studio if CUSTOM_RELATIVE_DATES flag is active
    relative_weeks_due = Integer(
        display_name=_("Number of Relative Weeks Due By"),
        help=
        _("Enter the number of weeks the problems are due by relative to the learner's enrollment date"
          ),
        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", "after_all_attempts", '
            '"after_all_attempts_or_correct", "attempted_no_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://."
          ),  # lint-amnesty, pylint: disable=line-too-long
        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."
          ),  # lint-amnesty, pylint: disable=line-too-long
        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."
          ),  # lint-amnesty, pylint: disable=line-too-long
        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."  # lint-amnesty, pylint: disable=line-too-long
        ),
        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()