diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 4100c2dd47a0..bbffe1ff8196 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -211,6 +211,7 @@ def _preview_module_system(request, descriptor, field_data): track_function=lambda event_type, event: None, get_module=partial(_load_preview_module, request), mixins=settings.XBLOCK_MIXINS, + course_id=course_id, # Set up functions to modify the fragment produced by student_view wrappers=wrappers, diff --git a/cms/djangoapps/contentstore/views/tests/test_preview.py b/cms/djangoapps/contentstore/views/tests/test_preview.py index a46f35cef5f3..66e934911f06 100644 --- a/cms/djangoapps/contentstore/views/tests/test_preview.py +++ b/cms/djangoapps/contentstore/views/tests/test_preview.py @@ -296,7 +296,7 @@ def test_cache(self): def test_replace_urls(self): html = '' assert self.runtime.replace_urls(html) == \ - static_replace.replace_static_urls(html, course_id=self.course.id) + static_replace.replace_static_urls(html, course_id=self.runtime.course_id) def test_anonymous_user_id_preview(self): assert self.runtime.anonymous_student_id == 'student' diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 04015787c6f1..bd64896cb792 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -7,12 +7,10 @@ import logging import textwrap from collections import OrderedDict - from functools import partial from completion.waffle import ENABLE_COMPLETION_TRACKING_SWITCH from completion.models import BlockCompletion -from completion.services import CompletionService from django.conf import settings from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.core.cache import cache @@ -24,7 +22,7 @@ from django.utils.text import slugify from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.csrf import csrf_exempt -from edx_django_utils.cache import DEFAULT_REQUEST_CACHE, RequestCache +from edx_django_utils.cache import RequestCache from edx_django_utils.monitoring import set_custom_attributes_for_course_key, set_monitoring_transaction_name from edx_proctoring.api import get_attempt_status_summary from edx_proctoring.services import ProctoringService @@ -41,18 +39,12 @@ from xblock.reference.plugins import FSService from xblock.runtime import KvsFieldData -from lms.djangoapps.badges.service import BadgingService -from lms.djangoapps.badges.utils import badges_enabled -from lms.djangoapps.teams.services import TeamsService -from openedx.core.lib.xblock_services.call_to_action import CallToActionService from xmodule.contentstore.django import contentstore from xmodule.exceptions import NotFoundError, ProcessingError -from xmodule.library_tools import LibraryToolsService -from xmodule.modulestore.django import ModuleI18nService, modulestore +from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError -from xmodule.partitions.partitions_service import PartitionService from xmodule.util.sandboxing import SandboxService -from xmodule.services import RebindUserService, SettingsService, TeamsConfigurationService +from xmodule.services import RebindUserService from common.djangoapps.static_replace.services import ReplaceURLService from common.djangoapps.static_replace.wrapper import replace_urls_wrapper from common.djangoapps.xblock_django.constants import ATTR_KEY_USER_ID @@ -71,7 +63,7 @@ from lms.djangoapps.grades.api import GradesUtilService from lms.djangoapps.grades.api import signals as grades_signals from lms.djangoapps.lms_xblock.field_data import LmsFieldData -from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem, UserTagsService +from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem from lms.djangoapps.verify_student.services import XBlockVerificationService from openedx.core.djangoapps.bookmarks.services import BookmarksService from openedx.core.djangoapps.crawlers.models import CrawlersConfig @@ -686,12 +678,12 @@ def handle_deprecated_progress_event(block, event): field_data = DateLookupFieldData(descriptor._field_data, course_id, user) # pylint: disable=protected-access field_data = LmsFieldData(field_data, student_data) - store = modulestore() - system = LmsModuleSystem( track_function=track_function, get_module=inner_get_module, + user=user, publish=publish, + course_id=course_id, # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington) mixins=descriptor.runtime.mixologist._mixins, # pylint: disable=protected-access wrappers=block_wrappers, @@ -714,18 +706,6 @@ def handle_deprecated_progress_event(block, event): 'xqueue': xqueue_service, 'replace_urls': replace_url_service, 'rebind_user': rebind_user_service, - 'completion': CompletionService(user=user, context_key=course_id) - if user and user.is_authenticated - else None, - 'i18n': ModuleI18nService, - 'library_tools': LibraryToolsService(store, user_id=user.id if user else None), - 'partitions': PartitionService(course_id=course_id, cache=DEFAULT_REQUEST_CACHE.data), - 'settings': SettingsService(), - 'user_tags': UserTagsService(user=user, course_id=course_id), - 'badging': BadgingService(course_id=course_id, modulestore=store) if badges_enabled() else None, - 'teams': TeamsService(), - 'teams_configuration': TeamsConfigurationService(), - 'call_to_action': CallToActionService(), }, descriptor_runtime=descriptor._runtime, # pylint: disable=protected-access request_token=request_token, diff --git a/lms/djangoapps/courseware/student_field_overrides.py b/lms/djangoapps/courseware/student_field_overrides.py index 2d5ad21fb985..4eceebf9fc77 100644 --- a/lms/djangoapps/courseware/student_field_overrides.py +++ b/lms/djangoapps/courseware/student_field_overrides.py @@ -57,7 +57,7 @@ def _get_overrides_for_user(user, block): location = block.location query = StudentFieldOverride.objects.filter( - course_id=block.scope_ids.usage_id.context_key, + course_id=block.runtime.course_id, location=location, student_id=user.id, ) @@ -76,7 +76,7 @@ def override_field_for_user(user, block, name, value): value to set for the given field. """ override, _ = StudentFieldOverride.objects.get_or_create( - course_id=block.scope_ids.usage_id.context_key, + course_id=block.runtime.course_id, location=block.location, student_id=user.id, field=name) @@ -94,7 +94,7 @@ def clear_override_for_user(user, block, name): """ try: StudentFieldOverride.objects.get( - course_id=block.scope_ids.usage_id.context_key, + course_id=block.runtime.course_id, student_id=user.id, location=block.location, field=name).delete() diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 9e187fd1ebe2..15b33844790d 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -34,7 +34,6 @@ from web_fragments.fragment import Fragment # lint-amnesty, pylint: disable=wrong-import-order from xblock.completable import CompletableXBlockMixin # lint-amnesty, pylint: disable=wrong-import-order from xblock.core import XBlock, XBlockAside # lint-amnesty, pylint: disable=wrong-import-order -from xblock.exceptions import NoSuchServiceError from xblock.field_data import FieldData # lint-amnesty, pylint: disable=wrong-import-order from xblock.fields import ScopeIds # lint-amnesty, pylint: disable=wrong-import-order from xblock.runtime import DictKeyValueStore, KvsFieldData, Runtime # lint-amnesty, pylint: disable=wrong-import-order @@ -47,7 +46,7 @@ from xmodule.html_module import AboutBlock, CourseInfoBlock, HtmlBlock, StaticTabBlock from xmodule.lti_module import LTIBlock from xmodule.modulestore import ModuleStoreEnum -from xmodule.modulestore.django import ModuleI18nService, modulestore +from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ( TEST_DATA_MONGO_AMNESTY_MODULESTORE, ModuleStoreTestCase, @@ -65,8 +64,6 @@ from common.djangoapps.student.tests.factories import RequestFactoryNoCsrf from common.djangoapps.student.tests.factories import UserFactory from common.djangoapps.xblock_django.constants import ATTR_KEY_ANONYMOUS_USER_ID -from lms.djangoapps.badges.tests.factories import BadgeClassFactory -from lms.djangoapps.badges.tests.test_models import get_image from lms.djangoapps.courseware import module_render as render from lms.djangoapps.courseware.access_response import AccessResponse from lms.djangoapps.courseware.courses import get_course_info_section, get_course_with_access @@ -94,34 +91,11 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -@XBlock.needs('fs') -@XBlock.needs('field-data') -@XBlock.needs('mako') -@XBlock.needs('user') -@XBlock.needs('verification') -@XBlock.needs('proctoring') -@XBlock.needs('milestones') -@XBlock.needs('credit') -@XBlock.needs('bookmarks') -@XBlock.needs('gating') -@XBlock.needs('grade_utils') -@XBlock.needs('user_state') -@XBlock.needs('content_type_gating') -@XBlock.needs('cache') -@XBlock.needs('sandbox') -@XBlock.needs('xqueue') -@XBlock.needs('replace_urls') -@XBlock.needs('rebind_user') -@XBlock.needs('completion') -@XBlock.needs('i18n') -@XBlock.needs('library_tools') -@XBlock.needs('partitions') -@XBlock.needs('settings') -@XBlock.needs('user_tags') -@XBlock.needs('badging') -@XBlock.needs('teams') -@XBlock.needs('teams_configuration') -@XBlock.needs('call_to_action') +@XBlock.needs("field-data") +@XBlock.needs("i18n") +@XBlock.needs("fs") +@XBlock.needs("user") +@XBlock.needs("bookmarks") class PureXBlock(XBlock): """ Pure XBlock to use in tests. @@ -2258,25 +2232,12 @@ def test_event_publishing(self, mock_track_function): mock_track_function.return_value.assert_called_once_with(event_type, event) -class LMSXBlockServiceMixin(SharedModuleStoreTestCase): +@ddt.ddt +class LMSXBlockServiceBindingTest(SharedModuleStoreTestCase): """ - Helper class that initializes the LmsModuleSystem. + Tests that the LMS Module System (XBlock Runtime) provides an expected set of services. """ - def _prepare_runtime(self): - """ - Instantiate the LmsModuleSystem. - """ - self.runtime, _ = render.get_module_system_for_user( - self.user, - self.student_data, - self.descriptor, - self.course.id, - self.track_function, - self.request_token, - course=self.course - ) - @XBlock.register_temp_plugin(PureXBlock, identifier='pure') def setUp(self): """ Set up the user and other fields that will be used to instantiate the runtime. @@ -2287,168 +2248,46 @@ def setUp(self): self.student_data = Mock() self.track_function = Mock() self.request_token = Mock() - self.descriptor = ItemFactory(category="pure", parent=self.course) - self._prepare_runtime() - - -@ddt.ddt -class LMSXBlockServiceBindingTest(LMSXBlockServiceMixin): - """ - Tests that the LMS Module System (XBlock Runtime) provides an expected set of services. - """ - @ddt.data( - 'fs', - 'field-data', - 'mako', - 'user', - 'verification', - 'proctoring', - 'milestones', - 'credit', - 'bookmarks', - 'gating', - 'grade_utils', - 'user_state', - 'content_type_gating', - 'cache', - 'sandbox', - 'xqueue', - 'replace_urls', - 'rebind_user', - 'completion', - 'i18n', - 'library_tools', - 'partitions', - 'settings', - 'user_tags', - 'teams', - 'teams_configuration', - 'call_to_action', - ) + @XBlock.register_temp_plugin(PureXBlock, identifier='pure') + @ddt.data("user", "i18n", "fs", "field-data", "bookmarks") def test_expected_services_exist(self, expected_service): """ Tests that the 'user', 'i18n', and 'fs' services are provided by the LMS runtime. """ - service = self.runtime.service(self.descriptor, expected_service) + descriptor = ItemFactory(category="pure", parent=self.course) + runtime, _ = render.get_module_system_for_user( + self.user, + self.student_data, + descriptor, + self.course.id, + self.track_function, + self.request_token, + course=self.course + ) + service = runtime.service(descriptor, expected_service) assert service is not None + @XBlock.register_temp_plugin(PureXBlock, identifier='pure') def test_beta_tester_fields_added(self): """ Tests that the beta tester fields are set on LMS runtime. """ - self.descriptor.days_early_for_beta = 5 - self._prepare_runtime() + descriptor = ItemFactory(category="pure", parent=self.course) + descriptor.days_early_for_beta = 5 + runtime, _ = render.get_module_system_for_user( + self.user, + self.student_data, + descriptor, + self.course.id, + self.track_function, + self.request_token, + course=self.course + ) # pylint: disable=no-member - assert not self.runtime.user_is_beta_tester - assert self.runtime.days_early_for_beta == 5 - - def test_get_set_tag(self): - """ - Tests the user service interface. - """ - scope = 'course' - key = 'key1' - - # test for when we haven't set the tag yet - tag = self.runtime.service(self.descriptor, 'user_tags').get_tag(scope, key) - assert tag is None - - # set the tag - set_value = 'value' - self.runtime.service(self.descriptor, 'user_tags').set_tag(scope, key, set_value) - tag = self.runtime.service(self.descriptor, 'user_tags').get_tag(scope, key) - - assert tag == set_value - - # Try to set tag in wrong scope - with pytest.raises(ValueError): - self.runtime.service(self.descriptor, 'user_tags').set_tag('fake_scope', key, set_value) - - # Try to get tag in wrong scope - with pytest.raises(ValueError): - self.runtime.service(self.descriptor, 'user_tags').get_tag('fake_scope', key) - - -@ddt.ddt -class TestBadgingService(LMSXBlockServiceMixin): - """Test the badging service interface""" - - @patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True}) - def test_service_rendered(self): - self._prepare_runtime() - assert self.runtime.service(self.descriptor, 'badging') - - def test_no_service_rendered(self): - with pytest.raises(NoSuchServiceError): - self.runtime.service(self.descriptor, 'badging') - - @ddt.data(True, False) - @patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True}) - def test_course_badges_toggle(self, toggle): - self.course = CourseFactory.create(metadata={'issue_badges': toggle}) - self._prepare_runtime() - assert self.runtime.service(self.descriptor, 'badging').course_badges_enabled is toggle - - @patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True}) - def test_get_badge_class(self): - self._prepare_runtime() - badge_service = self.runtime.service(self.descriptor, 'badging') - premade_badge_class = BadgeClassFactory.create() - # Ignore additional parameters. This class already exists. - # We should get back the first class we created, rather than a new one. - with get_image('good') as image_handle: - badge_class = badge_service.get_badge_class( - slug='test_slug', issuing_component='test_component', description='Attempted override', - criteria='test', display_name='Testola', image_file_handle=image_handle - ) - # These defaults are set on the factory. - assert badge_class.criteria == 'https://example.com/syllabus' - assert badge_class.display_name == 'Test Badge' - assert badge_class.description == "Yay! It's a test badge." - # File name won't always be the same. - assert badge_class.image.path == premade_badge_class.image.path - - -class TestI18nService(LMSXBlockServiceMixin): - """ Test ModuleI18nService """ - - def test_module_i18n_lms_service(self): - """ - Test: module i18n service in LMS - """ - i18n_service = self.runtime.service(self.descriptor, 'i18n') - assert i18n_service is not None - assert isinstance(i18n_service, ModuleI18nService) - - def test_no_service_exception_with_none_declaration_(self): - """ - Test: NoSuchServiceError should be raised block declaration returns none - """ - self.descriptor.service_declaration = Mock(return_value=None) - with pytest.raises(NoSuchServiceError): - self.runtime.service(self.descriptor, 'i18n') - - def test_no_service_exception_(self): - """ - Test: NoSuchServiceError should be raised if i18n service is none. - """ - self.runtime._services['i18n'] = None # pylint: disable=protected-access - with pytest.raises(NoSuchServiceError): - self.runtime.service(self.descriptor, 'i18n') - - def test_i18n_service_callable(self): - """ - Test: _services dict should contain the callable i18n service in LMS. - """ - assert callable(self.runtime._services.get('i18n')) # pylint: disable=protected-access - - def test_i18n_service_not_callable(self): - """ - Test: i18n service should not be callable in LMS after initialization. - """ - assert not callable(self.runtime.service(self.descriptor, 'i18n')) + assert not runtime.user_is_beta_tester + assert runtime.days_early_for_beta == 5 class PureXBlockWithChildren(PureXBlock): @@ -2867,22 +2706,15 @@ def test_cache(self): def test_replace_urls(self): html = '' assert self.runtime.replace_urls(html) == \ - static_replace.replace_static_urls(html, course_id=self.course.id) + static_replace.replace_static_urls(html, course_id=self.runtime.course_id) def test_replace_course_urls(self): html = '' assert self.runtime.replace_course_urls(html) == \ - static_replace.replace_course_urls(html, course_key=self.course.id) + static_replace.replace_course_urls(html, course_key=self.runtime.course_id) def test_replace_jump_to_id_urls(self): html = '' - jump_to_id_base_url = reverse('jump_to_id', kwargs={'course_id': str(self.course.id), 'module_id': ''}) + jump_to_id_base_url = reverse('jump_to_id', kwargs={'course_id': str(self.runtime.course_id), 'module_id': ''}) assert self.runtime.replace_jump_to_id_urls(html) == \ - static_replace.replace_jump_to_id_urls(html, self.course.id, jump_to_id_base_url) - - @XBlock.register_temp_plugin(PureXBlockWithChildren, identifier='xblock') - def test_course_id(self): - descriptor = ItemFactory(category="pure", parent=self.course) - - block = render.get_module(self.user, Mock(), descriptor.location, None) - assert str(block.runtime.course_id) == self.COURSE_ID + static_replace.replace_jump_to_id_urls(html, self.runtime.course_id, jump_to_id_base_url) diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index bb4feb852d9e..c84bf675b794 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -1424,6 +1424,7 @@ def setUp(self): self.initialize_block(data=sample_xml) self.video = self.item_descriptor self.video.runtime.handler_url = Mock(return_value=self.transcript_url) + self.video.runtime.course_id = MagicMock() def setup_val_video(self, associate_course_in_val=False): """ @@ -1526,6 +1527,7 @@ def test_no_edx_video_id_and_no_fallback(self): self.initialize_block(data=sample_xml) self.video = self.item_descriptor self.video.runtime.handler_url = Mock(return_value=self.transcript_url) + self.video.runtime.course_id = MagicMock() result = self.get_result() self.verify_result_with_youtube_url(result) @@ -1593,6 +1595,7 @@ class VideoBlockTest(TestCase, VideoBlockTestBase): def setUp(self): super().setUp() self.descriptor.runtime.handler_url = MagicMock() + self.descriptor.runtime.course_id = MagicMock() self.temp_dir = mkdtemp() file_system = OSFS(self.temp_dir) self.file_system = file_system.makedir(EXPORT_IMPORT_COURSE_DIR, recreate=True) diff --git a/lms/djangoapps/edxnotes/decorators.py b/lms/djangoapps/edxnotes/decorators.py index 247bb6b913f4..c641bd230c66 100644 --- a/lms/djangoapps/edxnotes/decorators.py +++ b/lms/djangoapps/edxnotes/decorators.py @@ -33,8 +33,8 @@ def get_html(self, *args, **kwargs): if not hasattr(runtime, 'modulestore'): return original_get_html(self, *args, **kwargs) - is_studio = getattr(self.runtime, "is_author_mode", False) - course = getattr(self, 'descriptor', self).runtime.modulestore.get_course(self.scope_ids.usage_id.context_key) + is_studio = getattr(self.system, "is_author_mode", False) + course = getattr(self, 'descriptor', self).runtime.modulestore.get_course(self.runtime.course_id) # Must be disabled when: # - in Studio @@ -57,10 +57,10 @@ def get_html(self, *args, **kwargs): ), "params": { # Use camelCase to name keys. - "usageId": self.scope_ids.usage_id, - "courseId": course.id, + "usageId": str(self.scope_ids.usage_id), + "courseId": str(self.runtime.course_id), "token": get_edxnotes_id_token(user), - "tokenUrl": get_token_url(course.id), + "tokenUrl": get_token_url(self.runtime.course_id), "endpoint": get_public_endpoint(), "debug": settings.DEBUG, "eventStringLimit": settings.TRACK_MAX_EVENT / 6, diff --git a/lms/djangoapps/edxnotes/tests.py b/lms/djangoapps/edxnotes/tests.py index b89b4a0afe13..a67ba7e0fa2e 100644 --- a/lms/djangoapps/edxnotes/tests.py +++ b/lms/djangoapps/edxnotes/tests.py @@ -79,10 +79,11 @@ class TestProblem: The purpose of this class is to imitate any problem. """ def __init__(self, course, user=None): - self.scope_ids = MagicMock(usage_id=course.id.make_usage_key('test_problem', 'test_usage_id')) + self.system = MagicMock(is_author_mode=False) + self.scope_ids = MagicMock(usage_id="test_usage_id") user = user or UserFactory() user_service = StubUserService(user) - self.runtime = MagicMock(service=lambda _a, _b: user_service, is_author_mode=False) + self.runtime = MagicMock(course_id=course.id, service=lambda _a, _b: user_service) self.descriptor = MagicMock() self.descriptor.runtime.modulestore.get_course.return_value = course @@ -135,7 +136,7 @@ def test_edxnotes_enabled(self, mock_generate_uid, mock_get_id_token, mock_get_t "uid": "uid", "edxnotes_visibility": "true", "params": { - "usageId": problem.scope_ids.usage_id, + "usageId": "test_usage_id", "courseId": course.id, "token": "token", "tokenUrl": "/tokenUrl", @@ -166,7 +167,7 @@ def test_edxnotes_studio(self): """ Tests that get_html is not wrapped when problem is rendered in Studio. """ - self.problem.runtime.is_author_mode = True + self.problem.system.is_author_mode = True assert 'original_get_html' == self.problem.get_html() def test_edxnotes_blockstore_runtime(self): diff --git a/lms/djangoapps/instructor/tasks.py b/lms/djangoapps/instructor/tasks.py index b28f5387f87c..eedfe07fee1d 100644 --- a/lms/djangoapps/instructor/tasks.py +++ b/lms/djangoapps/instructor/tasks.py @@ -69,10 +69,10 @@ def update_exam_completion_task(user_identifier: str, content_id: str, completio # Now evil modulestore magic to inflate our descriptor with user state and # permissions checks. field_data_cache = FieldDataCache.cache_for_descriptor_descendents( - root_descriptor.scope_ids.usage_id.context_key, user, root_descriptor, read_only=True, + root_descriptor.course_id, user, root_descriptor, read_only=True, ) root_module = get_module_for_descriptor( - user, request, root_descriptor, field_data_cache, root_descriptor.scope_ids.usage_id.context_key, + user, request, root_descriptor, field_data_cache, root_descriptor.course_id, ) if not root_module: err_msg = err_msg_prefix + 'Module unable to be created from descriptor!' diff --git a/lms/djangoapps/lms_xblock/runtime.py b/lms/djangoapps/lms_xblock/runtime.py index f79f75da0879..859d01c1568e 100644 --- a/lms/djangoapps/lms_xblock/runtime.py +++ b/lms/djangoapps/lms_xblock/runtime.py @@ -2,13 +2,25 @@ Module implementing `xblock.runtime.Runtime` functionality for the LMS """ + +import xblock.reference.plugins +from completion.services import CompletionService from django.conf import settings from django.urls import reverse +from edx_django_utils.cache import DEFAULT_REQUEST_CACHE +from lms.djangoapps.badges.service import BadgingService +from lms.djangoapps.badges.utils import badges_enabled from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig +from lms.djangoapps.teams.services import TeamsService from openedx.core.djangoapps.user_api.course_tag import api as user_course_tag_api from openedx.core.lib.url_utils import quote_slashes +from openedx.core.lib.xblock_services.call_to_action import CallToActionService from openedx.core.lib.xblock_utils import wrap_xblock_aside, xblock_local_resource_url +from xmodule.library_tools import LibraryToolsService # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.django import ModuleI18nService, modulestore # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.partitions.partitions_service import PartitionService # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.services import SettingsService, TeamsConfigurationService # lint-amnesty, pylint: disable=wrong-import-order from xmodule.x_module import ModuleSystem # lint-amnesty, pylint: disable=wrong-import-order @@ -39,7 +51,7 @@ def handler_url(block, handler_name, suffix='', query='', thirdparty=False): view_name = 'xblock_handler_noauth' url = reverse(view_name, kwargs={ - 'course_id': str(block.scope_ids.usage_id.context_key), + 'course_id': str(block.location.course_key), 'usage_id': quote_slashes(str(block.scope_ids.usage_id)), 'handler': handler_name, 'suffix': suffix, @@ -120,8 +132,32 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method """ ModuleSystem specialized to the LMS """ - def __init__(self, **kwargs): + def __init__(self, user, **kwargs): + request_cache_dict = DEFAULT_REQUEST_CACHE.data + store = modulestore() + course_id = kwargs.get('course_id') + + services = kwargs.setdefault('services', {}) + if user and user.is_authenticated: + services['completion'] = CompletionService(user=user, context_key=course_id) + services['fs'] = xblock.reference.plugins.FSService() + services['i18n'] = ModuleI18nService + services['library_tools'] = LibraryToolsService(store, user_id=user.id if user else None) + services['partitions'] = PartitionService( + course_id=course_id, + cache=request_cache_dict + ) + services['settings'] = SettingsService() + services['user_tags'] = UserTagsService( + user=user, + course_id=course_id, + ) + if badges_enabled(): + services['badging'] = BadgingService(course_id=course_id, modulestore=store) self.request_token = kwargs.pop('request_token', None) + services['teams'] = TeamsService() + services['teams_configuration'] = TeamsConfigurationService() + services['call_to_action'] = CallToActionService() super().__init__(**kwargs) def handler_url(self, *args, **kwargs): # lint-amnesty, pylint: disable=signature-differs diff --git a/lms/djangoapps/lms_xblock/test/test_runtime.py b/lms/djangoapps/lms_xblock/test/test_runtime.py index 08a7ff47a02c..f90722a746ec 100644 --- a/lms/djangoapps/lms_xblock/test/test_runtime.py +++ b/lms/djangoapps/lms_xblock/test/test_runtime.py @@ -3,27 +3,29 @@ """ -from unittest.mock import Mock +from unittest.mock import Mock, patch from urllib.parse import urlparse +import pytest +from ddt import data, ddt from django.conf import settings from django.test import TestCase +from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locations import BlockUsageLocator, CourseLocator +from xblock.exceptions import NoSuchServiceError from xblock.fields import ScopeIds +from common.djangoapps.student.tests.factories import UserFactory +from lms.djangoapps.badges.tests.factories import BadgeClassFactory +from lms.djangoapps.badges.tests.test_models import get_image from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem +from xmodule.modulestore.django import ModuleI18nService # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order class BlockMock(Mock): """Mock class that we fill with our "handler" methods.""" - scope_ids = ScopeIds( - None, - None, - None, - BlockUsageLocator( - CourseLocator(org="mockx", course="100", run="2015"), block_type='mock_type', block_id="mock_id" - ), - ) def handler(self, _context): """ @@ -43,16 +45,25 @@ def handler_a(self, _context): """ pass # lint-amnesty, pylint: disable=unnecessary-pass + @property + def location(self): + """Create a functional BlockUsageLocator for testing URL generation.""" + course_key = CourseLocator(org="mockx", course="100", run="2015") + return BlockUsageLocator(course_key, block_type='mock_type', block_id="mock_id") + class TestHandlerUrl(TestCase): """Test the LMS handler_url""" def setUp(self): super().setUp() - self.block = BlockMock(name='block') + self.block = BlockMock(name='block', scope_ids=ScopeIds(None, None, None, 'dummy')) + self.course_key = CourseLocator("org", "course", "run") self.runtime = LmsModuleSystem( track_function=Mock(), get_module=Mock(), + course_id=self.course_key, + user=Mock(), descriptor_runtime=Mock(), ) @@ -102,3 +113,161 @@ def test_not_thirdparty_rel(self): parsed_fq_url = urlparse(self.runtime.handler_url(self.block, 'handler', thirdparty=False)) assert parsed_fq_url.scheme == '' assert parsed_fq_url.hostname is None + + +class TestUserServiceAPI(TestCase): + """Test the user service interface""" + + def setUp(self): + super().setUp() + self.course_id = CourseLocator("org", "course", "run") + self.user = UserFactory.create() + + self.runtime = LmsModuleSystem( + track_function=Mock(), + get_module=Mock(), + user=self.user, + course_id=self.course_id, + descriptor_runtime=Mock(), + ) + self.scope = 'course' + self.key = 'key1' + + self.mock_block = Mock() + self.mock_block.service_declaration.return_value = 'needs' + + def test_get_set_tag(self): + # test for when we haven't set the tag yet + tag = self.runtime.service(self.mock_block, 'user_tags').get_tag(self.scope, self.key) + assert tag is None + + # set the tag + set_value = 'value' + self.runtime.service(self.mock_block, 'user_tags').set_tag(self.scope, self.key, set_value) + tag = self.runtime.service(self.mock_block, 'user_tags').get_tag(self.scope, self.key) + + assert tag == set_value + + # Try to set tag in wrong scope + with pytest.raises(ValueError): + self.runtime.service(self.mock_block, 'user_tags').set_tag('fake_scope', self.key, set_value) + + # Try to get tag in wrong scope + with pytest.raises(ValueError): + self.runtime.service(self.mock_block, 'user_tags').get_tag('fake_scope', self.key) + + +@ddt +class TestBadgingService(ModuleStoreTestCase): + """Test the badging service interface""" + + def setUp(self): + super().setUp() + self.course_id = CourseKey.from_string('course-v1:org+course+run') + + self.mock_block = Mock() + self.mock_block.service_declaration.return_value = 'needs' + + def create_runtime(self): + """ + Create the testing runtime. + """ + return LmsModuleSystem( + track_function=Mock(), + get_module=Mock(), + course_id=self.course_id, + user=self.user, + descriptor_runtime=Mock(), + ) + + @patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True}) + def test_service_rendered(self): + runtime = self.create_runtime() + assert runtime.service(self.mock_block, 'badging') + + @patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': False}) + def test_no_service_rendered(self): + runtime = self.create_runtime() + assert not runtime.service(self.mock_block, 'badging') + + @data(True, False) + @patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True}) + def test_course_badges_toggle(self, toggle): + self.course_id = CourseFactory.create(metadata={'issue_badges': toggle}).location.course_key + runtime = self.create_runtime() + assert runtime.service(self.mock_block, 'badging').course_badges_enabled is toggle + + @patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True}) + def test_get_badge_class(self): + runtime = self.create_runtime() + badge_service = runtime.service(self.mock_block, 'badging') + premade_badge_class = BadgeClassFactory.create() + # Ignore additional parameters. This class already exists. + # We should get back the first class we created, rather than a new one. + with get_image('good') as image_handle: + badge_class = badge_service.get_badge_class( + slug='test_slug', issuing_component='test_component', description='Attempted override', + criteria='test', display_name='Testola', image_file_handle=image_handle + ) + # These defaults are set on the factory. + assert badge_class.criteria == 'https://example.com/syllabus' + assert badge_class.display_name == 'Test Badge' + assert badge_class.description == "Yay! It's a test badge." + # File name won't always be the same. + assert badge_class.image.path == premade_badge_class.image.path + + +class TestI18nService(ModuleStoreTestCase): + """ Test ModuleI18nService """ + + def setUp(self): + """ Setting up tests """ + super().setUp() + self.course = CourseFactory.create() + self.test_language = 'dummy language' + self.runtime = LmsModuleSystem( + track_function=Mock(), + get_module=Mock(), + course_id=self.course.id, + user=Mock(), + descriptor_runtime=Mock(), + ) + + self.mock_block = Mock() + self.mock_block.service_declaration.return_value = 'need' + + def test_module_i18n_lms_service(self): + """ + Test: module i18n service in LMS + """ + i18n_service = self.runtime.service(self.mock_block, 'i18n') + assert i18n_service is not None + assert isinstance(i18n_service, ModuleI18nService) + + def test_no_service_exception_with_none_declaration_(self): + """ + Test: NoSuchServiceError should be raised block declaration returns none + """ + self.mock_block.service_declaration.return_value = None + with pytest.raises(NoSuchServiceError): + self.runtime.service(self.mock_block, 'i18n') + + def test_no_service_exception_(self): + """ + Test: NoSuchServiceError should be raised if i18n service is none. + """ + self.runtime._services['i18n'] = None # pylint: disable=protected-access + with pytest.raises(NoSuchServiceError): + self.runtime.service(self.mock_block, 'i18n') + + def test_i18n_service_callable(self): + """ + Test: _services dict should contain the callable i18n service in LMS. + """ + assert callable(self.runtime._services.get('i18n')) # pylint: disable=protected-access + + def test_i18n_service_not_callable(self): + """ + Test: i18n service should not be callable in LMS after initialization. + """ + assert not callable(self.runtime.service(self.mock_block, 'i18n')) diff --git a/openedx/features/course_duration_limits/access.py b/openedx/features/course_duration_limits/access.py index ff817a315054..cee69dd4f3e9 100644 --- a/openedx/features/course_duration_limits/access.py +++ b/openedx/features/course_duration_limits/access.py @@ -249,7 +249,7 @@ def course_expiration_wrapper(user, block, view, frag, context): # pylint: disa return frag course_expiration_fragment = generate_course_expired_fragment_from_key( - user, block.scope_ids.usage_id.context_key + user, block.course_id ) if not course_expiration_fragment: return frag diff --git a/xmodule/capa_module.py b/xmodule/capa_module.py index 71be84139302..4e1822dd28e3 100644 --- a/xmodule/capa_module.py +++ b/xmodule/capa_module.py @@ -36,7 +36,7 @@ from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.graders import ShowCorrectness from xmodule.raw_module import RawMixin -from xmodule.util.sandboxing import SandboxService +from xmodule.util.sandboxing import get_python_lib_zip from xmodule.util.xmodule_django import add_webpack_to_fragment from xmodule.x_module import ( HTMLSnippet, @@ -684,9 +684,7 @@ def generate_report_data(self, user_state_iterator, limit_responses=None): anonymous_student_id=None, cache=None, can_execute_unsafe_code=lambda: None, - get_python_lib_zip=( - lambda: SandboxService(contentstore, self.scope_ids.usage_id.context_key).get_python_lib_zip() - ), + get_python_lib_zip=(lambda: get_python_lib_zip(contentstore, self.runtime.course_id)), DEBUG=None, i18n=self.runtime.service(self, "i18n"), render_template=None, diff --git a/xmodule/discussion_block.py b/xmodule/discussion_block.py index 3e0d4ed254f0..c7eb09d2a278 100644 --- a/xmodule/discussion_block.py +++ b/xmodule/discussion_block.py @@ -73,6 +73,13 @@ class DiscussionXBlock(XBlock, StudioEditableXBlockMixin, XmlParserMixin): # li @property def course_key(self): + """ + :return: int course id + + NB: The goal is to move this XBlock out of edx-platform, and so we use + scope_ids.usage_id instead of runtime.course_id so that the code will + continue to work with workbench-based testing. + """ return getattr(self.scope_ids.usage_id, 'course_key', None) @property diff --git a/xmodule/seq_module.py b/xmodule/seq_module.py index a0b11ee793a6..f8c4ce95b898 100644 --- a/xmodule/seq_module.py +++ b/xmodule/seq_module.py @@ -213,7 +213,7 @@ def _get_course(self): """ Return course by course id. """ - return self.runtime.modulestore.get_course(self.scope_ids.usage_id.context_key) # pylint: disable=no-member + return self.runtime.modulestore.get_course(self.course_id) # pylint: disable=no-member @property def is_timed_exam(self): @@ -451,7 +451,7 @@ def gate_entire_sequence_if_it_is_a_timed_exam_and_contains_content_type_gated_p content_type_gating_service = self.runtime.service(self, 'content_type_gating') if content_type_gating_service: self.gated_sequence_paywall = content_type_gating_service.check_children_for_content_type_gating_paywall( - self, self.scope_ids.usage_id.context_key + self, self.course_id ) def student_view(self, context): @@ -614,7 +614,7 @@ def _student_or_public_view(self, context, prereq_met, prereq_meta_info, banner_ if SHOW_PROGRESS_BAR.is_enabled() and getattr(settings, 'COMPLETION_AGGREGATOR_URL', ''): parent_block_id = self.get_parent().scope_ids.usage_id.block_id params['chapter_completion_aggregator_url'] = '/'.join( - [settings.COMPLETION_AGGREGATOR_URL, str(self.scope_ids.usage_id.context_key), parent_block_id]) + '/' + [settings.COMPLETION_AGGREGATOR_URL, str(self.course_id), parent_block_id]) + '/' fragment.add_content(self.runtime.service(self, 'mako').render_template("seq_module.html", params)) self._capture_full_seq_item_metrics(display_items) @@ -655,7 +655,7 @@ def _is_gate_fulfilled(self): if gating_service: user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_ID) fulfilled = gating_service.is_gate_fulfilled( - self.scope_ids.usage_id.context_key, self.location, user_id + self.course_id, self.location, user_id ) return fulfilled @@ -671,7 +671,7 @@ def _required_prereq(self): gating_service = self.runtime.service(self, 'gating') if gating_service: milestone = gating_service.required_prereq( - self.scope_ids.usage_id.context_key, self.location, 'requires' + self.course_id, self.location, 'requires' ) return milestone @@ -810,7 +810,7 @@ def _render_student_view_for_items(self, context, display_items, fragment, view= contains_content_type_gated_content = False if content_type_gating_service: contains_content_type_gated_content = content_type_gating_service.check_children_for_content_type_gating_paywall( # pylint:disable=line-too-long - item, self.scope_ids.usage_id.context_key + item, self.course_id ) is not None iteminfo = { 'content': content, @@ -931,7 +931,7 @@ def _time_limited_student_view(self): user_id = current_user.opt_attrs.get(ATTR_KEY_USER_ID) user_is_staff = current_user.opt_attrs.get(ATTR_KEY_USER_IS_STAFF) user_role_in_course = 'staff' if user_is_staff else 'student' - course_id = self.scope_ids.usage_id.context_key + course_id = self.runtime.course_id content_id = self.location context = { diff --git a/xmodule/tests/__init__.py b/xmodule/tests/__init__.py index 4554d2e19f0b..c5808ab5a32d 100644 --- a/xmodule/tests/__init__.py +++ b/xmodule/tests/__init__.py @@ -50,6 +50,19 @@ class TestModuleSystem(ModuleSystem): # pylint: disable=abstract-method """ ModuleSystem for testing """ + def __init__(self, **kwargs): + course_id = kwargs['course_id'] + id_manager = CourseLocationManager(course_id) + kwargs.setdefault('id_reader', id_manager) + kwargs.setdefault('id_generator', id_manager) + + services = kwargs.get('services', {}) + services.setdefault('cache', CacheService(DoNothingCache())) + services.setdefault('field-data', DictFieldData({})) + services.setdefault('sandbox', SandboxService(contentstore, course_id)) + kwargs['services'] = services + super().__init__(**kwargs) + def handler_url(self, block, handler, suffix='', query='', thirdparty=False): # lint-amnesty, pylint: disable=arguments-differ return '{usage_id}/{handler}{suffix}?{query}'.format( usage_id=str(block.scope_ids.usage_id), @@ -119,8 +132,6 @@ def get_test_system( descriptor_system = get_test_descriptor_system() - id_manager = CourseLocationManager(course_id) - def get_module(descriptor): """Mocks module_system get_module function""" @@ -151,14 +162,10 @@ def get_module(descriptor): waittime=10, construct_callback=Mock(name='get_test_system.xqueue.construct_callback', side_effect="/"), ), - 'replace_urls': replace_url_service, - 'cache': CacheService(DoNothingCache()), - 'field-data': DictFieldData({}), - 'sandbox': SandboxService(contentstore, course_id), + 'replace_urls': replace_url_service }, + course_id=course_id, descriptor_runtime=descriptor_system, - id_reader=id_manager, - id_generator=id_manager, ) diff --git a/xmodule/tests/test_lti_unit.py b/xmodule/tests/test_lti_unit.py index 6ef8cb846e9e..2ca2a6492c19 100644 --- a/xmodule/tests/test_lti_unit.py +++ b/xmodule/tests/test_lti_unit.py @@ -12,7 +12,6 @@ from django.conf import settings from django.test import TestCase, override_settings from lxml import etree -from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import BlockUsageLocator from pytz import UTC from webob.request import Request @@ -62,15 +61,14 @@ def setUp(self): """) - self.course_id = CourseKey.from_string('org/course/run') - self.system = get_test_system(self.course_id) + self.system = get_test_system() self.system.publish = Mock() self.system._services['rebind_user'] = Mock() # pylint: disable=protected-access self.xmodule = LTIBlock( self.system, DictFieldData({}), - ScopeIds(None, None, None, BlockUsageLocator(self.course_id, 'lti', 'name')) + ScopeIds(None, None, None, BlockUsageLocator(self.system.course_id, 'lti', 'name')) ) current_user = self.system.service(self.xmodule, 'user').get_current_user() self.user_id = current_user.opt_attrs.get(ATTR_KEY_ANONYMOUS_USER_ID) @@ -321,7 +319,7 @@ def test_resource_link_id(self): def test_lis_result_sourcedid(self): expected_sourced_id = ':'.join(parse.quote(i) for i in ( - str(self.course_id), + str(self.system.course_id), self.xmodule.get_resource_link_id(), self.user_id )) @@ -541,4 +539,4 @@ def test_context_id(self): """ Tests that LTI parameter context_id is equal to course_id. """ - assert str(self.course_id) == self.xmodule.context_id + assert str(self.system.course_id) == self.xmodule.context_id diff --git a/xmodule/tests/test_poll.py b/xmodule/tests/test_poll.py index ea0f6049db62..b7a58bda94bf 100644 --- a/xmodule/tests/test_poll.py +++ b/xmodule/tests/test_poll.py @@ -5,7 +5,6 @@ from unittest.mock import Mock -from opaque_keys.edx.keys import CourseKey from xblock.field_data import DictFieldData from xblock.fields import ScopeIds from xmodule.poll_module import PollBlock @@ -25,9 +24,8 @@ class PollBlockTest(unittest.TestCase): def setUp(self): super().setUp() - course_key = CourseKey.from_string('org/course/run') - self.system = get_test_system(course_key) - usage_key = course_key.make_usage_key(PollBlock.category, 'test_loc') + self.system = get_test_system() + usage_key = self.system.course_id.make_usage_key(PollBlock.category, 'test_loc') # ScopeIds has 4 fields: user_id, block_type, def_id, usage_id scope_ids = ScopeIds(1, PollBlock.category, usage_key, usage_key) self.xmodule = PollBlock( diff --git a/xmodule/tests/test_video.py b/xmodule/tests/test_video.py index 79e2522ce230..d82b43ecaff1 100644 --- a/xmodule/tests/test_video.py +++ b/xmodule/tests/test_video.py @@ -701,6 +701,7 @@ def test_export_to_xml(self, mock_val_api): self.descriptor.download_video = True self.descriptor.transcripts = {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'} self.descriptor.edx_video_id = edx_video_id + self.descriptor.runtime.course_id = MagicMock() xml = self.descriptor.definition_to_xml(self.file_system) parser = etree.XMLParser(remove_blank_text=True) @@ -730,7 +731,7 @@ def test_export_to_xml(self, mock_val_api): video_id=edx_video_id, static_dir=EXPORT_IMPORT_STATIC_DIR, resource_fs=self.file_system, - course_id=self.descriptor.scope_ids.usage_id.context_key, + course_id=str(self.descriptor.runtime.course_id.for_branch(None)), ) @patch('xmodule.video_module.video_module.edxval_api') @@ -739,6 +740,7 @@ def test_export_to_xml_val_error(self, mock_val_api): mock_val_api.ValVideoNotFoundError = _MockValVideoNotFoundError mock_val_api.export_to_xml = Mock(side_effect=mock_val_api.ValVideoNotFoundError) self.descriptor.edx_video_id = 'test_edx_video_id' + self.descriptor.runtime.course_id = MagicMock() xml = self.descriptor.definition_to_xml(self.file_system) parser = etree.XMLParser(remove_blank_text=True) @@ -859,6 +861,7 @@ def test_student_view_data(self, field_data, expected_student_view_data): Ensure that student_view_data returns the expected results for video modules. """ descriptor = instantiate_descriptor(**field_data) + descriptor.runtime.course_id = MagicMock() student_view_data = descriptor.student_view_data() assert student_view_data == expected_student_view_data @@ -893,6 +896,7 @@ def test_student_view_data_with_hls_flag(self, mock_get_video_info, mock_get_vid } descriptor = instantiate_descriptor(edx_video_id='example_id', only_on_web=False) + descriptor.runtime.course_id = MagicMock() descriptor.runtime.handler_url = MagicMock() student_view_data = descriptor.student_view_data() expected_video_data = {'hls': {'url': 'http://www.meowmix.com', 'file_size': 25556}} diff --git a/xmodule/video_module/video_module.py b/xmodule/video_module/video_module.py index a7aad184ff98..4527907102c4 100644 --- a/xmodule/video_module/video_module.py +++ b/xmodule/video_module/video_module.py @@ -371,7 +371,7 @@ def get_html(self, view=STUDENT_VIEW): # lint-amnesty, pylint: disable=argument poster = None if edxval_api and self.edx_video_id: poster = edxval_api.get_course_video_image_url( - course_id=self.scope_ids.usage_id.context_key.for_branch(None), + course_id=self.runtime.course_id.for_branch(None), edx_video_id=self.edx_video_id.strip() ) @@ -741,11 +741,12 @@ def definition_to_xml(self, resource_fs): # lint-amnesty, pylint: disable=too-m # (i.e. `self.transcripts`) on import and older open-releases (<= ginkgo), # who do not have deprecated contentstore yet, can also import and use new-style # transcripts into their openedX instances. + exported_metadata = edxval_api.export_to_xml( video_id=edx_video_id, resource_fs=resource_fs, static_dir=EXPORT_IMPORT_STATIC_DIR, - course_id=self.scope_ids.usage_id.context_key.for_branch(None), + course_id=str(self.runtime.course_id.for_branch(None)) ) # Update xml with edxval metadata xml.append(exported_metadata['xml']) @@ -831,7 +832,7 @@ def get_youtube_link(video_id): if self.edx_video_id and edxval_api: val_profiles = ['youtube', 'desktop_webm', 'desktop_mp4'] - if HLSPlaybackEnabledFlag.feature_enabled(self.scope_ids.usage_id.context_key.for_branch(None)): + if HLSPlaybackEnabledFlag.feature_enabled(self.runtime.course_id.for_branch(None)): val_profiles.append('hls') # Get video encodings for val profiles. diff --git a/xmodule/x_module.py b/xmodule/x_module.py index 7859bc8370bc..c617d78613a8 100644 --- a/xmodule/x_module.py +++ b/xmodule/x_module.py @@ -1073,11 +1073,25 @@ class MetricsMixin: def render(self, block, view_name, context=None): # lint-amnesty, pylint: disable=missing-function-docstring start_time = time.time() + status = "success" try: return super().render(block, view_name, context=context) + except: + status = "failure" + raise + finally: end_time = time.time() duration = end_time - start_time + course_id = getattr(self, 'course_id', '') + tags = [ # lint-amnesty, pylint: disable=unused-variable + f'view_name:{view_name}', + 'action:render', + f'action_status:{status}', + f'course_id:{course_id}', + f'block_type:{block.scope_ids.block_type}', + f'block_family:{block.entry_point}', + ] log.debug( "%.3fs - render %s.%s (%s)", duration, @@ -1088,11 +1102,25 @@ def render(self, block, view_name, context=None): # lint-amnesty, pylint: disab def handle(self, block, handler_name, request, suffix=''): # lint-amnesty, pylint: disable=missing-function-docstring start_time = time.time() + status = "success" try: return super().handle(block, handler_name, request, suffix=suffix) + except: + status = "failure" + raise + finally: end_time = time.time() duration = end_time - start_time + course_id = getattr(self, 'course_id', '') + tags = [ # lint-amnesty, pylint: disable=unused-variable + f'handler_name:{handler_name}', + 'action:handle', + f'action_status:{status}', + f'course_id:{course_id}', + f'block_type:{block.scope_ids.block_type}', + f'block_family:{block.entry_point}', + ] log.debug( "%.3fs - handle %s.%s (%s)", duration, @@ -1690,19 +1718,6 @@ def STATIC_URL(self): # pylint: disable=invalid-name ) return settings.STATIC_URL - @property - def course_id(self): - """ - Old API to get the course ID. - - Deprecated in favor of `runtime.scope_ids.usage_id.context_key`. - """ - warnings.warn( - "`runtime.course_id` is deprecated. Use `context_key` instead: `runtime.scope_ids.usage_id.context_key`.", - DeprecationWarning, stacklevel=3, - ) - return self.descriptor_runtime.course_id.for_branch(None) - class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, ModuleSystemShim, Runtime): """ @@ -1723,6 +1738,7 @@ def __init__( get_module, descriptor_runtime, publish=None, + course_id=None, **kwargs, ): """ @@ -1739,6 +1755,8 @@ def __init__( descriptor_runtime - A `DescriptorSystem` to use for loading xblocks by id + course_id - the course_id containing this module + publish(event) - A function that allows XModules to publish events (such as grade changes) """ @@ -1748,6 +1766,7 @@ def __init__( self.track_function = track_function self.get_module = get_module + self.course_id = course_id if publish: self.publish = publish