Merge pull request #30715 from open-craft/agrendalath/bd-13-deprecate_course_id

refactor: deprecate course_id from ModuleSystem [BD-13]
This commit is contained in:
Piotr Surowiec
2022-09-26 14:23:11 +02:00
committed by GitHub
21 changed files with 308 additions and 362 deletions

View File

@@ -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,

View File

@@ -296,7 +296,7 @@ class CmsModuleSystemShimTest(ModuleStoreTestCase):
def test_replace_urls(self):
html = '<a href="/static/id">'
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'

View File

@@ -7,10 +7,12 @@ import json
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.urls import reverse
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.exceptions import NoSuchHandlerError, NoSuchViewError
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.courseware.services import UserStateService
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 get_module_system_for_user(
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 get_module_system_for_user(
'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,

View File

@@ -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()

View File

@@ -34,6 +34,7 @@ from pyquery import PyQuery # lint-amnesty, pylint: disable=wrong-import-order
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.contentstore.django import contentstore
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 GlobalStaffFactory
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 @@ from common.djangoapps.xblock_django.models import XBlockConfiguration
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 @@ class TestEventPublishing(ModuleStoreTestCase, LoginEnrollmentTestCase):
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 @@ class LMSXBlockServiceBindingTest(SharedModuleStoreTestCase):
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 @@ class LmsModuleSystemShimTest(SharedModuleStoreTestCase):
def test_replace_urls(self):
html = '<a href="/static/id">'
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 = '<a href="/course/id">'
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 = '<a href="/jump_to_id/id">'
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

View File

@@ -1424,7 +1424,6 @@ class TestVideoBlockStudentViewJson(BaseTestVideoXBlock, CacheIsolationTestCase)
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 @@ class TestVideoBlockStudentViewJson(BaseTestVideoXBlock, CacheIsolationTestCase)
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)

View File

@@ -33,8 +33,8 @@ def edxnotes(cls):
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 edxnotes(cls):
),
"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,

View File

@@ -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 @@ class EdxNotesDecoratorTest(ModuleStoreTestCase):
"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 @@ class EdxNotesDecoratorTest(ModuleStoreTestCase):
"""
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):

View File

@@ -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!'

View File

@@ -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

View File

@@ -3,29 +3,27 @@ Tests of the LMS XBlock Runtime and associated utilities
"""
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 @@ class BlockMock(Mock):
"""
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 @@ class TestHandlerUrl(TestCase):
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'))

View File

@@ -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

View File

@@ -36,7 +36,7 @@ from xmodule.editing_module import EditingMixin
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 @@ class ProblemBlock(
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,

View File

@@ -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

View File

@@ -213,7 +213,7 @@ class ProctoringFields:
"""
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 @@ class SequenceBlock(
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 @@ class SequenceBlock(
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 @@ class SequenceBlock(
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 @@ class SequenceBlock(
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 @@ class SequenceBlock(
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 @@ class SequenceBlock(
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 = {

View File

@@ -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_test_system(
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,
)

View File

@@ -12,6 +12,7 @@ import pytest
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 @@ class LTIBlockTest(TestCase):
</imsx_POXBody>
</imsx_POXEnvelopeRequest>
""")
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 @@ class LTIBlockTest(TestCase):
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 @@ class LTIBlockTest(TestCase):
"""
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

View File

@@ -5,6 +5,7 @@ import unittest
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(

View File

@@ -701,7 +701,6 @@ class VideoExportTestCase(VideoBlockTestBase):
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 @@ class VideoExportTestCase(VideoBlockTestBase):
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 @@ class VideoExportTestCase(VideoBlockTestBase):
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 @@ class VideoBlockStudentViewDataTestCase(unittest.TestCase):
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 @@ class VideoBlockStudentViewDataTestCase(unittest.TestCase):
}
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}}

View File

@@ -371,7 +371,7 @@ class VideoBlock(
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 @@ class VideoBlock(
# (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 @@ class VideoBlock(
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.

View File

@@ -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 @@ class MetricsMixin:
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 @@ class ModuleSystemShim:
)
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 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, ModuleSystemShim,
get_module,
descriptor_runtime,
publish=None,
course_id=None,
**kwargs,
):
"""
@@ -1755,8 +1739,6 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, ModuleSystemShim,
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 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, ModuleSystemShim,
self.track_function = track_function
self.get_module = get_module
self.course_id = course_id
if publish:
self.publish = publish