222 lines
7.3 KiB
Python
222 lines
7.3 KiB
Python
"""
|
|
API related to providing field overrides for individual students. This is used
|
|
by the individual custom courses feature.
|
|
"""
|
|
|
|
|
|
import json
|
|
import logging
|
|
|
|
from ccx_keys.locator import CCXBlockUsageLocator, CCXLocator
|
|
from django.db import transaction
|
|
from opaque_keys.edx.keys import CourseKey, UsageKey
|
|
|
|
from lms.djangoapps.ccx.models import CcxFieldOverride, CustomCourseForEdX
|
|
from lms.djangoapps.courseware.field_overrides import FieldOverrideProvider
|
|
from openedx.core.lib.cache_utils import get_cache
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
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):
|
|
"""
|
|
Just call the get_override_for_ccx method if there is a ccx
|
|
"""
|
|
# 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
|
|
ccx = course_key = None
|
|
identifier = getattr(block, 'id', None)
|
|
if isinstance(identifier, CourseKey):
|
|
course_key = block.id
|
|
elif isinstance(identifier, UsageKey):
|
|
course_key = block.id.course_key
|
|
elif hasattr(block, 'location'):
|
|
course_key = block.location.course_key
|
|
else:
|
|
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)
|
|
if ccx:
|
|
return get_override_for_ccx(ccx, block, name, default)
|
|
return default
|
|
|
|
@classmethod
|
|
def enabled_for(cls, block): # lint-amnesty, pylint: disable=arguments-differ
|
|
"""
|
|
CCX field overrides are enabled for CCX blocks.
|
|
"""
|
|
return getattr(block.location, 'ccx', None) or getattr(block, 'enable_ccx', False)
|
|
|
|
|
|
def get_current_ccx(course_key):
|
|
"""
|
|
Return the ccx that is active for this course.
|
|
|
|
course_key is expected to be an instance of an opaque CourseKey, a
|
|
ValueError is raised if this expectation is not met.
|
|
"""
|
|
if not isinstance(course_key, CourseKey):
|
|
raise ValueError("get_current_ccx requires a CourseKey instance")
|
|
|
|
if not isinstance(course_key, CCXLocator):
|
|
return None
|
|
|
|
ccx_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]
|
|
|
|
|
|
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`.
|
|
"""
|
|
overrides = _get_overrides_for_ccx(ccx)
|
|
|
|
clean_ccx_key = _clean_ccx_key(block.location)
|
|
|
|
block_overrides = overrides.get(clean_ccx_key, {})
|
|
|
|
# Hardcode the course_edit_method to be None instead of 'Studio', so,
|
|
# the LMS never tries to link back to Studio. CCX courses
|
|
# can't be edited in Studio.
|
|
block_overrides['course_edit_method'] = None
|
|
|
|
if name in block_overrides:
|
|
try:
|
|
return block.fields[name].from_json(block_overrides[name])
|
|
except KeyError:
|
|
return block_overrides[name]
|
|
else:
|
|
return default
|
|
|
|
|
|
def _clean_ccx_key(block_location):
|
|
"""
|
|
Converts the given BlockUsageKey from a CCX key to the
|
|
corresponding key for its parent course, while handling the case
|
|
where no conversion is needed. Also strips any version and
|
|
branch information from the key.
|
|
Returns the cleaned key.
|
|
"""
|
|
if isinstance(block_location, CCXBlockUsageLocator):
|
|
clean_key = block_location.to_block_locator()
|
|
else:
|
|
clean_key = block_location
|
|
return clean_key.version_agnostic().for_branch(None)
|
|
|
|
|
|
def _get_overrides_for_ccx(ccx):
|
|
"""
|
|
Returns a dictionary mapping field name to overriden value for any
|
|
overrides set on this block for this CCX.
|
|
"""
|
|
overrides_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)
|
|
block_overrides[override.field + "_id"] = override.id
|
|
block_overrides[override.field + "_instance"] = override
|
|
|
|
overrides_cache[ccx] = overrides
|
|
|
|
return overrides_cache[ccx]
|
|
|
|
|
|
@transaction.atomic
|
|
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.
|
|
"""
|
|
field = block.fields[name]
|
|
value_json = field.to_json(value)
|
|
serialized_value = json.dumps(value_json)
|
|
override_has_changes = False
|
|
clean_ccx_key = _clean_ccx_key(block.location)
|
|
|
|
override = get_override_for_ccx(ccx, block, name + "_instance")
|
|
if override:
|
|
override_has_changes = serialized_value != override.value
|
|
|
|
if not override:
|
|
override, created = CcxFieldOverride.objects.get_or_create(
|
|
ccx=ccx,
|
|
location=block.location,
|
|
field=name,
|
|
defaults={'value': serialized_value},
|
|
)
|
|
if created:
|
|
_get_overrides_for_ccx(ccx).setdefault(clean_ccx_key, {})[name + "_id"] = override.id
|
|
else:
|
|
override_has_changes = serialized_value != override.value
|
|
|
|
if override_has_changes:
|
|
override.value = serialized_value
|
|
override.save()
|
|
|
|
_get_overrides_for_ccx(ccx).setdefault(clean_ccx_key, {})[name] = value_json
|
|
_get_overrides_for_ccx(ccx).setdefault(clean_ccx_key, {})[name + "_instance"] = override
|
|
|
|
|
|
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()
|
|
|
|
clear_ccx_field_info_from_ccx_map(ccx, block, name)
|
|
|
|
except CcxFieldOverride.DoesNotExist:
|
|
pass
|
|
|
|
|
|
def clear_ccx_field_info_from_ccx_map(ccx, block, name):
|
|
"""
|
|
Remove field information from ccx overrides mapping dictionary
|
|
"""
|
|
try:
|
|
clean_ccx_key = _clean_ccx_key(block.location)
|
|
ccx_override_map = _get_overrides_for_ccx(ccx).setdefault(clean_ccx_key, {})
|
|
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 = [ccx_id for ccx_id in ids if ccx_id]
|
|
ids = list(set(ids))
|
|
if ids:
|
|
CcxFieldOverride.objects.filter(ccx=ccx, id__in=ids).delete()
|