import random, numpy, math, scipy, sys, StringIO, os, struct, json from xml.dom.minidom import parse, parseString from calc import evaluator def strip_dict(d): ''' Takes a dict. Returns an identical dict, with all non-word keys stripped out. ''' d=dict([(k, float(d[k])) for k in d if type(k)==str and \ k.isalnum() and \ (type(d[k]) == float or type(d[k]) == int) ]) return d 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, 'correct_map':self.correct_map, 'done':self.done}) 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 'done' in state: self.done=state['done'] 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 for e in dom.childNodes: # print e, ot # if e.localName=='script': #print e.childNodes[0].data exec e.childNodes[0].data in g,self.context elif e.localName=='endouttext': ot=False elif ot: # print e, "::", e.toxml() e.writexml(buf) elif e.localName=='startouttext': ot=True 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 self.filename=filename done=False 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]) grader=self.graders[self.questions[key]['type']] print grader correct_map[id]=grader(self, 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(evaluator({},{},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":evaluator({},{},self.contextualize_text(answer)), "type":"numericalresponse", "tolerance":evaluator({},{},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=' '.format(id=id,value=value,icon=icon) return html def grade_fr(self, question, answer): correct = True for i in range(question['samples_count']): instructor_variables = strip_dict(dict(self.context)) student_variables = dict() for var in question['sample_range']: value = random.uniform(*question['sample_range'][var]) instructor_variables[str(var)] = value student_variables[str(var)] = value instructor_result = evaluator(instructor_variables,{},str(question['answer'])) student_result = evaluator(student_variables,{},str(answer)) if abs( student_result - instructor_result ) > question['tolerance']: return "incorrect" return "correct" def handle_fr(self, element): ## Extract description from element samples=element.getAttribute('samples') variables=samples.split('@')[0].split(',') numsamples=int(samples.split('@')[1].split('#')[1]) sranges=zip(*map(lambda x:map(float, x.split(",")), samples.split('@')[1].split('#')[0].split(':'))) answer=element.getAttribute('answer') for e in element.childNodes: if e.nodeType==1 and e.getAttribute('type')=="tolerance": tolerance=e.getAttribute('default') # Store element self.lid+=1 id=str(self.gid)+'_'+str(self.lid) problem={"answer":self.contextualize_text(answer), "type":"formularesponse", "tolerance":evaluator({},{},self.contextualize_text(tolerance)), "sample_range":dict(zip(variables, sranges)), "samples_count": numsamples, "id":id, "lid":self.lid, } self.questions[self.lid]=problem # Generate HTML 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=' '.format(id=id,value=value,icon=icon) return html graders={'numericalresponse':grade_nr, 'formularesponse':grade_fr} handlers={'numericalresponse':handle_nr, 'formularesponse':handle_fr} 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()