overrides.py 6.5 KB
Newer Older
cewing committed
1 2
"""
API related to providing field overrides for individual students.  This is used
3
by the individual custom courses feature.
cewing committed
4 5
"""
import json
cewing committed
6
import logging
cewing committed
7

8 9
from django.db import transaction, IntegrityError

10 11
import request_cache

12
from courseware.field_overrides import FieldOverrideProvider
cewing committed
13 14 15
from opaque_keys.edx.keys import CourseKey, UsageKey
from ccx_keys.locator import CCXLocator, CCXBlockUsageLocator

16
from lms.djangoapps.ccx.models import CcxFieldOverride, CustomCourseForEdX
cewing committed
17

cewing committed
18

cewing committed
19
log = logging.getLogger(__name__)
cewing committed
20 21 22 23 24 25 26 27 28


class CustomCoursesForEdxOverrideProvider(FieldOverrideProvider):
    """
    A concrete implementation of
    :class:`~courseware.field_overrides.FieldOverrideProvider` which allows for
    overrides to be made on a per user basis.
    """
    def get(self, block, name, default):
29 30 31
        """
        Just call the get_override_for_ccx method if there is a ccx
        """
cewing committed
32 33 34
        # The incoming block might be a CourseKey instance of some type, a
        # UsageKey instance of some type, or it might be something that has a
        # location attribute.  That location attribute will be a UsageKey
35
        ccx = course_key = None
cewing committed
36 37
        identifier = getattr(block, 'id', None)
        if isinstance(identifier, CourseKey):
38
            course_key = block.id
cewing committed
39
        elif isinstance(identifier, UsageKey):
40
            course_key = block.id.course_key
cewing committed
41
        elif hasattr(block, 'location'):
42
            course_key = block.location.course_key
cewing committed
43
        else:
44 45 46 47
            msg = "Unable to get course id when calculating ccx overide for block type %r"
            log.error(msg, type(block))
        if course_key is not None:
            ccx = get_current_ccx(course_key)
cewing committed
48 49 50 51
        if ccx:
            return get_override_for_ccx(ccx, block, name, default)
        return default

52 53 54 55 56 57 58 59
    @classmethod
    def enabled_for(cls, course):
        """CCX field overrides are enabled per-course

        protect against missing attributes
        """
        return getattr(course, 'enable_ccx', False)

cewing committed
60

61
def get_current_ccx(course_key):
cewing committed
62
    """
cewing committed
63
    Return the ccx that is active for this course.
64 65 66

    course_key is expected to be an instance of an opaque CourseKey, a
    ValueError is raised if this expectation is not met.
cewing committed
67
    """
68 69 70 71 72 73
    if not isinstance(course_key, CourseKey):
        raise ValueError("get_current_ccx requires a CourseKey instance")

    if not isinstance(course_key, CCXLocator):
        return None

74 75 76 77 78
    ccx_cache = request_cache.get_cache('ccx')
    if course_key not in ccx_cache:
        ccx_cache[course_key] = CustomCourseForEdX.objects.get(pk=course_key.ccx)

    return ccx_cache[course_key]
cewing committed
79 80 81 82 83 84 85 86


def get_override_for_ccx(ccx, block, name, default=None):
    """
    Gets the value of the overridden field for the `ccx`.  `block` and `name`
    specify the block and the name of the field.  If the field is not
    overridden for the given ccx, returns `default`.
    """
87 88 89 90 91 92 93 94 95
    overrides = _get_overrides_for_ccx(ccx)

    if isinstance(block.location, CCXBlockUsageLocator):
        non_ccx_key = block.location.to_block_locator()
    else:
        non_ccx_key = block.location

    block_overrides = overrides.get(non_ccx_key, {})
    if name in block_overrides:
96 97 98 99
        try:
            return block.fields[name].from_json(block_overrides[name])
        except KeyError:
            return block_overrides[name]
100 101
    else:
        return default
cewing committed
102 103


104
def _get_overrides_for_ccx(ccx):
cewing committed
105 106 107 108
    """
    Returns a dictionary mapping field name to overriden value for any
    overrides set on this block for this CCX.
    """
109 110 111 112 113 114 115 116 117 118 119
    overrides_cache = request_cache.get_cache('ccx-overrides')

    if ccx not in overrides_cache:
        overrides = {}
        query = CcxFieldOverride.objects.filter(
            ccx=ccx,
        )

        for override in query:
            block_overrides = overrides.setdefault(override.location, {})
            block_overrides[override.field] = json.loads(override.value)
120 121
            block_overrides[override.field + "_id"] = override.id
            block_overrides[override.field + "_instance"] = override
122 123 124 125

        overrides_cache[ccx] = overrides

    return overrides_cache[ccx]
cewing committed
126 127


128
@transaction.atomic
cewing committed
129 130 131 132 133 134
def override_field_for_ccx(ccx, block, name, value):
    """
    Overrides a field for the `ccx`.  `block` and `name` specify the block
    and the name of the field on that block to override.  `value` is the
    value to set for the given field.
    """
135
    field = block.fields[name]
136 137
    value_json = field.to_json(value)
    serialized_value = json.dumps(value_json)
138 139 140 141 142 143 144
    override_has_changes = False

    override = get_override_for_ccx(ccx, block, name + "_instance")
    if override:
        override_has_changes = serialized_value != override.value

    if not override:
145 146 147 148 149 150 151
        override, created = CcxFieldOverride.objects.get_or_create(
            ccx=ccx,
            location=block.location,
            field=name,
            defaults={'value': serialized_value},
        )
        if created:
152
            _get_overrides_for_ccx(ccx).setdefault(block.location, {})[name + "_id"] = override.id
153
        else:
154 155 156
            override_has_changes = serialized_value != override.value

    if override_has_changes:
157
        override.value = serialized_value
158 159
        override.save()

160
    _get_overrides_for_ccx(ccx).setdefault(block.location, {})[name] = value_json
161
    _get_overrides_for_ccx(ccx).setdefault(block.location, {})[name + "_instance"] = override
cewing committed
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176


def clear_override_for_ccx(ccx, block, name):
    """
    Clears a previously set field override for the `ccx`.  `block` and `name`
    specify the block and the name of the field on that block to clear.
    This function is idempotent--if no override is set, nothing action is
    performed.
    """
    try:
        CcxFieldOverride.objects.get(
            ccx=ccx,
            location=block.location,
            field=name).delete()

177
        clear_ccx_field_info_from_ccx_map(ccx, block, name)
cewing committed
178 179 180

    except CcxFieldOverride.DoesNotExist:
        pass
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203


def clear_ccx_field_info_from_ccx_map(ccx, block, name):  # pylint: disable=invalid-name
    """
    Remove field information from ccx overrides mapping dictionary
    """
    try:
        ccx_override_map = _get_overrides_for_ccx(ccx).setdefault(block.location, {})
        ccx_override_map.pop(name)
        ccx_override_map.pop(name + "_id")
        ccx_override_map.pop(name + "_instance")
    except KeyError:
        pass


def bulk_delete_ccx_override_fields(ccx, ids):
    """
    Bulk delete for CcxFieldOverride model
    """
    ids = filter(None, ids)
    ids = list(set(ids))
    if ids:
        CcxFieldOverride.objects.filter(ccx=ccx, id__in=ids).delete()