Newer
Older
# For calculator:
# http://pyparsing.wikispaces.com/file/view/fourFn.py
import random, numpy, math, scipy, sys, StringIO, os, struct, json
from xml.dom.minidom import parse, parseString
class LoncapaProblem():
def get_state(self):
''' Stored per-user session data neeeded to:
1) Recreate the problem
2) Populate any student answers. '''
return json.dumps({'seed':self.seed,
'answers':self.answers,
def get_score(self):
correct=0
for key in self.correct_map:
if self.correct_map[key] == u'correct':
correct += 1
if len(self.answers)==0:
return {'score':0,
'total':len(self.questions)}
else:
return {'score':correct,
'total':len(self.questions)}
def get_html(self):
''' Return the HTML of the question '''
return self.text
def __init__(self, filename, id=None, state=None):
''' Create a new problem of the type defined in filename.
By default, this will generate a random problem. Passing
seed will provide the random seed. Alternatively, passing
context will bypass all script execution, and use the
given execution context. '''
if state!=None:
state=json.loads(state)
else:
state={}
self.gid=id
if 'seed' in state and state['seed']!=None and state['seed']!="":
self.seed=state['seed']
else:
# TODO: Check performance of urandom -- depending on
# implementation, it may slow down to the point of causing
# performance issues if we deplete the kernel entropy
# pool.
self.seed=struct.unpack('i', os.urandom(4))[0]
if 'answers' in state:
self.answers=state['answers']
if 'correct_map' in state:
self.correct_map=state['correct_map']
random.seed(self.seed)
dom=parse(filename).childNodes[0]
g={'random':random,'numpy':numpy,'math':math,'scipy':scipy}
# Buffer stores HTML for problem
buf=StringIO.StringIO()
ot=False ## Are we in an outtext context?
# Loop through the nodes of the problem, and
print e, ot
#
print e.childNodes[0].data
elif e.localName=='endouttext':
elif ot:
print e, "::", e.toxml()
elif e.localName=='startouttext':
elif e.localName in self.handlers:
problem=self.handlers[e.localName](self,e)
buf.write(problem)
elif e.localName==None:
pass
else:
raise Exception("ERROR: UNRECOGNIZED XML"+e.localName)
self.text=buf.getvalue()
self.text=self.contextualize_text(self.text)
print self.text
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
text=""
context={} # Execution context from loncapa/python
questions={} # Detailed info about questions in problem instance. TODO: Should be by id and not lid.
answers={} # Student answers
correct_map={}
seed=None
gid="" # ID of the problem
lid=-1 # ID of the field within the problem
def get_context(self):
''' Return the execution context '''
return self.context
def get_seed(self):
''' Return the random seed used to generate the problem '''
return self.seed
def get_correct_map(self):
return self.correct_map
def set_answers(self, answers):
self.answers=answers
def grade_answers(self, answers):
''' Takes a map of IDs to answers. Return which ones are correct '''
self.answers=answers
correct_map={}
for key in self.questions:
id=self.questions[key]['id']
if id not in answers:
correct_map[id]='incorrect' # Should always be there
else:
correct_map[id]=self.grade_nr(self.questions[key],
self.answers[id])
self.correct_map=correct_map
return correct_map
## Internal methods
def number(self,text):
''' Convert a number to a float, understanding suffixes '''
try:
text.strip()
suffixes={'%':0.01,'k':1e3,'M':1e6,'G':1e9,'T':1e12,'P':1e15,
'E':1e18,'Z':1e21,'Y':1e24,'c':1e-2,'m':1e-3,'u':1e-6,
'n':1e-9,'p':1e-12,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
if text[-1] in suffixes:
return float(text[:-1])*suffixes[text[-1]]
else:
return float(text)
except:
return 0 # TODO: Better error handling?
def grade_nr(self, question, answer):
error = abs(self.number(answer) - question['answer'])
allowed_error = abs(question['answer']*question['tolerance'])
if error <= allowed_error:
return 'correct'
else:
return 'incorrect'
def handle_nr(self, element):
answer=element.getAttribute('answer')
for e in element.childNodes:
if e.nodeType==1 and e.getAttribute('type')=="tolerance":
tolerance=e.getAttribute('default')
self.lid+=1
id=str(self.gid)+'_'+str(self.lid)
problem={"answer":self.number(self.contextualize_text(answer)),
"type":"numericalresponse",
"tolerance":self.number(self.contextualize_text(tolerance)),
"id":id,
"lid":self.lid,
}
self.questions[self.lid]=problem
if id in self.answers:
value=self.answers[id]
else:
value=""
icon='bullet'
if id in self.correct_map and self.correct_map[id]=='correct':
icon='check'
if id in self.correct_map and self.correct_map[id]=='incorrect':
icon='close'
html='<input type="text" name="input_{id}" id="input_{id}" value="{value}"><span class="ui-icon ui-icon-{icon}" style="display:inline-block;" id="status_{id}"></span> '.format(id=id,value=value,icon=icon)
return html
graders={'numericalresponse':grade_nr}
handlers={'numericalresponse':handle_nr}
def contextualize_text(self, text):
''' Takes a string with variables. E.g. $a+$b.
Does a substitution of those variables from the context '''
for key in sorted(self.context, lambda x,y:cmp(len(y),len(x))):
text=text.replace('$'+key, str(self.context[key]))
return text
if __name__=='__main__':
p=LoncapaProblem('resistor.xml', seed=-1601461296)
print p.getHtml()
print p.getContext()
print p.getSeed()