diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index b655270a9a..f790190215 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -1,6 +1,11 @@ # # File: capa/capa_problem.py # +# Nomenclature: +# +# A capa Problem is a collection of text and capa Response questions. Each Response may have one or more +# Input entry fields. The capa Problem may include a solution. +# ''' Main module which shows problems (of "capa" type). @@ -83,17 +88,32 @@ html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formul class LoncapaProblem(object): + ''' + Main class for capa Problems. + ''' + def __init__(self, fileobject, id, state=None, seed=None, system=None): + ''' + Initializes capa Problem. The problem itself is defined by the XML file + pointed to by fileobject. + + Arguments: + + - filesobject : an OSFS instance: see fs.osfs + - id : string used as the identifier for this problem; often a filename (no spaces) + - state : student state (represented as a dict) + - seed : random number generator seed (int) + - system : I4xSystem instance which provides OS, rendering, and user context + + ''' + ## Initialize class variables from state - self.seed = None self.student_answers = dict() self.correct_map = dict() self.done = False self.problem_id = id self.system = system - - if seed is not None: - self.seed = seed + self.seed = seed if state: if 'seed' in state: @@ -109,22 +129,21 @@ class LoncapaProblem(object): if not self.seed: self.seed = struct.unpack('i', os.urandom(4))[0] - ## Parse XML file - if getattr(system, 'DEBUG', False): + self.fileobject = fileobject # save problem file object, so we can use for debugging information later + if getattr(system, 'DEBUG', False): # get the problem XML string from the problem file 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 - # Convert startouttext and endouttext to proper - # TODO: Do with XML operations - file_text = re.sub("startouttext\s*/", "text", file_text) + file_text = re.sub("startouttext\s*/", "text", file_text) # Convert startouttext and endouttext to proper 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.tree = etree.XML(file_text) # parse problem XML file into an element tree + + # construct script processor context (eg for customresponse problems) self.context = self.extract_context(self.tree, seed=self.seed) - for response in self.tree.xpath('//' + "|//".join(response_types)): - responder = response_types[response.tag](response, self.context, self.system) - responder.preprocess_response() + + # pre-parse the XML tree: modifies it to add ID's and perform some in-place transformations + # this also creates the list (self.responders) of Response instances for each question in the problem + self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map=self.student_answers) def __unicode__(self): return u"LoncapaProblem ({0})".format(self.fileobject) @@ -140,12 +159,27 @@ class LoncapaProblem(object): def get_max_score(self): ''' - TODO: multiple points for programming problems. + Return maximum score for this problem. + We do this by counting the number of answers available for each question + in the problem. If the Response for a question has a get_max_score() method + then we call that and add its return value to the count. That can be + used to give complex problems (eg programming questions) multiple points. ''' - sum = 0 - for et in entry_types: - sum = sum + self.tree.xpath('count(//' + et + ')') - return int(sum) + maxscore = 0 + for responder in self.responders: + if hasattr(responder,'get_max_score'): + try: + maxscore += responder.get_max_score() + except Exception, err: + log.error('responder %s failed to properly return from get_max_score()' % responder) + raise + else: + try: + maxscore += len(responder.get_answers()) + except: + log.error('responder %s failed to properly return get_answers()' % responder) + raise + return maxscore def get_score(self): correct = 0 @@ -166,34 +200,35 @@ class LoncapaProblem(object): of each key removed (the string before the first "_"). Thus, for example, input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123 + + Calles the Response for each question in this problem, to do the actual grading. ''' self.student_answers = answers 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 + log.info('%s: in grade_answers, answers=%s' % (self,answers)) + for responder in self.responders: + results = responder.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 + """Returns a dict of answer_ids to answer values. If we cannot generate an answer (this sometimes happens in customresponses), that answer_id is not included. Called by "show answers" button JSON request (see capa_module) """ answer_map = dict() - 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,... + for responder in self.responders: results = responder.get_answers() answer_map.update(results) # dict of (id,correct_answer) + # This should be handled in each responsetype, not here. # example for the following: - 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) + for responder in self.responders: + for entry in responder.inputfields: + 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 ... stanzas # Tentative merge; we should figure out how we want to handle hints and solutions @@ -209,17 +244,16 @@ class LoncapaProblem(object): the dicts returned by grade_answers and get_question_answers. (Though get_question_answers may only return a subset of these.""" answer_ids = [] - problems_simple = self.extract_problems(self.tree) - for response in problems_simple: - responder = response_types[response.tag](response, self.context) - if hasattr(responder, "answer_id"): - answer_ids.append(responder.answer_id) - # customresponse types can have multiple answer_ids - elif hasattr(responder, "answer_ids"): - answer_ids.extend(responder.answer_ids) - + for responder in self.responders: + answer_ids.append(responder.get_answers().keys()) return answer_ids + def get_html(self): + ''' + Main method called externally to get the HTML to be rendered for this capa Problem. + ''' + return contextualize_text(etree.tostring(self.extract_html(self.tree)[0]), self.context) + # ======= Private ======== def extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private ''' @@ -253,9 +287,6 @@ class LoncapaProblem(object): log.exception("Error while execing code: " + code) 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 ''' @@ -335,76 +366,34 @@ class LoncapaProblem(object): Assign sub-IDs to all entries (textline, schematic, etc.) Annoted correctness and value In-place transformation + + Also create capa Response instances for each responsetype and save as self.responders ''' response_id = 1 + self.responders = [] 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 + response.attrib['id'] = response_id_str # create and save ID for this response + + # if response_id not in correct_map: correct = 'unsubmitted' # unused - to be removed + # response.attrib['state'] = correct + response_id += response_id + answer_id = 1 - 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 + inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]), + id=response_id_str) + for entry in inputfields: # 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 + responder = response_types[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response + self.responders.append(responder) # save in list in self + # ... 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). solution_id = 1 for solution in tree.findall('.//solution'): 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)): - 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 diff --git a/common/lib/capa/responsetypes.py b/common/lib/capa/responsetypes.py index c5683bb0bf..c0ad98baa2 100644 --- a/common/lib/capa/responsetypes.py +++ b/common/lib/capa/responsetypes.py @@ -21,40 +21,123 @@ import abc # specific library imports from calc import evaluator, UndefinedVariable -from util import contextualize_text +from util import * from lxml import etree from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? -log = logging.getLogger(__name__) +#log = logging.getLogger(__name__) +log = logging.getLogger('mitx.common.lib.capa.responsetypes') -def compare_with_tolerance(v1, v2, tol): - ''' Compare v1 to v2 with maximum tolerance tol - tol is relative if it ends in %; otherwise, it is absolute +#----------------------------------------------------------------------------- +# Exceptions + +class LoncapaProblemError(Exception): ''' - relative = "%" in tol - if relative: - tolerance_rel = evaluator(dict(),dict(),tol[:-1]) * 0.01 - tolerance = tolerance_rel * max(abs(v1), abs(v2)) - else: - tolerance = evaluator(dict(),dict(),tol) - return abs(v1-v2) <= tolerance + Error in specification of a problem + ''' + pass + +class ResponseError(Exception): + ''' + Error for failure in processing a response + ''' + pass + +class StudentInputError(Exception): + pass + +#----------------------------------------------------------------------------- +# +# Main base class for CAPA responsetypes class GenericResponse(object): + ''' + Base class for CAPA responsetypes. Each response type (ie a capa question, + which is part of a capa problem) is represented as a superclass, + which should provide the following methods: + + - get_score : evaluate the given student answers, and return a CorrectMap + - get_answers : provide a dict of the expected answers for this problem + + In addition, these methods are optional: + + - get_max_score : if defined, this is called to obtain the maximum score possible for this question + - setup_response : find and note the answer input field IDs for the response; called by __init__ + + Each response type may also specify the following attributes: + + - max_inputfields : (int) maximum number of answer input fields (checked in __init__ if not None) + - allowed_inputfields : list of allowed input fields (each a string) for this Response + - required_attributes : list of required attributes (each a string) on the main response XML stanza + + ''' __metaclass__=abc.ABCMeta # abc = Abstract Base Class + max_inputfields = None + allowed_inputfields = [] + required_attributes = [] + + def __init__(self, xml, inputfields, context, system=None): + ''' + Init is passed the following arguments: + + - xml : ElementTree of this Response + - inputfields : list of ElementTrees for each input entry field in this Response + - context : script processor context + - system : I4xSystem instance which provides OS, rendering, and user context + - __unicode__ : unicode representation of this Response + + ''' + self.xml = xml + self.inputfields = inputfields + self.context = context + self.system = system + + for abox in inputfields: + if not abox.tag in self.allowed_inputfields: + msg = "%s: cannot have input field %s" % (unicode(self),abox.tag) + msg += "\nSee XML source line %s" % getattr(xml,'sourceline','') + raise LoncapaProblemError(msg) + + if self.max_inputfields and len(inputfields)>self.max_inputfields: + msg = "%s: cannot have more than %s input fields" % (unicode(self),self.max_inputfields) + msg += "\nSee XML source line %s" % getattr(xml,'sourceline','') + raise LoncapaProblemError(msg) + + for prop in self.required_attributes: + if not xml.get(prop): + msg = "Error in problem specification: %s missing required attribute %s" % (unicode(self),prop) + msg += "\nSee XML source line %s" % getattr(xml,'sourceline','') + raise LoncapaProblemError(msg) + + self.answer_ids = [x.get('id') for x in self.inputfields] + if self.max_inputfields==1: + self.answer_id = self.answer_ids[0] # for convenience + + if hasattr(self,'setup_response'): + self.setup_response() + @abc.abstractmethod def get_score(self, student_answers): + ''' + Return a CorrectMap for the answers expected vs given. This includes + (correctness, npoints, msg) for each answer_id. + ''' pass @abc.abstractmethod def get_answers(self): + ''' + Return a dict of (answer_id,answer_text) for each answer for this question. + ''' pass #not an abstract method because plenty of responses will not want to preprocess anything, and we should not require that they override this method. - def preprocess_response(self): + def setup_response(self): pass -#Every response type needs methods "get_score" and "get_answers" + def __unicode__(self): + return 'LoncapaProblem Response %s' % self.xml.tag #----------------------------------------------------------------------------- @@ -69,30 +152,19 @@ class MultipleChoiceResponse(GenericResponse): '''}] - def __init__(self, xml, context, system=None): - self.xml = xml - self.correct_choices = xml.xpath('//*[@id=$id]//choice[@correct="true"]', - id=xml.get('id')) - self.correct_choices = [choice.get('name') for choice in self.correct_choices] - self.context = context - self.answer_field = xml.find('choicegroup') # assumes only ONE choicegroup within this response - self.answer_id = xml.xpath('//*[@id=$id]//choicegroup/@id', - id=xml.get('id')) - if not len(self.answer_id) == 1: - raise Exception("should have exactly one choice group per multiplechoicceresponse") - self.answer_id=self.answer_id[0] + max_inputfields = 1 + allowed_inputfields = ['choicegroup'] - 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: - return {self.answer_id:'incorrect'} + def setup_response(self): + self.mc_setup_response() # call secondary setup for MultipleChoice questions, to set name attributes - def get_answers(self): - return {self.answer_id:self.correct_choices} + # define correct choices (after calling secondary setup) + xml = self.xml + cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]',id=xml.get('id')) + self.correct_choices = [choice.get('name') for choice in cxml] - def preprocess_response(self): + def mc_setup_response(self): ''' Initialize name attributes in stanzas in the in this response. ''' @@ -107,9 +179,22 @@ class MultipleChoiceResponse(GenericResponse): i+=1 else: choice.set("name", "choice_"+choice.get("name")) - + + def get_score(self, student_answers): + ''' + grade student response. + ''' + # log.debug('%s: student_answers=%s, correct_choices=%s' % (unicode(self),student_answers,self.correct_choices)) + if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices: + return {self.answer_id:'correct'} + else: + return {self.answer_id:'incorrect'} + + def get_answers(self): + return {self.answer_id:self.correct_choices} + class TrueFalseResponse(MultipleChoiceResponse): - def preprocess_response(self): + def mc_setup_response(self): i=0 for response in self.xml.xpath("choicegroup"): response.set("type", "TrueFalse") @@ -140,12 +225,13 @@ class OptionResponse(GenericResponse): The location of the earth '''}] - def __init__(self, xml, context, system=None): - self.xml = xml - self.answer_fields = xml.findall('optioninput') - self.context = context + allowed_inputfields = ['optioninput'] + + def setup_response(self): + self.answer_fields = self.inputfields def get_score(self, student_answers): + # log.debug('%s: student_answers=%s' % (unicode(self),student_answers)) cmap = {} amap = self.get_answers() for aid in amap: @@ -157,17 +243,20 @@ class OptionResponse(GenericResponse): def get_answers(self): amap = dict([(af.get('id'),af.get('correct')) for af in self.answer_fields]) + # log.debug('%s: expected answers=%s' % (unicode(self),amap)) return amap #----------------------------------------------------------------------------- class NumericalResponse(GenericResponse): - def __init__(self, xml, context, system=None): - self.xml = xml - if not xml.get('answer'): - msg = "Error in problem specification: numericalresponse missing required answer attribute\n" - msg += "See XML source line %s" % getattr(xml,'sourceline','') - raise Exception,msg + + allowed_inputfields = ['textline'] + required_attributes = ['answer'] + max_inputfields = 1 + + def setup_response(self): + xml = self.xml + context = self.context self.correct_answer = contextualize_text(xml.get('answer'), context) try: self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', @@ -182,7 +271,7 @@ class NumericalResponse(GenericResponse): self.answer_id = None def get_score(self, student_answers): - ''' Display HTML for a numeric response ''' + '''Grade a numeric response ''' student_answer = student_answers[self.answer_id] try: correct = compare_with_tolerance (evaluator(dict(),dict(),student_answer), complex(self.correct_answer), self.tolerance) @@ -241,16 +330,11 @@ def sympy_check2(): '''}] - def __init__(self, xml, context, system=None): - self.xml = xml - self.system = system - ## CRITICAL TODO: Should cover all entrytypes - ## NOTE: xpath will look at root of XML tree, not just - ## what's in xml. @id=id keeps us in the right customresponse. - self.answer_ids = xml.xpath('//*[@id=$id]//textline/@id', - id=xml.get('id')) - self.answer_ids += [x.get('id') for x in xml.findall('textbox')] # also allow textbox inputs - self.context = context + allowed_inputfields = ['textline','textbox'] + + def setup_response(self): + xml = self.xml + context = self.context # if has an "expect" (or "answer") attribute then save that self.expect = xml.get('expect') or xml.get('answer') @@ -271,15 +355,17 @@ def sympy_check2(): cfn = xml.get('cfn') if cfn: log.debug("cfn = %s" % cfn) - if cfn in context: - self.code = context[cfn] + if cfn in self.context: + self.code = self.context[cfn] else: - print "can't find cfn in context = ",context + msg = "%s: can't find cfn in context = %s" % (unicode(self),self.context) + msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','') + raise LoncapaProblemError(msg) if not self.code: 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 + log.error("[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid) self.code = '' else: answer_src = answer.get('src') @@ -294,6 +380,8 @@ def sympy_check2(): of each key removed (the string before the first "_"). ''' + log.debug('%s: student_answers=%s' % (unicode(self),student_answers)) + idset = sorted(self.answer_ids) # ordered list of answer id's try: submission = [student_answers[k] for k in idset] # ordered list of answers @@ -425,12 +513,12 @@ class SymbolicResponse(CustomResponse): Your input should be typed in as a list of lists, eg [[1,2],[3,4]]. '''}] - def __init__(self, xml, context, system=None): - xml.set('cfn','symmath_check') + + def setup_response(self): + self.xml.set('cfn','symmath_check') code = "from symmath import *" - exec code in context,context - CustomResponse.__init__(self,xml,context,system) - + exec code in self.context,self.context + CustomResponse.setup_response(self) #----------------------------------------------------------------------------- @@ -480,15 +568,13 @@ main() '''}] - def __init__(self, xml, context, system=None): - self.xml = xml - self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL - self.answer_ids = xml.xpath('//*[@id=$id]//textbox/@id|//*[@id=$id]//textline/@id', - id=xml.get('id')) - self.context = context - answer = xml.xpath('//*[@id=$id]//answer', - id=xml.get('id'))[0] + allowed_inputfields = ['textline','textbox'] + def setup_response(self): + xml = self.xml + self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL + + answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors answer_src = answer.get('src') if answer_src is not None: self.code = self.system.filesystem.open('src/'+answer_src).read() @@ -590,8 +676,6 @@ main() raise Exception,'Short response from external server' return dict(zip(self.answer_ids,exans)) -class StudentInputError(Exception): - pass #----------------------------------------------------------------------------- @@ -617,8 +701,13 @@ class FormulaResponse(GenericResponse): '''}] - def __init__(self, xml, context, system=None): - self.xml = xml + allowed_inputfields = ['textline'] + required_attributes = ['answer'] + max_inputfields = 1 + + def setup_response(self): + xml = self.xml + context = self.context self.correct_answer = contextualize_text(xml.get('answer'), context) self.samples = contextualize_text(xml.get('samples'), context) try: @@ -628,14 +717,6 @@ class FormulaResponse(GenericResponse): except Exception: self.tolerance = 0 - try: - self.answer_id = xml.xpath('//*[@id=$id]//textline/@id', - id=xml.get('id'))[0] - except Exception: - self.answer_id = None - raise Exception, "[courseware.capa.responsetypes.FormulaResponse] Error: missing answer_id!!" - - self.context = context ts = xml.get('type') if ts is None: typeslist = [] @@ -648,7 +729,6 @@ class FormulaResponse(GenericResponse): else: # Default self.case_sensitive = False - def get_score(self, student_answers): variables=self.samples.split('@')[0].split(',') numsamples=int(self.samples.split('@')[1].split('#')[1]) @@ -697,13 +777,12 @@ class FormulaResponse(GenericResponse): #----------------------------------------------------------------------------- class SchematicResponse(GenericResponse): - def __init__(self, xml, context, system=None): - self.xml = xml - self.answer_ids = xml.xpath('//*[@id=$id]//schematic/@id', - id=xml.get('id')) - self.context = context - answer = xml.xpath('//*[@id=$id]//answer', - id=xml.get('id'))[0] + + allowed_inputfields = ['schematic'] + + def setup_response(self): + xml = self.xml + answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0] answer_src = answer.get('src') if answer_src is not None: self.code = self.system.filestore.open('src/'+answer_src).read() # Untested; never used @@ -740,10 +819,10 @@ class ImageResponse(GenericResponse): '''}] - def __init__(self, xml, context, system=None): - self.xml = xml - self.context = context - self.ielements = xml.findall('imageinput') + allowed_inputfields = ['imageinput'] + + def setup_response(self): + self.ielements = self.inputfields self.answer_ids = [ie.get('id') for ie in self.ielements] def get_score(self, student_answers): diff --git a/common/lib/capa/util.py b/common/lib/capa/util.py index d042aa21d3..996f6c8dac 100644 --- a/common/lib/capa/util.py +++ b/common/lib/capa/util.py @@ -1,3 +1,21 @@ +from calc import evaluator, UndefinedVariable + +#----------------------------------------------------------------------------- +# +# Utility functions used in CAPA responsetypes + +def compare_with_tolerance(v1, v2, tol): + ''' Compare v1 to v2 with maximum tolerance tol + tol is relative if it ends in %; otherwise, it is absolute + ''' + relative = "%" in tol + if relative: + tolerance_rel = evaluator(dict(),dict(),tol[:-1]) * 0.01 + tolerance = tolerance_rel * max(abs(v1), abs(v2)) + else: + tolerance = evaluator(dict(),dict(),tol) + return abs(v1-v2) <= tolerance + def contextualize_text(text, context): # private ''' Takes a string with variables. E.g. $a+$b. Does a substitution of those variables from the context ''' diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 97fd1b948c..0f82d9ba94 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -169,7 +169,7 @@ def render_x_module(user, request, xml_module, module_object_preload, position=N content = instance.get_html() # special extra information about each problem, only for users who are staff - if user.is_staff: + if False and user.is_staff: module_id = xml_module.get('id') histogram = grade_histogram(module_id) render_histogram = len(histogram) > 0 diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee index c1a801d2b2..e1e062e949 100644 --- a/lms/static/coffee/src/modules/problem.coffee +++ b/lms/static/coffee/src/modules/problem.coffee @@ -48,7 +48,7 @@ class @Problem @$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true' else - @$("#answer_#{key}").text(value) + @$("#answer_#{key}").html(value) // needs to be html, not text, for complex solutions (eg coding) @$('.show').val 'Hide Answer' @element.addClass 'showed' else