Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cms/djangoapps/contentstore/views/tests/test_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,13 @@ def test_render_template(self):
descriptor = ItemFactory(category="pure", parent=self.course)
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(),
)
assert runtime.xqueue is None
assert runtime.service(descriptor, 'xqueue') is None
1 change: 1 addition & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@
CSRF_TRUSTED_ORIGINS = []

#################### CAPA External Code Evaluation #############################
XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
XQUEUE_INTERFACE = {
'url': 'http://localhost:18040',
'basic_auth': ['edx', 'edx'],
Expand Down
4 changes: 2 additions & 2 deletions common/lib/capa/capa/inputtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,9 +994,9 @@ def _plot_data(self, data):
response = data['submission']

# construct xqueue headers
qinterface = self.capa_system.xqueue['interface']
qinterface = self.capa_system.xqueue.interface
qtime = datetime.utcnow().strftime(xqueue_interface.dateformat)
callback_url = self.capa_system.xqueue['construct_callback']('ungraded_response')
callback_url = self.capa_system.xqueue.construct_callback('ungraded_response')
anonymous_student_id = self.capa_system.anonymous_student_id
# TODO: Why is this using self.capa_system.seed when we have self.seed???
queuekey = xqueue_interface.make_hashkey(str(self.capa_system.seed) + qtime +
Expand Down
16 changes: 8 additions & 8 deletions common/lib/capa/capa/responsetypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2586,14 +2586,14 @@ class CodeResponse(LoncapaResponse):
"""
Grade student code using an external queueing server, called 'xqueue'.

Expects 'xqueue' dict in LoncapaSystem with the following keys that are
Expects 'xqueue' dict in LoncapaSystem with the following properties that are
needed by CodeResponse::

capa_system.xqueue = {
'interface': XQueueInterface object.
'construct_callback': Per-StudentModule callback URL constructor,
capa_system.xqueue = object with properties:
interface: XQueueInterface object.
construct_callback: Per-StudentModule callback URL constructor,
defaults to using 'score_update' as the correct dispatch (function).
'default_queuename': Default queue name to submit request (string).
default_queuename: Default queue name to submit request (string).
}

External requests are only submitted for student submission grading, not
Expand Down Expand Up @@ -2623,7 +2623,7 @@ def setup_response(self):

# We do not support xqueue within Studio.
if self.capa_system.xqueue is not None:
default_queuename = self.capa_system.xqueue['default_queuename']
default_queuename = self.capa_system.xqueue.default_queuename
else:
default_queuename = None
self.queue_name = xml.get('queuename', default_queuename)
Expand Down Expand Up @@ -2684,7 +2684,7 @@ def get_score(self, student_answers):
# Prepare xqueue request
#------------------------------------------------------------

qinterface = self.capa_system.xqueue['interface']
qinterface = self.capa_system.xqueue.interface
qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat)

anonymous_student_id = self.capa_system.anonymous_student_id
Expand All @@ -2693,7 +2693,7 @@ def get_score(self, student_answers):
queuekey = xqueue_interface.make_hashkey(
str(self.capa_system.seed) + qtime + anonymous_student_id + self.answer_id
)
callback_url = self.capa_system.xqueue['construct_callback']()
callback_url = self.capa_system.xqueue.construct_callback()
xheader = xqueue_interface.make_xheader(
lms_callback_url=callback_url,
lms_key=queuekey,
Expand Down
24 changes: 13 additions & 11 deletions common/lib/capa/capa/tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,19 @@ def tst_render_template(template, context): # pylint: disable=unused-argument
return '<div>{0}</div>'.format(saxutils.escape(repr(context)))


def calledback_url(dispatch='score_update'):
"""A callback url method to use in tests."""
return dispatch
class StubXQueueService:
"""
Stubs out the XQueueService for Capa problem tests.
"""
def __init__(self):
self.interface = MagicMock()
self.interface.send_to_queue.return_value = (0, 'Success!')
self.default_queuename = 'testqueue'
self.waittime = 10

xqueue_interface = MagicMock() # pylint: disable=invalid-name
xqueue_interface.send_to_queue.return_value = (0, 'Success!')
def construct_callback(self, dispatch='score_update'):
"""A callback url method to use in tests."""
return dispatch


def test_capa_system(render_template=None):
Expand All @@ -72,12 +79,7 @@ def test_capa_system(render_template=None):
seed=0,
STATIC_URL='/dummy-static/',
STATUS_CLASS=Status,
xqueue={
'interface': xqueue_interface,
'construct_callback': calledback_url,
'default_queuename': 'testqueue',
'waittime': 10
},
xqueue=StubXQueueService(),
)
return the_system

Expand Down
8 changes: 3 additions & 5 deletions common/lib/capa/capa/tests/test_inputtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,17 +643,15 @@ def test_rendering_while_queued(self, time): # lint-amnesty, pylint: disable=un
def test_plot_data(self):
data = {'submission': 'x = 1234;'}
response = self.the_input.handle_ajax("plot", data)

test_capa_system().xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY)

self.the_input.capa_system.xqueue.interface.send_to_queue.assert_called_with(header=ANY, body=ANY)
assert response['success']
assert self.the_input.input_state['queuekey'] is not None
assert self.the_input.input_state['queuestate'] == 'queued'

def test_plot_data_failure(self):
data = {'submission': 'x = 1234;'}
error_message = 'Error message!'
test_capa_system().xqueue['interface'].send_to_queue.return_value = (1, error_message)
self.the_input.capa_system.xqueue.interface.send_to_queue.return_value = (1, error_message)
response = self.the_input.handle_ajax("plot", data)
assert not response['success']
assert response['message'] == error_message
Expand Down Expand Up @@ -740,7 +738,7 @@ def test_matlab_api_key(self):
data = {'submission': 'x = 1234;'}
response = the_input.handle_ajax("plot", data) # lint-amnesty, pylint: disable=unused-variable

body = system.xqueue['interface'].send_to_queue.call_args[1]['body']
body = system.xqueue.interface.send_to_queue.call_args[1]['body']
payload = json.loads(body)
assert 'test_api_key' == payload['token']
assert '2' == payload['endpoint_version']
Expand Down
41 changes: 41 additions & 0 deletions common/lib/capa/capa/tests/test_xqueue_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
"""
Tests the xqueue service interface.
"""

from unittest import TestCase
from django.conf import settings

from capa.xqueue_interface import XQueueInterface, XQueueService


class XQueueServiceTest(TestCase):
"""
Tests the XQueue service methods.
"""
@staticmethod
def construct_callback(*args, **kwargs):
return 'https://lms.url/callback'

def setUp(self):
super().setUp()
self.service = XQueueService(
url=settings.XQUEUE_INTERFACE['url'],
django_auth=settings.XQUEUE_INTERFACE['django_auth'],
basic_auth=settings.XQUEUE_INTERFACE['basic_auth'],
construct_callback=self.construct_callback,
default_queuename='my-very-own-queue',
waittime=settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS,
)

def test_interface(self):
assert isinstance(self.service.interface, XQueueInterface)

def test_construct_callback(self):
assert self.service.construct_callback() == 'https://lms.url/callback'

def test_default_queuename(self):
assert self.service.default_queuename == 'my-very-own-queue'

def test_waittime(self):
assert self.service.waittime == 5
57 changes: 53 additions & 4 deletions common/lib/capa/capa/xqueue_interface.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# lint-amnesty, pylint: disable=missing-module-docstring
# LMS Interface to external queueing system (xqueue)
#

"""
LMS Interface to external queueing system (xqueue)
"""

import hashlib
import json
Expand Down Expand Up @@ -149,3 +148,53 @@ def _http_post(self, url, data, files=None): # lint-amnesty, pylint: disable=mi
return 1, 'unexpected HTTP status code [%d]' % response.status_code

return parse_xreply(response.text)


class XQueueService:
"""
XBlock service providing an interface to the XQueue service.

Args:
construct_callback(callable): function which constructs a fully-qualified callback URL to make xqueue requests.
default_queuename(string): course-specific queue name.
waittime(int): number of seconds to wait between xqueue requests
url(string): base URL for the XQueue service.
django_auth(dict): username and password for the XQueue service.
basic_auth(array or None): basic authentication credentials, if needed.
"""
def __init__(self, construct_callback, default_queuename, waittime, url, django_auth, basic_auth=None):

requests_auth = requests.auth.HTTPBasicAuth(*basic_auth) if basic_auth else None
self._interface = XQueueInterface(url, django_auth, requests_auth)

self._construct_callback = construct_callback
self._default_queuename = default_queuename.replace(' ', '_')
self._waittime = waittime

@property
def interface(self):
"""
Returns the XQueueInterface instance.
"""
return self._interface

@property
def construct_callback(self):
"""
Returns the function to construct the XQueue callback.
"""
return self._construct_callback

@property
def default_queuename(self):
"""
Returns the default queue name for the current course.
"""
return self._default_queuename

@property
def waittime(self):
"""
Returns the number of seconds to wait in between calls to XQueue.
"""
return self._waittime
7 changes: 5 additions & 2 deletions common/lib/xmodule/xmodule/capa_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ def from_json(self, value):
@XBlock.needs('user')
@XBlock.needs('i18n')
@XBlock.needs('mako')
# Studio doesn't provide XQueueService, but the LMS does.
@XBlock.wants('xqueue')
@XBlock.wants('call_to_action')
class ProblemBlock(
ScorableXBlockMixin,
Expand Down Expand Up @@ -825,7 +827,7 @@ def new_lcp(self, state, text=None):
render_template=self.runtime.service(self, 'mako').render_template,
seed=seed, # Why do we do this if we have self.seed?
STATIC_URL=self.runtime.STATIC_URL,
xqueue=self.runtime.xqueue,
xqueue=self.runtime.service(self, 'xqueue'),
matlab_api_key=self.matlab_api_key
)

Expand Down Expand Up @@ -1744,7 +1746,8 @@ def submit_problem(self, data, override_time=False):
if self.lcp.is_queued():
prev_submit_time = self.lcp.get_recentmost_queuetime()

waittime_between_requests = self.runtime.xqueue['waittime']
xqueue_service = self.runtime.service(self, 'xqueue')
waittime_between_requests = xqueue_service.waittime if xqueue_service else 0
if (current_time - prev_submit_time).total_seconds() < waittime_between_requests:
msg = _("You must wait at least {wait} seconds between submissions.").format(
wait=waittime_between_requests)
Expand Down
16 changes: 9 additions & 7 deletions common/lib/xmodule/xmodule/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from xblock.field_data import DictFieldData
from xblock.fields import Reference, ReferenceList, ReferenceValueDict, ScopeIds

from capa.xqueue_interface import XQueueService
from xmodule.assetstore import AssetMetadata
from xmodule.error_module import ErrorBlock
from xmodule.mako_module import MakoDescriptorSystem
Expand Down Expand Up @@ -140,13 +141,14 @@ def get_module(descriptor):
services={
'user': user_service,
'mako': mako_service,
},
xqueue={
'interface': None,
'callback_url': '/',
'default_queuename': 'testqueue',
'waittime': 10,
'construct_callback': Mock(name='get_test_system.xqueue.construct_callback', side_effect="/"),
'xqueue': XQueueService(
url='http://xqueue.url',
django_auth={},
basic_auth=[],
default_queuename='testqueue',
waittime=10,
construct_callback=Mock(name='get_test_system.xqueue.construct_callback', side_effect="/"),
),
},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
course_id=course_id,
Expand Down
Loading