diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index 33838c7ee1..baf3086e02 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -17,14 +17,16 @@ log = logging.getLogger("mitx.courseware") #----------------------------------------------------------------------------- TIMEDELTA_REGEX = re.compile(r'^((?P\d+?) day(?:s?))?(\s)?((?P\d+?) hour(?:s?))?(\s)?((?P\d+?) minute(?:s)?)?(\s)?((?P\d+?) second(?:s)?)?$') -def item(l, default="", process=lambda x:x): - if len(l)==0: + +def item(l, default="", process=lambda x: x): + if len(l) == 0: return default - elif len(l)==1: + elif len(l) == 1: return process(l[0]) else: raise Exception('Malformed XML') + def parse_timedelta(time_str): parts = TIMEDELTA_REGEX.match(time_str) if not parts: @@ -36,20 +38,23 @@ def parse_timedelta(time_str): time_params[name] = int(param) return timedelta(**time_params) + class ComplexEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, complex): - return "{real:.7g}{imag:+.7g}*j".format(real = obj.real,imag = obj.imag) + return "{real:.7g}{imag:+.7g}*j".format(real=obj.real, imag=obj.imag) return json.JSONEncoder.default(self, obj) + class ModuleDescriptor(XModuleDescriptor): pass + class Module(XModule): ''' Interface between capa_problem and x_module. Originally a hack meant to be refactored out, but it seems to be serving a useful prupose now. We can e.g .destroy and create the capa_problem on a - reset. + reset. ''' id_attribute = "filename" @@ -77,31 +82,30 @@ class Module(XModule): def get_problem_html(self, encapsulate=True): html = self.lcp.get_html() - content={'name':self.name, - 'html':html, - 'weight': self.weight, - } - + content = {'name': self.name, + 'html': html, + 'weight': self.weight, + } + # We using strings as truthy values, because the terminology of the check button # is context-specific. check_button = "Grade" if self.max_attempts else "Check" reset_button = True save_button = True - # If we're after deadline, or user has exhuasted attempts, - # question is read-only. + # If we're after deadline, or user has exhuasted attempts, + # question is read-only. if self.closed(): check_button = False reset_button = False save_button = False - # User submitted a problem, and hasn't reset. We don't want - # more submissions. + # more submissions. if self.lcp.done and self.rerandomize == "always": check_button = False save_button = False - + # Only show the reset button if pressing it will show different values if self.rerandomize != 'always': reset_button = False @@ -115,30 +119,30 @@ class Module(XModule): save_button = False # Check if explanation is available, and if so, give a link - explain="" - if self.lcp.done and self.explain_available=='attempted': - explain=self.explanation - if self.closed() and self.explain_available=='closed': - explain=self.explanation - + explain = "" + if self.lcp.done and self.explain_available == 'attempted': + explain = self.explanation + if self.closed() and self.explain_available == 'closed': + explain = self.explanation + if len(explain) == 0: explain = False - context = {'problem' : content, - 'id' : self.item_id, - 'check_button' : check_button, - 'reset_button' : reset_button, - 'save_button' : save_button, - 'answer_available' : self.answer_available(), - 'ajax_url' : self.ajax_url, - 'attempts_used': self.attempts, - 'attempts_allowed': self.max_attempts, + context = {'problem': content, + 'id': self.item_id, + 'check_button': check_button, + 'reset_button': reset_button, + 'save_button': save_button, + 'answer_available': self.answer_available(), + 'ajax_url': self.ajax_url, + 'attempts_used': self.attempts, + 'attempts_allowed': self.max_attempts, 'explain': explain, } html = self.system.render_template('problem.html', context) if encapsulate: - html = '
'.format(id=self.item_id,ajax_url=self.ajax_url)+html+"
" + html = '
'.format(id=self.item_id, ajax_url=self.ajax_url) + html + "
" return html @@ -147,43 +151,42 @@ class Module(XModule): self.attempts = 0 self.max_attempts = None - - dom2 = etree.fromstring(xml) - - self.explanation="problems/"+item(dom2.xpath('/problem/@explain'), default="closed") - # TODO: Should be converted to: self.explanation=item(dom2.xpath('/problem/@explain'), default="closed") - self.explain_available=item(dom2.xpath('/problem/@explain_available')) - display_due_date_string=item(dom2.xpath('/problem/@due')) - if len(display_due_date_string)>0: - self.display_due_date=dateutil.parser.parse(display_due_date_string) + dom2 = etree.fromstring(xml) + + self.explanation = "problems/" + item(dom2.xpath('/problem/@explain'), default="closed") + # TODO: Should be converted to: self.explanation=item(dom2.xpath('/problem/@explain'), default="closed") + self.explain_available = item(dom2.xpath('/problem/@explain_available')) + + display_due_date_string = item(dom2.xpath('/problem/@due')) + if len(display_due_date_string) > 0: + self.display_due_date = dateutil.parser.parse(display_due_date_string) #log.debug("Parsed " + display_due_date_string + " to " + str(self.display_due_date)) else: - self.display_due_date=None - - + self.display_due_date = None + grace_period_string = item(dom2.xpath('/problem/@graceperiod')) - if len(grace_period_string)>0 and self.display_due_date: + if len(grace_period_string) >0 and self.display_due_date: self.grace_period = parse_timedelta(grace_period_string) self.close_date = self.display_due_date + self.grace_period #log.debug("Then parsed " + grace_period_string + " to closing date" + str(self.close_date)) else: self.grace_period = None self.close_date = self.display_due_date - - self.max_attempts=item(dom2.xpath('/problem/@attempts')) + + self.max_attempts =item(dom2.xpath('/problem/@attempts')) if len(self.max_attempts)>0: - self.max_attempts=int(self.max_attempts) + self.max_attempts =int(self.max_attempts) else: - self.max_attempts=None + self.max_attempts =None - self.show_answer=item(dom2.xpath('/problem/@showanswer')) + self.show_answer =item(dom2.xpath('/problem/@showanswer')) - if self.show_answer=="": - self.show_answer="closed" + if self.show_answer =="": + self.show_answer ="closed" - self.rerandomize=item(dom2.xpath('/problem/@rerandomize')) - if self.rerandomize=="" or self.rerandomize=="always" or self.rerandomize=="true": + self.rerandomize =item(dom2.xpath('/problem/@rerandomize')) + if self.rerandomize =="" or self.rerandomize=="always" or self.rerandomize=="true": self.rerandomize="always" elif self.rerandomize=="false" or self.rerandomize=="per_student": self.rerandomize="per_student" @@ -197,7 +200,7 @@ class Module(XModule): if state!=None and 'attempts' in state: self.attempts=state['attempts'] - # TODO: Should be: self.filename=item(dom2.xpath('/problem/@filename')) + # TODO: Should be: self.filename=item(dom2.xpath('/problem/@filename')) self.filename= "problems/"+item(dom2.xpath('/problem/@filename'))+".xml" self.name=item(dom2.xpath('/problem/@name')) self.weight=item(dom2.xpath('/problem/@weight')) @@ -232,13 +235,13 @@ class Module(XModule): def handle_ajax(self, dispatch, get): ''' - This is called by courseware.module_render, to handle an AJAX call. "get" is request.POST + This is called by courseware.module_render, to handle an AJAX call. "get" is request.POST ''' if dispatch=='problem_get': response = self.get_problem(get) - elif False: #self.close_date > + elif False: #self.close_date > return json.dumps({"error":"Past due date"}) - elif dispatch=='problem_check': + elif dispatch=='problem_check': response = self.check_problem(get) elif dispatch=='problem_reset': response = self.reset_problem(get) @@ -246,7 +249,7 @@ class Module(XModule): response = self.save_problem(get) elif dispatch=='problem_show': response = self.get_answer(get) - else: + else: return "Error" return response @@ -258,11 +261,11 @@ class Module(XModule): return True return False - + def answer_available(self): - ''' Is the user allowed to see an answer? - ''' + ''' Is the user allowed to see an answer? + ''' if self.show_answer == '': return False if self.show_answer == "never": @@ -291,16 +294,16 @@ class Module(XModule): ''' if not self.answer_available(): raise self.system.exception404 - else: + else: answers = self.lcp.get_question_answers() - return json.dumps(answers, + return json.dumps(answers, cls=ComplexEncoder) # Figure out if we should move these to capa_problem? def get_problem(self, get): ''' Same as get_problem_html -- if we want to reconfirm we have the right thing e.g. after several AJAX calls.''' - return self.get_problem_html(encapsulate=False) + return self.get_problem_html(encapsulate=False) def check_problem(self, get): ''' Checks whether answers to a problem are correct, and @@ -322,7 +325,7 @@ class Module(XModule): event_info['failure']='closed' self.tracker('save_problem_check_fail', event_info) raise self.system.exception404 - + # Problem submitted. Student should reset before checking # again. if self.lcp.done and self.rerandomize == "always": @@ -334,19 +337,19 @@ class Module(XModule): old_state = self.lcp.get_state() lcp_id = self.lcp.problem_id correct_map = self.lcp.grade_answers(answers) - except StudentInputError as inst: + except StudentInputError as inst: self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) traceback.print_exc() return json.dumps({'success':inst.message}) - except: + except: self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) traceback.print_exc() raise Exception,"error in capa_module" return json.dumps({'success':'Unknown Error'}) - + self.attempts = self.attempts + 1 self.lcp.done=True - + success = 'correct' for i in correct_map: if correct_map[i]!='correct': @@ -382,7 +385,7 @@ class Module(XModule): event_info['failure']='closed' self.tracker('save_problem_fail', event_info) return "Problem is closed" - + # Problem submitted. Student should reset before saving # again. if self.lcp.done and self.rerandomize == "always": @@ -396,7 +399,7 @@ class Module(XModule): return json.dumps({'success':True}) def reset_problem(self, get): - ''' Changes problem state to unfinished -- removes student answers, + ''' Changes problem state to unfinished -- removes student answers, and causes problem to rerender itself. ''' event_info = dict() event_info['old_state']=self.lcp.get_state() @@ -406,7 +409,7 @@ class Module(XModule): event_info['failure']='closed' self.tracker('reset_problem_fail', event_info) return "Problem is closed" - + if not self.lcp.done: event_info['failure']='not_done' self.tracker('reset_problem_fail', event_info) @@ -420,7 +423,7 @@ class Module(XModule): if self.rerandomize == "always": self.lcp.context=dict() - self.lcp.questions=dict() # Detailed info about questions in problem instance. TODO: Should be by id and not lid. + self.lcp.questions=dict() # Detailed info about questions in problem instance. TODO: Should be by id and not lid. self.lcp.seed=None self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state(), system=self.system)