Commit bd2a44ad by David Ormsbee

Merge pull request #90 from MITx/cpennington/capa_django_deps

Removing references to django.conf.settings from capa/responsetypes.py
parents 2991eba1 c3e730a0
......@@ -12,7 +12,6 @@ import logging
import math
import numpy
import os
import os.path
import random
import re
import scipy
......@@ -20,58 +19,58 @@ import struct
from lxml import etree
from lxml.etree import Element
from xml.sax.saxutils import escape, unescape
from xml.sax.saxutils import unescape
from util import contextualize_text
import inputtypes
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse,OptionResponse, SymbolicResponse
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse
import calc
import eia
log = logging.getLogger(__name__)
response_types = {'numericalresponse':NumericalResponse,
'formularesponse':FormulaResponse,
'customresponse':CustomResponse,
'schematicresponse':SchematicResponse,
'externalresponse':ExternalResponse,
'multiplechoiceresponse':MultipleChoiceResponse,
'truefalseresponse':TrueFalseResponse,
'imageresponse':ImageResponse,
'optionresponse':OptionResponse,
'symbolicresponse':SymbolicResponse,
response_types = {'numericalresponse': NumericalResponse,
'formularesponse': FormulaResponse,
'customresponse': CustomResponse,
'schematicresponse': SchematicResponse,
'externalresponse': ExternalResponse,
'multiplechoiceresponse': MultipleChoiceResponse,
'truefalseresponse': TrueFalseResponse,
'imageresponse': ImageResponse,
'optionresponse': OptionResponse,
'symbolicresponse': SymbolicResponse,
}
entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput','optioninput']
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
response_properties = ["responseparam", "answer"] # these get captured as student responses
entry_types = ['textline', 'schematic', 'choicegroup', 'textbox', 'imageinput', 'optioninput']
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
response_properties = ["responseparam", "answer"] # these get captured as student responses
# How to convert from original XML to HTML
# We should do this with xlst later
html_transforms = {'problem': {'tag':'div'},
"numericalresponse": {'tag':'span'},
"customresponse": {'tag':'span'},
"externalresponse": {'tag':'span'},
"schematicresponse": {'tag':'span'},
"formularesponse": {'tag':'span'},
"symbolicresponse": {'tag':'span'},
"multiplechoiceresponse": {'tag':'span'},
"text": {'tag':'span'},
"math": {'tag':'span'},
html_transforms = {'problem': {'tag': 'div'},
"numericalresponse": {'tag': 'span'},
"customresponse": {'tag': 'span'},
"externalresponse": {'tag': 'span'},
"schematicresponse": {'tag': 'span'},
"formularesponse": {'tag': 'span'},
"symbolicresponse": {'tag': 'span'},
"multiplechoiceresponse": {'tag': 'span'},
"text": {'tag': 'span'},
"math": {'tag': 'span'},
}
global_context={'random':random,
'numpy':numpy,
'math':math,
'scipy':scipy,
'calc':calc,
'eia':eia}
global_context = {'random': random,
'numpy': numpy,
'math': math,
'scipy': scipy,
'calc': calc,
'eia': eia}
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["responseparam", "answer", "script"]
# These should be removed from HTML output, but keeping subelements
html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text","externalresponse",'symbolicresponse']
html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text", "externalresponse", 'symbolicresponse']
# removed in MC
## These should be transformed
......@@ -82,6 +81,7 @@ html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formul
# "solution":inputtypes.solution.render,
# }
class LoncapaProblem(object):
def __init__(self, fileobject, id, state=None, seed=None, system=None):
## Initialize class variables from state
......@@ -107,22 +107,22 @@ class LoncapaProblem(object):
# TODO: Does this deplete the Linux entropy pool? Is this fast enough?
if not self.seed:
self.seed=struct.unpack('i', os.urandom(4))[0]
self.seed = struct.unpack('i', os.urandom(4))[0]
## Parse XML file
if getattr(system,'DEBUG',False):
if getattr(system, 'DEBUG', False):
log.info("[courseware.capa.capa_problem.lcp.init] fileobject = %s" % fileobject)
file_text = fileobject.read()
self.fileobject = fileobject # save it, so we can use for debugging information later
self.fileobject = fileobject # save it, so we can use for debugging information later
# Convert startouttext and endouttext to proper <text></text>
# TODO: Do with XML operations
file_text = re.sub("startouttext\s*/","text",file_text)
file_text = re.sub("endouttext\s*/","/text",file_text)
file_text = re.sub("startouttext\s*/", "text", file_text)
file_text = re.sub("endouttext\s*/", "/text", file_text)
self.tree = etree.XML(file_text)
self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map = self.student_answers)
self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map=self.student_answers)
self.context = self.extract_context(self.tree, seed=self.seed)
for response in self.tree.xpath('//'+"|//".join(response_types)):
for response in self.tree.xpath('//' + "|//".join(response_types)):
responder = response_types[response.tag](response, self.context, self.system)
responder.preprocess_response()
......@@ -130,34 +130,34 @@ class LoncapaProblem(object):
return u"LoncapaProblem ({0})".format(self.fileobject)
def get_state(self):
''' Stored per-user session data neeeded to:
''' Stored per-user session data neeeded to:
1) Recreate the problem
2) Populate any student answers. '''
return {'seed':self.seed,
'student_answers':self.student_answers,
'correct_map':self.correct_map,
'done':self.done}
return {'seed': self.seed,
'student_answers': self.student_answers,
'correct_map': self.correct_map,
'done': self.done}
def get_max_score(self):
'''
TODO: multiple points for programming problems.
'''
sum = 0
for et in entry_types:
sum = sum + self.tree.xpath('count(//'+et+')')
sum = 0
for et in entry_types:
sum = sum + self.tree.xpath('count(//' + et + ')')
return int(sum)
def get_score(self):
correct=0
correct = 0
for key in self.correct_map:
if self.correct_map[key] == u'correct':
correct += 1
if (not self.student_answers) or len(self.student_answers)==0:
return {'score':0,
'total':self.get_max_score()}
if (not self.student_answers) or len(self.student_answers) == 0:
return {'score': 0,
'total': self.get_max_score()}
else:
return {'score':correct,
'total':self.get_max_score()}
return {'score': correct,
'total': self.get_max_score()}
def grade_answers(self, answers):
'''
......@@ -168,38 +168,36 @@ class LoncapaProblem(object):
Thus, for example, input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123
'''
self.student_answers = answers
context=self.extract_context(self.tree)
self.correct_map = dict()
problems_simple = self.extract_problems(self.tree)
for response in problems_simple:
grader = response_types[response.tag](response, self.context, self.system)
results = grader.get_score(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
def get_question_answers(self):
"""Returns a dict of answer_ids to answer values. If we can't generate
an answer (this sometimes happens in customresponses), that answer_id is
not included. Called by "show answers" button JSON request
an answer (this sometimes happens in customresponses), that answer_id is
not included. Called by "show answers" button JSON request
(see capa_module)
"""
context=self.extract_context(self.tree)
answer_map = dict()
problems_simple = self.extract_problems(self.tree) # purified (flat) XML tree of just response queries
problems_simple = self.extract_problems(self.tree) # purified (flat) XML tree of just response queries
for response in problems_simple:
responder = response_types[response.tag](response, self.context, self.system) # instance of numericalresponse, customresponse,...
responder = response_types[response.tag](response, self.context, self.system) # instance of numericalresponse, customresponse,...
results = responder.get_answers()
answer_map.update(results) # dict of (id,correct_answer)
answer_map.update(results) # dict of (id,correct_answer)
# example for the following: <textline size="5" correct_answer="saturated" />
for entry in problems_simple.xpath("//"+"|//".join(response_properties+entry_types)):
answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline
for entry in problems_simple.xpath("//" + "|//".join(response_properties + entry_types)):
answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline
if answer:
answer_map[entry.get('id')] = contextualize_text(answer, self.context)
# include solutions from <solution>...</solution> stanzas
# Tentative merge; we should figure out how we want to handle hints and solutions
for entry in self.tree.xpath("//"+"|//".join(solution_types)):
for entry in self.tree.xpath("//" + "|//".join(solution_types)):
answer = etree.tostring(entry)
if answer:
answer_map[entry.get('id')] = answer
......@@ -207,11 +205,10 @@ class LoncapaProblem(object):
return answer_map
def get_answer_ids(self):
"""Return the IDs of all the responses -- these are the keys used for
the dicts returned by grade_answers and get_question_answers. (Though
"""Return the IDs of all the responses -- these are the keys used for
the dicts returned by grade_answers and get_question_answers. (Though
get_question_answers may only return a subset of these."""
answer_ids = []
context=self.extract_context(self.tree)
problems_simple = self.extract_problems(self.tree)
for response in problems_simple:
responder = response_types[response.tag](response, self.context)
......@@ -223,35 +220,35 @@ class LoncapaProblem(object):
return answer_ids
# ======= Private ========
def extract_context(self, tree, seed = struct.unpack('i', os.urandom(4))[0]): # private
def extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private
'''
Extract content of <script>...</script> from the problem.xml file, and exec it in the
context of this problem. Provides ability to randomize problems, and also set
variables for problem answer checking.
Problem XML goes to Python execution context. Runs everything in script tags
'''
random.seed(self.seed)
context = {'global_context':global_context} # save global context in here also
context.update(global_context) # initialize context to have stuff in global_context
context['__builtins__'] = globals()['__builtins__'] # put globals there also
context['the_lcp'] = self # pass instance of LoncapaProblem in
context = {'global_context': global_context} # save global context in here also
context.update(global_context) # initialize context to have stuff in global_context
context['__builtins__'] = globals()['__builtins__'] # put globals there also
context['the_lcp'] = self # pass instance of LoncapaProblem in
#for script in tree.xpath('/problem/script'):
for script in tree.findall('.//script'):
stype = script.get('type')
if stype:
if 'javascript' in stype: continue # skip javascript
if 'perl' in stype: continue # skip perl
# TODO: evaluate only python
if 'javascript' in stype:
continue # skip javascript
if 'perl' in stype:
continue # skip perl
# TODO: evaluate only python
code = script.text
XMLESC = {"&apos;": "'", "&quot;": '"'}
code = unescape(code,XMLESC)
code = unescape(code, XMLESC)
try:
exec code in context, context # use "context" for global context; thus defs in code are global within code
exec code in context, context # use "context" for global context; thus defs in code are global within code
except Exception:
log.exception("Error while execing code: " + code)
return context
......@@ -265,11 +262,11 @@ class LoncapaProblem(object):
if problemtree.tag in html_problem_semantics:
return
problemid = problemtree.get('id') # my ID
problemid = problemtree.get('id') # my ID
# used to be
# if problemtree.tag in html_special_response:
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
......@@ -283,7 +280,7 @@ class LoncapaProblem(object):
value = self.student_answers[problemid]
#### This code is a hack. It was merged to help bring two branches
#### in sync, but should be replaced. msg should be passed in a
#### in sync, but should be replaced. msg should be passed in a
#### response_type
# prepare the response message, if it exists in correct_map
if 'msg' in self.correct_map:
......@@ -296,112 +293,111 @@ class LoncapaProblem(object):
# do the rendering
# 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)
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:
subitems = self.extract_html(item)
if subitems is not None:
for subitem in subitems:
tree.append(subitem)
for (key,value) in problemtree.items():
for (key, value) in problemtree.items():
tree.set(key, value)
tree.text=problemtree.text
tree.tail=problemtree.tail
tree.text = problemtree.text
tree.tail = problemtree.tail
if problemtree.tag in html_transforms:
tree.tag=html_transforms[problemtree.tag]['tag']
tree.tag = html_transforms[problemtree.tag]['tag']
# Reset attributes. Otherwise, we get metadata in HTML
# (e.g. answers)
# (e.g. answers)
# TODO: We should remove and not zero them.
# I'm not sure how to do that quickly with lxml
for k in tree.keys():
tree.set(k,"")
tree.set(k, "")
# TODO: Fix. This loses Element().tail
#if problemtree.tag in html_skip:
# return tree
return [tree]
def preprocess_problem(self, tree, correct_map=dict(), answer_map=dict()): # private
def preprocess_problem(self, tree, correct_map=dict(), answer_map=dict()): # private
'''
Assign IDs to all the responses
Assign IDs to all the responses
Assign sub-IDs to all entries (textline, schematic, etc.)
Annoted correctness and value
In-place transformation
'''
response_id = 1
for response in tree.xpath('//'+"|//".join(response_types)):
response_id_str=self.problem_id+"_"+str(response_id)
response.attrib['id']=response_id_str
for response in tree.xpath('//' + "|//".join(response_types)):
response_id_str = self.problem_id + "_" + str(response_id)
response.attrib['id'] = response_id_str
if response_id not in correct_map:
correct = 'unsubmitted'
response.attrib['state'] = correct
response_id = response_id + 1
answer_id = 1
for entry in tree.xpath("|".join(['//'+response.tag+'[@id=$id]//'+x for x in (entry_types + solution_types)]),
for entry in tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]),
id=response_id_str):
# assign one answer_id for each entry_type or solution_type
# assign one answer_id for each entry_type or solution_type
entry.attrib['response_id'] = str(response_id)
entry.attrib['answer_id'] = str(answer_id)
entry.attrib['id'] = "%s_%i_%i"%(self.problem_id, response_id, answer_id)
answer_id=answer_id+1
entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id)
answer_id = answer_id + 1
# <solution>...</solution> may not be associated with any specific response; give IDs for those separately
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
solution_id = 1
for solution in tree.findall('.//solution'):
solution.attrib['id'] = "%s_solution_%i"%(self.problem_id, solution_id)
solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id)
solution_id += 1
def extract_problems(self, problem_tree):
''' Remove layout from the problem, and give a purified XML tree of just the problems '''
problem_tree=copy.deepcopy(problem_tree)
tree=Element('problem')
for response in problem_tree.xpath("//"+"|//".join(response_types)):
problem_tree = copy.deepcopy(problem_tree)
tree = Element('problem')
for response in problem_tree.xpath("//" + "|//".join(response_types)):
newresponse = copy.copy(response)
for e in newresponse:
for e in newresponse:
newresponse.remove(e)
# copy.copy is needed to make xpath work right. Otherwise, it starts at the root
# of the tree. We should figure out if there's some work-around
for e in copy.copy(response).xpath("//"+"|//".join(response_properties+entry_types)):
for e in copy.copy(response).xpath("//" + "|//".join(response_properties + entry_types)):
newresponse.append(e)
tree.append(newresponse)
return tree
if __name__=='__main__':
problem_id='simpleFormula'
if __name__ == '__main__':
problem_id = 'simpleFormula'
filename = 'simpleFormula.xml'
problem_id='resistor'
problem_id = 'resistor'
filename = 'resistor.xml'
lcp = LoncapaProblem(filename, problem_id)
context = lcp.extract_context(lcp.tree)
problem = lcp.extract_problems(lcp.tree)
print lcp.grade_problems({'resistor_2_1':'1.0','resistor_3_1':'2.0'})
print lcp.grade_problems({'resistor_2_1': '1.0', 'resistor_3_1': '2.0'})
#print lcp.grade_problems({'simpleFormula_2_1':'3*x^3'})
#numericalresponse(problem, context)
#print etree.tostring((lcp.tree))
print '============'
print
#print etree.tostring(lcp.extract_problems(lcp.tree))
print lcp.get_html()
#print extract_context(tree)
# def handle_fr(self, element):
......@@ -411,4 +407,4 @@ if __name__=='__main__':
# "sample_range":dict(zip(variables, sranges)),
# "samples_count": numsamples,
# "id":id,
# self.questions[self.lid]=problem
# self.questions[self.lid]=problem
......@@ -23,6 +23,7 @@ log = logging.getLogger('capa.checker')
class DemoSystem(object):
def __init__(self):
self.lookup = TemplateLookup(directories=[path(__file__).dirname() / 'templates'])
self.DEBUG = True
def render_template(self, template_filename, dictionary, context=None):
if context is None:
......
......@@ -277,14 +277,14 @@ def sympy_check2():
print "can't find cfn in context = ",context
if not self.code:
if not answer:
if answer is None:
# raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid
print "[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid
self.code = ''
else:
answer_src = answer.get('src')
if answer_src is not None:
self.code = open(settings.DATA_DIR+'src/'+answer_src).read()
self.code = self.system.filesystem.open('src/'+answer_src).read()
else:
self.code = answer.text
......@@ -329,8 +329,7 @@ def sympy_check2():
})
# pass self.system.debug to cfn
# if hasattr(self.system,'debug'): self.context['debug'] = self.system.debug
self.context['debug'] = settings.DEBUG
self.context['debug'] = self.system.DEBUG
# exec the check function
if type(self.code)==str:
......@@ -492,7 +491,7 @@ main()
answer_src = answer.get('src')
if answer_src is not None:
self.code = open(settings.DATA_DIR+'src/'+answer_src).read()
self.code = self.system.filesystem.open('src/'+answer_src).read()
else:
self.code = answer.text
......@@ -522,7 +521,7 @@ main()
log.error(msg)
raise Exception, msg
if settings.DEBUG: log.info('response = %s' % r.text)
if self.system.DEBUG: log.info('response = %s' % r.text)
if (not r.text ) or (not r.text.strip()):
raise Exception,'Error: no response from external server url=%s' % self.url
......@@ -551,7 +550,7 @@ main()
rxml = self.do_external_request('get_score',extra_payload)
except Exception, err:
log.error('Error %s' % err)
if settings.DEBUG:
if self.system.DEBUG:
correct_map = dict(zip(sorted(self.answer_ids), ['incorrect'] * len(self.answer_ids) ))
correct_map['msg_%s' % self.answer_ids[0]] = '<font color="red" size="+2">%s</font>' % str(err).replace('<','&lt;')
return correct_map
......@@ -581,7 +580,7 @@ main()
exans = json.loads(rxml.find('expected').text)
except Exception,err:
log.error('Error %s' % err)
if settings.DEBUG:
if self.system.DEBUG:
msg = '<font color=red size=+2>%s</font>' % str(err).replace('<','&lt;')
exans = [''] * len(self.answer_ids)
exans[0] = msg
......
......@@ -11,7 +11,8 @@ from datetime import timedelta
from lxml import etree
from x_module import XModule, XModuleDescriptor
from capa.capa_problem import LoncapaProblem, StudentInputError
from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError
log = logging.getLogger("mitx.courseware")
#-----------------------------------------------------------------------------
......
......@@ -70,7 +70,8 @@ MAKO_TEMPLATES = {}
MAKO_TEMPLATES['course'] = [DATA_DIR]
MAKO_TEMPLATES['sections'] = [DATA_DIR / 'sections']
MAKO_TEMPLATES['custom_tags'] = [DATA_DIR / 'custom_tags']
MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
COMMON_ROOT / 'lib' / 'capa' / 'templates',
DATA_DIR / 'info',
DATA_DIR / 'problems']
......
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