diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 08a223f609..c1eb078a20 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -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 diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 12bee69b01..e81390724d 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -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) diff --git a/common/lib/capa/capa/registry.py b/common/lib/capa/capa/registry.py index 94a2853dec..bcc002c046 100644 --- a/common/lib/capa/capa/registry.py +++ b/common/lib/capa/capa/registry.py @@ -1,3 +1,5 @@ +"""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. diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index ac19fd1907..3bdd154525 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -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): ''' - 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 ... or in a ''' - 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',