Commit 42c543b7 by Ned Batchelder

Merge pull request #2183 from edx/ned/add-i18n-for-inputtypes

Ned/add i18n for inputtypes
parents e74ddf81 fe5d2c74
......@@ -63,37 +63,79 @@ 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.
Attributes:
i18n: an object implementing the `gettext.Translations` interface so
that we can use `.ugettext` to localize strings.
See :class:`ModuleSystem` for documentation of other attributes.
"""
def __init__( # pylint: disable=invalid-name
self,
ajax_url,
anonymous_student_id,
cache,
can_execute_unsafe_code,
DEBUG, # pylint: disable=invalid-name
filestore,
i18n,
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.i18n = i18n
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 +454,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 +464,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 +485,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 +518,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 +569,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 +642,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 +655,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 +712,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 gettext
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 +29,29 @@ 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")),
i18n=gettext.NullTranslations(),
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",
......@@ -1140,6 +1140,24 @@ class NumericalResponseTest(ResponseTest):
"Content error--answer '%s' is not a valid number", staff_ans
)
@mock.patch('capa.responsetypes.log')
def test_responsetype_i18n(self, mock_log):
"""Test that LoncapaSystem has an i18n that works."""
staff_ans = "clearly bad syntax )[+1e"
problem = self.build_problem(answer=staff_ans, tolerance=1e-3)
class FakeTranslations(object):
"""A fake gettext.Translations object."""
def ugettext(self, text):
"""Return the 'translation' of `text`."""
if text == "There was a problem with the staff answer to this problem":
text = "TRANSLATED!"
return text
problem.capa_system.i18n = FakeTranslations()
with self.assertRaisesRegexp(StudentInputError, "TRANSLATED!"):
self.assert_grade(problem, '1+j', 'correct')
def test_grade_infinity(self):
"""
Check that infinity doesn't automatically get marked correct.
......
......@@ -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,27 @@ class CapaMixin(CapaFields):
if text is None:
text = self.data
capa_system = LoncapaSystem(
ajax_url=self.runtime.ajax_url,
anonymous_student_id=self.runtime.anonymous_student_id,
cache=self.runtime.cache,
can_execute_unsafe_code=self.runtime.can_execute_unsafe_code,
DEBUG=self.runtime.DEBUG,
filestore=self.runtime.filestore,
i18n=self.runtime.service(self, "i18n"),
node_path=self.runtime.node_path,
render_template=self.runtime.render_template,
seed=self.runtime.seed, # Why do we do this if we have self.seed?
STATIC_URL=self.runtime.STATIC_URL,
xqueue=self.runtime.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