import copy import logging import math import numpy import os import random import re import scipy import struct from lxml import etree from lxml.etree import Element from mako.template import Template from util import contextualize_text from inputtypes import textline, schematic from responsetypes import numericalresponse, formularesponse, customresponse, schematicresponse import calc import eia log = logging.getLogger("mitx.courseware") response_types = {'numericalresponse':numericalresponse, 'formularesponse':formularesponse, 'customresponse':customresponse, 'schematicresponse':schematicresponse} entry_types = ['textline', 'schematic'] response_properties = ["responseparam", "answer"] # 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'}, "schematicresponse": {'tag':'span'}, "formularesponse": {'tag':'span'}, "text": {'tag':'span'}} 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"] # These should be transformed html_special_response = {"textline":textline.render, "schematic":schematic.render} class LoncapaProblem(object): def __init__(self, filename, id=None, state=None): ## Initialize class variables from state self.seed = None self.student_answers = dict() self.correct_map = dict() self.done = False self.filename = filename if id: self.problem_id = id else: print "NO ID" raise Exception("This should never happen (183)") #self.problem_id = filename if state: if 'seed' in state: self.seed = state['seed'] if 'student_answers' in state: self.student_answers = state['student_answers'] if 'correct_map' in state: self.correct_map = state['correct_map'] if 'done' in state: self.done = state['done'] # 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] ## Parse XML file log.debug(u"LoncapaProblem() opening file {0}".format(filename)) file_text = open(filename).read() # Convert startouttext and endouttext to proper # TODO: Do with XML operations 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.context = self.extract_context(self.tree, seed=self.seed) def get_state(self): ''' 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} def get_max_score(self): sum = 0 for et in entry_types: sum = sum + self.tree.xpath('count(//'+et+')') return int(sum) def get_score(self): 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()} else: return {'score':correct, 'total':self.get_max_score()} def grade_answers(self, answers): 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) results = grader.grade(answers) self.correct_map.update(results) return self.correct_map def get_question_answers(self): context=self.extract_context(self.tree) answer_map = dict() problems_simple = self.extract_problems(self.tree) for response in problems_simple: responder = response_types[response.tag](response, self.context) results = responder.get_answers() answer_map.update(results) for entry in problems_simple.xpath("//"+"|//".join(response_properties+entry_types)): answer = entry.get('correct_answer') if answer: answer_map[entry.get('id')] = contextualize_text(answer, self.context) return answer_map # ======= Private ======== def extract_context(self, tree, seed = struct.unpack('i', os.urandom(4))[0]): # private ''' Problem XML goes to Python execution context. Runs everything in script tags ''' random.seed(self.seed) context = dict() for script in tree.xpath('/problem/script'): exec script.text in global_context, context return context def get_html(self): return contextualize_text(etree.tostring(self.extract_html(self.tree)[0]), self.context) def extract_html(self, problemtree): # private ''' Helper function for get_html. Recursively converts XML tree to HTML ''' if problemtree.tag in html_problem_semantics: return if problemtree.tag in html_special_response: status = "unsubmitted" if problemtree.get('id') in self.correct_map: status = self.correct_map[problemtree.get('id')] value = "" if self.student_answers and problemtree.get('id') in self.student_answers: value = self.student_answers[problemtree.get('id')] return html_special_response[problemtree.tag](problemtree, value, status) #TODO tree=Element(problemtree.tag) for item in problemtree: subitems = self.extract_html(item) if subitems: for subitem in subitems: tree.append(subitem) for (key,value) in problemtree.items(): tree.set(key, value) tree.text=problemtree.text tree.tail=problemtree.tail if problemtree.tag in html_transforms: tree.tag=html_transforms[problemtree.tag]['tag'] # 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 ''' 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 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]), id=response_id_str): 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 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)): newresponse = copy.copy(response) 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)): newresponse.append(e) tree.append(newresponse) return tree if __name__=='__main__': problem_id='simpleFormula' filename = 'simpleFormula.xml' 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({'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): # 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, # self.questions[self.lid]=problem