Commit 4fe22be5 by David Ormsbee

Merge pull request #59 from MITx/pmitros/modular-refactor

First part of courseware refactor
parents fb146bf5 bb66cad6
...@@ -25,6 +25,7 @@ from mako.template import Template ...@@ -25,6 +25,7 @@ from mako.template import Template
from util import contextualize_text from util import contextualize_text
import inputtypes import inputtypes
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse,OptionResponse from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse,OptionResponse
import calc import calc
...@@ -166,7 +167,7 @@ class LoncapaProblem(object): ...@@ -166,7 +167,7 @@ class LoncapaProblem(object):
problems_simple = self.extract_problems(self.tree) problems_simple = self.extract_problems(self.tree)
for response in problems_simple: for response in problems_simple:
grader = response_types[response.tag](response, self.context, self.system) grader = response_types[response.tag](response, self.context, self.system)
results = grader.grade(answers) # call the responsetype instance to do the actual grading results = grader.get_score(answers) # call the responsetype instance to do the actual grading
self.correct_map.update(results) self.correct_map.update(results)
return self.correct_map return self.correct_map
...@@ -239,7 +240,7 @@ class LoncapaProblem(object): ...@@ -239,7 +240,7 @@ class LoncapaProblem(object):
# used to be # used to be
# if problemtree.tag in html_special_response: # if problemtree.tag in html_special_response:
if hasattr(inputtypes, problemtree.tag): if problemtree.tag in inputtypes.get_input_xml_tags():
# status is currently the answer for the problem ID for the input element, # status is currently the answer for the problem ID for the input element,
# but it will turn into a dict containing both the answer and any associated message # but it will turn into a dict containing both the answer and any associated message
# for the problem ID for the input element. # for the problem ID for the input element.
...@@ -266,9 +267,17 @@ class LoncapaProblem(object): ...@@ -266,9 +267,17 @@ class LoncapaProblem(object):
# print "[courseware.capa.capa_problem.extract_html] msg = ",msg # print "[courseware.capa.capa_problem.extract_html] msg = ",msg
# do the rendering # do the rendering
#render_function = html_special_response[problemtree.tag] # This should be broken out into a helper function
render_function = getattr(inputtypes, problemtree.tag) # that handles all input objects
return render_function(problemtree, value, status, msg) # render the special response (textline, schematic,...) render_object = inputtypes.SimpleInput(system = self.system,
xml = problemtree,
state = {'value':value,
'status': status,
'id':problemtree.get('id'),
'feedback':{'message':msg}
},
use = 'capa_input')
return render_object.get_html() #function(problemtree, value, status, msg) # render the special response (textline, schematic,...)
tree=Element(problemtree.tag) tree=Element(problemtree.tag)
for item in problemtree: for item in problemtree:
......
...@@ -32,8 +32,121 @@ from lxml import etree ...@@ -32,8 +32,121 @@ from lxml import etree
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
def get_input_xml_tags():
''' Eventually, this will be for all registered input types '''
return SimpleInput.get_xml_tags()
class SimpleInput():# XModule
''' Type for simple inputs -- plain HTML with a form element
State is a dictionary with optional keys:
* Value
* ID
* Status (answered, unanswered, unsubmitted)
* Feedback (dictionary containing keys for hints, errors, or other
feedback from previous attempt)
'''
xml_tags = {} ## Maps tags to functions
@classmethod
def get_xml_tags(c):
return c.xml_tags.keys()
@classmethod
def get_uses(c):
return ['capa_input', 'capa_transform']
def get_html(self):
return self.xml_tags[self.tag](self.xml, self.value, self.status, self.msg)
def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'):
self.xml = xml
self.tag = xml.tag
if not state:
state = {}
## ID should only come from one place.
## If it comes from multiple, we use state first, XML second, and parameter
## third. Since we don't make this guarantee, we can swap this around in
## the future if there's a more logical order.
if item_id:
self.id = item_id
if xml.get('id'):
self.id = xml.get('id')
if 'id' in state:
self.id = state['id']
self.system = system
self.value = ''
if 'value' in state:
self.value = state['value']
self.msg = ''
if 'feedback' in state and 'message' in state['feedback']:
self.msg = state['feedback']['message']
self.status = 'unanswered'
if 'status' in state:
self.status = state['status']
## TODO
# class SimpleTransform():
# ''' Type for simple XML to HTML transforms. Examples:
# * Math tags, which go from LON-CAPA-style m-tags to MathJAX
# '''
# xml_tags = {} ## Maps tags to functions
# @classmethod
# def get_xml_tags(c):
# return c.xml_tags.keys()
# @classmethod
# def get_uses(c):
# return ['capa_transform']
# def get_html(self):
# return self.xml_tags[self.tag](self.xml, self.value, self.status, self.msg)
# def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'):
# self.xml = xml
# self.tag = xml.tag
# if not state:
# state = {}
# if item_id:
# self.id = item_id
# if xml.get('id'):
# self.id = xml.get('id')
# if 'id' in state:
# self.id = state['id']
# self.system = system
# self.value = ''
# if 'value' in state:
# self.value = state['value']
# self.msg = ''
# if 'feedback' in state and 'message' in state['feedback']:
# self.msg = state['feedback']['message']
# self.status = 'unanswered'
# if 'status' in state:
# self.status = state['status']
def register_render_function(fn, names=None, cls=SimpleInput):
if names == None:
SimpleInput.xml_tags[fn.__name__] = fn
else:
raise NotImplementedError
def wrapped():
return fn
return wrapped
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
@register_render_function
def optioninput(element, value, status, msg=''): def optioninput(element, value, status, msg=''):
''' '''
Select option input type. Select option input type.
...@@ -67,7 +180,7 @@ def optioninput(element, value, status, msg=''): ...@@ -67,7 +180,7 @@ def optioninput(element, value, status, msg=''):
return etree.XML(html) return etree.XML(html)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
@register_render_function
def choicegroup(element, value, status, msg=''): def choicegroup(element, value, status, msg=''):
''' '''
Radio button inputs: multiple choice or true/false Radio button inputs: multiple choice or true/false
...@@ -90,6 +203,7 @@ def choicegroup(element, value, status, msg=''): ...@@ -90,6 +203,7 @@ def choicegroup(element, value, status, msg=''):
html=render_to_string("choicegroup.html", context) html=render_to_string("choicegroup.html", context)
return etree.XML(html) return etree.XML(html)
@register_render_function
def textline(element, value, state, msg=""): def textline(element, value, state, msg=""):
eid=element.get('id') eid=element.get('id')
count = int(eid.split('_')[-2])-1 # HACK count = int(eid.split('_')[-2])-1 # HACK
...@@ -100,6 +214,7 @@ def textline(element, value, state, msg=""): ...@@ -100,6 +214,7 @@ def textline(element, value, state, msg=""):
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
@register_render_function
def js_textline(element, value, status, msg=''): def js_textline(element, value, status, msg=''):
''' '''
Plan: We will inspect element to figure out type Plan: We will inspect element to figure out type
...@@ -125,6 +240,7 @@ def js_textline(element, value, status, msg=''): ...@@ -125,6 +240,7 @@ def js_textline(element, value, status, msg=''):
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
## TODO: Make a wrapper for <codeinput> ## TODO: Make a wrapper for <codeinput>
@register_render_function
def textbox(element, value, status, msg=''): def textbox(element, value, status, msg=''):
''' '''
The textbox is used for code input. The message is the return HTML string from The textbox is used for code input. The message is the return HTML string from
...@@ -140,6 +256,7 @@ def textbox(element, value, status, msg=''): ...@@ -140,6 +256,7 @@ def textbox(element, value, status, msg=''):
return etree.XML(html) return etree.XML(html)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
@register_render_function
def schematic(element, value, status, msg=''): def schematic(element, value, status, msg=''):
eid = element.get('id') eid = element.get('id')
height = element.get('height') height = element.get('height')
...@@ -164,6 +281,7 @@ def schematic(element, value, status, msg=''): ...@@ -164,6 +281,7 @@ def schematic(element, value, status, msg=''):
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
### TODO: Move out of inputtypes ### TODO: Move out of inputtypes
@register_render_function
def math(element, value, status, msg=''): def math(element, value, status, msg=''):
''' '''
This is not really an input type. It is a convention from Lon-CAPA, used for This is not really an input type. It is a convention from Lon-CAPA, used for
...@@ -198,6 +316,7 @@ def math(element, value, status, msg=''): ...@@ -198,6 +316,7 @@ def math(element, value, status, msg=''):
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
@register_render_function
def solution(element, value, status, msg=''): def solution(element, value, status, msg=''):
''' '''
This is not really an input type. It is just a <span>...</span> which is given an ID, This is not really an input type. It is just a <span>...</span> which is given an ID,
...@@ -218,6 +337,7 @@ def solution(element, value, status, msg=''): ...@@ -218,6 +337,7 @@ def solution(element, value, status, msg=''):
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
@register_render_function
def imageinput(element, value, status, msg=''): def imageinput(element, value, status, msg=''):
''' '''
Clickable image as an input field. Element should specify the image source, height, and width, eg Clickable image as an input field. Element should specify the image source, height, and width, eg
...@@ -253,4 +373,3 @@ def imageinput(element, value, status, msg=''): ...@@ -253,4 +373,3 @@ def imageinput(element, value, status, msg=''):
print '[courseware.capa.inputtypes.imageinput] context=',context print '[courseware.capa.inputtypes.imageinput] context=',context
html=render_to_string("imageinput.html", context) html=render_to_string("imageinput.html", context)
return etree.XML(html) return etree.XML(html)
...@@ -47,10 +47,10 @@ def compare_with_tolerance(v1, v2, tol): ...@@ -47,10 +47,10 @@ def compare_with_tolerance(v1, v2, tol):
return abs(v1-v2) <= tolerance return abs(v1-v2) <= tolerance
class GenericResponse(object): class GenericResponse(object):
__metaclass__=abc.ABCMeta __metaclass__=abc.ABCMeta # abc = Abstract Base Class
@abc.abstractmethod @abc.abstractmethod
def grade(self, student_answers): def get_score(self, student_answers):
pass pass
@abc.abstractmethod @abc.abstractmethod
...@@ -61,7 +61,7 @@ class GenericResponse(object): ...@@ -61,7 +61,7 @@ class GenericResponse(object):
def preprocess_response(self): def preprocess_response(self):
pass pass
#Every response type needs methods "grade" and "get_answers" #Every response type needs methods "get_score" and "get_answers"
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -95,7 +95,7 @@ class MultipleChoiceResponse(GenericResponse): ...@@ -95,7 +95,7 @@ class MultipleChoiceResponse(GenericResponse):
raise Exception("should have exactly one choice group per multiplechoicceresponse") raise Exception("should have exactly one choice group per multiplechoicceresponse")
self.answer_id=self.answer_id[0] self.answer_id=self.answer_id[0]
def grade(self, student_answers): def get_score(self, student_answers):
if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices: if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices:
return {self.answer_id:'correct'} return {self.answer_id:'correct'}
else: else:
...@@ -132,7 +132,7 @@ class TrueFalseResponse(MultipleChoiceResponse): ...@@ -132,7 +132,7 @@ class TrueFalseResponse(MultipleChoiceResponse):
else: else:
choice.set("name", "choice_"+choice.get("name")) choice.set("name", "choice_"+choice.get("name"))
def grade(self, student_answers): def get_score(self, student_answers):
correct = set(self.correct_choices) correct = set(self.correct_choices)
answers = set(student_answers.get(self.answer_id, [])) answers = set(student_answers.get(self.answer_id, []))
...@@ -162,7 +162,7 @@ class OptionResponse(GenericResponse): ...@@ -162,7 +162,7 @@ class OptionResponse(GenericResponse):
print '[courseware.capa.responsetypes.OR.init] answer_fields=%s' % (self.answer_fields) print '[courseware.capa.responsetypes.OR.init] answer_fields=%s' % (self.answer_fields)
self.context = context self.context = context
def grade(self, student_answers): def get_score(self, student_answers):
cmap = {} cmap = {}
amap = self.get_answers() amap = self.get_answers()
for aid in amap: for aid in amap:
...@@ -194,7 +194,7 @@ class NumericalResponse(GenericResponse): ...@@ -194,7 +194,7 @@ class NumericalResponse(GenericResponse):
except Exception, err: except Exception, err:
self.answer_id = None self.answer_id = None
def grade(self, student_answers): def get_score(self, student_answers):
''' Display HTML for a numeric response ''' ''' Display HTML for a numeric response '''
student_answer = student_answers[self.answer_id] student_answer = student_answers[self.answer_id]
try: try:
...@@ -300,7 +300,7 @@ def sympy_check2(): ...@@ -300,7 +300,7 @@ def sympy_check2():
else: else:
self.code = answer.text self.code = answer.text
def grade(self, student_answers): def get_score(self, student_answers):
''' '''
student_answers is a dict with everything from request.POST, but with the first part student_answers is a dict with everything from request.POST, but with the first part
of each key removed (the string before the first "_"). of each key removed (the string before the first "_").
...@@ -363,7 +363,7 @@ def sympy_check2(): ...@@ -363,7 +363,7 @@ def sympy_check2():
print "oops in customresponse (cfn) error %s" % err print "oops in customresponse (cfn) error %s" % err
# print "context = ",self.context # print "context = ",self.context
print traceback.format_exc() print traceback.format_exc()
if settings.DEBUG: print "[courseware.capa.responsetypes.customresponse.grade] ret = ",ret if settings.DEBUG: print "[courseware.capa.responsetypes.customresponse.get_score] ret = ",ret
if type(ret)==dict: if type(ret)==dict:
correct[0] = 'correct' if ret['ok'] else 'incorrect' correct[0] = 'correct' if ret['ok'] else 'incorrect'
msg = ret['msg'] msg = ret['msg']
...@@ -428,7 +428,7 @@ class ExternalResponse(GenericResponse): ...@@ -428,7 +428,7 @@ class ExternalResponse(GenericResponse):
self.tests = xml.get('answer') self.tests = xml.get('answer')
def grade(self, student_answers): def get_score(self, student_answers):
submission = [student_answers[k] for k in sorted(self.answer_ids)] submission = [student_answers[k] for k in sorted(self.answer_ids)]
self.context.update({'submission':submission}) self.context.update({'submission':submission})
...@@ -504,7 +504,7 @@ class FormulaResponse(GenericResponse): ...@@ -504,7 +504,7 @@ class FormulaResponse(GenericResponse):
self.case_sensitive = False self.case_sensitive = False
def grade(self, student_answers): def get_score(self, student_answers):
variables=self.samples.split('@')[0].split(',') variables=self.samples.split('@')[0].split(',')
numsamples=int(self.samples.split('@')[1].split('#')[1]) numsamples=int(self.samples.split('@')[1].split('#')[1])
sranges=zip(*map(lambda x:map(float, x.split(",")), sranges=zip(*map(lambda x:map(float, x.split(",")),
...@@ -566,7 +566,7 @@ class SchematicResponse(GenericResponse): ...@@ -566,7 +566,7 @@ class SchematicResponse(GenericResponse):
else: else:
self.code = answer.text self.code = answer.text
def grade(self, student_answers): def get_score(self, student_answers):
from capa_problem import global_context from capa_problem import global_context
submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)] submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)]
self.context.update({'submission':submission}) self.context.update({'submission':submission})
...@@ -605,7 +605,7 @@ class ImageResponse(GenericResponse): ...@@ -605,7 +605,7 @@ class ImageResponse(GenericResponse):
self.ielements = xml.findall('imageinput') self.ielements = xml.findall('imageinput')
self.answer_ids = [ie.get('id') for ie in self.ielements] self.answer_ids = [ie.get('id') for ie in self.ielements]
def grade(self, student_answers): def get_score(self, student_answers):
correct_map = {} correct_map = {}
expectedset = self.get_answers() expectedset = self.get_answers()
......
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