Commit d2146978 by Ned Batchelder

Give LoncapaProblem its own LoncapaSystem instead of ModuleSystem

LoncapaProblem was using ModuleSystem directly, but we're about to need
i18n passed in, so this gives LoncapaProblem its own LoncapaSystem
object to better module its dependencies.  This commit simply passes
through the attributes of ModuleSystem that Loncapa needs.

[LMS-1597]
parent 6c0ac477
......@@ -63,37 +63,73 @@ log = logging.getLogger(__name__)
# main class for this module
class LoncapaSystem(object):
"""
An encapsulation of resources needed from the outside.
These interfaces are collected here so that a caller of LoncapaProblem
can provide these resources however make sense for their environment, and
this code can remain independent.
See :class:`ModuleSystem` for documentation of these attributes.
"""
def __init__( # pylint: disable=invalid-name
self,
ajax_url,
anonymous_student_id,
cache,
can_execute_unsafe_code,
DEBUG, # pylint: disable=invalid-name
filestore,
node_path,
render_template,
seed, # Why do we do this if we have self.seed?
STATIC_URL, # pylint: disable=invalid-name
xqueue,
):
self.ajax_url = ajax_url
self.anonymous_student_id = anonymous_student_id
self.cache = cache
self.can_execute_unsafe_code = can_execute_unsafe_code
self.DEBUG = DEBUG # pylint: disable=invalid-name
self.filestore = filestore
self.node_path = node_path
self.render_template = render_template
self.seed = seed # Why do we do this if we have self.seed?
self.STATIC_URL = STATIC_URL # pylint: disable=invalid-name
self.xqueue = xqueue
class LoncapaProblem(object):
'''
Main class for capa Problems.
'''
def __init__(self, problem_text, id, state=None, seed=None, system=None):
'''
def __init__(self, problem_text, id, capa_system, state=None, seed=None):
"""
Initializes capa Problem.
Arguments:
- problem_text (string): xml defining the problem
- id (string): identifier for this problem; often a filename (no spaces)
- seed (int): random number generator seed (int)
- state (dict): containing the following keys:
- 'seed' - (int) random number generator seed
- 'student_answers' - (dict) maps input id to the stored answer for that input
- 'correct_map' (CorrectMap) a map of each input to their 'correctness'
- 'done' - (bool) indicates whether or not this problem is considered done
- 'input_state' - (dict) maps input_id to a dictionary that holds the state for that input
- system (ModuleSystem): ModuleSystem instance which provides OS,
rendering, and user context
problem_text (string): xml defining the problem.
id (string): identifier for this problem, often a filename (no spaces).
capa_system (LoncapaSystem): LoncapaSystem instance which provides OS,
rendering, user context, and other resources.
state (dict): containing the following keys:
- `seed` (int) random number generator seed
- `student_answers` (dict) maps input id to the stored answer for that input
- `correct_map` (CorrectMap) a map of each input to their 'correctness'
- `done` (bool) indicates whether or not this problem is considered done
- `input_state` (dict) maps input_id to a dictionary that holds the state for that input
seed (int): random number generator seed.
'''
"""
## Initialize class variables from state
self.do_reset()
self.problem_id = id
self.system = system
if self.system is None:
raise Exception()
self.capa_system = capa_system
state = state or {}
......@@ -412,8 +448,8 @@ class LoncapaProblem(object):
filename = inc.get('file')
if filename is not None:
try:
# open using ModuleSystem OSFS filestore
ifp = self.system.filestore.open(filename)
# open using LoncapaSystem OSFS filestore
ifp = self.capa_system.filestore.open(filename)
except Exception as err:
log.warning(
'Error %s in problem xml include: %s' % (
......@@ -422,12 +458,12 @@ class LoncapaProblem(object):
)
log.warning(
'Cannot find file %s in %s' % (
filename, self.system.filestore
filename, self.capa_system.filestore
)
)
# if debugging, don't fail - just log error
# TODO (vshnayder): need real error handling, display to users
if not self.system.get('DEBUG'):
if not self.capa_system.DEBUG:
raise
else:
continue
......@@ -443,7 +479,7 @@ class LoncapaProblem(object):
log.warning('Cannot parse XML in %s' % (filename))
# if debugging, don't fail - just log error
# TODO (vshnayder): same as above
if not self.system.get('DEBUG'):
if not self.capa_system.DEBUG:
raise
else:
continue
......@@ -476,9 +512,9 @@ class LoncapaProblem(object):
continue
# path is an absolute path or a path relative to the data dir
dir = os.path.join(self.system.filestore.root_path, dir)
dir = os.path.join(self.capa_system.filestore.root_path, dir)
# Check that we are within the filestore tree.
reldir = os.path.relpath(dir, self.system.filestore.root_path)
reldir = os.path.relpath(dir, self.capa_system.filestore.root_path)
if ".." in reldir:
log.warning("Ignoring Python directory outside of course: %r" % dir)
continue
......@@ -527,9 +563,9 @@ class LoncapaProblem(object):
context,
random_seed=self.seed,
python_path=python_path,
cache=self.system.cache,
cache=self.capa_system.cache,
slug=self.problem_id,
unsafely=self.system.can_execute_unsafe_code(),
unsafely=self.capa_system.can_execute_unsafe_code(),
)
except Exception as err:
log.exception("Error while execing script code: " + all_code)
......@@ -600,7 +636,7 @@ class LoncapaProblem(object):
input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag)
# save the input type so that we can make ajax calls on it if we need to
self.inputs[input_id] = input_type_cls(self.system, problemtree, state)
self.inputs[input_id] = input_type_cls(self.capa_system, problemtree, state)
return self.inputs[input_id].get_html()
# let each Response render itself
......@@ -613,7 +649,7 @@ class LoncapaProblem(object):
# let each custom renderer render itself:
if problemtree.tag in customrender.registry.registered_tags():
renderer_class = customrender.registry.get_class_for_tag(problemtree.tag)
renderer = renderer_class(self.system, problemtree)
renderer = renderer_class(self.capa_system, problemtree)
return renderer.get_html()
# otherwise, render children recursively, and copy over attributes
......@@ -670,7 +706,7 @@ class LoncapaProblem(object):
# instantiate capa Response
responsetype_cls = responsetypes.registry.get_class_for_tag(response.tag)
responder = responsetype_cls(response, inputfields, self.context, self.system)
responder = responsetype_cls(response, inputfields, self.context, self.capa_system)
# save in list in self
self.responders[response] = responder
......
......@@ -128,7 +128,7 @@ class InputTypeBase(object):
"""
Instantiate an InputType class. Arguments:
- system : ModuleSystem instance which provides OS, rendering, and user context.
- system : LoncapaModule instance which provides OS, rendering, and user context.
Specifically, must have a render_template function.
- xml : Element tree of this Input element
- state : a dictionary with optional keys:
......@@ -146,7 +146,7 @@ class InputTypeBase(object):
self.xml = xml
self.tag = xml.tag
self.system = system
self.capa_system = system
# NOTE: ID should only come from one place. If it comes from multiple,
# we use state first, XML second (in case the xml changed, but we have
......@@ -257,7 +257,7 @@ class InputTypeBase(object):
'value': self.value,
'status': self.status,
'msg': self.msg,
'STATIC_URL': self.system.STATIC_URL,
'STATIC_URL': self.capa_system.STATIC_URL,
}
context.update((a, v) for (
a, v) in self.loaded_attributes.iteritems() if a in self.to_render)
......@@ -282,7 +282,7 @@ class InputTypeBase(object):
context = self._get_render_context()
html = self.system.render_template(self.template, context)
html = self.capa_system.render_template(self.template, context)
return etree.XML(html)
......@@ -505,9 +505,9 @@ class JSInput(InputTypeBase):
def _extra_context(self):
context = {
'jschannel_loader': '{static_url}js/capa/src/jschannel.js'.format(
static_url=self.system.STATIC_URL),
static_url=self.capa_system.STATIC_URL),
'jsinput_loader': '{static_url}js/capa/src/jsinput.js'.format(
static_url=self.system.STATIC_URL),
static_url=self.capa_system.STATIC_URL),
'saved_state': self.value
}
......@@ -822,18 +822,19 @@ class MatlabInput(CodeInput):
- 'message' - message to be rendered in case of error
'''
# only send data if xqueue exists
if self.system.xqueue is None:
if self.capa_system.xqueue is None:
return {'success': False, 'message': 'Cannot connect to the queue'}
# pull relevant info out of get
response = data['submission']
# construct xqueue headers
qinterface = self.system.xqueue['interface']
qinterface = self.capa_system.xqueue['interface']
qtime = datetime.utcnow().strftime(xqueue_interface.dateformat)
callback_url = self.system.xqueue['construct_callback']('ungraded_response')
anonymous_student_id = self.system.anonymous_student_id
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
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 +
anonymous_student_id +
self.input_id)
xheader = xqueue_interface.make_xheader(
......@@ -1006,7 +1007,7 @@ class ChemicalEquationInput(InputTypeBase):
"""
return {
'previewer': '{static_url}js/capa/chemical_equation_preview.js'.format(
static_url=self.system.STATIC_URL),
static_url=self.capa_system.STATIC_URL),
}
def handle_ajax(self, dispatch, data):
......@@ -1091,7 +1092,7 @@ class FormulaEquationInput(InputTypeBase):
return {
'previewer': '{static_url}js/capa/src/formula_equation_preview.js'.format(
static_url=self.system.STATIC_URL),
static_url=self.capa_system.STATIC_URL),
'reported_status': reported_status,
}
......@@ -1274,7 +1275,7 @@ class EditAMoleculeInput(InputTypeBase):
"""
context = {
'applet_loader': '{static_url}js/capa/editamolecule.js'.format(
static_url=self.system.STATIC_URL),
static_url=self.capa_system.STATIC_URL),
}
return context
......@@ -1310,7 +1311,7 @@ class DesignProtein2dInput(InputTypeBase):
"""
context = {
'applet_loader': '{static_url}js/capa/design-protein-2d.js'.format(
static_url=self.system.STATIC_URL),
static_url=self.capa_system.STATIC_URL),
}
return context
......@@ -1346,7 +1347,7 @@ class EditAGeneInput(InputTypeBase):
"""
context = {
'applet_loader': '{static_url}js/capa/edit-a-gene.js'.format(
static_url=self.system.STATIC_URL),
static_url=self.capa_system.STATIC_URL),
}
return context
......
import fs.osfs
"""Tools for helping with testing capa."""
import os
import os.path
from capa.capa_problem import LoncapaProblem
from xmodule.x_module import ModuleSystem
import fs.osfs
from capa.capa_problem import LoncapaProblem, LoncapaSystem
from mock import Mock, MagicMock
import xml.sax.saxutils as saxutils
......@@ -26,34 +28,28 @@ xqueue_interface = MagicMock()
xqueue_interface.send_to_queue.return_value = (0, 'Success!')
def test_system():
def test_capa_system():
"""
Construct a mock ModuleSystem instance.
Construct a mock LoncapaSystem instance.
"""
the_system = Mock(
spec=ModuleSystem,
spec=LoncapaSystem,
ajax_url='/dummy-ajax-url',
STATIC_URL='/dummy-static/',
anonymous_student_id='student',
cache=None,
can_execute_unsafe_code=lambda: False,
DEBUG=True,
track_function=Mock(),
get_module=Mock(),
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
render_template=tst_render_template,
replace_urls=Mock(),
user=Mock(),
seed=0,
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
debug=True,
hostname="edx.org",
STATIC_URL='/dummy-static/',
xqueue={'interface': xqueue_interface, 'construct_callback': calledback_url, 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id='student',
cache=None,
can_execute_unsafe_code=lambda: False,
)
return the_system
def new_loncapa_problem(xml, system=None):
def new_loncapa_problem(xml, capa_system=None):
"""Construct a `LoncapaProblem` suitable for unit tests."""
return LoncapaProblem(xml, id='1', seed=723, system=system or test_system())
return LoncapaProblem(xml, id='1', seed=723, capa_system=capa_system or test_capa_system())
......@@ -2,7 +2,7 @@ from lxml import etree
import unittest
import xml.sax.saxutils as saxutils
from . import test_system
from . import test_capa_system
from capa import customrender
# just a handy shortcut
......@@ -11,7 +11,7 @@ lookup_tag = customrender.registry.get_class_for_tag
def extract_context(xml):
"""
Given an xml element corresponding to the output of test_system.render_template, get back the
Given an xml element corresponding to the output of test_capa_system.render_template, get back the
original context
"""
return eval(xml.text)
......@@ -26,7 +26,7 @@ class HelperTest(unittest.TestCase):
Make sure that our helper function works!
'''
def check(self, d):
xml = etree.XML(test_system().render_template('blah', d))
xml = etree.XML(test_capa_system().render_template('blah', d))
self.assertEqual(d, extract_context(xml))
def test_extract_context(self):
......@@ -46,11 +46,11 @@ class SolutionRenderTest(unittest.TestCase):
xml_str = """<solution id="solution_12">{s}</solution>""".format(s=solution)
element = etree.fromstring(xml_str)
renderer = lookup_tag('solution')(test_system(), element)
renderer = lookup_tag('solution')(test_capa_system(), element)
self.assertEqual(renderer.id, 'solution_12')
# Our test_system "renders" templates to a div with the repr of the context.
# Our test_capa_system "renders" templates to a div with the repr of the context.
xml = renderer.get_html()
context = extract_context(xml)
self.assertEqual(context, {'id': 'solution_12'})
......@@ -65,7 +65,7 @@ class MathRenderTest(unittest.TestCase):
xml_str = """<math>{tex}</math>""".format(tex=latex_in)
element = etree.fromstring(xml_str)
renderer = lookup_tag('math')(test_system(), element)
renderer = lookup_tag('math')(test_capa_system(), element)
self.assertEqual(renderer.mathstr, mathjax_out)
......
......@@ -6,14 +6,14 @@ import textwrap
import mock
from .response_xml_factory import StringResponseXMLFactory, CustomResponseXMLFactory
from . import test_system, new_loncapa_problem
from . import test_capa_system, new_loncapa_problem
class CapaHtmlRenderTest(unittest.TestCase):
def setUp(self):
super(CapaHtmlRenderTest, self).setUp()
self.system = test_system()
self.capa_system = test_capa_system()
def test_blank_problem(self):
"""
......@@ -44,7 +44,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
""")
# Create the problem
problem = new_loncapa_problem(xml_str, system=self.system)
problem = new_loncapa_problem(xml_str, capa_system=self.capa_system)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
......@@ -119,12 +119,12 @@ class CapaHtmlRenderTest(unittest.TestCase):
xml_str = StringResponseXMLFactory().build_xml(**kwargs)
# Mock out the template renderer
the_system = test_system()
the_system = test_capa_system()
the_system.render_template = mock.Mock()
the_system.render_template.return_value = "<div>Input Template Render</div>"
# Create the problem and render the HTML
problem = new_loncapa_problem(xml_str, system=the_system)
problem = new_loncapa_problem(xml_str, capa_system=the_system)
rendered_html = etree.XML(problem.get_html())
# Expect problem has been turned into a <div>
......@@ -253,7 +253,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
self.assertRegexpMatches(the_html, r"<div>\s+</div>")
def _create_test_file(self, path, content_str):
test_fp = self.system.filestore.open(path, "w")
test_fp = self.capa_system.filestore.open(path, "w")
test_fp.write(content_str)
test_fp.close()
......
......@@ -13,7 +13,7 @@ import textwrap
import requests
import mock
from . import new_loncapa_problem, test_system
from . import new_loncapa_problem, test_capa_system
import calc
from capa.responsetypes import LoncapaProblemError, \
......@@ -37,9 +37,9 @@ class ResponseTest(unittest.TestCase):
if self.xml_factory_class:
self.xml_factory = self.xml_factory_class()
def build_problem(self, system=None, **kwargs):
def build_problem(self, capa_system=None, **kwargs):
xml = self.xml_factory.build_xml(**kwargs)
return new_loncapa_problem(xml, system=system)
return new_loncapa_problem(xml, capa_system=capa_system)
def assert_grade(self, problem, submission, expected_correctness, msg=None):
input_dict = {'1_2_1': submission}
......@@ -1022,10 +1022,10 @@ class JavascriptResponseTest(ResponseTest):
coffee_file_path = os.path.dirname(__file__) + "/test_files/js/*.coffee"
os.system("node_modules/.bin/coffee -c %s" % (coffee_file_path))
system = test_system()
system.can_execute_unsafe_code = lambda: True
capa_system = test_capa_system()
capa_system.can_execute_unsafe_code = lambda: True
problem = self.build_problem(
system=system,
capa_system=capa_system,
generator_src="test_problem_generator.js",
grader_src="test_problem_grader.js",
display_class="TestProblemDisplay",
......@@ -1040,12 +1040,12 @@ class JavascriptResponseTest(ResponseTest):
def test_cant_execute_javascript(self):
# If the system says to disallow unsafe code execution, then making
# this problem will raise an exception.
system = test_system()
system.can_execute_unsafe_code = lambda: False
capa_system = test_capa_system()
capa_system.can_execute_unsafe_code = lambda: False
with self.assertRaises(LoncapaProblemError):
self.build_problem(
system=system,
capa_system=capa_system,
generator_src="test_problem_generator.js",
grader_src="test_problem_grader.js",
display_class="TestProblemDisplay",
......
......@@ -11,7 +11,7 @@ import sys
from pkg_resources import resource_string
from capa.capa_problem import LoncapaProblem
from capa.capa_problem import LoncapaProblem, LoncapaSystem
from capa.responsetypes import StudentInputError, \
ResponseError, LoncapaProblemError
from capa.util import convert_files_to_filenames
......@@ -260,12 +260,26 @@ class CapaMixin(CapaFields):
if text is None:
text = self.data
capa_system = LoncapaSystem(
ajax_url=self.system.ajax_url,
anonymous_student_id=self.system.anonymous_student_id,
cache=self.system.cache,
can_execute_unsafe_code=self.system.can_execute_unsafe_code,
DEBUG=self.system.DEBUG,
filestore=self.system.filestore,
node_path=self.system.node_path,
render_template=self.system.render_template,
seed=self.system.seed, # Why do we do this if we have self.seed?
STATIC_URL=self.system.STATIC_URL,
xqueue=self.system.xqueue,
)
return LoncapaProblem(
problem_text=text,
id=self.location.html_id(),
state=state,
seed=self.seed,
system=self.runtime,
capa_system=capa_system,
)
def get_state_for_lcp(self):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment