Files
edx-platform/xmodule/services.py
2022-06-20 18:20:06 +05:00

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