course_metadata.py 7.89 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 xblock_django.models import XBlockStudioConfigurationFlag
6
from xmodule.modulestore.django import modulestore
7

8
from django.utils.translation import ugettext as _
Oleg Marshev committed
9
from django.conf import settings
10

11

12 13
class CourseMetadata(object):
    '''
14 15 16 17
    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.
18
    '''
19
    # The list of fields that wouldn't be shown in Advanced Settings.
20 21
    # 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.
22
    FILTERED_LIST = [
23
        'cohort_config',
24 25 26 27 28 29 30 31 32 33 34 35
        'xml_attributes',
        'start',
        'end',
        'enrollment_start',
        'enrollment_end',
        'tabs',
        'graceperiod',
        'show_timezone',
        'format',
        'graded',
        'hide_from_toc',
        'pdf_textbooks',
36
        'user_partitions',
37 38 39
        'name',  # from xblock
        'tags',  # from xblock
        'visible_to_staff_only',
40
        'group_access',
41 42 43 44
        'pre_requisite_courses',
        'entrance_exam_enabled',
        'entrance_exam_minimum_score_pct',
        'entrance_exam_id',
45 46
        'is_entrance_exam',
        'in_entrance_exam',
47
        'language',
48 49
        'certificates',
        'minimum_grade_credit',
50 51 52 53
        'default_time_limit_minutes',
        'is_proctored_enabled',
        'is_time_limited',
        'is_practice_exam',
Muhammad Shoaib committed
54
        'exam_review_rules',
55
        'hide_after_due',
56 57 58
        'self_paced',
        'chrome',
        'default_tab',
cahrens committed
59
    ]
60

61
    @classmethod
Oleg Marshev committed
62 63 64 65 66 67 68 69 70 71 72
    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')

73 74 75 76
        # Do not show edxnotes if the feature is disabled.
        if not settings.FEATURES.get('ENABLE_EDXNOTES'):
            filtered_list.append('edxnotes')

77 78 79 80
        # 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')

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

86 87 88 89
        # Do not show teams configuration if feature is disabled.
        if not settings.FEATURES.get('ENABLE_TEAMS'):
            filtered_list.append('teams_configuration')

Alexander Kryklia committed
90 91 92
        if not settings.FEATURES.get('ENABLE_VIDEO_BUMPER'):
            filtered_list.append('video_bumper')

93 94 95
        # 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
96
            filtered_list.append('ccx_connector')
97

98 99 100 101 102
        # If the XBlockStudioConfiguration table is not being used, there is no need to
        # display the "Allow Unsupported XBlocks" setting.
        if not XBlockStudioConfigurationFlag.is_enabled():
            filtered_list.append('allow_unsupported_xblocks')

Oleg Marshev committed
103 104 105
        return filtered_list

    @classmethod
106
    def fetch(cls, descriptor):
107
        """
108 109
        Fetch the key:value editable course details for the given course from
        persistence and return a CourseMetadata model.
110
        """
111
        result = {}
112 113 114 115 116 117
        metadata = cls.fetch_all(descriptor)
        for key, value in metadata.iteritems():
            if key in cls.filtered_list():
                continue
            result[key] = value
        return result
118

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

136
    @classmethod
137
    def update_from_json(cls, descriptor, jsondict, user, filter_tabs=True):
138
        """
Don Mitchell committed
139
        Decode the json into CourseMetadata and save any changed attrs to the db.
140

Don Mitchell committed
141
        Ensures none of the fields are in the blacklist.
142
        """
Oleg Marshev committed
143
        filtered_list = cls.filtered_list()
144
        # Don't filter on the tab attribute if filter_tabs is False.
145 146 147
        if not filter_tabs:
            filtered_list.remove("tabs")

148 149 150 151
        # Validate the values before actually setting them.
        key_values = {}

        for key, model in jsondict.iteritems():
152
            # should it be an error if one of the filtered list items is in the payload?
David Baumgold committed
153
            if key in filtered_list:
154
                continue
155 156 157 158 159
            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:
160 161
                raise ValueError(_("Incorrect format for field '{name}'. {detailed_message}").format(
                    name=model['display_name'], detailed_message=err.message))
162

163 164 165
        return cls.update_from_dict(key_values, descriptor, user)

    @classmethod
166
    def validate_and_update_from_json(cls, descriptor, jsondict, user, filter_tabs=True):
167 168 169
        """
        Validate the values in the json dict (validated by xblock fields from_json method)

170 171
        If all fields validate, go ahead and update those values on the object and return it without
        persisting it to the DB.
172 173 174 175 176 177 178
        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
179
        filtered_list = cls.filtered_list()
180 181
        if not filter_tabs:
            filtered_list.remove("tabs")
Oleg Marshev committed
182

183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
        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:
200
            updated_data = cls.update_from_dict(key_values, descriptor, user, save=False)
201 202 203 204

        return did_validate, errors, updated_data

    @classmethod
205
    def update_from_dict(cls, key_values, descriptor, user, save=True):
206
        """
207
        Update metadata descriptor from key_values. Saves to modulestore if save is true.
208
        """
209 210 211
        for key, value in key_values.iteritems():
            setattr(descriptor, key, value)

212
        if save and len(key_values):
213
            modulestore().update_item(descriptor, user.id)
214

215
        return cls.fetch(descriptor)