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
from util import contextualize_text
import inputtypes
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse,OptionResponse
import calc
......@@ -166,7 +167,7 @@ class LoncapaProblem(object):
problems_simple = self.extract_problems(self.tree)
for response in problems_simple:
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)
return self.correct_map
......@@ -239,7 +240,7 @@ class LoncapaProblem(object):
# used to be
# 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,
# but it will turn into a dict containing both the answer and any associated message
# for the problem ID for the input element.
......@@ -266,9 +267,17 @@ class LoncapaProblem(object):
# print "[courseware.capa.capa_problem.extract_html] msg = ",msg
# do the rendering
#render_function = html_special_response[problemtree.tag]
render_function = getattr(inputtypes, problemtree.tag)
return render_function(problemtree, value, status, msg) # render the special response (textline, schematic,...)
# This should be broken out into a helper function
# that handles all input objects
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)
for item in problemtree:
......
......@@ -32,8 +32,121 @@ from lxml import etree
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=''):
'''
Select option input type.
......@@ -67,7 +180,7 @@ def optioninput(element, value, status, msg=''):
return etree.XML(html)
#-----------------------------------------------------------------------------
@register_render_function
def choicegroup(element, value, status, msg=''):
'''
Radio button inputs: multiple choice or true/false
......@@ -90,6 +203,7 @@ def choicegroup(element, value, status, msg=''):
html=render_to_string("choicegroup.html", context)
return etree.XML(html)
@register_render_function
def textline(element, value, state, msg=""):
eid=element.get('id')
count = int(eid.split('_')[-2])-1 # HACK
......@@ -100,6 +214,7 @@ def textline(element, value, state, msg=""):
#-----------------------------------------------------------------------------
@register_render_function
def js_textline(element, value, status, msg=''):
'''
Plan: We will inspect element to figure out type
......@@ -125,6 +240,7 @@ def js_textline(element, value, status, msg=''):
#-----------------------------------------------------------------------------
## TODO: Make a wrapper for <codeinput>
@register_render_function
def textbox(element, value, status, msg=''):
'''
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=''):
return etree.XML(html)
#-----------------------------------------------------------------------------
@register_render_function
def schematic(element, value, status, msg=''):
eid = element.get('id')
height = element.get('height')
......@@ -164,6 +281,7 @@ def schematic(element, value, status, msg=''):
#-----------------------------------------------------------------------------
### TODO: Move out of inputtypes
@register_render_function
def math(element, value, status, msg=''):
'''
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=''):
#-----------------------------------------------------------------------------
@register_render_function
def solution(element, value, status, msg=''):
'''
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=''):
#-----------------------------------------------------------------------------
@register_render_function
def imageinput(element, value, status, msg=''):
'''
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=''):
print '[courseware.capa.inputtypes.imageinput] context=',context
html=render_to_string("imageinput.html", context)
return etree.XML(html)
......@@ -47,10 +47,10 @@ def compare_with_tolerance(v1, v2, tol):
return abs(v1-v2) <= tolerance
class GenericResponse(object):
__metaclass__=abc.ABCMeta
__metaclass__=abc.ABCMeta # abc = Abstract Base Class
@abc.abstractmethod
def grade(self, student_answers):
def get_score(self, student_answers):
pass
@abc.abstractmethod
......@@ -61,7 +61,7 @@ class GenericResponse(object):
def preprocess_response(self):
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):
raise Exception("should have exactly one choice group per multiplechoicceresponse")
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:
return {self.answer_id:'correct'}
else:
......@@ -132,7 +132,7 @@ class TrueFalseResponse(MultipleChoiceResponse):
else:
choice.set("name", "choice_"+choice.get("name"))
def grade(self, student_answers):
def get_score(self, student_answers):
correct = set(self.correct_choices)
answers = set(student_answers.get(self.answer_id, []))
......@@ -162,7 +162,7 @@ class OptionResponse(GenericResponse):
print '[courseware.capa.responsetypes.OR.init] answer_fields=%s' % (self.answer_fields)
self.context = context
def grade(self, student_answers):
def get_score(self, student_answers):
cmap = {}
amap = self.get_answers()
for aid in amap:
......@@ -194,7 +194,7 @@ class NumericalResponse(GenericResponse):
except Exception, err:
self.answer_id = None
def grade(self, student_answers):
def get_score(self, student_answers):
''' Display HTML for a numeric response '''
student_answer = student_answers[self.answer_id]
try:
......@@ -300,7 +300,7 @@ def sympy_check2():
else:
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
of each key removed (the string before the first "_").
......@@ -363,7 +363,7 @@ def sympy_check2():
print "oops in customresponse (cfn) error %s" % err
# print "context = ",self.context
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:
correct[0] = 'correct' if ret['ok'] else 'incorrect'
msg = ret['msg']
......@@ -428,7 +428,7 @@ class ExternalResponse(GenericResponse):
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)]
self.context.update({'submission':submission})
......@@ -504,7 +504,7 @@ class FormulaResponse(GenericResponse):
self.case_sensitive = False
def grade(self, student_answers):
def get_score(self, student_answers):
variables=self.samples.split('@')[0].split(',')
numsamples=int(self.samples.split('@')[1].split('#')[1])
sranges=zip(*map(lambda x:map(float, x.split(",")),
......@@ -566,7 +566,7 @@ class SchematicResponse(GenericResponse):
else:
self.code = answer.text
def grade(self, student_answers):
def get_score(self, student_answers):
from capa_problem import global_context
submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)]
self.context.update({'submission':submission})
......@@ -605,7 +605,7 @@ class ImageResponse(GenericResponse):
self.ielements = xml.findall('imageinput')
self.answer_ids = [ie.get('id') for ie in self.ielements]
def grade(self, student_answers):
def get_score(self, student_answers):
correct_map = {}
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