course_metadata.py 7.54 KB
Newer Older
1 2 3
"""
Django module for Course Metadata class -- manages advanced settings and related parameters
"""
4
from xblock.fields import Scope
5
from xmodule.modulestore.django import modulestore
6
from django.utils.translation import ugettext as _
Oleg Marshev committed
7
from django.conf import settings
8

9

10 11
class CourseMetadata(object):
    '''
12 13 14 15
    For CRUD operations on metadata fields which do not have specific editors
    on the other pages including any user generated ones.
    The objects have no predefined attrs but instead are obj encodings of the
    editable metadata.
16
    '''
17
    # The list of fields that wouldn't be shown in Advanced Settings.
18 19
    # Should not be used directly. Instead the filtered_list method should
    # be used if the field needs to be filtered depending on the feature flag.
20
    FILTERED_LIST = [
21
        'cohort_config',
22 23 24 25 26 27 28 29 30 31 32 33
        'xml_attributes',
        'start',
        'end',
        'enrollment_start',
        'enrollment_end',
        'tabs',
        'graceperiod',
        'show_timezone',
        'format',
        'graded',
        'hide_from_toc',
        'pdf_textbooks',
34
        'user_partitions',
35 36 37
        'name',  # from xblock
        'tags',  # from xblock
        'visible_to_staff_only',
38
        'group_access',
39 40 41 42
        'pre_requisite_courses',
        'entrance_exam_enabled',
        'entrance_exam_minimum_score_pct',
        'entrance_exam_id',
43 44
        'is_entrance_exam',
        'in_entrance_exam',
45
        'language',
46 47
        'certificates',
        'minimum_grade_credit',
48 49 50 51
        'default_time_limit_minutes',
        'is_proctored_enabled',
        'is_time_limited',
        'is_practice_exam',
Muhammad Shoaib committed
52
        'exam_review_rules',
53 54 55
        'self_paced',
        'chrome',
        'default_tab',
cahrens committed
56
    ]
57

58
    @classmethod
Oleg Marshev committed
59 60 61 62 63 64 65 66 67 68 69
    def filtered_list(cls):
        """
        Filter fields based on feature flag, i.e. enabled, disabled.
        """
        # Copy the filtered list to avoid permanently changing the class attribute.
        filtered_list = list(cls.FILTERED_LIST)

        # Do not show giturl if feature is not enabled.
        if not settings.FEATURES.get('ENABLE_EXPORT_GIT'):
            filtered_list.append('giturl')

70 71 72 73
        # Do not show edxnotes if the feature is disabled.
        if not settings.FEATURES.get('ENABLE_EDXNOTES'):
            filtered_list.append('edxnotes')

74 75 76 77
        # Do not show video_upload_pipeline if the feature is disabled.
        if not settings.FEATURES.get('ENABLE_VIDEO_UPLOAD_PIPELINE'):
            filtered_list.append('video_upload_pipeline')

78
        # Do not show social sharing url field if the feature is disabled.
79 80
        if (not hasattr(settings, 'SOCIAL_SHARING_SETTINGS') or
                not getattr(settings, 'SOCIAL_SHARING_SETTINGS', {}).get("CUSTOM_COURSE_URLS")):
81 82
            filtered_list.append('social_sharing_url')

83 84 85 86
        # Do not show teams configuration if feature is disabled.
        if not settings.FEATURES.get('ENABLE_TEAMS'):
            filtered_list.append('teams_configuration')

Alexander Kryklia committed
87 88 89
        if not settings.FEATURES.get('ENABLE_VIDEO_BUMPER'):
            filtered_list.append('video_bumper')

90 91 92
        # Do not show enable_ccx if feature is not enabled.
        if not settings.FEATURES.get('CUSTOM_COURSES_EDX'):
            filtered_list.append('enable_ccx')
Giovanni Di Milia committed
93
            filtered_list.append('ccx_connector')
94

Oleg Marshev committed
95 96 97
        return filtered_list

    @classmethod
98
    def fetch(cls, descriptor):
