225 lines
8.8 KiB
Python
225 lines
8.8 KiB
Python
"""
|
|
Module contains various XModule/XBlock services
|
|
"""
|
|
|
|
|
|
import inspect
|
|
import logging
|
|
|
|
from functools import partial
|
|
|
|
from config_models.models import ConfigurationModel
|
|
from django.conf import settings
|
|
from edx_when.field_data import DateLookupFieldData
|
|
from xblock.reference.plugins import Service
|
|
from xblock.runtime import KvsFieldData
|
|
|
|
from xmodule.modulestore.django import modulestore
|
|
|
|
from lms.djangoapps.courseware.field_overrides import OverrideFieldData
|
|
from lms.djangoapps.courseware.model_data import DjangoKeyValueStore, FieldDataCache
|
|
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
|
|
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class SettingsService:
|
|
"""
|
|
Allows server-wide configuration of XBlocks on a per-type basis
|
|
|
|
XBlock settings are read from XBLOCK_SETTINGS settings key. Each XBlock is allowed access
|
|
to single settings bucket. Bucket is determined by this service using the following rules:
|
|
|
|
* Value of SettingsService.xblock_settings_bucket_selector is examined. If XBlock have attribute/property
|
|
with the name of that value this attribute/property is read to get the bucket key (e.g. if XBlock have
|
|
`block_settings_key = 'my_block_settings'`, bucket key would be 'my_block_settings').
|
|
* Otherwise, XBlock class name is used
|
|
|
|
Service is content-agnostic: it just returns whatever happen to be in the settings bucket (technically, it returns
|
|
the bucket itself).
|
|
|
|
If `default` argument is specified it is returned if:
|
|
* There are no XBLOCK_SETTINGS setting
|
|
* XBLOCK_SETTINGS is empty
|
|
* XBLOCK_SETTINGS does not contain settings bucket
|
|
|
|
If `default` is not specified or None, empty dictionary is used for default.
|
|
|
|
Example:
|
|
|
|
"XBLOCK_SETTINGS": {
|
|
"my_block": {
|
|
"setting1": 1,
|
|
"setting2": []
|
|
},
|
|
"my_other_block": [1, 2, 3],
|
|
"MyThirdBlock": "QWERTY"
|
|
}
|
|
|
|
class MyBlock: block_settings_key='my_block'
|
|
class MyOtherBlock: block_settings_key='my_other_block'
|
|
class MyThirdBlock: pass
|
|
class MissingBlock: pass
|
|
|
|
service = SettingsService()
|
|
service.get_settings_bucket(MyBlock()) # { "setting1": 1, "setting2": [] }
|
|
service.get_settings_bucket(MyOtherBlock()) # [1, 2, 3]
|
|
service.get_settings_bucket(MyThirdBlock()) # "QWERTY"
|
|
service.get_settings_bucket(MissingBlock()) # {}
|
|
service.get_settings_bucket(MissingBlock(), "default") # "default"
|
|
service.get_settings_bucket(MissingBlock(), None) # {}
|
|
"""
|
|
xblock_settings_bucket_selector = 'block_settings_key'
|
|
|
|
def get_settings_bucket(self, block, default=None):
|
|
""" Gets xblock settings dictionary from settings. """
|
|
if not block:
|
|
raise ValueError(f"Expected XBlock instance, got {block} of type {type(block)}")
|
|
|
|
actual_default = default if default is not None else {}
|
|
xblock_settings_bucket = getattr(block, self.xblock_settings_bucket_selector, block.unmixed_class.__name__)
|
|
xblock_settings = settings.XBLOCK_SETTINGS if hasattr(settings, "XBLOCK_SETTINGS") else {}
|
|
return xblock_settings.get(xblock_settings_bucket, actual_default)
|
|
|
|
|
|
# TODO: ConfigurationService and its usage will be removed as a part of EDUCATOR-121
|
|
# reference: https://openedx.atlassian.net/browse/EDUCATOR-121
|
|
class ConfigurationService:
|
|
"""
|
|
An XBlock service to talk with the Configuration Models. This service should provide
|
|
a pathway to Configuration Model which is designed to configure the corresponding XBlock.
|
|
"""
|
|
def __init__(self, configuration_model):
|
|
"""
|
|
Class initializer, this exposes configuration model to XBlock.
|
|
|
|
Arguments:
|
|
configuration_model (ConfigurationModel): configurations for an XBlock
|
|
|
|
Raises:
|
|
exception (ValueError): when configuration_model is not a subclass of
|
|
ConfigurationModel.
|
|
"""
|
|
if not (inspect.isclass(configuration_model) and issubclass(configuration_model, ConfigurationModel)):
|
|
raise ValueError(
|
|
"Expected ConfigurationModel got {} of type {}".format(
|
|
configuration_model,
|
|
type(configuration_model)
|
|
)
|
|
)
|
|
|
|
self.configuration = configuration_model
|
|
|
|
|
|
class TeamsConfigurationService:
|
|
"""
|
|
An XBlock service that returns the teams_configuration object for a course.
|
|
"""
|
|
def __init__(self):
|
|
self._course = None
|
|
|
|
def get_course(self, course_id):
|
|
"""
|
|
Return the course instance associated with this TeamsConfigurationService.
|
|
This default implementation looks up the course from the modulestore.
|
|
"""
|
|
return modulestore().get_course(course_id)
|
|
|
|
def get_teams_configuration(self, course_id):
|
|
"""
|
|
Returns the team configuration for a given course.id
|
|
"""
|
|
if not self._course:
|
|
self._course = self.get_course(course_id)
|
|
return self._course.teams_configuration
|
|
|
|
|
|
class RebindUserServiceError(Exception):
|
|
pass
|
|
|
|
|
|
class RebindUserService(Service):
|
|
"""
|
|
An XBlock Service that allows modules to get rebound to real users if it was previously bound to an AnonymousUser.
|
|
|
|
This used to be a local function inside the `lms.djangoapps.courseware.module_render.get_module_system_for_user`
|
|
method, and was passed as a constructor argument to x_module.ModuleSystem. This has been refactored out into a
|
|
service to simplify the ModuleSystem and lives in this module temporarily.
|
|
|
|
TODO: Only the old LTI XBlock uses it in 2 places for LTI 2.0 integration. As the LTI XBlock is deprecated in
|
|
favour of the LTI Consumer XBlock, this should be removed when the LTI XBlock is removed.
|
|
|
|
Arguments:
|
|
user (User) - A Django User object
|
|
course_id (str) - Course ID
|
|
course (Course) - Course Object
|
|
get_module_system_for_user (function) - The helper function that will be called to create a module system
|
|
for a specfic user. This is the parent function from which this service was reactored out.
|
|
`lms.djangoapps.courseware.module_render.get_module_system_for_user`
|
|
kwargs (dict) - all the keyword arguments that need to be passed to the `get_module_system_for_user`
|
|
function when it is called during rebinding
|
|
"""
|
|
def __init__(self, user, course_id, get_module_system_for_user, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.user = user
|
|
self.course_id = course_id
|
|
self._ref = {
|
|
"get_module_system_for_user": get_module_system_for_user
|
|
}
|
|
self._kwargs = kwargs
|
|
|
|
def rebind_noauth_module_to_user(self, block, real_user):
|
|
"""
|
|
Function that rebinds the module to the real_user.
|
|
|
|
Will only work within a module bound to an AnonymousUser, e.g. one that's instantiated by the noauth_handler.
|
|
|
|
Arguments:
|
|
block (any xblock type): the module to rebind
|
|
real_user (django.contrib.auth.models.User): the user to bind to
|
|
|
|
Returns:
|
|
nothing (but the side effect is that module is re-bound to real_user)
|
|
"""
|
|
if self.user.is_authenticated:
|
|
err_msg = "rebind_noauth_module_to_user can only be called from a module bound to an anonymous user"
|
|
log.error(err_msg)
|
|
raise RebindUserServiceError(err_msg)
|
|
|
|
field_data_cache_real_user = FieldDataCache.cache_for_descriptor_descendents(
|
|
self.course_id,
|
|
real_user,
|
|
block,
|
|
asides=XBlockAsidesConfig.possible_asides(),
|
|
)
|
|
student_data_real_user = KvsFieldData(DjangoKeyValueStore(field_data_cache_real_user))
|
|
|
|
with modulestore().bulk_operations(self.course_id):
|
|
course = modulestore().get_course(course_key=self.course_id)
|
|
|
|
(inner_system, inner_student_data) = self._ref["get_module_system_for_user"](
|
|
user=real_user,
|
|
student_data=student_data_real_user, # These have implicit user bindings, rest of args considered not to
|
|
descriptor=block,
|
|
course_id=self.course_id,
|
|
course=course,
|
|
**self._kwargs
|
|
)
|
|
|
|
block.bind_for_student(
|
|
inner_system,
|
|
real_user.id,
|
|
[
|
|
partial(DateLookupFieldData, course_id=self.course_id, user=self.user),
|
|
partial(OverrideFieldData.wrap, real_user, course),
|
|
partial(LmsFieldData, student_data=inner_student_data),
|
|
],
|
|
)
|
|
|
|
block.scope_ids = block.scope_ids._replace(user_id=real_user.id)
|
|
# now bind the module to the new ModuleSystem instance and vice-versa
|
|
block.runtime = inner_system
|
|
inner_system.xmodule_instance = block
|