Commit 9dbd4c5b by Ned Batchelder

Merge pull request #2197 from edx/ned/add-responsetypes-registry

Make a ResponseType registry.
parents a505fa95 3942876e
......@@ -25,18 +25,14 @@ from copy import deepcopy
from capa.correctmap import CorrectMap
import capa.inputtypes as inputtypes
import capa.customrender as customrender
import capa.responsetypes as responsetypes
from capa.util import contextualize_text, convert_files_to_filenames
import capa.xqueue_interface as xqueue_interface
# to be replaced with auto-registering
import capa.responsetypes as responsetypes
from capa.safe_exec import safe_exec
from pytz import UTC
# dict of tagname, Response Class -- this should come from auto-registering
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
# extra things displayed after "show answers" is pressed
solution_tags = ['solution']
......@@ -652,7 +648,7 @@ class LoncapaProblem(object):
'''
response_id = 1
self.responders = {}
for response in tree.xpath('//' + "|//".join(response_tag_dict)):
for response in tree.xpath('//' + "|//".join(responsetypes.registry.registered_tags())):
response_id_str = self.problem_id + "_" + str(response_id)
# create and save ID for this response
response.set('id', response_id_str)
......@@ -673,8 +669,8 @@ class LoncapaProblem(object):
answer_id = answer_id + 1
# instantiate capa Response
responder = response_tag_dict[response.tag](response, inputfields,
self.context, self.system)
responsetype_cls = responsetypes.registry.get_class_for_tag(response.tag)
responder = responsetype_cls(response, inputfields, self.context, self.system)
# save in list in self
self.responders[response] = responder
......
......@@ -289,6 +289,7 @@ class InputTypeBase(object):
#-----------------------------------------------------------------------------
@registry.register
class OptionInput(InputTypeBase):
"""
Input type for selecting and Select option input type.
......@@ -333,14 +334,13 @@ class OptionInput(InputTypeBase):
return [Attribute('options', transform=cls.parse_options),
Attribute('inline', False)]
registry.register(OptionInput)
#-----------------------------------------------------------------------------
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
# desired semantics.
@registry.register
class ChoiceGroup(InputTypeBase):
"""
Radio button or checkbox inputs: multiple choice or true/false
......@@ -415,12 +415,10 @@ class ChoiceGroup(InputTypeBase):
return choices
registry.register(ChoiceGroup)
#-----------------------------------------------------------------------------
@registry.register
class JavascriptInput(InputTypeBase):
"""
Hidden field for javascript to communicate via; also loads the required
......@@ -451,13 +449,11 @@ class JavascriptInput(InputTypeBase):
if self.value == "":
self.value = 'null'
registry.register(JavascriptInput)
#-----------------------------------------------------------------------------
@registry.register
class JSInput(InputTypeBase):
"""
Inputtype for general javascript inputs. Intended to be used with
......@@ -480,7 +476,7 @@ class JSInput(InputTypeBase):
height="500"
width="400"/>
See the documentation in docs/data/source/course_data_formats/jsinput.rst
See the documentation in docs/data/source/course_data_formats/jsinput.rst
for more information.
"""
......@@ -517,11 +513,10 @@ class JSInput(InputTypeBase):
return context
registry.register(JSInput)
#-----------------------------------------------------------------------------
@registry.register
class TextLine(InputTypeBase):
"""
A text line input. Can do math preview if "math"="1" is specified.
......@@ -587,11 +582,10 @@ class TextLine(InputTypeBase):
return {'do_math': self.do_math,
'preprocessor': self.preprocessor, }
registry.register(TextLine)
#-----------------------------------------------------------------------------
@registry.register
class FileSubmission(InputTypeBase):
"""
Upload some files (e.g. for programming assignments)
......@@ -636,11 +630,10 @@ class FileSubmission(InputTypeBase):
def _extra_context(self):
return {'queue_len': self.queue_len, }
registry.register(FileSubmission)
#-----------------------------------------------------------------------------
@registry.register
class CodeInput(InputTypeBase):
"""
A text area input for code--uses codemirror, does syntax highlighting, special tab handling,
......@@ -700,12 +693,11 @@ class CodeInput(InputTypeBase):
"""Defined queue_len, add it """
return {'queue_len': self.queue_len, }
registry.register(CodeInput)
#-----------------------------------------------------------------------------
@registry.register
class MatlabInput(CodeInput):
'''
InputType for handling Matlab code input
......@@ -866,11 +858,9 @@ class MatlabInput(CodeInput):
return {'success': error == 0, 'message': msg}
registry.register(MatlabInput)
#-----------------------------------------------------------------------------
@registry.register
class Schematic(InputTypeBase):
"""
InputType for the schematic editor
......@@ -893,11 +883,10 @@ class Schematic(InputTypeBase):
Attribute('submit_analyses', None), ]
registry.register(Schematic)
#-----------------------------------------------------------------------------
@registry.register
class ImageInput(InputTypeBase):
"""
Clickable image as an input field. Element should specify the image source, height,
......@@ -939,11 +928,10 @@ class ImageInput(InputTypeBase):
return {'gx': self.gx,
'gy': self.gy}
registry.register(ImageInput)
#-----------------------------------------------------------------------------
@registry.register
class Crystallography(InputTypeBase):
"""
An input for crystallography -- user selects 3 points on the axes, and we get a plane.
......@@ -963,11 +951,10 @@ class Crystallography(InputTypeBase):
Attribute('width'),
]
registry.register(Crystallography)
# -------------------------------------------------------------------------
@registry.register
class VseprInput(InputTypeBase):
"""
Input for molecular geometry--show possible structures, let student
......@@ -988,11 +975,10 @@ class VseprInput(InputTypeBase):
Attribute('geometries'),
]
registry.register(VseprInput)
#-------------------------------------------------------------------------
@registry.register
class ChemicalEquationInput(InputTypeBase):
"""
An input type for entering chemical equations. Supports live preview.
......@@ -1064,11 +1050,10 @@ class ChemicalEquationInput(InputTypeBase):
return result
registry.register(ChemicalEquationInput)
#-------------------------------------------------------------------------
@registry.register
class FormulaEquationInput(InputTypeBase):
"""
An input type for entering formula equations. Supports live preview.
......@@ -1160,11 +1145,10 @@ class FormulaEquationInput(InputTypeBase):
return result
registry.register(FormulaEquationInput)
#-----------------------------------------------------------------------------
@registry.register
class DragAndDropInput(InputTypeBase):
"""
Input for drag and drop problems. Allows student to drag and drop images and
......@@ -1259,11 +1243,10 @@ class DragAndDropInput(InputTypeBase):
self.loaded_attributes['drag_and_drop_json'] = json.dumps(to_js)
self.to_render.add('drag_and_drop_json')
registry.register(DragAndDropInput)
#-------------------------------------------------------------------------
@registry.register
class EditAMoleculeInput(InputTypeBase):
"""
An input type for edit-a-molecule. Integrates with the molecule editor java applet.
......@@ -1296,11 +1279,10 @@ class EditAMoleculeInput(InputTypeBase):
return context
registry.register(EditAMoleculeInput)
#-----------------------------------------------------------------------------
@registry.register
class DesignProtein2dInput(InputTypeBase):
"""
An input type for design of a protein in 2D. Integrates with the Protex java applet.
......@@ -1333,11 +1315,10 @@ class DesignProtein2dInput(InputTypeBase):
return context
registry.register(DesignProtein2dInput)
#-----------------------------------------------------------------------------
@registry.register
class EditAGeneInput(InputTypeBase):
"""
An input type for editing a gene.
......@@ -1370,11 +1351,10 @@ class EditAGeneInput(InputTypeBase):
return context
registry.register(EditAGeneInput)
#---------------------------------------------------------------------
@registry.register
class AnnotationInput(InputTypeBase):
"""
Input type for annotations: students can enter some notes or other text
......@@ -1481,9 +1461,8 @@ class AnnotationInput(InputTypeBase):
return extra_context
registry.register(AnnotationInput)
@registry.register
class ChoiceTextGroup(InputTypeBase):
"""
Groups of radiobutton/checkboxes with text inputs.
......@@ -1686,5 +1665,3 @@ class ChoiceTextGroup(InputTypeBase):
# Add the tuple for the current choice to the list of choices
choices.append((choice.get("name"), components))
return choices
registry.register(ChoiceTextGroup)
"""A registry for finding classes based on tags in the class."""
class TagRegistry(object):
"""
A registry mapping tags to handlers.
......@@ -35,6 +37,9 @@ class TagRegistry(object):
for t in cls.tags:
self._mapping[t] = cls
# Returning the cls means we can use this as a decorator.
return cls
def registered_tags(self):
"""
Get a list of all the tags that have been registered.
......
......@@ -33,6 +33,7 @@ from shapely.geometry import Point, MultiPoint
# specific library imports
from calc import evaluator, UndefinedVariable
from . import correctmap
from .registry import TagRegistry
from datetime import datetime
from pytz import UTC
from .util import (compare_with_tolerance, contextualize_text, convert_files_to_filenames,
......@@ -45,6 +46,7 @@ import capa.safe_exec as safe_exec
log = logging.getLogger(__name__)
registry = TagRegistry()
CorrectMap = correctmap.CorrectMap # pylint: disable=C0103
CORRECTMAP_PY = None
......@@ -92,7 +94,7 @@ class LoncapaResponse(object):
Each subclass must also define the following attributes:
- response_tag : xhtml tag identifying this response (used in auto-registering)
- tags : xhtml tags identifying this response (used in auto-registering)
In addition, these methods are optional:
......@@ -120,7 +122,7 @@ class LoncapaResponse(object):
"""
__metaclass__ = abc.ABCMeta # abc = Abstract Base Class
response_tag = None
tags = None
hint_tag = None
max_inputfields = None
......@@ -405,13 +407,14 @@ class LoncapaResponse(object):
#-----------------------------------------------------------------------------
@registry.register
class JavascriptResponse(LoncapaResponse):
"""
This response type is used when the student's answer is graded via
Javascript using Node.js.
"""
response_tag = 'javascriptresponse'
tags = ['javascriptresponse']
max_inputfields = 1
allowed_inputfields = ['javascriptinput']
......@@ -605,6 +608,7 @@ class JavascriptResponse(LoncapaResponse):
#-----------------------------------------------------------------------------
@registry.register
class ChoiceResponse(LoncapaResponse):
"""
This response type is used when the student chooses from a discrete set of
......@@ -653,7 +657,7 @@ class ChoiceResponse(LoncapaResponse):
"""
response_tag = 'choiceresponse'
tags = ['choiceresponse']
max_inputfields = 1
allowed_inputfields = ['checkboxgroup', 'radiogroup']
correct_choices = None
......@@ -702,10 +706,11 @@ class ChoiceResponse(LoncapaResponse):
#-----------------------------------------------------------------------------
@registry.register
class MultipleChoiceResponse(LoncapaResponse):
# TODO: handle direction and randomize
response_tag = 'multiplechoiceresponse'
tags = ['multiplechoiceresponse']
max_inputfields = 1
allowed_inputfields = ['choicegroup']
correct_choices = None
......@@ -759,9 +764,10 @@ class MultipleChoiceResponse(LoncapaResponse):
return {self.answer_id: self.correct_choices}
@registry.register
class TrueFalseResponse(MultipleChoiceResponse):
response_tag = 'truefalseresponse'
tags = ['truefalseresponse']
def mc_setup_response(self):
i = 0
......@@ -786,12 +792,13 @@ class TrueFalseResponse(MultipleChoiceResponse):
#-----------------------------------------------------------------------------
@registry.register
class OptionResponse(LoncapaResponse):
'''
TODO: handle direction and randomize
'''
response_tag = 'optionresponse'
tags = ['optionresponse']
hint_tag = 'optionhint'
allowed_inputfields = ['optioninput']
answer_fields = None
......@@ -819,13 +826,14 @@ class OptionResponse(LoncapaResponse):
#-----------------------------------------------------------------------------
@registry.register
class NumericalResponse(LoncapaResponse):
'''
This response type expects a number or formulaic expression that evaluates
to a number (e.g. `4+5/2^2`), and accepts with a tolerance.
'''
response_tag = 'numericalresponse'
tags = ['numericalresponse']
hint_tag = 'numericalhint'
allowed_inputfields = ['textline', 'formulaequationinput']
required_attributes = ['answer']
......@@ -946,6 +954,7 @@ class NumericalResponse(LoncapaResponse):
#-----------------------------------------------------------------------------
@registry.register
class StringResponse(LoncapaResponse):
'''
This response type allows one or more answers.
......@@ -978,7 +987,7 @@ class StringResponse(LoncapaResponse):
</hintgroup>
</stringresponse>
'''
response_tag = 'stringresponse'
tags = ['stringresponse']
hint_tag = 'stringhint'
allowed_inputfields = ['textline']
required_attributes = ['answer']
......@@ -1080,13 +1089,14 @@ class StringResponse(LoncapaResponse):
#-----------------------------------------------------------------------------
@registry.register
class CustomResponse(LoncapaResponse):
'''
Custom response. The python code to be run should be in <answer>...</answer>
or in a <script>...</script>
'''
response_tag = 'customresponse'
tags = ['customresponse']
allowed_inputfields = ['textline', 'textbox', 'crystallography',
'chemicalequationinput', 'vsepr_input',
......@@ -1408,12 +1418,13 @@ class CustomResponse(LoncapaResponse):
#-----------------------------------------------------------------------------
@registry.register
class SymbolicResponse(CustomResponse):
"""
Symbolic math response checking, using symmath library.
"""
response_tag = 'symbolicresponse'
tags = ['symbolicresponse']
max_inputfields = 1
def setup_response(self):
......@@ -1456,6 +1467,7 @@ class SymbolicResponse(CustomResponse):
ScoreMessage = namedtuple('ScoreMessage', ['valid', 'correct', 'points', 'msg']) # pylint: disable=invalid-name
@registry.register
class CodeResponse(LoncapaResponse):
"""
Grade student code using an external queueing server, called 'xqueue'
......@@ -1472,7 +1484,7 @@ class CodeResponse(LoncapaResponse):
(i.e. and not for getting reference answers)
"""
response_tag = 'coderesponse'
tags = ['coderesponse']
allowed_inputfields = ['textbox', 'filesubmission', 'matlabinput']
max_inputfields = 1
payload = None
......@@ -1705,6 +1717,7 @@ class CodeResponse(LoncapaResponse):
#-----------------------------------------------------------------------------
@registry.register
class ExternalResponse(LoncapaResponse):
"""
Grade the students input using an external server.
......@@ -1713,7 +1726,7 @@ class ExternalResponse(LoncapaResponse):
"""
response_tag = 'externalresponse'
tags = ['externalresponse']
allowed_inputfields = ['textline', 'textbox']
awdmap = {
'EXACT_ANS': 'correct', # TODO: handle other loncapa responses
......@@ -1864,12 +1877,13 @@ class ExternalResponse(LoncapaResponse):
#-----------------------------------------------------------------------------
@registry.register
class FormulaResponse(LoncapaResponse):
"""
Checking of symbolic math response using numerical sampling.
"""
response_tag = 'formularesponse'
tags = ['formularesponse']
hint_tag = 'formulahint'
allowed_inputfields = ['textline', 'formulaequationinput']
required_attributes = ['answer', 'samples']
......@@ -2068,11 +2082,12 @@ class FormulaResponse(LoncapaResponse):
#-----------------------------------------------------------------------------
@registry.register
class SchematicResponse(LoncapaResponse):
"""
Circuit schematic response type.
"""
response_tag = 'schematicresponse'
tags = ['schematicresponse']
allowed_inputfields = ['schematic']
def __init__(self, *args, **kwargs):
......@@ -2118,6 +2133,7 @@ class SchematicResponse(LoncapaResponse):
#-----------------------------------------------------------------------------
@registry.register
class ImageResponse(LoncapaResponse):
"""
Handle student response for image input: the input is a click on an image,
......@@ -2145,7 +2161,7 @@ class ImageResponse(LoncapaResponse):
True, if click is inside any region or rectangle. Otherwise False.
"""
response_tag = 'imageresponse'
tags = ['imageresponse']
allowed_inputfields = ['imageinput']
def __init__(self, *args, **kwargs):
......@@ -2248,6 +2264,7 @@ class ImageResponse(LoncapaResponse):
#-----------------------------------------------------------------------------
@registry.register
class AnnotationResponse(LoncapaResponse):
"""
Checking of annotation responses.
......@@ -2255,7 +2272,7 @@ class AnnotationResponse(LoncapaResponse):
The response contains both a comment (student commentary) and an option (student tag).
Only the tag is currently graded. Answers may be incorrect, partially correct, or correct.
"""
response_tag = 'annotationresponse'
tags = ['annotationresponse']
allowed_inputfields = ['annotationinput']
max_inputfields = 1
default_scoring = {'incorrect': 0, 'partially-correct': 1, 'correct': 2}
......@@ -2371,6 +2388,7 @@ class AnnotationResponse(LoncapaResponse):
return None
@registry.register
class ChoiceTextResponse(LoncapaResponse):
"""
Allows for multiple choice responses with text inputs
......@@ -2378,7 +2396,7 @@ class ChoiceTextResponse(LoncapaResponse):
ChoiceResponse.
"""
response_tag = 'choicetextresponse'
tags = ['choicetextresponse']
max_inputfields = 1
allowed_inputfields = ['choicetextgroup',
'checkboxtextgroup',
......
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