Use runtime-provided XQueueService instead of constructing it in ProblemBlock (#37998)
* fix: move xqueue services
This commit is contained in:
@@ -1,94 +1,21 @@
|
||||
"""Test the XQueue service and interface."""
|
||||
"""Test the XQueue interface."""
|
||||
|
||||
import json
|
||||
from unittest import TestCase
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from xblock.fields import ScopeIds
|
||||
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from xmodule.capa.xqueue_interface import XQueueInterface, XQueueService
|
||||
from xmodule.capa.xqueue_interface import XQueueInterface
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@skip_unless_lms
|
||||
class XQueueServiceTest(TestCase):
|
||||
"""Test the XQueue service methods."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
location = BlockUsageLocator(
|
||||
CourseLocator("test_org", "test_course", "test_run"),
|
||||
"problem",
|
||||
"ExampleProblem",
|
||||
)
|
||||
self.block = Mock(scope_ids=ScopeIds("user1", "mock_problem", location, location))
|
||||
self.block.max_score = Mock(return_value=10) # Mock max_score method
|
||||
self.service = XQueueService(self.block)
|
||||
|
||||
def test_interface(self):
|
||||
"""Test that the `XQUEUE_INTERFACE` settings are passed from the service to the interface."""
|
||||
assert isinstance(self.service.interface, XQueueInterface)
|
||||
assert self.service.interface.url == "http://sandbox-xqueue.edx.org"
|
||||
assert self.service.interface.auth["username"] == "lms"
|
||||
assert self.service.interface.auth["password"] == "***REMOVED***"
|
||||
assert self.service.interface.session.auth.username == "anant"
|
||||
assert self.service.interface.session.auth.password == "agarwal"
|
||||
|
||||
@patch("xmodule.capa.xqueue_interface.use_edx_submissions_for_xqueue", return_value=True)
|
||||
def test_construct_callback_with_flag_enabled(self, mock_flag): # pylint: disable=unused-argument
|
||||
"""Test construct_callback when the waffle flag is enabled."""
|
||||
usage_id = self.block.scope_ids.usage_id
|
||||
course_id = str(usage_id.course_key)
|
||||
callback_url = f"courses/{course_id}/xqueue/user1/{usage_id}"
|
||||
|
||||
assert self.service.construct_callback() == f"{settings.LMS_ROOT_URL}/{callback_url}/score_update"
|
||||
assert self.service.construct_callback("alt_dispatch") == (
|
||||
f"{settings.LMS_ROOT_URL}/{callback_url}/alt_dispatch"
|
||||
)
|
||||
|
||||
custom_callback_url = "http://alt.url"
|
||||
with override_settings(XQUEUE_INTERFACE={**settings.XQUEUE_INTERFACE, "callback_url": custom_callback_url}):
|
||||
assert self.service.construct_callback() == f"{custom_callback_url}/{callback_url}/score_update"
|
||||
|
||||
@patch("xmodule.capa.xqueue_interface.use_edx_submissions_for_xqueue", return_value=False)
|
||||
def test_construct_callback_with_flag_disabled(self, mock_flag): # pylint: disable=unused-argument
|
||||
"""Test construct_callback when the waffle flag is disabled."""
|
||||
usage_id = self.block.scope_ids.usage_id
|
||||
callback_url = f"courses/{usage_id.context_key}/xqueue/user1/{usage_id}"
|
||||
|
||||
assert self.service.construct_callback() == f"{settings.LMS_ROOT_URL}/{callback_url}/score_update"
|
||||
assert self.service.construct_callback("alt_dispatch") == f"{settings.LMS_ROOT_URL}/{callback_url}/alt_dispatch"
|
||||
|
||||
custom_callback_url = "http://alt.url"
|
||||
with override_settings(XQUEUE_INTERFACE={**settings.XQUEUE_INTERFACE, "callback_url": custom_callback_url}):
|
||||
assert self.service.construct_callback() == f"{custom_callback_url}/{callback_url}/score_update"
|
||||
|
||||
def test_default_queuename(self):
|
||||
"""Check the format of the default queue name."""
|
||||
assert self.service.default_queuename == "test_org-test_course"
|
||||
|
||||
def test_waittime(self):
|
||||
"""Check that the time between requests is retrieved correctly from the settings."""
|
||||
assert self.service.waittime == 5
|
||||
|
||||
with override_settings(XQUEUE_WAITTIME_BETWEEN_REQUESTS=15):
|
||||
assert self.service.waittime == 15
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("xmodule.capa.xqueue_interface.use_edx_submissions_for_xqueue", return_value=True)
|
||||
@patch("xmodule.capa.xqueue_submission.XQueueInterfaceSubmission.send_to_submission")
|
||||
def test_send_to_queue_with_flag_enabled(mock_send_to_submission, mock_flag): # pylint: disable=unused-argument
|
||||
def test_send_to_queue_with_flag_enabled(mock_send_to_submission):
|
||||
"""Test send_to_queue when the waffle flag is enabled."""
|
||||
url = "http://example.com/xqueue"
|
||||
django_auth = {"username": "user", "password": "pass"}
|
||||
block = Mock() # Mock block for the constructor
|
||||
xqueue_interface = XQueueInterface(url, django_auth, block=block)
|
||||
xqueue_interface = XQueueInterface(url, django_auth, block=block, use_submission_service=True)
|
||||
|
||||
header = json.dumps(
|
||||
{
|
||||
@@ -114,14 +41,13 @@ def test_send_to_queue_with_flag_enabled(mock_send_to_submission, mock_flag): #
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("xmodule.capa.xqueue_interface.use_edx_submissions_for_xqueue", return_value=False)
|
||||
@patch("xmodule.capa.xqueue_interface.XQueueInterface._http_post")
|
||||
def test_send_to_queue_with_flag_disabled(mock_http_post, mock_flag): # pylint: disable=unused-argument
|
||||
def test_send_to_queue_with_flag_disabled(mock_http_post):
|
||||
"""Test send_to_queue when the waffle flag is disabled."""
|
||||
url = "http://example.com/xqueue"
|
||||
django_auth = {"username": "user", "password": "pass"}
|
||||
block = Mock() # Mock block for the constructor
|
||||
xqueue_interface = XQueueInterface(url, django_auth, block=block)
|
||||
xqueue_interface = XQueueInterface(url, django_auth, block=block, use_submission_service=False)
|
||||
|
||||
header = json.dumps(
|
||||
{
|
||||
|
||||
@@ -8,12 +8,8 @@ import logging
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
|
||||
from xmodule.capa.xqueue_submission import XQueueInterfaceSubmission
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -29,34 +25,6 @@ XQUEUE_TIMEOUT = 35 # seconds
|
||||
CONNECT_TIMEOUT = 3.05 # seconds
|
||||
READ_TIMEOUT = 10 # seconds
|
||||
|
||||
# .. toggle_name: send_to_submission_course.enable
|
||||
# .. toggle_implementation: CourseWaffleFlag
|
||||
# .. toggle_description: Enables use of the submissions service instead of legacy xqueue for course problem submissions.
|
||||
# .. toggle_default: False
|
||||
# .. toggle_use_cases: opt_in
|
||||
# .. toggle_creation_date: 2024-04-03
|
||||
# .. toggle_expiration_date: 2025-08-12
|
||||
# .. toggle_will_remain_in_codebase: True
|
||||
# .. toggle_tickets: none
|
||||
# .. toggle_status: supported
|
||||
SEND_TO_SUBMISSION_COURSE_FLAG = CourseWaffleFlag("send_to_submission_course.enable", __name__)
|
||||
|
||||
|
||||
def use_edx_submissions_for_xqueue(course_key: CourseKey | None = None) -> bool:
|
||||
"""
|
||||
Determines whether edx-submissions should be used instead of legacy XQueue.
|
||||
|
||||
This helper abstracts the toggle logic so that the rest of the codebase is not tied
|
||||
to specific feature flag mechanics or rollout strategies.
|
||||
|
||||
Args:
|
||||
course_key (CourseKey | None): Optional course key. If None, fallback to site-level toggle.
|
||||
|
||||
Returns:
|
||||
bool: True if edx-submissions should be used, False otherwise.
|
||||
"""
|
||||
return SEND_TO_SUBMISSION_COURSE_FLAG.is_enabled(course_key)
|
||||
|
||||
|
||||
def make_hashkey(seed):
|
||||
"""
|
||||
@@ -108,6 +76,7 @@ class XQueueInterface: # pylint: disable=too-few-public-methods
|
||||
django_auth: Dict[str, str],
|
||||
requests_auth: Optional[HTTPBasicAuth] = None,
|
||||
block: "ProblemBlock" = None,
|
||||
use_submission_service: bool = False,
|
||||
):
|
||||
"""
|
||||
Initializes the XQueue interface.
|
||||
@@ -119,6 +88,7 @@ class XQueueInterface: # pylint: disable=too-few-public-methods
|
||||
block ('ProblemBlock', optional): Added as a parameter only to extract the course_id
|
||||
to check the course waffle flag `send_to_submission_course.enable`.
|
||||
This can be removed after the legacy xqueue is deprecated. Defaults to None.
|
||||
use_submission_service (bool): If True, use the edx-submissions service instead of XQueue.
|
||||
"""
|
||||
self.url = url
|
||||
self.auth = django_auth
|
||||
@@ -126,6 +96,7 @@ class XQueueInterface: # pylint: disable=too-few-public-methods
|
||||
self.session.auth = requests_auth
|
||||
self.block = block
|
||||
self.submission = XQueueInterfaceSubmission(self.block)
|
||||
self.use_submission_service = use_submission_service
|
||||
|
||||
def send_to_queue(self, header, body, files_to_upload=None):
|
||||
"""
|
||||
@@ -134,7 +105,7 @@ class XQueueInterface: # pylint: disable=too-few-public-methods
|
||||
header: JSON-serialized dict in the format described in 'xqueue_interface.make_xheader'
|
||||
|
||||
body: Serialized data for the receipient behind the queueing service. The operation of
|
||||
xqueue is agnostic to the contents of 'body'
|
||||
xqueue is agnostic to the contents of 'body'
|
||||
|
||||
files_to_upload: List of file objects to be uploaded to xqueue along with queue request
|
||||
|
||||
@@ -184,11 +155,10 @@ class XQueueInterface: # pylint: disable=too-few-public-methods
|
||||
)
|
||||
return self._http_post(self.url + "/xqueue/submit/", payload, files=files)
|
||||
|
||||
course_key = self.block.scope_ids.usage_id.context_key
|
||||
header_info = json.loads(header)
|
||||
queue_key = header_info["lms_key"] # pylint: disable=unused-variable
|
||||
|
||||
if use_edx_submissions_for_xqueue(course_key):
|
||||
if self.use_submission_service:
|
||||
submission = self.submission.send_to_submission( # pylint: disable=unused-variable
|
||||
header, body, queue_key, files
|
||||
)
|
||||
@@ -213,66 +183,3 @@ class XQueueInterface: # pylint: disable=too-few-public-methods
|
||||
return 1, f"unexpected HTTP status code [{response.status_code}]"
|
||||
|
||||
return parse_xreply(response.text)
|
||||
|
||||
|
||||
class XQueueService:
|
||||
"""
|
||||
XBlock service providing an interface to the XQueue service.
|
||||
|
||||
Args:
|
||||
block: The `ProblemBlock` instance.
|
||||
"""
|
||||
|
||||
def __init__(self, block: "ProblemBlock"):
|
||||
basic_auth = settings.XQUEUE_INTERFACE.get("basic_auth")
|
||||
requests_auth = HTTPBasicAuth(*basic_auth) if basic_auth else None
|
||||
self._interface = XQueueInterface(
|
||||
settings.XQUEUE_INTERFACE["url"], settings.XQUEUE_INTERFACE["django_auth"], requests_auth, block=block
|
||||
)
|
||||
|
||||
self._block = block
|
||||
|
||||
@property
|
||||
def interface(self):
|
||||
"""
|
||||
Returns the XQueueInterface instance.
|
||||
"""
|
||||
return self._interface
|
||||
|
||||
def construct_callback(self, dispatch: str = "score_update") -> str:
|
||||
"""
|
||||
Return a fully qualified callback URL for the external queueing system.
|
||||
"""
|
||||
course_key = self._block.scope_ids.usage_id.context_key
|
||||
userid = str(self._block.scope_ids.user_id)
|
||||
mod_id = str(self._block.scope_ids.usage_id)
|
||||
|
||||
callback_type = "xqueue_callback"
|
||||
|
||||
relative_xqueue_callback_url = reverse(
|
||||
callback_type,
|
||||
kwargs={
|
||||
"course_id": str(course_key),
|
||||
"userid": userid,
|
||||
"mod_id": mod_id,
|
||||
"dispatch": dispatch,
|
||||
},
|
||||
)
|
||||
|
||||
xqueue_callback_url_prefix = settings.XQUEUE_INTERFACE.get("callback_url", settings.LMS_ROOT_URL)
|
||||
return f"{xqueue_callback_url_prefix}{relative_xqueue_callback_url}"
|
||||
|
||||
@property
|
||||
def default_queuename(self) -> str:
|
||||
"""
|
||||
Returns the default queue name for the current course.
|
||||
"""
|
||||
course_id = self._block.scope_ids.usage_id.context_key
|
||||
return f"{course_id.org}-{course_id.course}".replace(" ", "_")
|
||||
|
||||
@property
|
||||
def waittime(self) -> int:
|
||||
"""
|
||||
Returns the number of seconds to wait in between calls to XQueue.
|
||||
"""
|
||||
return settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
|
||||
|
||||
@@ -61,8 +61,6 @@ from xmodule.util.builtin_assets import add_css_to_fragment, add_webpack_js_to_f
|
||||
from xmodule.x_module import XModuleMixin, XModuleToXBlockMixin, shim_xmodule_js
|
||||
from xmodule.xml_block import XmlMixin
|
||||
|
||||
from .capa.xqueue_interface import XQueueService
|
||||
|
||||
log = logging.getLogger("edx.courseware")
|
||||
|
||||
# Make '_' a no-op so we can scrape strings. Using lambda instead of
|
||||
@@ -144,6 +142,7 @@ class Randomization(String): # pylint: disable=too-few-public-methods
|
||||
@XBlock.needs("i18n")
|
||||
@XBlock.needs("cache")
|
||||
@XBlock.needs("sandbox")
|
||||
@XBlock.needs("xqueue")
|
||||
@XBlock.needs("replace_urls")
|
||||
@XBlock.wants("call_to_action")
|
||||
class _BuiltInProblemBlock( # pylint: disable=too-many-public-methods,too-many-instance-attributes,too-many-ancestors
|
||||
@@ -856,6 +855,7 @@ class _BuiltInProblemBlock( # pylint: disable=too-many-public-methods,too-many-
|
||||
|
||||
sandbox_service = self.runtime.service(self, "sandbox")
|
||||
cache_service = self.runtime.service(self, "cache")
|
||||
xqueue_service = self.runtime.service(self, "xqueue")
|
||||
|
||||
is_studio = getattr(self.runtime, "is_author_mode", False)
|
||||
|
||||
@@ -870,7 +870,7 @@ class _BuiltInProblemBlock( # pylint: disable=too-many-public-methods,too-many-
|
||||
render_template=render_to_string,
|
||||
resources_fs=self.runtime.resources_fs,
|
||||
seed=seed, # Why do we do this if we have self.seed?
|
||||
xqueue=None if is_studio else XQueueService(self),
|
||||
xqueue=None if is_studio else xqueue_service,
|
||||
matlab_api_key=self.matlab_api_key,
|
||||
)
|
||||
|
||||
|
||||
@@ -6,11 +6,14 @@ Module contains various XModule/XBlock services
|
||||
import inspect
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from config_models.models import ConfigurationModel
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from eventtracking import tracker
|
||||
from edx_when.field_data import DateLookupFieldData
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from xblock.reference.plugins import Service
|
||||
from xblock.runtime import KvsFieldData
|
||||
|
||||
@@ -22,9 +25,14 @@ from lms.djangoapps.courseware.field_overrides import OverrideFieldData
|
||||
from lms.djangoapps.courseware.model_data import DjangoKeyValueStore, FieldDataCache
|
||||
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
|
||||
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
|
||||
from xmodule.capa.xqueue_interface import XQueueInterface
|
||||
|
||||
from lms.djangoapps.grades.api import signals as grades_signals
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from xmodule.capa_block import ProblemBlock
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -308,3 +316,98 @@ class EventPublishingService(Service):
|
||||
# in order to avoid duplicate work and possibly conflicting semantics.
|
||||
if not getattr(block, 'has_custom_completion', False):
|
||||
self.completion_service.submit_completion(block.scope_ids.usage_id, 1.0)
|
||||
|
||||
|
||||
# .. toggle_name: send_to_submission_course.enable
|
||||
# .. toggle_implementation: CourseWaffleFlag
|
||||
# .. toggle_description: Enables use of the submissions service instead of legacy xqueue for course problem submissions.
|
||||
# .. toggle_default: False
|
||||
# .. toggle_use_cases: opt_in
|
||||
# .. toggle_creation_date: 2024-04-03
|
||||
# .. toggle_expiration_date: 2025-08-12
|
||||
# .. toggle_will_remain_in_codebase: True
|
||||
# .. toggle_tickets: none
|
||||
# .. toggle_status: supported
|
||||
SEND_TO_SUBMISSION_COURSE_FLAG = CourseWaffleFlag("send_to_submission_course.enable", __name__)
|
||||
|
||||
|
||||
class XQueueService:
|
||||
"""
|
||||
XBlock service providing an interface to the XQueue service.
|
||||
|
||||
Args:
|
||||
block: The `ProblemBlock` instance.
|
||||
"""
|
||||
|
||||
def __init__(self, block: "ProblemBlock"):
|
||||
self._block = block
|
||||
|
||||
basic_auth = settings.XQUEUE_INTERFACE.get("basic_auth")
|
||||
requests_auth = HTTPBasicAuth(*basic_auth) if basic_auth else None
|
||||
|
||||
use_submission = self.use_edx_submissions_for_xqueue()
|
||||
|
||||
self._interface = XQueueInterface(
|
||||
settings.XQUEUE_INTERFACE["url"],
|
||||
settings.XQUEUE_INTERFACE["django_auth"],
|
||||
requests_auth,
|
||||
block=block,
|
||||
use_submission_service=use_submission,
|
||||
)
|
||||
|
||||
@property
|
||||
def interface(self):
|
||||
"""
|
||||
Returns the XQueueInterface instance.
|
||||
"""
|
||||
return self._interface
|
||||
|
||||
def use_edx_submissions_for_xqueue(self) -> bool:
|
||||
"""
|
||||
Determines whether edx-submissions should be used instead of legacy XQueue.
|
||||
|
||||
This helper abstracts the toggle logic so that the rest of the codebase is not tied
|
||||
to specific feature flag mechanics or rollout strategies.
|
||||
|
||||
Returns:
|
||||
bool: True if edx-submissions should be used, False otherwise.
|
||||
"""
|
||||
return SEND_TO_SUBMISSION_COURSE_FLAG.is_enabled(self._block.scope_ids.usage_id.context_key)
|
||||
|
||||
def construct_callback(self, dispatch: str = "score_update") -> str:
|
||||
"""
|
||||
Return a fully qualified callback URL for the external queueing system.
|
||||
"""
|
||||
course_key = self._block.scope_ids.usage_id.context_key
|
||||
userid = str(self._block.scope_ids.user_id)
|
||||
mod_id = str(self._block.scope_ids.usage_id)
|
||||
|
||||
callback_type = "xqueue_callback"
|
||||
|
||||
relative_xqueue_callback_url = reverse(
|
||||
callback_type,
|
||||
kwargs={
|
||||
"course_id": str(course_key),
|
||||
"userid": userid,
|
||||
"mod_id": mod_id,
|
||||
"dispatch": dispatch,
|
||||
},
|
||||
)
|
||||
|
||||
xqueue_callback_url_prefix = settings.XQUEUE_INTERFACE.get("callback_url", settings.LMS_ROOT_URL)
|
||||
return f"{xqueue_callback_url_prefix}{relative_xqueue_callback_url}"
|
||||
|
||||
@property
|
||||
def default_queuename(self) -> str:
|
||||
"""
|
||||
Returns the default queue name for the current course.
|
||||
"""
|
||||
course_id = self._block.scope_ids.usage_id.context_key
|
||||
return f"{course_id.org}-{course_id.course}".replace(" ", "_")
|
||||
|
||||
@property
|
||||
def waittime(self) -> int:
|
||||
"""
|
||||
Returns the number of seconds to wait in between calls to XQueue.
|
||||
"""
|
||||
return settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
|
||||
|
||||
@@ -21,13 +21,13 @@ from xblock.core import XBlock
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import Reference, ReferenceList, ReferenceValueDict, ScopeIds
|
||||
|
||||
from xmodule.capa.xqueue_interface import XQueueService
|
||||
from xmodule.assetstore import AssetMetadata
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore.xml import CourseLocationManager
|
||||
from xmodule.services import XQueueService
|
||||
from xmodule.tests.helpers import StubReplaceURLService, mock_render_template, StubMakoService, StubUserService
|
||||
from xmodule.util.sandboxing import SandboxService
|
||||
from xmodule.x_module import DoNothingCache, XModuleMixin, ModuleStoreRuntime
|
||||
@@ -161,7 +161,8 @@ def get_test_system(
|
||||
'field-data': DictFieldData({}),
|
||||
'sandbox': SandboxService(contentstore, course_id),
|
||||
'video_config': VideoConfigService(),
|
||||
'discussion_config_service': DiscussionConfigService()
|
||||
'discussion_config_service': DiscussionConfigService(),
|
||||
'xqueue': XQueueService,
|
||||
}
|
||||
|
||||
descriptor_system.get_block_for_descriptor = get_block # lint-amnesty, pylint: disable=attribute-defined-outside-init
|
||||
@@ -218,7 +219,8 @@ def prepare_block_runtime(
|
||||
'field-data': DictFieldData({}),
|
||||
'sandbox': SandboxService(contentstore, course_id),
|
||||
'video_config': VideoConfigService(),
|
||||
'discussion_config_service': DiscussionConfigService()
|
||||
'discussion_config_service': DiscussionConfigService(),
|
||||
'xqueue': XQueueService,
|
||||
}
|
||||
|
||||
if add_overrides:
|
||||
|
||||
@@ -203,6 +203,7 @@ if submission[0] == '':
|
||||
|
||||
@ddt.ddt
|
||||
@skip_unless_lms
|
||||
@pytest.mark.django_db
|
||||
class ProblemBlockTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
||||
"""Tests for various problem types in XBlocks."""
|
||||
|
||||
@@ -2844,6 +2845,7 @@ class ProblemBlockTest(unittest.TestCase): # pylint: disable=too-many-public-me
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@pytest.mark.django_db
|
||||
class ProblemBlockXMLTest(unittest.TestCase):
|
||||
"""Tests XML strings for various problem types in XBlocks."""
|
||||
|
||||
@@ -3709,6 +3711,7 @@ class ComplexEncoderTest(unittest.TestCase):
|
||||
|
||||
@skip_unless_lms
|
||||
@UseUnsafeCodejail()
|
||||
@pytest.mark.django_db
|
||||
class ProblemCheckTrackingTest(unittest.TestCase):
|
||||
"""
|
||||
Ensure correct tracking information is included in events emitted during problem checks.
|
||||
|
||||
@@ -119,6 +119,7 @@ class CapaFactoryWithDelay:
|
||||
return block
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class XModuleQuizAttemptsDelayTest(unittest.TestCase):
|
||||
"""
|
||||
Class to test delay between quiz attempts.
|
||||
|
||||
@@ -2,20 +2,22 @@
|
||||
Tests for SettingsService
|
||||
"""
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from unittest import TestCase, mock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
import ddt
|
||||
|
||||
import pytest
|
||||
from config_models.models import ConfigurationModel
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from xblock.fields import ScopeIds
|
||||
from xblock.runtime import Mixologist
|
||||
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from xmodule.services import ConfigurationService, SettingsService, TeamsConfigurationService
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from openedx.core.lib.teams_config import TeamsConfig
|
||||
from xmodule.capa.xqueue_interface import XQueueInterface
|
||||
from xmodule.services import ConfigurationService, SettingsService, TeamsConfigurationService, XQueueService
|
||||
|
||||
|
||||
class _DummyBlock:
|
||||
@@ -163,3 +165,71 @@ class TestTeamsConfigurationService(ConfigurationServiceBaseClass):
|
||||
def test_get_teamsconfiguration(self):
|
||||
teams_config = self.configuration_service.get_teams_configuration(self.course.id)
|
||||
assert teams_config == self.teams_config
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@skip_unless_lms
|
||||
class XQueueServiceTest(TestCase):
|
||||
"""Test the XQueue service methods."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
location = BlockUsageLocator(
|
||||
CourseLocator("test_org", "test_course", "test_run"),
|
||||
"problem",
|
||||
"ExampleProblem",
|
||||
)
|
||||
self.block = Mock(scope_ids=ScopeIds("user1", "mock_problem", location, location))
|
||||
self.block.max_score = Mock(return_value=10) # Mock max_score method
|
||||
self.service = XQueueService(self.block)
|
||||
|
||||
def test_interface(self):
|
||||
"""Test that the `XQUEUE_INTERFACE` settings are passed from the service to the interface."""
|
||||
assert isinstance(self.service.interface, XQueueInterface)
|
||||
assert self.service.interface.url == "http://sandbox-xqueue.edx.org"
|
||||
assert self.service.interface.auth["username"] == "lms"
|
||||
assert self.service.interface.auth["password"] == "***REMOVED***"
|
||||
assert self.service.interface.session.auth.username == "anant"
|
||||
assert self.service.interface.session.auth.password == "agarwal"
|
||||
|
||||
@patch("xmodule.services.XQueueService.use_edx_submissions_for_xqueue", return_value=True)
|
||||
def test_construct_callback_with_flag_enabled(self, mock_flag): # pylint: disable=unused-argument
|
||||
"""Test construct_callback when the waffle flag is enabled."""
|
||||
self.service = XQueueService(self.block)
|
||||
usage_id = self.block.scope_ids.usage_id
|
||||
course_id = str(usage_id.course_key)
|
||||
callback_url = f"courses/{course_id}/xqueue/user1/{usage_id}"
|
||||
|
||||
assert self.service.construct_callback() == f"{settings.LMS_ROOT_URL}/{callback_url}/score_update"
|
||||
assert self.service.construct_callback("alt_dispatch") == (
|
||||
f"{settings.LMS_ROOT_URL}/{callback_url}/alt_dispatch"
|
||||
)
|
||||
|
||||
custom_callback_url = "http://alt.url"
|
||||
with override_settings(XQUEUE_INTERFACE={**settings.XQUEUE_INTERFACE, "callback_url": custom_callback_url}):
|
||||
assert self.service.construct_callback() == f"{custom_callback_url}/{callback_url}/score_update"
|
||||
|
||||
@patch("xmodule.services.XQueueService.use_edx_submissions_for_xqueue", return_value=False)
|
||||
def test_construct_callback_with_flag_disabled(self, mock_flag): # pylint: disable=unused-argument
|
||||
"""Test construct_callback when the waffle flag is disabled."""
|
||||
self.service = XQueueService(self.block)
|
||||
usage_id = self.block.scope_ids.usage_id
|
||||
callback_url = f"courses/{usage_id.context_key}/xqueue/user1/{usage_id}"
|
||||
|
||||
assert self.service.construct_callback() == f"{settings.LMS_ROOT_URL}/{callback_url}/score_update"
|
||||
assert self.service.construct_callback("alt_dispatch") == f"{settings.LMS_ROOT_URL}/{callback_url}/alt_dispatch"
|
||||
|
||||
custom_callback_url = "http://alt.url"
|
||||
with override_settings(XQUEUE_INTERFACE={**settings.XQUEUE_INTERFACE, "callback_url": custom_callback_url}):
|
||||
assert self.service.construct_callback() == f"{custom_callback_url}/{callback_url}/score_update"
|
||||
|
||||
def test_default_queuename(self):
|
||||
"""Check the format of the default queue name."""
|
||||
assert self.service.default_queuename == "test_org-test_course"
|
||||
|
||||
def test_waittime(self):
|
||||
"""Check that the time between requests is retrieved correctly from the settings."""
|
||||
assert self.service.waittime == 5
|
||||
|
||||
with override_settings(XQUEUE_WAITTIME_BETWEEN_REQUESTS=15):
|
||||
assert self.service.waittime == 15
|
||||
|
||||
Reference in New Issue
Block a user