99
        """
100 101
        Fetch the key:value editable course details for the given course from
        persistence and return a CourseMetadata model.
102
        """
103
        result = {}
104 105 106 107 108 109
        metadata = cls.fetch_all(descriptor)
        for key, value in metadata.iteritems():
            if key in cls.filtered_list():
                continue
            result[key] = value
        return result
110

111 112 113 114 115 116
    @classmethod
    def fetch_all(cls, descriptor):
        """
        Fetches all key:value pairs from persistence and returns a CourseMetadata model.
        """
        result = {}
Calen Pennington committed
117
        for field in descriptor.fields.values():
118 119
            if field.scope != Scope.settings:
                continue
120 121
            result[field.name] = {
                'value': field.read_json(descriptor),
122 123
                'display_name': _(field.display_name),    # pylint: disable=translation-of-non-string
                'help': _(field.help),                    # pylint: disable=translation-of-non-string
124 125
                'deprecated': field.runtime_options.get('deprecated', False)
            }
126
        return result
127

128
    @classmethod
129
    def update_from_json(cls, descriptor, jsondict, user, filter_tabs=True):
130
        """
Don Mitchell committed
131
        Decode the json into CourseMetadata and save any changed attrs to the db.
132

Don Mitchell committed
133
        Ensures none of the fields are in the blacklist.
134
        """
Oleg Marshev committed
135
        filtered_list = cls.filtered_list()
136
        # Don't filter on the tab attribute if filter_tabs is False.
137 138 139
        if not filter_tabs:
            filtered_list.remove("tabs")

140 141 142 143
        # Validate the values before actually setting them.
        key_values = {}

        for key, model in jsondict.iteritems():
144
            # should it be an error if one of the filtered list items is in the payload?
David Baumgold committed
145
            if key in filtered_list:
146
                continue
147 148 149 150 151
            try:
                val = model['value']
                if hasattr(descriptor, key) and getattr(descriptor, key) != val:
                    key_values[key] = descriptor.fields[key].from_json(val)
            except (TypeError, ValueError) as err:
152 153
                raise ValueError(_("Incorrect format for field '{name}'. {detailed_message}").format(
                    name=model['display_name'], detailed_message=err.message))
154

155 156 157
        return cls.update_from_dict(key_values, descriptor, user)

    @classmethod
158
    def validate_and_update_from_json(cls, descriptor, jsondict, user, filter_tabs=True):
159 160 161
        """
        Validate the values in the json dict (validated by xblock fields from_json method)

162 163
        If all fields validate, go ahead and update those values on the object and return it without
        persisting it to the DB.
164 165 166 167 168 169 170
        If not, return the error objects list.

        Returns:
            did_validate: whether values pass validation or not
            errors: list of error objects
            result: the updated course metadata or None if error
        """
Oleg Marshev committed
171
        filtered_list = cls.filtered_list()
172 173
        if not filter_tabs:
            filtered_list.remove("tabs")
Oleg Marshev committed
174

175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
        filtered_dict = dict((k, v) for k, v in jsondict.iteritems() if k not in filtered_list)
        did_validate = True
        errors = []
        key_values = {}
        updated_data = None

        for key, model in filtered_dict.iteritems():
            try:
                val = model['value']
                if hasattr(descriptor, key) and getattr(descriptor, key) != val:
                    key_values[key] = descriptor.fields[key].from_json(val)
            except (TypeError, ValueError) as err:
                did_validate = False
                errors.append({'message': err.message, 'model': model})

        # If did validate, go ahead and update the metadata
        if did_validate:
192
            updated_data = cls.update_from_dict(key_values, descriptor, user, save=False)
193 194 195 196

        return did_validate, errors, updated_data

    @classmethod
197
    def update_from_dict(cls, key_values, descriptor, user, save=True):
198
        """
199
        Update metadata descriptor from key_values. Saves to modulestore if save is true.
200
        """
201 202 203
        for key, value in key_values.iteritems():
            setattr(descriptor, key, value)

204
        if save and len(key_values):
205
            modulestore().update_item(descriptor, user.id)
206

207
        return cls.fetch(descriptor)