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:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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!'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'))
|
||||
|
||||
Reference in New Issue
Block a user