diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index bbffe1ff8196..4100c2dd47a0 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -211,7 +211,6 @@ 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 66e934911f06..a46f35cef5f3 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.runtime.course_id) + static_replace.replace_static_urls(html, course_id=self.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 bd64896cb792..04015787c6f1 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -7,10 +7,12 @@ 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 @@ -22,7 +24,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 RequestCache +from edx_django_utils.cache import DEFAULT_REQUEST_CACHE, 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 @@ -39,12 +41,18 @@ 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.modulestore.django import modulestore +from xmodule.library_tools import LibraryToolsService +from xmodule.modulestore.django import ModuleI18nService, 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 +from xmodule.services import RebindUserService, SettingsService, TeamsConfigurationService 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 @@ -63,7 +71,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 +from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem, UserTagsService from lms.djangoapps.verify_student.services import XBlockVerificationService from openedx.core.djangoapps.bookmarks.services import BookmarksService from openedx.core.djangoapps.crawlers.models import CrawlersConfig @@ -678,12 +686,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, @@ -706,6 +714,18 @@ 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 4eceebf9fc77..2d5ad21fb985 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.runtime.course_id, + course_id=block.scope_ids.usage_id.context_key, 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.runtime.course_id, + course_id=block.scope_ids.usage_id.context_key, 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.runtime.course_id, + course_id=block.scope_ids.usage_id.context_key, 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 15b33844790d..9e187fd1ebe2 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -34,6 +34,7 @@ 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 @@ -46,7 +47,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 modulestore +from xmodule.modulestore.django import ModuleI18nService, modulestore from xmodule.modulestore.tests.django_utils import ( TEST_DATA_MONGO_AMNESTY_MODULESTORE, ModuleStoreTestCase, @@ -64,6 +65,8 @@ 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 @@ -91,11 +94,34 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -@XBlock.needs("field-data") -@XBlock.needs("i18n") -@XBlock.needs("fs") -@XBlock.needs("user") -@XBlock.needs("bookmarks") +@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') class PureXBlock(XBlock): """ Pure XBlock to use in tests. @@ -2232,12 +2258,25 @@ def test_event_publishing(self, mock_track_function): mock_track_function.return_value.assert_called_once_with(event_type, event) -@ddt.ddt -class LMSXBlockServiceBindingTest(SharedModuleStoreTestCase): +class LMSXBlockServiceMixin(SharedModuleStoreTestCase): """ - Tests that the LMS Module System (XBlock Runtime) provides an expected set of services. + Helper class that initializes the LmsModuleSystem. """ + 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. @@ -2248,46 +2287,168 @@ 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() - @XBlock.register_temp_plugin(PureXBlock, identifier='pure') - @ddt.data("user", "i18n", "fs", "field-data", "bookmarks") + +@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', + ) def test_expected_services_exist(self, expected_service): """ Tests that the 'user', 'i18n', and 'fs' services are provided by the LMS runtime. """ - 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) + service = self.runtime.service(self.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. """ - 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 - ) + self.descriptor.days_early_for_beta = 5 + self._prepare_runtime() # pylint: disable=no-member - assert not runtime.user_is_beta_tester - assert runtime.days_early_for_beta == 5 + 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')) class PureXBlockWithChildren(PureXBlock): @@ -2706,15 +2867,22 @@ 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.runtime.course_id) + static_replace.replace_static_urls(html, course_id=self.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.runtime.course_id) + static_replace.replace_course_urls(html, course_key=self.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.runtime.course_id), 'module_id': ''}) + jump_to_id_base_url = reverse('jump_to_id', kwargs={'course_id': str(self.course.id), 'module_id': ''}) assert self.runtime.replace_jump_to_id_urls(html) == \ - static_replace.replace_jump_to_id_urls(html, self.runtime.course_id, jump_to_id_base_url) + 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 diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index c84bf675b794..bb4feb852d9e 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -1424,7 +1424,6 @@ 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): """ @@ -1527,7 +1526,6 @@ 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) @@ -1595,7 +1593,6 @@ 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 c641bd230c66..247bb6b913f4 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.system, "is_author_mode", False) - course = getattr(self, 'descriptor', self).runtime.modulestore.get_course(self.runtime.course_id) + 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) # Must be disabled when: # - in Studio @@ -57,10 +57,10 @@ def get_html(self, *args, **kwargs): ), "params": { # Use camelCase to name keys. - "usageId": str(self.scope_ids.usage_id), - "courseId": str(self.runtime.course_id), + "usageId": self.scope_ids.usage_id, + "courseId": course.id, "token": get_edxnotes_id_token(user), - "tokenUrl": get_token_url(self.runtime.course_id), + "tokenUrl": get_token_url(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 a67ba7e0fa2e..b89b4a0afe13 100644 --- a/lms/djangoapps/edxnotes/tests.py +++ b/lms/djangoapps/edxnotes/tests.py @@ -79,11 +79,10 @@ class TestProblem: The purpose of this class is to imitate any problem. """ def __init__(self, course, user=None): - self.system = MagicMock(is_author_mode=False) - self.scope_ids = MagicMock(usage_id="test_usage_id") + self.scope_ids = MagicMock(usage_id=course.id.make_usage_key('test_problem', 'test_usage_id')) user = user or UserFactory() user_service = StubUserService(user) - self.runtime = MagicMock(course_id=course.id, service=lambda _a, _b: user_service) + self.runtime = MagicMock(service=lambda _a, _b: user_service, is_author_mode=False) self.descriptor = MagicMock() self.descriptor.runtime.modulestore.get_course.return_value = course @@ -136,7 +135,7 @@ def test_edxnotes_enabled(self, mock_generate_uid, mock_get_id_token, mock_get_t "uid": "uid", "edxnotes_visibility": "true", "params": { - "usageId": "test_usage_id", + "usageId": problem.scope_ids.usage_id, "courseId": course.id, "token": "token", "tokenUrl": "/tokenUrl", @@ -167,7 +166,7 @@ def test_edxnotes_studio(self): """ Tests that get_html is not wrapped when problem is rendered in Studio. """ - self.problem.system.is_author_mode = True + self.problem.runtime.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 eedfe07fee1d..b28f5387f87c 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.course_id, user, root_descriptor, read_only=True, + root_descriptor.scope_ids.usage_id.context_key, user, root_descriptor, read_only=True, ) root_module = get_module_for_descriptor( - user, request, root_descriptor, field_data_cache, root_descriptor.course_id, + user, request, root_descriptor, field_data_cache, root_descriptor.scope_ids.usage_id.context_key, ) 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 859d01c1568e..f79f75da0879 100644 --- a/lms/djangoapps/lms_xblock/runtime.py +++ b/lms/djangoapps/lms_xblock/runtime.py @@ -2,25 +2,13 @@ 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 @@ -51,7 +39,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.location.course_key), + 'course_id': str(block.scope_ids.usage_id.context_key), 'usage_id': quote_slashes(str(block.scope_ids.usage_id)), 'handler': handler_name, 'suffix': suffix, @@ -132,32 +120,8 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method """ ModuleSystem specialized to the LMS """ - 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) + def __init__(self, **kwargs): 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 f90722a746ec..08a7ff47a02c 100644 --- a/lms/djangoapps/lms_xblock/test/test_runtime.py +++ b/lms/djangoapps/lms_xblock/test/test_runtime.py @@ -3,29 +3,27 @@ """ -from unittest.mock import Mock, patch +from unittest.mock import Mock 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): """ @@ -45,25 +43,16 @@ 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', scope_ids=ScopeIds(None, None, None, 'dummy')) - self.course_key = CourseLocator("org", "course", "run") + self.block = BlockMock(name='block') self.runtime = LmsModuleSystem( track_function=Mock(), get_module=Mock(), - course_id=self.course_key, - user=Mock(), descriptor_runtime=Mock(), ) @@ -113,161 +102,3 @@ 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 cee69dd4f3e9..ff817a315054 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.course_id + user, block.scope_ids.usage_id.context_key ) if not course_expiration_fragment: return frag diff --git a/xmodule/capa_module.py b/xmodule/capa_module.py index 4e1822dd28e3..71be84139302 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 get_python_lib_zip +from xmodule.util.sandboxing import SandboxService from xmodule.util.xmodule_django import add_webpack_to_fragment from xmodule.x_module import ( HTMLSnippet, @@ -684,7 +684,9 @@ 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: get_python_lib_zip(contentstore, self.runtime.course_id)), + get_python_lib_zip=( + lambda: SandboxService(contentstore, self.scope_ids.usage_id.context_key).get_python_lib_zip() + ), DEBUG=None, i18n=self.runtime.service(self, "i18n"), render_template=None, diff --git a/xmodule/discussion_block.py b/xmodule/discussion_block.py index c7eb09d2a278..3e0d4ed254f0 100644 --- a/xmodule/discussion_block.py +++ b/xmodule/discussion_block.py @@ -73,13 +73,6 @@ 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 f8c4ce95b898..a0b11ee793a6 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.course_id) # pylint: disable=no-member + return self.runtime.modulestore.get_course(self.scope_ids.usage_id.context_key) # 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.course_id + self, self.scope_ids.usage_id.context_key ) 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.course_id), parent_block_id]) + '/' + [settings.COMPLETION_AGGREGATOR_URL, str(self.scope_ids.usage_id.context_key), 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.course_id, self.location, user_id + self.scope_ids.usage_id.context_key, 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.course_id, self.location, 'requires' + self.scope_ids.usage_id.context_key, 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.course_id + item, self.scope_ids.usage_id.context_key ) 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.runtime.course_id + course_id = self.scope_ids.usage_id.context_key content_id = self.location context = { diff --git a/xmodule/tests/__init__.py b/xmodule/tests/__init__.py index c5808ab5a32d..4554d2e19f0b 100644 --- a/xmodule/tests/__init__.py +++ b/xmodule/tests/__init__.py @@ -50,19 +50,6 @@ 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), @@ -132,6 +119,8 @@ 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""" @@ -162,10 +151,14 @@ def get_module(descriptor): waittime=10, construct_callback=Mock(name='get_test_system.xqueue.construct_callback', side_effect="/"), ), - 'replace_urls': replace_url_service + 'replace_urls': replace_url_service, + 'cache': CacheService(DoNothingCache()), + 'field-data': DictFieldData({}), + 'sandbox': SandboxService(contentstore, course_id), }, - 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 2ca2a6492c19..6ef8cb846e9e 100644 --- a/xmodule/tests/test_lti_unit.py +++ b/xmodule/tests/test_lti_unit.py @@ -12,6 +12,7 @@ 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 @@ -61,14 +62,15 @@ def setUp(self): """) - self.system = get_test_system() + self.course_id = CourseKey.from_string('org/course/run') + self.system = get_test_system(self.course_id) 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.system.course_id, 'lti', 'name')) + ScopeIds(None, None, None, BlockUsageLocator(self.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) @@ -319,7 +321,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.system.course_id), + str(self.course_id), self.xmodule.get_resource_link_id(), self.user_id )) @@ -539,4 +541,4 @@ def test_context_id(self): """ Tests that LTI parameter context_id is equal to course_id. """ - assert str(self.system.course_id) == self.xmodule.context_id + assert str(self.course_id) == self.xmodule.context_id diff --git a/xmodule/tests/test_poll.py b/xmodule/tests/test_poll.py index b7a58bda94bf..ea0f6049db62 100644 --- a/xmodule/tests/test_poll.py +++ b/xmodule/tests/test_poll.py @@ -5,6 +5,7 @@ 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 @@ -24,8 +25,9 @@ class PollBlockTest(unittest.TestCase): def setUp(self): super().setUp() - self.system = get_test_system() - usage_key = self.system.course_id.make_usage_key(PollBlock.category, 'test_loc') + 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') # 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 d82b43ecaff1..79e2522ce230 100644 --- a/xmodule/tests/test_video.py +++ b/xmodule/tests/test_video.py @@ -701,7 +701,6 @@ 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) @@ -731,7 +730,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=str(self.descriptor.runtime.course_id.for_branch(None)), + course_id=self.descriptor.scope_ids.usage_id.context_key, ) @patch('xmodule.video_module.video_module.edxval_api') @@ -740,7 +739,6 @@ 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) @@ -861,7 +859,6 @@ 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 @@ -896,7 +893,6 @@ 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 4527907102c4..a7aad184ff98 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.runtime.course_id.for_branch(None), + course_id=self.scope_ids.usage_id.context_key.for_branch(None), edx_video_id=self.edx_video_id.strip() ) @@ -741,12 +741,11 @@ 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=str(self.runtime.course_id.for_branch(None)) + course_id=self.scope_ids.usage_id.context_key.for_branch(None), ) # Update xml with edxval metadata xml.append(exported_metadata['xml']) @@ -832,7 +831,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.runtime.course_id.for_branch(None)): + if HLSPlaybackEnabledFlag.feature_enabled(self.scope_ids.usage_id.context_key.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 c617d78613a8..7859bc8370bc 100644 --- a/xmodule/x_module.py +++ b/xmodule/x_module.py @@ -1073,25 +1073,11 @@ 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, @@ -1102,25 +1088,11 @@ 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, @@ -1718,6 +1690,19 @@ 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): """ @@ -1738,7 +1723,6 @@ def __init__( get_module, descriptor_runtime, publish=None, - course_id=None, **kwargs, ): """ @@ -1755,8 +1739,6 @@ 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) """ @@ -1766,7 +1748,6 @@ def __init__( self.track_function = track_function self.get_module = get_module - self.course_id = course_id if publish: self.publish = publish