refactor: deprecates ModuleSystem properties for code sandboxing and cache
* Deprecates ModuleSystem can_execute_unsafe_code, get_python_lib_zip and cache properties * Adds a new CacheService and SandboxService to provide the deprecated property * Adds tests for the added CacheService and SandboxService * Updates the ModuleSystemShim tests in Lms and Studio
This commit is contained in:
@@ -4,6 +4,7 @@ import logging
|
||||
from functools import partial
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import Http404, HttpResponseBadRequest
|
||||
from django.urls import reverse
|
||||
@@ -15,6 +16,16 @@ from xblock.django.request import django_to_webob_request, webob_to_django_respo
|
||||
from xblock.exceptions import NoSuchHandlerError
|
||||
from xblock.runtime import KvsFieldData
|
||||
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.error_module import ErrorBlock
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xmodule.modulestore.django import ModuleI18nService, modulestore
|
||||
from xmodule.partitions.partitions_service import PartitionService
|
||||
from xmodule.services import SettingsService, TeamsConfigurationService
|
||||
from xmodule.studio_editable import has_author_view
|
||||
from xmodule.util.sandboxing import SandboxService
|
||||
from xmodule.util.xmodule_django import add_webpack_to_fragment
|
||||
from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, ModuleSystem, XModule, XModuleDescriptor
|
||||
from cms.djangoapps.xblock_config.models import StudioConfig
|
||||
from cms.lib.xblock.field_data import CmsFieldData
|
||||
from common.djangoapps import static_replace
|
||||
@@ -23,6 +34,7 @@ from common.djangoapps.edxmako.services import MakoService
|
||||
from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService
|
||||
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
|
||||
from openedx.core.lib.license import wrap_with_license
|
||||
from openedx.core.lib.cache_utils import CacheService
|
||||
from openedx.core.lib.xblock_utils import (
|
||||
replace_static_urls,
|
||||
request_token,
|
||||
@@ -31,16 +43,6 @@ from openedx.core.lib.xblock_utils import (
|
||||
wrap_xblock_aside,
|
||||
xblock_local_resource_url
|
||||
)
|
||||
from xmodule.contentstore.django import contentstore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.error_module import ErrorBlock # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError # 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.studio_editable import has_author_view # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.util.sandboxing import can_execute_unsafe_code, get_python_lib_zip # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.util.xmodule_django import add_webpack_to_fragment # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, ModuleSystem, XModule, XModuleDescriptor # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from ..utils import get_visibility_partition_info
|
||||
from .access import get_user_role
|
||||
@@ -198,8 +200,6 @@ def _preview_module_system(request, descriptor, field_data):
|
||||
get_module=partial(_load_preview_module, request),
|
||||
debug=True,
|
||||
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
|
||||
can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
|
||||
get_python_lib_zip=(lambda: get_python_lib_zip(contentstore, course_id)),
|
||||
mixins=settings.XBLOCK_MIXINS,
|
||||
course_id=course_id,
|
||||
|
||||
@@ -221,6 +221,8 @@ def _preview_module_system(request, descriptor, field_data):
|
||||
),
|
||||
"partitions": StudioPartitionService(course_id=course_id),
|
||||
"teams_configuration": TeamsConfigurationService(),
|
||||
"sandbox": SandboxService(contentstore=contentstore, course_id=course_id),
|
||||
"cache": CacheService(cache),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -8,17 +8,19 @@ from unittest import mock
|
||||
|
||||
import ddt
|
||||
from django.test.client import Client, RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from web_fragments.fragment import Fragment
|
||||
from xblock.core import XBlock, XBlockAside
|
||||
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, upload_file_to_course
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.modulestore.tests.test_asides import AsideTestType
|
||||
from cms.djangoapps.contentstore.utils import reverse_usage_url
|
||||
from cms.djangoapps.xblock_config.models import StudioConfig
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.django import modulestore # 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, ItemFactory # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.test_asides import AsideTestType # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from ..preview import _preview_module_system, get_preview_fragment
|
||||
|
||||
@@ -220,22 +222,28 @@ class CmsModuleSystemShimTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests that the deprecated attributes in the Module System (XBlock Runtime) return the expected values.
|
||||
"""
|
||||
COURSE_ID = 'edX/CmsModuleShimTest/2021_Fall'
|
||||
PYTHON_LIB_FILENAME = 'test_python_lib.zip'
|
||||
PYTHON_LIB_SOURCE_FILE = './common/test/data/uploads/python_lib.zip'
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up the user and other fields that will be used to instantiate the runtime.
|
||||
Set up the user, course and other fields that will be used to instantiate the runtime.
|
||||
"""
|
||||
super().setUp()
|
||||
self.course = CourseFactory.create()
|
||||
org, number, run = self.COURSE_ID.split('/')
|
||||
self.course = CourseFactory.create(org=org, number=number, run=run)
|
||||
self.user = UserFactory()
|
||||
self.request = RequestFactory().get('/dummy-url')
|
||||
self.request.user = self.user
|
||||
self.request.session = {}
|
||||
self.descriptor = ItemFactory(category="video", parent=self.course)
|
||||
self.field_data = mock.Mock()
|
||||
self.contentstore = contentstore()
|
||||
self.runtime = _preview_module_system(
|
||||
self.request,
|
||||
self.descriptor,
|
||||
self.field_data,
|
||||
descriptor=ItemFactory(category="problem", parent=self.course),
|
||||
field_data=mock.Mock(),
|
||||
)
|
||||
|
||||
def test_get_user_role(self):
|
||||
@@ -247,12 +255,32 @@ class CmsModuleSystemShimTest(ModuleStoreTestCase):
|
||||
html = get_preview_fragment(self.request, descriptor, {'element_id': 142}).content
|
||||
assert '<div id="142" ns="main">Testing the MakoService</div>' in html
|
||||
|
||||
def test_xqueue_is_not_available_in_studio(self):
|
||||
descriptor = ItemFactory(category="problem", parent=self.course)
|
||||
runtime = _preview_module_system(
|
||||
self.request,
|
||||
descriptor=descriptor,
|
||||
field_data=mock.Mock(),
|
||||
@override_settings(COURSES_WITH_UNSAFE_CODE=[COURSE_ID])
|
||||
def test_can_execute_unsafe_code(self):
|
||||
assert self.runtime.can_execute_unsafe_code()
|
||||
|
||||
def test_cannot_execute_unsafe_code(self):
|
||||
assert not self.runtime.can_execute_unsafe_code()
|
||||
|
||||
@override_settings(PYTHON_LIB_FILENAME=PYTHON_LIB_FILENAME)
|
||||
def test_get_python_lib_zip(self):
|
||||
zipfile = upload_file_to_course(
|
||||
course_key=self.course.id,
|
||||
contentstore=self.contentstore,
|
||||
source_file=self.PYTHON_LIB_SOURCE_FILE,
|
||||
target_filename=self.PYTHON_LIB_FILENAME,
|
||||
)
|
||||
assert runtime.xqueue is None
|
||||
assert runtime.service(descriptor, 'xqueue') is None
|
||||
assert self.runtime.get_python_lib_zip() == zipfile
|
||||
|
||||
def test_no_get_python_lib_zip(self):
|
||||
zipfile = upload_file_to_course(
|
||||
course_key=self.course.id,
|
||||
contentstore=self.contentstore,
|
||||
source_file=self.PYTHON_LIB_SOURCE_FILE,
|
||||
target_filename=self.PYTHON_LIB_FILENAME,
|
||||
)
|
||||
assert self.runtime.get_python_lib_zip() is None
|
||||
|
||||
def test_cache(self):
|
||||
assert hasattr(self.runtime.cache, 'get')
|
||||
assert hasattr(self.runtime.cache, 'set')
|
||||
|
||||
@@ -3,12 +3,15 @@ Tests for sandboxing.py in util app
|
||||
"""
|
||||
|
||||
|
||||
import ddt
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locator import CourseLocator, LibraryLocator
|
||||
|
||||
from xmodule.util.sandboxing import can_execute_unsafe_code
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.tests.django_utils import upload_file_to_course
|
||||
from xmodule.util.sandboxing import can_execute_unsafe_code, SandboxService
|
||||
|
||||
|
||||
class SandboxingTest(TestCase):
|
||||
@@ -39,3 +42,75 @@ class SandboxingTest(TestCase):
|
||||
assert not can_execute_unsafe_code(CourseLocator('edX', 'full', '2012_Fall'))
|
||||
assert not can_execute_unsafe_code(CourseLocator('edX', 'full', '2013_Spring'))
|
||||
assert not can_execute_unsafe_code(LibraryLocator('edX', 'test_bank'))
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class SandboxServiceTest(TestCase):
|
||||
"""
|
||||
Test SandboxService methods.
|
||||
"""
|
||||
PYTHON_LIB_FILENAME = 'test_python_lib.zip'
|
||||
PYTHON_LIB_SOURCE_FILE = './common/test/data/uploads/python_lib.zip'
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Upload the python lib file to the test course.
|
||||
"""
|
||||
super().setUpClass()
|
||||
|
||||
course_key = CourseLocator('test', 'sandbox_test', '2021_01')
|
||||
cls.sandbox_service = SandboxService(course_id=course_key, contentstore=contentstore)
|
||||
cls.zipfile = upload_file_to_course(
|
||||
course_key=course_key,
|
||||
contentstore=cls.sandbox_service.contentstore(),
|
||||
source_file=cls.PYTHON_LIB_SOURCE_FILE,
|
||||
target_filename=cls.PYTHON_LIB_FILENAME,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_can_execute_unsafe_code(context_key, expected_result):
|
||||
sandbox_service = SandboxService(course_id=context_key, contentstore=None)
|
||||
assert expected_result == sandbox_service.can_execute_unsafe_code()
|
||||
|
||||
@ddt.data(
|
||||
CourseLocator('edX', 'notful', 'empty'),
|
||||
LibraryLocator('edY', 'test_bank'),
|
||||
)
|
||||
@override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*', 'library:v1-edX+.*'])
|
||||
def test_sandbox_exclusion(self, context_key):
|
||||
"""
|
||||
Test to make sure that a non-match returns false
|
||||
"""
|
||||
self.validate_can_execute_unsafe_code(context_key, False)
|
||||
|
||||
@ddt.data(
|
||||
CourseKey.from_string('edX/full/2012_Fall'),
|
||||
CourseKey.from_string('edX/full/2013_Spring'),
|
||||
)
|
||||
@override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*'])
|
||||
def test_sandbox_inclusion(self, context_key):
|
||||
"""
|
||||
Test to make sure that a match works across course runs
|
||||
"""
|
||||
self.validate_can_execute_unsafe_code(context_key, True)
|
||||
self.validate_can_execute_unsafe_code(LibraryLocator('edX', 'test_bank'), False)
|
||||
|
||||
@ddt.data(
|
||||
CourseLocator('edX', 'full', '2012_Fall'),
|
||||
CourseLocator('edX', 'full', '2013_Spring'),
|
||||
LibraryLocator('edX', 'test_bank'),
|
||||
)
|
||||
def test_courselikes_with_unsafe_code_default(self, context_key):
|
||||
"""
|
||||
Test that the default setting for COURSES_WITH_UNSAFE_CODE is an empty setting,
|
||||
i.e., we don't use @override_settings in these tests
|
||||
"""
|
||||
self.validate_can_execute_unsafe_code(context_key, False)
|
||||
|
||||
@override_settings(PYTHON_LIB_FILENAME=PYTHON_LIB_FILENAME)
|
||||
def test_get_python_lib_zip(self):
|
||||
assert self.sandbox_service.get_python_lib_zip() == self.zipfile
|
||||
|
||||
def test_no_python_lib_zip(self):
|
||||
assert self.sandbox_service.get_python_lib_zip() is None
|
||||
|
||||
@@ -8,6 +8,7 @@ import functools
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from enum import Enum
|
||||
from mimetypes import guess_type
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
@@ -16,6 +17,12 @@ from django.db import connections, transaction
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import _CONTENTSTORE
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import SignalHandler, clear_existing_modulestores, modulestore
|
||||
from xmodule.modulestore.tests.factories import XMODULE_FACTORY_LOCK
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from lms.djangoapps.courseware.field_overrides import OverrideFieldData
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationMixin, CacheIsolationTestCase, FilteredQueryCountMixin
|
||||
from openedx.core.lib.tempdir import mkdtemp_clean
|
||||
@@ -23,11 +30,6 @@ from common.djangoapps.split_modulestore_django.models import SplitModulestoreCo
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import AdminFactory, UserFactory, InstructorFactory
|
||||
from common.djangoapps.student.tests.factories import StaffFactory
|
||||
from xmodule.contentstore.django import _CONTENTSTORE
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import SignalHandler, clear_existing_modulestores, modulestore
|
||||
from xmodule.modulestore.tests.factories import XMODULE_FACTORY_LOCK
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
|
||||
|
||||
class CourseUserType(Enum):
|
||||
@@ -604,3 +606,16 @@ class ModuleStoreTestCase(
|
||||
self.store.update_item(course, user_id)
|
||||
updated_course = self.store.get_course(course.id)
|
||||
return updated_course
|
||||
|
||||
|
||||
def upload_file_to_course(course_key, contentstore, source_file, target_filename):
|
||||
'''
|
||||
Uploads the given source file to the given course, and returns the content of the file.
|
||||
'''
|
||||
asset_key = course_key.make_asset_key('asset', target_filename)
|
||||
with open(source_file, "rb") as f:
|
||||
file_contents = f.read()
|
||||
mimetype = guess_type(source_file)[0]
|
||||
content = StaticContent(asset_key, target_filename, mimetype, file_contents, locked=False)
|
||||
contentstore.save(content)
|
||||
return file_contents
|
||||
|
||||
@@ -41,3 +41,29 @@ def get_python_lib_zip(contentstore, course_id):
|
||||
return zip_lib.data
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class SandboxService:
|
||||
"""
|
||||
A service which provides utilities for executing sandboxed Python code, for example, inside custom Python questions.
|
||||
|
||||
Args:
|
||||
contentstore(function): function which creates an instance of xmodule.content.ContentStore
|
||||
course_id(string or CourseLocator): identifier for the course
|
||||
"""
|
||||
def __init__(self, contentstore, course_id, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.contentstore = contentstore
|
||||
self.course_id = course_id
|
||||
|
||||
def can_execute_unsafe_code(self):
|
||||
"""
|
||||
Returns a boolean, true if the course can run outside the sandbox.
|
||||
"""
|
||||
return can_execute_unsafe_code(self.course_id)
|
||||
|
||||
def get_python_lib_zip(self):
|
||||
"""
|
||||
Return the bytes of the course code library file, if it exists.
|
||||
"""
|
||||
return get_python_lib_zip(self.contentstore, self.course_id)
|
||||
|
||||
@@ -34,13 +34,13 @@ from xblock.fields import (
|
||||
)
|
||||
from xblock.runtime import IdGenerator, IdReader, Runtime
|
||||
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from xmodule import block_metadata_utils
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from xmodule.exceptions import UndefinedContext
|
||||
from xmodule.fields import RelativeTime
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.util.xmodule_django import add_webpack_to_fragment
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
|
||||
from common.djangoapps.xblock_django.constants import (
|
||||
ATTR_KEY_ANONYMOUS_USER_ID,
|
||||
@@ -1906,6 +1906,59 @@ class ModuleSystemShim:
|
||||
}
|
||||
return None
|
||||
|
||||
@property
|
||||
def can_execute_unsafe_code(self):
|
||||
"""
|
||||
Returns a function which returns a boolean, indicating whether or not to allow the execution of unsafe,
|
||||
unsandboxed code.
|
||||
|
||||
Deprecated in favor of the sandbox service.
|
||||
"""
|
||||
warnings.warn(
|
||||
'runtime.can_execute_unsafe_code is deprecated. Please use the sandbox service instead.',
|
||||
DeprecationWarning, stacklevel=3,
|
||||
)
|
||||
sandbox_service = self._services.get('sandbox')
|
||||
if sandbox_service:
|
||||
return sandbox_service.can_execute_unsafe_code
|
||||
# Default to saying "no unsafe code".
|
||||
return lambda: False
|
||||
|
||||
@property
|
||||
def get_python_lib_zip(self):
|
||||
"""
|
||||
Returns a function returning a bytestring or None.
|
||||
|
||||
The bytestring is the contents of a zip file that should be importable by other Python code running in the
|
||||
module.
|
||||
|
||||
Deprecated in favor of the sandbox service.
|
||||
"""
|
||||
warnings.warn(
|
||||
'runtime.get_python_lib_zip is deprecated. Please use the sandbox service instead.',
|
||||
DeprecationWarning, stacklevel=3,
|
||||
)
|
||||
sandbox_service = self._services.get('sandbox')
|
||||
if sandbox_service:
|
||||
return sandbox_service.get_python_lib_zip
|
||||
# Default to saying "no lib data"
|
||||
return lambda: None
|
||||
|
||||
@property
|
||||
def cache(self):
|
||||
"""
|
||||
Returns a cache object with two methods:
|
||||
* .get(key) returns an object from the cache or None.
|
||||
* .set(key, value, timeout_secs=None) stores a value in the cache with a timeout.
|
||||
|
||||
Deprecated in favor of the cache service.
|
||||
"""
|
||||
warnings.warn(
|
||||
'runtime.cache is deprecated. Please use the cache service instead.',
|
||||
DeprecationWarning, stacklevel=3,
|
||||
)
|
||||
return self._services.get('cache') or DoNothingCache()
|
||||
|
||||
|
||||
class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, ModuleSystemShim, Runtime):
|
||||
"""
|
||||
@@ -1925,10 +1978,10 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, ModuleSystemShim,
|
||||
replace_urls, descriptor_runtime, filestore=None,
|
||||
debug=False, hostname="", publish=None, node_path="",
|
||||
course_id=None,
|
||||
cache=None, can_execute_unsafe_code=None, replace_course_urls=None,
|
||||
replace_course_urls=None,
|
||||
replace_jump_to_id_urls=None, error_descriptor_class=None,
|
||||
field_data=None, rebind_noauth_module_to_user=None,
|
||||
get_python_lib_zip=None, **kwargs):
|
||||
**kwargs):
|
||||
"""
|
||||
Create a closure around the system environment.
|
||||
|
||||
@@ -1956,17 +2009,6 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, ModuleSystemShim,
|
||||
|
||||
publish(event) - A function that allows XModules to publish events (such as grade changes)
|
||||
|
||||
cache - A cache object with two methods:
|
||||
.get(key) returns an object from the cache or None.
|
||||
.set(key, value, timeout_secs=None) stores a value in the cache with a timeout.
|
||||
|
||||
can_execute_unsafe_code - A function returning a boolean, whether or
|
||||
not to allow the execution of unsafe, unsandboxed code.
|
||||
|
||||
get_python_lib_zip - A function returning a bytestring or None. The
|
||||
bytestring is the contents of a zip file that should be importable
|
||||
by other Python code running in the module.
|
||||
|
||||
error_descriptor_class - The class to use to render XModules with errors
|
||||
|
||||
field_data - the `FieldData` to use for backing XBlock storage.
|
||||
@@ -1993,10 +2035,6 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, ModuleSystemShim,
|
||||
|
||||
if publish:
|
||||
self.publish = publish
|
||||
|
||||
self.cache = cache or DoNothingCache()
|
||||
self.can_execute_unsafe_code = can_execute_unsafe_code or (lambda: False)
|
||||
self.get_python_lib_zip = get_python_lib_zip or (lambda: None)
|
||||
self.replace_course_urls = replace_course_urls
|
||||
self.replace_jump_to_id_urls = replace_jump_to_id_urls
|
||||
self.error_descriptor_class = error_descriptor_class
|
||||
|
||||
@@ -39,6 +39,12 @@ from xblock.exceptions import NoSuchHandlerError, NoSuchViewError
|
||||
from xblock.reference.plugins import FSService
|
||||
from xblock.runtime import KvsFieldData
|
||||
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.error_module import ErrorBlock, NonStaffErrorBlock
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.util.sandboxing import SandboxService
|
||||
from common.djangoapps import static_replace
|
||||
from common.djangoapps.xblock_django.constants import ATTR_KEY_USER_ID
|
||||
from capa.xqueue_interface import XQueueService # lint-amnesty, pylint: disable=wrong-import-order
|
||||
@@ -90,12 +96,7 @@ from common.djangoapps.util import milestones_helpers
|
||||
from common.djangoapps.util.json_request import JsonResponse
|
||||
from common.djangoapps.edxmako.services import MakoService
|
||||
from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService
|
||||
from xmodule.contentstore.django import contentstore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.error_module import ErrorBlock, NonStaffErrorBlock # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.util.sandboxing import can_execute_unsafe_code, get_python_lib_zip # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from openedx.core.lib.cache_utils import CacheService
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -772,9 +773,6 @@ def get_module_system_for_user(
|
||||
node_path=settings.NODE_PATH,
|
||||
publish=publish,
|
||||
course_id=course_id,
|
||||
cache=cache,
|
||||
can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
|
||||
get_python_lib_zip=(lambda: get_python_lib_zip(contentstore, 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,
|
||||
@@ -792,6 +790,8 @@ def get_module_system_for_user(
|
||||
'grade_utils': GradesUtilService(course_id=course_id),
|
||||
'user_state': UserStateService(),
|
||||
'content_type_gating': ContentTypeGatingService(),
|
||||
'cache': CacheService(cache),
|
||||
'sandbox': SandboxService(contentstore=contentstore, course_id=course_id),
|
||||
'xqueue': xqueue_service,
|
||||
},
|
||||
descriptor_runtime=descriptor._runtime, # pylint: disable=protected-access
|
||||
|
||||
@@ -42,6 +42,21 @@ from xblock.test.tools import TestRuntime # lint-amnesty, pylint: disable=wrong
|
||||
|
||||
from capa.tests.response_xml_factory import OptionResponseXMLFactory # lint-amnesty, pylint: disable=reimported
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
from xmodule.capa_module import ProblemBlock
|
||||
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.tests.django_utils import (
|
||||
ModuleStoreTestCase,
|
||||
SharedModuleStoreTestCase,
|
||||
upload_file_to_course,
|
||||
)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, ToyCourseFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.test_asides import AsideTestType # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.video_module import VideoBlock # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.x_module import STUDENT_VIEW, CombinedSystem, XModule, XModuleDescriptor # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from common.djangoapps.course_modes.models import CourseMode # lint-amnesty, pylint: disable=reimported
|
||||
from common.djangoapps.student.tests.factories import GlobalStaffFactory
|
||||
from common.djangoapps.student.tests.factories import RequestFactoryNoCsrf
|
||||
@@ -69,19 +84,6 @@ from openedx.core.lib.url_utils import quote_slashes
|
||||
from common.djangoapps.student.models import CourseEnrollment, anonymous_id_for_user
|
||||
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
|
||||
from common.djangoapps.xblock_django.models import XBlockConfiguration
|
||||
from xmodule.capa_module import ProblemBlock # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.html_module import AboutBlock, CourseInfoBlock, HtmlBlock, StaticTabBlock # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.lti_module import LTIBlock # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.django_utils import ( # lint-amnesty, pylint: disable=wrong-import-order
|
||||
ModuleStoreTestCase,
|
||||
SharedModuleStoreTestCase
|
||||
)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, ToyCourseFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.test_asides import AsideTestType # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.video_module import VideoBlock # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.x_module import STUDENT_VIEW, CombinedSystem, XModule, XModuleDescriptor # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
@@ -2565,6 +2567,8 @@ class LmsModuleSystemShimTest(SharedModuleStoreTestCase):
|
||||
Tests that the deprecated attributes in the LMS Module System (XBlock Runtime) return the expected values.
|
||||
"""
|
||||
COURSE_ID = 'edX/LmsModuleShimTest/2021_Fall'
|
||||
PYTHON_LIB_FILENAME = 'test_python_lib.zip'
|
||||
PYTHON_LIB_SOURCE_FILE = './common/test/data/uploads/python_lib.zip'
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -2586,6 +2590,16 @@ class LmsModuleSystemShimTest(SharedModuleStoreTestCase):
|
||||
self.student_data = Mock()
|
||||
self.track_function = Mock()
|
||||
self.request_token = Mock()
|
||||
self.contentstore = contentstore()
|
||||
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,
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
('seed', 232),
|
||||
@@ -2597,16 +2611,7 @@ class LmsModuleSystemShimTest(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Tests that the deprecated attributes provided by the user service match expected values.
|
||||
"""
|
||||
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,
|
||||
)
|
||||
assert getattr(runtime, attribute) == expected_value
|
||||
assert getattr(self.runtime, attribute) == expected_value
|
||||
|
||||
@patch('lms.djangoapps.courseware.module_render.has_access', Mock(return_value=True, autospec=True))
|
||||
def test_user_is_staff(self):
|
||||
@@ -2636,16 +2641,7 @@ class LmsModuleSystemShimTest(SharedModuleStoreTestCase):
|
||||
assert runtime.get_user_role() == 'instructor'
|
||||
|
||||
def test_anonymous_student_id(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,
|
||||
)
|
||||
assert runtime.anonymous_student_id == anonymous_id_for_user(self.user, self.course.id)
|
||||
assert self.runtime.anonymous_student_id == anonymous_id_for_user(self.user, self.course.id)
|
||||
|
||||
def test_anonymous_student_id_bug(self):
|
||||
"""
|
||||
@@ -2715,29 +2711,11 @@ class LmsModuleSystemShimTest(SharedModuleStoreTestCase):
|
||||
assert runtime.get_real_user() == self.user # pylint: disable=not-callable
|
||||
|
||||
def test_render_template(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,
|
||||
)
|
||||
rendered = runtime.render_template('templates/edxmako.html', {'element_id': 'hi'}) # pylint: disable=not-callable
|
||||
rendered = self.runtime.render_template('templates/edxmako.html', {'element_id': 'hi'}) # pylint: disable=not-callable
|
||||
assert rendered == '<div id="hi" ns="main">Testing the MakoService</div>\n'
|
||||
|
||||
def test_xqueue(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,
|
||||
)
|
||||
xqueue = runtime.xqueue
|
||||
xqueue = self.runtime.xqueue
|
||||
assert isinstance(xqueue['interface'], XQueueInterface)
|
||||
assert xqueue['interface'].url == 'http://sandbox-xqueue.edx.org'
|
||||
assert xqueue['default_queuename'] == 'edX-LmsModuleShimTest'
|
||||
@@ -2777,3 +2755,37 @@ class LmsModuleSystemShimTest(SharedModuleStoreTestCase):
|
||||
callback_url = f'http://alt.url/courses/edX/LmsModuleShimTest/2021_Fall/xqueue/232/{self.descriptor.location}'
|
||||
assert xqueue['construct_callback']() == f'{callback_url}/score_update'
|
||||
assert xqueue['construct_callback']('mock_dispatch') == f'{callback_url}/mock_dispatch'
|
||||
|
||||
@override_settings(COURSES_WITH_UNSAFE_CODE=[COURSE_ID])
|
||||
def test_can_execute_unsafe_code_when_allowed(self):
|
||||
assert self.runtime.can_execute_unsafe_code()
|
||||
|
||||
@override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/2012_Fall'])
|
||||
def test_cannot_execute_unsafe_code_when_disallowed(self):
|
||||
assert not self.runtime.can_execute_unsafe_code()
|
||||
|
||||
def test_cannot_execute_unsafe_code(self):
|
||||
assert not self.runtime.can_execute_unsafe_code()
|
||||
|
||||
@override_settings(PYTHON_LIB_FILENAME=PYTHON_LIB_FILENAME)
|
||||
def test_get_python_lib_zip(self):
|
||||
zipfile = upload_file_to_course(
|
||||
course_key=self.course.id,
|
||||
contentstore=self.contentstore,
|
||||
source_file=self.PYTHON_LIB_SOURCE_FILE,
|
||||
target_filename=self.PYTHON_LIB_FILENAME,
|
||||
)
|
||||
assert self.runtime.get_python_lib_zip() == zipfile
|
||||
|
||||
def test_no_get_python_lib_zip(self):
|
||||
zipfile = upload_file_to_course(
|
||||
course_key=self.course.id,
|
||||
contentstore=self.contentstore,
|
||||
source_file=self.PYTHON_LIB_SOURCE_FILE,
|
||||
target_filename=self.PYTHON_LIB_FILENAME,
|
||||
)
|
||||
assert self.runtime.get_python_lib_zip() is None
|
||||
|
||||
def test_cache(self):
|
||||
assert hasattr(self.runtime.cache, 'get')
|
||||
assert hasattr(self.runtime.cache, 'set')
|
||||
|
||||
@@ -10,6 +10,7 @@ from completion.waffle import ENABLE_COMPLETION_TRACKING_SWITCH
|
||||
from completion.models import BlockCompletion
|
||||
from completion.services import CompletionService
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from functools import lru_cache # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from eventtracking import tracker
|
||||
@@ -19,6 +20,10 @@ from xblock.field_data import SplitFieldData
|
||||
from xblock.fields import Scope
|
||||
from xblock.runtime import KvsFieldData, MemoryIdManager, Runtime
|
||||
|
||||
from xmodule.errortracker import make_error_tracker
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.django import ModuleI18nService
|
||||
from xmodule.util.sandboxing import SandboxService
|
||||
from common.djangoapps.edxmako.services import MakoService
|
||||
from common.djangoapps.track import contexts as track_contexts
|
||||
from common.djangoapps.track import views as track_views
|
||||
@@ -30,10 +35,9 @@ from openedx.core.djangoapps.xblock.runtime.blockstore_field_data import Blockst
|
||||
from openedx.core.djangoapps.xblock.runtime.ephemeral_field_data import EphemeralKeyValueStore
|
||||
from openedx.core.djangoapps.xblock.runtime.mixin import LmsBlockMixin
|
||||
from openedx.core.djangoapps.xblock.utils import get_xblock_id_for_anonymous_user
|
||||
from openedx.core.lib.cache_utils import CacheService
|
||||
from openedx.core.lib.xblock_utils import wrap_fragment, xblock_local_resource_url
|
||||
from common.djangoapps.static_replace import process_static_urls
|
||||
from xmodule.errortracker import make_error_tracker # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.django import ModuleI18nService # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from .id_managers import OpaqueKeyReader
|
||||
from .shims import RuntimeShim, XBlockShim
|
||||
@@ -240,6 +244,12 @@ class XBlockRuntime(RuntimeShim, Runtime):
|
||||
return MakoService()
|
||||
elif service_name == "i18n":
|
||||
return ModuleI18nService(block=block)
|
||||
elif service_name == 'sandbox':
|
||||
context_key = block.scope_ids.usage_id.context_key
|
||||
return SandboxService(contentstore=contentstore, course_id=context_key)
|
||||
elif service_name == 'cache':
|
||||
return CacheService(cache)
|
||||
|
||||
# Check if the XBlockRuntimeSystem wants to handle this:
|
||||
service = self.system.get_service(block, service_name)
|
||||
# Otherwise, fall back to the base implementation which loads services
|
||||
|
||||
@@ -223,3 +223,27 @@ def get_cache(name):
|
||||
"""
|
||||
assert name is not None
|
||||
return RequestCache(name).data
|
||||
|
||||
|
||||
class CacheService:
|
||||
"""
|
||||
An XBlock service which provides a cache.
|
||||
|
||||
Args:
|
||||
cache(object): provides get/set functions for retrieving/storing key/value pairs.
|
||||
"""
|
||||
def __init__(self, cache, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._cache = cache
|
||||
|
||||
def get(self, key, *args, **kwargs):
|
||||
"""
|
||||
Returns the value cached against the given key, or None.
|
||||
"""
|
||||
return self._cache.get(key, *args, **kwargs)
|
||||
|
||||
def set(self, key, value, *args, **kwargs):
|
||||
"""
|
||||
Caches the value against the given key.
|
||||
"""
|
||||
return self._cache.set(key, value, *args, **kwargs)
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
"""
|
||||
Tests for cache_utils.py
|
||||
"""
|
||||
|
||||
from time import sleep
|
||||
from unittest import TestCase
|
||||
from unittest.mock import Mock
|
||||
|
||||
import ddt
|
||||
from edx_django_utils.cache import RequestCache
|
||||
from django.core.cache import cache
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
from openedx.core.lib.cache_utils import CacheService, request_cached
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -295,3 +297,26 @@ class TestRequestCachedDecorator(TestCase):
|
||||
result = wrapped(3)
|
||||
assert result == 2
|
||||
assert to_be_wrapped.call_count == 2
|
||||
|
||||
|
||||
class CacheServiceTest(TestCase):
|
||||
"""
|
||||
Test CacheService methods.
|
||||
"""
|
||||
@override_settings(CACHES={
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
}
|
||||
})
|
||||
def test_cache(self):
|
||||
'''
|
||||
Ensure the default cache works as expected.
|
||||
'''
|
||||
cache_service = CacheService(cache)
|
||||
key = 'my_key'
|
||||
value = 'some random value'
|
||||
timeout = 1
|
||||
cache_service.set(key, value, timeout=timeout)
|
||||
assert cache_service.get(key) == value
|
||||
sleep(timeout)
|
||||
assert cache_service.get(key) is None
|
||||
|
||||
Reference in New Issue
Block a user