diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index c1eb078a20..209c7ddd50 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -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
diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py
index e81390724d..af9619296e 100644
--- a/common/lib/capa/capa/inputtypes.py
+++ b/common/lib/capa/capa/inputtypes.py
@@ -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
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index 3bdd154525..5db1f61920 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -129,20 +129,20 @@ class LoncapaResponse(object):
allowed_inputfields = []
required_attributes = []
- def __init__(self, xml, inputfields, context, system=None):
+ def __init__(self, xml, inputfields, context, system):
'''
Init is passed the following arguments:
- xml : ElementTree of this Response
- inputfields : ordered list of ElementTrees for each input entry field in this Response
- context : script processor context
- - system : ModuleSystem instance which provides OS, rendering, and user context
+ - system : LoncapaSystem instance which provides OS, rendering, and user context
'''
self.xml = xml
self.inputfields = inputfields
self.context = context
- self.system = system
+ self.capa_system = system
self.id = xml.get('id')
@@ -298,7 +298,7 @@ class LoncapaResponse(object):
python_path=self.context['python_path'],
slug=self.id,
random_seed=self.context['seed'],
- unsafely=self.system.can_execute_unsafe_code(),
+ unsafely=self.capa_system.can_execute_unsafe_code(),
)
except Exception as err:
msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
@@ -444,7 +444,7 @@ class JavascriptResponse(LoncapaResponse):
# manually being compiled to DATA_DIR/js/compiled.
# latestTimestamp = 0
- # basepath = self.system.filestore.root_path + '/js/'
+ # basepath = self.capa_system.filestore.root_path + '/js/'
# for filename in (self.display_dependencies + [self.display]):
# filepath = basepath + filename
# timestamp = os.stat(filepath).st_mtime
@@ -467,7 +467,7 @@ class JavascriptResponse(LoncapaResponse):
# outfile.close()
# TODO this should also be fixed when the above is fixed.
- filename = self.system.ajax_url.split('/')[-1] + '.js'
+ filename = self.capa_system.ajax_url.split('/')[-1] + '.js'
self.display_filename = 'compiled/' + filename
def parse_xml(self):
@@ -510,16 +510,16 @@ class JavascriptResponse(LoncapaResponse):
def get_node_env(self):
- js_dir = os.path.join(self.system.filestore.root_path, 'js')
+ js_dir = os.path.join(self.capa_system.filestore.root_path, 'js')
tmp_env = os.environ.copy()
- node_path = self.system.node_path + ":" + os.path.normpath(js_dir)
+ node_path = self.capa_system.node_path + ":" + os.path.normpath(js_dir)
tmp_env["NODE_PATH"] = node_path
return tmp_env
def call_node(self, args):
- # Node.js code is un-sandboxed. If the XModuleSystem says we aren't
+ # Node.js code is un-sandboxed. If the LoncapaSystem says we aren't
# allowed to run unsafe code, then stop now.
- if not self.system.can_execute_unsafe_code():
+ if not self.capa_system.can_execute_unsafe_code():
raise LoncapaProblemError("Execution of unsafe Javascript code is not allowed.")
subprocess_args = ["node"]
@@ -1154,7 +1154,7 @@ class CustomResponse(LoncapaResponse):
python_path=self.context['python_path'],
slug=self.id,
random_seed=self.context['seed'],
- unsafely=self.system.can_execute_unsafe_code(),
+ unsafely=self.capa_system.can_execute_unsafe_code(),
)
return globals_dict['cfn_return']
return check_function
@@ -1169,8 +1169,8 @@ class CustomResponse(LoncapaResponse):
else:
answer_src = answer.get('src')
if answer_src is not None:
- self.code = self.system.filesystem.open(
- 'src/' + answer_src).read()
+ # TODO: this code seems not to be used any more since self.capa_system.filesystem doesn't exist.
+ self.code = self.capa_system.filesystem.open('src/' + answer_src).read()
else:
self.code = answer.text
@@ -1249,8 +1249,8 @@ class CustomResponse(LoncapaResponse):
'testdat': 'hello world',
})
- # pass self.system.debug to cfn
- self.context['debug'] = self.system.DEBUG
+ # Pass DEBUG to the check function.
+ self.context['debug'] = self.capa_system.DEBUG
# Run the check function
self.execute_check_function(idset, submission)
@@ -1275,10 +1275,10 @@ class CustomResponse(LoncapaResponse):
safe_exec.safe_exec(
self.code,
self.context,
- cache=self.system.cache,
+ cache=self.capa_system.cache,
slug=self.id,
random_seed=self.context['seed'],
- unsafely=self.system.can_execute_unsafe_code(),
+ unsafely=self.capa_system.can_execute_unsafe_code(),
)
except Exception as err:
self._handle_exec_exception(err)
@@ -1470,18 +1470,21 @@ ScoreMessage = namedtuple('ScoreMessage', ['valid', 'correct', 'points', 'msg'])
@registry.register
class CodeResponse(LoncapaResponse):
"""
- Grade student code using an external queueing server, called 'xqueue'
+ Grade student code using an external queueing server, called 'xqueue'.
- Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse:
- system.xqueue = { 'interface': XqueueInterface object,
- 'construct_callback': Per-StudentModule callback URL
- constructor, defaults to using 'score_update'
- as the correct dispatch (function),
- 'default_queuename': Default queuename to submit request (string)
- }
+ Expects 'xqueue' dict in LoncapaSystem with the following keys that are
+ needed by CodeResponse::
+
+ capa_system.xqueue = {
+ '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).
+ }
+
+ External requests are only submitted for student submission grading, not
+ for getting reference answers.
- External requests are only submitted for student submission grading
- (i.e. and not for getting reference answers)
"""
tags = ['coderesponse']
@@ -1504,8 +1507,8 @@ class CodeResponse(LoncapaResponse):
self.url = xml.get('url', None)
# We do not support xqueue within Studio.
- if self.system.xqueue is not None:
- default_queuename = self.system.xqueue['default_queuename']
+ if self.capa_system.xqueue is not None:
+ default_queuename = self.capa_system.xqueue['default_queuename']
else:
default_queuename = None
self.queue_name = xml.get('queuename', default_queuename)
@@ -1548,7 +1551,7 @@ class CodeResponse(LoncapaResponse):
raise Exception(err)
# We do not support xqueue within Studio.
- if self.system.xqueue is None:
+ if self.capa_system.xqueue is None:
cmap = CorrectMap()
cmap.set(self.answer_id, queuestate=None,
msg='Error checking problem: no external queueing server is configured.')
@@ -1557,16 +1560,16 @@ class CodeResponse(LoncapaResponse):
# Prepare xqueue request
#------------------------------------------------------------
- qinterface = self.system.xqueue['interface']
+ qinterface = self.capa_system.xqueue['interface']
qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat)
- anonymous_student_id = self.system.anonymous_student_id
+ anonymous_student_id = self.capa_system.anonymous_student_id
# Generate header
queuekey = xqueue_interface.make_hashkey(
- str(self.system.seed) + qtime + anonymous_student_id + self.answer_id
+ str(self.capa_system.seed) + qtime + anonymous_student_id + self.answer_id
)
- callback_url = self.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,
@@ -1748,8 +1751,8 @@ class ExternalResponse(LoncapaResponse):
if answer is not None:
answer_src = answer.get('src')
if answer_src is not None:
- self.code = self.system.filesystem.open(
- 'src/' + answer_src).read()
+ # TODO: this code seems not to be used any more since self.capa_system.filesystem doesn't exist.
+ self.code = self.capa_system.filesystem.open('src/' + answer_src).read()
else:
self.code = answer.text
else:
@@ -1791,7 +1794,7 @@ class ExternalResponse(LoncapaResponse):
log.error(msg)
raise Exception(msg)
- if self.system.DEBUG:
+ if self.capa_system.DEBUG:
log.info('response = %s', req.text)
if (not req.text) or (not req.text.strip()):
@@ -1830,7 +1833,7 @@ class ExternalResponse(LoncapaResponse):
rxml = self.do_external_request('get_score', extra_payload)
except Exception as err: # pylint: disable=W0703
log.error('Error %s', err)
- if self.system.DEBUG:
+ if self.capa_system.DEBUG:
cmap.set_dict(dict(zip(sorted(
self.answer_ids), ['incorrect'] * len(idset))))
cmap.set_property(
@@ -1862,7 +1865,7 @@ class ExternalResponse(LoncapaResponse):
exans = json.loads(rxml.find('expected').text)
except Exception as err: # pylint: disable=W0703
log.error('Error %s', err)
- if self.system.DEBUG:
+ if self.capa_system.DEBUG:
msg = '%s' % str(
err).replace('<', '<')
exans = [''] * len(self.answer_ids)
@@ -2100,7 +2103,7 @@ class SchematicResponse(LoncapaResponse):
answer_src = answer.get('src')
if answer_src is not None:
# Untested; never used
- self.code = self.system.filestore.open('src/' + answer_src).read()
+ self.code = self.capa_system.filestore.open('src/' + answer_src).read()
else:
self.code = answer.text
@@ -2114,10 +2117,10 @@ class SchematicResponse(LoncapaResponse):
safe_exec.safe_exec(
self.code,
self.context,
- cache=self.system.cache,
+ cache=self.capa_system.cache,
slug=self.id,
random_seed=self.context['seed'],
- unsafely=self.system.can_execute_unsafe_code(),
+ unsafely=self.capa_system.can_execute_unsafe_code(),
)
except Exception as err:
msg = 'Error %s in evaluating SchematicResponse' % err
diff --git a/common/lib/capa/capa/tests/__init__.py b/common/lib/capa/capa/tests/__init__.py
index 7cc8a85a4a..db0af9cc8a 100644
--- a/common/lib/capa/capa/tests/__init__.py
+++ b/common/lib/capa/capa/tests/__init__.py
@@ -1,9 +1,11 @@
-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/',
- DEBUG=True,
- track_function=Mock(),
- get_module=Mock(),
- 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",
- 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,
+ DEBUG=True,
+ 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,
+ seed=0,
+ STATIC_URL='/dummy-static/',
+ xqueue={'interface': xqueue_interface, 'construct_callback': calledback_url, 'default_queuename': 'testqueue', 'waittime': 10},
)
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())
diff --git a/common/lib/capa/capa/tests/test_customrender.py b/common/lib/capa/capa/tests/test_customrender.py
index 8012804a40..8eeda47333 100644
--- a/common/lib/capa/capa/tests/test_customrender.py
+++ b/common/lib/capa/capa/tests/test_customrender.py
@@ -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 = """