From 58e8f7db12b387b829a3714dd6df08aa0ec1c09c Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 14 Mar 2013 15:10:33 -0400 Subject: [PATCH 01/17] - Pep8 and pylint fixes - beginnings of new Matlab input type - update progress after getting a response from xqueue --- common/lib/capa/capa/inputtypes.py | 181 ++++++--- common/lib/capa/capa/responsetypes.py | 354 +++++++++++------- .../lib/capa/capa/templates/matlabinput.html | 60 +++ .../xmodule/js/src/capa/display.coffee | 1 + 4 files changed, 404 insertions(+), 192 deletions(-) create mode 100644 common/lib/capa/capa/templates/matlabinput.html diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index c2babfa479..f49ad5b422 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -44,7 +44,6 @@ from lxml import etree import re import shlex # for splitting quoted strings import sys -import os import pyparsing from .registry import TagRegistry @@ -97,7 +96,8 @@ class Attribute(object): """ val = element.get(self.name) if self.default == self._sentinel and val is None: - raise ValueError('Missing required attribute {0}.'.format(self.name)) + raise ValueError( + 'Missing required attribute {0}.'.format(self.name)) if val is None: # not required, so return default @@ -149,7 +149,8 @@ class InputTypeBase(object): self.id = state.get('id', xml.get('id')) if self.id is None: - raise ValueError("input id state is None. xml is {0}".format(etree.tostring(xml))) + raise ValueError("input id state is None. xml is {0}".format( + etree.tostring(xml))) self.value = state.get('value', '') @@ -169,14 +170,15 @@ class InputTypeBase(object): self.process_requirements() # Call subclass "constructor" -- means they don't have to worry about calling - # super().__init__, and are isolated from changes to the input constructor interface. + # super().__init__, and are isolated from changes to the input + # constructor interface. self.setup() except Exception as err: # Something went wrong: add xml to message, but keep the traceback - msg = "Error in xml '{x}': {err} ".format(x=etree.tostring(xml), err=str(err)) + msg = "Error in xml '{x}': {err} ".format( + x=etree.tostring(xml), err=str(err)) raise Exception, msg, sys.exc_info()[2] - @classmethod def get_attributes(cls): """ @@ -186,7 +188,6 @@ class InputTypeBase(object): """ return [] - def process_requirements(self): """ Subclasses can declare lists of required and optional attributes. This @@ -196,7 +197,8 @@ class InputTypeBase(object): Processes attributes, putting the results in the self.loaded_attributes dictionary. Also creates a set self.to_render, containing the names of attributes that should be included in the context by default. """ - # Use local dicts and sets so that if there are exceptions, we don't end up in a partially-initialized state. + # Use local dicts and sets so that if there are exceptions, we don't + # end up in a partially-initialized state. loaded = {} to_render = set() for a in self.get_attributes(): @@ -226,7 +228,7 @@ class InputTypeBase(object): get: a dictionary containing the data that was sent with the ajax call Output: - a dictionary object that can be serialized into JSON. This will be sent back to the Javascript. + a dictionary object that can be serialized into JSON. This will be sent back to the Javascript. """ pass @@ -247,8 +249,9 @@ class InputTypeBase(object): 'value': self.value, 'status': self.status, 'msg': self.msg, - } - context.update((a, v) for (a, v) in self.loaded_attributes.iteritems() if a in self.to_render) + } + context.update((a, v) for ( + a, v) in self.loaded_attributes.iteritems() if a in self.to_render) context.update(self._extra_context()) return context @@ -371,7 +374,6 @@ class ChoiceGroup(InputTypeBase): return [Attribute("show_correctness", "always"), Attribute("submitted_message", "Answer received.")] - def _extra_context(self): return {'input_type': self.html_input_type, 'choices': self.choices, @@ -436,7 +438,6 @@ class JavascriptInput(InputTypeBase): Attribute('display_class', None), Attribute('display_file', None), ] - def setup(self): # Need to provide a value that JSON can parse if there is no # student-supplied value yet. @@ -459,7 +460,6 @@ class TextLine(InputTypeBase): template = "textline.html" tags = ['textline'] - @classmethod def get_attributes(cls): """ @@ -474,12 +474,12 @@ class TextLine(InputTypeBase): # Attributes below used in setup(), not rendered directly. Attribute('math', None, render=False), - # TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x + # TODO: 'dojs' flag is temporary, for backwards compatibility with + # 8.02x Attribute('dojs', None, render=False), Attribute('preprocessorClassName', None, render=False), Attribute('preprocessorSrc', None, render=False), - ] - + ] def setup(self): self.do_math = bool(self.loaded_attributes['math'] or @@ -490,12 +490,12 @@ class TextLine(InputTypeBase): self.preprocessor = None if self.do_math: # Preprocessor to insert between raw input and Mathjax - self.preprocessor = {'class_name': self.loaded_attributes['preprocessorClassName'], - 'script_src': self.loaded_attributes['preprocessorSrc']} + self.preprocessor = { + 'class_name': self.loaded_attributes['preprocessorClassName'], + 'script_src': self.loaded_attributes['preprocessorSrc']} if None in self.preprocessor.values(): self.preprocessor = None - def _extra_context(self): return {'do_math': self.do_math, 'preprocessor': self.preprocessor, } @@ -539,7 +539,8 @@ class FileSubmission(InputTypeBase): """ # Check if problem has been queued self.queue_len = 0 - # Flag indicating that the problem has been queued, 'msg' is length of queue + # Flag indicating that the problem has been queued, 'msg' is length of + # queue if self.status == 'incomplete': self.status = 'queued' self.queue_len = self.msg @@ -547,7 +548,6 @@ class FileSubmission(InputTypeBase): def _extra_context(self): return {'queue_len': self.queue_len, } - return context registry.register(FileSubmission) @@ -562,8 +562,9 @@ class CodeInput(InputTypeBase): template = "codeinput.html" tags = ['codeinput', - 'textbox', # Another (older) name--at some point we may want to make it use a - # non-codemirror editor. + 'textbox', + # Another (older) name--at some point we may want to make it use a + # non-codemirror editor. ] # pulled out for testing @@ -590,13 +591,15 @@ class CodeInput(InputTypeBase): """ Implement special logic: handle queueing state, and default input. """ - # if no student input yet, then use the default input given by the problem + # if no student input yet, then use the default input given by the + # problem if not self.value: self.value = self.xml.text # Check if problem has been queued self.queue_len = 0 - # Flag indicating that the problem has been queued, 'msg' is length of queue + # Flag indicating that the problem has been queued, 'msg' is length of + # queue if self.status == 'incomplete': self.status = 'queued' self.queue_len = self.msg @@ -610,8 +613,67 @@ registry.register(CodeInput) #----------------------------------------------------------------------------- + + +class MatlabInput(CodeInput): + ''' + InputType for handling Matlab code input + ''' + template = "matlabinput.html" + tags = ['matlabinput'] + + # pulled out for testing + submitted_msg = ("Submitted. As soon as your submission is" + " graded, this message will be replaced with the grader's feedback.") + + def setup(self): + ''' + Handle matlab-specific parsing + ''' + xml = self.xml + self.plot_payload = xml.findtext('./plot_payload') + # if no student input yet, then use the default input given by the + # problem + if not self.value: + self.value = self.xml.text + + # Check if problem has been queued + self.queue_len = 0 + # Flag indicating that the problem has been queued, 'msg' is length of + # queue + if self.status == 'incomplete': + self.status = 'queued' + self.queue_len = self.msg + self.msg = self.submitted_msg + + + + def handle_ajax(self, dispatch, get): + if dispatch == 'plot': + # put the data in the queue and ship it off + pass + elif dispatch == 'display': + # render the response + pass + + def plot_data(self, get): + ''' send data via xqueue to the mathworks backend''' + + # only send data if xqueue exists + if self.system.xqueue is not None: + pass + + + + +registry.register(MatlabInput) + + +#----------------------------------------------------------------------------- + class Schematic(InputTypeBase): """ + InputType for the schematic editor """ template = "schematicinput.html" @@ -630,7 +692,6 @@ class Schematic(InputTypeBase): Attribute('initial_value', None), Attribute('submit_analyses', None), ] - return context registry.register(Schematic) @@ -660,12 +721,12 @@ class ImageInput(InputTypeBase): Attribute('height'), Attribute('width'), ] - def setup(self): """ if value is of the form [x,y] then parse it and send along coordinates of previous answer """ - m = re.match('\[([0-9]+),([0-9]+)]', self.value.strip().replace(' ', '')) + m = re.match('\[([0-9]+),([0-9]+)]', + self.value.strip().replace(' ', '')) if m: # Note: we subtract 15 to compensate for the size of the dot on the screen. # (is a 30x30 image--lms/static/green-pointer.png). @@ -673,7 +734,6 @@ class ImageInput(InputTypeBase): else: (self.gx, self.gy) = (0, 0) - def _extra_context(self): return {'gx': self.gx, @@ -730,7 +790,7 @@ class VseprInput(InputTypeBase): registry.register(VseprInput) -#-------------------------------------------------------------------------------- +#------------------------------------------------------------------------- class ChemicalEquationInput(InputTypeBase): @@ -794,7 +854,8 @@ class ChemicalEquationInput(InputTypeBase): result['error'] = "Couldn't parse formula: {0}".format(p) except Exception: # this is unexpected, so log - log.warning("Error while previewing chemical formula", exc_info=True) + log.warning( + "Error while previewing chemical formula", exc_info=True) result['error'] = "Error while rendering preview" return result @@ -843,16 +904,16 @@ class DragAndDropInput(InputTypeBase): 'can_reuse': ""} tag_attrs['target'] = {'id': Attribute._sentinel, - 'x': Attribute._sentinel, - 'y': Attribute._sentinel, - 'w': Attribute._sentinel, - 'h': Attribute._sentinel} + 'x': Attribute._sentinel, + 'y': Attribute._sentinel, + 'w': Attribute._sentinel, + 'h': Attribute._sentinel} dic = dict() for attr_name in tag_attrs[tag_type].keys(): dic[attr_name] = Attribute(attr_name, - default=tag_attrs[tag_type][attr_name]).parse_from_xml(tag) + default=tag_attrs[tag_type][attr_name]).parse_from_xml(tag) if tag_type == 'draggable' and not self.no_labels: dic['label'] = dic['label'] or dic['id'] @@ -865,7 +926,7 @@ class DragAndDropInput(InputTypeBase): # add labels to images?: self.no_labels = Attribute('no_labels', - default="False").parse_from_xml(self.xml) + default="False").parse_from_xml(self.xml) to_js = dict() @@ -874,16 +935,16 @@ class DragAndDropInput(InputTypeBase): # outline places on image where to drag adn drop to_js['target_outline'] = Attribute('target_outline', - default="False").parse_from_xml(self.xml) + default="False").parse_from_xml(self.xml) # one draggable per target? to_js['one_per_target'] = Attribute('one_per_target', - default="True").parse_from_xml(self.xml) + default="True").parse_from_xml(self.xml) # list of draggables to_js['draggables'] = [parse(draggable, 'draggable') for draggable in - self.xml.iterchildren('draggable')] + self.xml.iterchildren('draggable')] # list of targets to_js['targets'] = [parse(target, 'target') for target in - self.xml.iterchildren('target')] + self.xml.iterchildren('target')] # custom background color for labels: label_bg_color = Attribute('label_bg_color', @@ -896,7 +957,7 @@ class DragAndDropInput(InputTypeBase): registry.register(DragAndDropInput) -#-------------------------------------------------------------------------------------------------------------------- +#------------------------------------------------------------------------- class EditAMoleculeInput(InputTypeBase): @@ -934,6 +995,7 @@ registry.register(EditAMoleculeInput) #----------------------------------------------------------------------------- + class DesignProtein2dInput(InputTypeBase): """ An input type for design of a protein in 2D. Integrates with the Protex java applet. @@ -969,6 +1031,7 @@ registry.register(DesignProtein2dInput) #----------------------------------------------------------------------------- + class EditAGeneInput(InputTypeBase): """ An input type for editing a gene. Integrates with the genex java applet. @@ -1005,6 +1068,7 @@ registry.register(EditAGeneInput) #--------------------------------------------------------------------- + class AnnotationInput(InputTypeBase): """ Input type for annotations: students can enter some notes or other text @@ -1037,13 +1101,14 @@ class AnnotationInput(InputTypeBase): def setup(self): xml = self.xml - self.debug = False # set to True to display extra debug info with input - self.return_to_annotation = True # return only works in conjunction with annotatable xmodule + self.debug = False # set to True to display extra debug info with input + self.return_to_annotation = True # return only works in conjunction with annotatable xmodule self.title = xml.findtext('./title', 'Annotation Exercise') self.text = xml.findtext('./text') self.comment = xml.findtext('./comment') - self.comment_prompt = xml.findtext('./comment_prompt', 'Type a commentary below:') + self.comment_prompt = xml.findtext( + './comment_prompt', 'Type a commentary below:') self.tag_prompt = xml.findtext('./tag_prompt', 'Select one tag:') self.options = self._find_options() @@ -1061,7 +1126,7 @@ class AnnotationInput(InputTypeBase): 'id': index, 'description': option.text, 'choice': option.get('choice') - } for (index, option) in enumerate(elements) ] + } for (index, option) in enumerate(elements)] def _validate_options(self): ''' Raises a ValueError if the choice attribute is missing or invalid. ''' @@ -1071,7 +1136,8 @@ class AnnotationInput(InputTypeBase): if choice is None: raise ValueError('Missing required choice attribute.') elif choice not in valid_choices: - raise ValueError('Invalid choice attribute: {0}. Must be one of: {1}'.format(choice, ', '.join(valid_choices))) + raise ValueError('Invalid choice attribute: {0}. Must be one of: {1}'.format( + choice, ', '.join(valid_choices))) def _unpack(self, json_value): ''' Unpacks the json input state into a dict. ''' @@ -1089,20 +1155,20 @@ class AnnotationInput(InputTypeBase): return { 'options_value': options_value, - 'has_options_value': len(options_value) > 0, # for convenience + 'has_options_value': len(options_value) > 0, # for convenience 'comment_value': comment_value, } def _extra_context(self): extra_context = { - 'title': self.title, - 'text': self.text, - 'comment': self.comment, - 'comment_prompt': self.comment_prompt, - 'tag_prompt': self.tag_prompt, - 'options': self.options, - 'return_to_annotation': self.return_to_annotation, - 'debug': self.debug + 'title': self.title, + 'text': self.text, + 'comment': self.comment, + 'comment_prompt': self.comment_prompt, + 'tag_prompt': self.tag_prompt, + 'options': self.options, + 'return_to_annotation': self.return_to_annotation, + 'debug': self.debug } extra_context.update(self._unpack(self.value)) @@ -1110,4 +1176,3 @@ class AnnotationInput(InputTypeBase): return extra_context registry.register(AnnotationInput) - diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 6bf98999d8..62da901656 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -128,21 +128,25 @@ class LoncapaResponse(object): for abox in inputfields: if abox.tag not in self.allowed_inputfields: - msg = "%s: cannot have input field %s" % (unicode(self), abox.tag) - msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '') + 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', '') + 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', '') + msg += "\nSee XML source line %s" % getattr( + xml, 'sourceline', '') raise LoncapaProblemError(msg) # ordered list of answer_id values for this response @@ -163,7 +167,8 @@ class LoncapaResponse(object): for entry in self.inputfields: answer = entry.get('correct_answer') if answer: - self.default_answer_map[entry.get('id')] = contextualize_text(answer, self.context) + self.default_answer_map[entry.get( + 'id')] = contextualize_text(answer, self.context) if hasattr(self, 'setup_response'): self.setup_response() @@ -211,7 +216,8 @@ class LoncapaResponse(object): Returns the new CorrectMap, with (correctness,msg,hint,hintmode) for each answer_id. ''' new_cmap = self.get_score(student_answers) - self.get_hints(convert_files_to_filenames(student_answers), new_cmap, old_cmap) + self.get_hints(convert_files_to_filenames( + student_answers), new_cmap, old_cmap) # log.debug('new_cmap = %s' % new_cmap) return new_cmap @@ -241,14 +247,17 @@ class LoncapaResponse(object): # callback procedure to a social hint generation system. if not hintfn in self.context: msg = 'missing specified hint function %s in script context' % hintfn - msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '') + msg += "\nSee XML source line %s" % getattr( + self.xml, 'sourceline', '') raise LoncapaProblemError(msg) try: - self.context[hintfn](self.answer_ids, student_answers, new_cmap, old_cmap) + self.context[hintfn]( + self.answer_ids, student_answers, new_cmap, old_cmap) except Exception as err: msg = 'Error %s in evaluating hint function %s' % (err, hintfn) - msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '') + msg += "\nSee XML source line %s" % getattr( + self.xml, 'sourceline', '') raise ResponseError(msg) return @@ -270,17 +279,19 @@ class LoncapaResponse(object): if (self.hint_tag is not None and hintgroup.find(self.hint_tag) is not None - and hasattr(self, 'check_hint_condition')): + and hasattr(self, 'check_hint_condition')): rephints = hintgroup.findall(self.hint_tag) - hints_to_show = self.check_hint_condition(rephints, student_answers) + hints_to_show = self.check_hint_condition( + rephints, student_answers) # can be 'on_request' or 'always' (default) hintmode = hintgroup.get('mode', 'always') for hintpart in hintgroup.findall('hintpart'): if hintpart.get('on') in hints_to_show: hint_text = hintpart.find('text').text - # make the hint appear after the last answer box in this response + # make the hint appear after the last answer box in this + # response aid = self.answer_ids[-1] new_cmap.set_hint_and_mode(aid, hint_text, hintmode) log.debug('after hint: new_cmap = %s' % new_cmap) @@ -340,7 +351,6 @@ class LoncapaResponse(object): response_msg_div = etree.Element('div') response_msg_div.text = str(response_msg) - # Set the css class of the message
response_msg_div.set("class", "response_message") @@ -384,20 +394,20 @@ class JavascriptResponse(LoncapaResponse): # until we decide on exactly how to solve this issue. For now, files are # manually being compiled to DATA_DIR/js/compiled. - #latestTimestamp = 0 - #basepath = self.system.filestore.root_path + '/js/' - #for filename in (self.display_dependencies + [self.display]): + # latestTimestamp = 0 + # basepath = self.system.filestore.root_path + '/js/' + # for filename in (self.display_dependencies + [self.display]): # filepath = basepath + filename # timestamp = os.stat(filepath).st_mtime # if timestamp > latestTimestamp: # latestTimestamp = timestamp # - #h = hashlib.md5() - #h.update(self.answer_id + str(self.display_dependencies)) - #compiled_filename = 'compiled/' + h.hexdigest() + '.js' - #compiled_filepath = basepath + compiled_filename + # h = hashlib.md5() + # h.update(self.answer_id + str(self.display_dependencies)) + # compiled_filename = 'compiled/' + h.hexdigest() + '.js' + # compiled_filepath = basepath + compiled_filename - #if not os.path.exists(compiled_filepath) or os.stat(compiled_filepath).st_mtime < latestTimestamp: + # if not os.path.exists(compiled_filepath) or os.stat(compiled_filepath).st_mtime < latestTimestamp: # outfile = open(compiled_filepath, 'w') # for filename in (self.display_dependencies + [self.display]): # filepath = basepath + filename @@ -419,7 +429,7 @@ class JavascriptResponse(LoncapaResponse): id=self.xml.get('id'))[0] self.display_xml = self.xml.xpath('//*[@id=$id]//display', - id=self.xml.get('id'))[0] + id=self.xml.get('id'))[0] self.xml.remove(self.generator_xml) self.xml.remove(self.grader_xml) @@ -430,17 +440,20 @@ class JavascriptResponse(LoncapaResponse): self.display = self.display_xml.get("src") if self.generator_xml.get("dependencies"): - self.generator_dependencies = self.generator_xml.get("dependencies").split() + self.generator_dependencies = self.generator_xml.get( + "dependencies").split() else: self.generator_dependencies = [] if self.grader_xml.get("dependencies"): - self.grader_dependencies = self.grader_xml.get("dependencies").split() + self.grader_dependencies = self.grader_xml.get( + "dependencies").split() else: self.grader_dependencies = [] if self.display_xml.get("dependencies"): - self.display_dependencies = self.display_xml.get("dependencies").split() + self.display_dependencies = self.display_xml.get( + "dependencies").split() else: self.display_dependencies = [] @@ -461,10 +474,10 @@ class JavascriptResponse(LoncapaResponse): return subprocess.check_output(subprocess_args, env=self.get_node_env()) - def generate_problem_state(self): - generator_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_generator.js' + generator_file = os.path.dirname(os.path.normpath( + __file__)) + '/javascript_problem_generator.js' output = self.call_node([generator_file, self.generator, json.dumps(self.generator_dependencies), @@ -478,17 +491,18 @@ class JavascriptResponse(LoncapaResponse): params = {} for param in self.xml.xpath('//*[@id=$id]//responseparam', - id=self.xml.get('id')): + id=self.xml.get('id')): raw_param = param.get("value") - params[param.get("name")] = json.loads(contextualize_text(raw_param, self.context)) + params[param.get("name")] = json.loads( + contextualize_text(raw_param, self.context)) return params def prepare_inputfield(self): for inputfield in self.xml.xpath('//*[@id=$id]//javascriptinput', - id=self.xml.get('id')): + id=self.xml.get('id')): escapedict = {'"': '"'} @@ -501,7 +515,7 @@ class JavascriptResponse(LoncapaResponse): escapedict) inputfield.set("problem_state", encoded_problem_state) - inputfield.set("display_file", self.display_filename) + inputfield.set("display_file", self.display_filename) inputfield.set("display_class", self.display_class) def get_score(self, student_answers): @@ -519,7 +533,8 @@ class JavascriptResponse(LoncapaResponse): if submission is None or submission == '': submission = json.dumps(None) - grader_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_grader.js' + grader_file = os.path.dirname(os.path.normpath( + __file__)) + '/javascript_problem_grader.js' outputs = self.call_node([grader_file, self.grader, json.dumps(self.grader_dependencies), @@ -528,8 +543,8 @@ class JavascriptResponse(LoncapaResponse): json.dumps(self.params)]).split('\n') all_correct = json.loads(outputs[0].strip()) - evaluation = outputs[1].strip() - solution = outputs[2].strip() + evaluation = outputs[1].strip() + solution = outputs[2].strip() return (all_correct, evaluation, solution) def get_answers(self): @@ -539,9 +554,7 @@ class JavascriptResponse(LoncapaResponse): return {self.answer_id: self.solution} - #----------------------------------------------------------------------------- - class ChoiceResponse(LoncapaResponse): """ This response type is used when the student chooses from a discrete set of @@ -599,9 +612,10 @@ class ChoiceResponse(LoncapaResponse): self.assign_choice_names() correct_xml = self.xml.xpath('//*[@id=$id]//choice[@correct="true"]', - id=self.xml.get('id')) + id=self.xml.get('id')) - self.correct_choices = set([choice.get('name') for choice in correct_xml]) + self.correct_choices = set([choice.get( + 'name') for choice in correct_xml]) def assign_choice_names(self): ''' @@ -654,7 +668,8 @@ class MultipleChoiceResponse(LoncapaResponse): allowed_inputfields = ['choicegroup'] def setup_response(self): - # call secondary setup for MultipleChoice questions, to set name attributes + # call secondary setup for MultipleChoice questions, to set name + # attributes self.mc_setup_response() # define correct choices (after calling secondary setup) @@ -692,7 +707,7 @@ class MultipleChoiceResponse(LoncapaResponse): # 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): + and student_answers[self.answer_id] in self.correct_choices): return CorrectMap(self.answer_id, 'correct') else: return CorrectMap(self.answer_id, 'incorrect') @@ -760,7 +775,8 @@ class OptionResponse(LoncapaResponse): return cmap def get_answers(self): - amap = dict([(af.get('id'), contextualize_text(af.get('correct'), self.context)) for af in self.answer_fields]) + amap = dict([(af.get('id'), contextualize_text(af.get( + 'correct'), self.context)) for af in self.answer_fields]) # log.debug('%s: expected answers=%s' % (unicode(self),amap)) return amap @@ -780,8 +796,9 @@ class NumericalResponse(LoncapaResponse): context = self.context self.correct_answer = contextualize_text(xml.get('answer'), context) try: - self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', - id=xml.get('id'))[0] + self.tolerance_xml = xml.xpath( + '//*[@id=$id]//responseparam[@type="tolerance"]/@default', + id=xml.get('id'))[0] self.tolerance = contextualize_text(self.tolerance_xml, context) except Exception: self.tolerance = '0' @@ -798,17 +815,21 @@ class NumericalResponse(LoncapaResponse): try: correct_ans = complex(self.correct_answer) except ValueError: - log.debug("Content error--answer '{0}' is not a valid complex number".format(self.correct_answer)) - raise StudentInputError("There was a problem with the staff answer to this problem") + log.debug("Content error--answer '{0}' is not a valid complex number".format( + self.correct_answer)) + raise StudentInputError( + "There was a problem with the staff answer to this problem") try: - correct = compare_with_tolerance(evaluator(dict(), dict(), student_answer), - correct_ans, self.tolerance) + correct = compare_with_tolerance( + evaluator(dict(), dict(), student_answer), + correct_ans, self.tolerance) # We should catch this explicitly. # I think this is just pyparsing.ParseException, calc.UndefinedVariable: # But we'd need to confirm except: - # Use the traceback-preserving version of re-raising with a different type + # Use the traceback-preserving version of re-raising with a + # different type import sys type, value, traceback = sys.exc_info() @@ -837,7 +858,8 @@ class StringResponse(LoncapaResponse): max_inputfields = 1 def setup_response(self): - self.correct_answer = contextualize_text(self.xml.get('answer'), self.context).strip() + self.correct_answer = contextualize_text( + self.xml.get('answer'), self.context).strip() def get_score(self, student_answers): '''Grade a string response ''' @@ -846,7 +868,8 @@ class StringResponse(LoncapaResponse): return CorrectMap(self.answer_id, 'correct' if correct else 'incorrect') def check_string(self, expected, given): - if self.xml.get('type') == 'ci': return given.lower() == expected.lower() + if self.xml.get('type') == 'ci': + return given.lower() == expected.lower() return given == expected def check_hint_condition(self, hxml_set, student_answers): @@ -854,8 +877,10 @@ class StringResponse(LoncapaResponse): hints_to_show = [] for hxml in hxml_set: name = hxml.get('name') - correct_answer = contextualize_text(hxml.get('answer'), self.context).strip() - if self.check_string(correct_answer, given): hints_to_show.append(name) + correct_answer = contextualize_text( + hxml.get('answer'), self.context).strip() + if self.check_string(correct_answer, given): + hints_to_show.append(name) log.debug('hints_to_show = %s' % hints_to_show) return hints_to_show @@ -889,7 +914,7 @@ class CustomResponse(LoncapaResponse): correct[0] ='incorrect' """}, - {'snippet': """ + diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 158c2b98d0..4173f424b6 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -70,6 +70,7 @@ class @Problem @bind() @num_queued_items = @new_queued_items.length + @updateProgress response if @num_queued_items == 0 delete window.queuePollerID else From eda6169b8b4a11dea221df37ec3cdfb93e36c819 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 15 Mar 2013 09:45:31 -0400 Subject: [PATCH 02/17] Pass along a url creator as opposed to just a url through the ModuleSystem. --- common/lib/capa/capa/responsetypes.py | 3 ++- .../open_ended_module.py | 4 ++-- .../xmodule/tests/test_combined_open_ended.py | 5 +++- lms/djangoapps/courseware/module_render.py | 23 +++++++++++++------ 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 62da901656..f997829cd0 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1413,8 +1413,9 @@ class CodeResponse(LoncapaResponse): queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + anonymous_student_id + self.answer_id) + callback_url = self.system.xqueue['construct_callback']() xheader = xqueue_interface.make_xheader( - lms_callback_url=self.system.xqueue['callback_url'], + lms_callback_url=callback_url, lms_key=queuekey, queue_name=self.queue_name) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py index 1f84d2ab8c..8373700837 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py @@ -174,7 +174,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): str(len(self.child_history))) xheader = xqueue_interface.make_xheader( - lms_callback_url=system.xqueue['callback_url'], + lms_callback_url=system.xqueue['construct_callback'](), lms_key=queuekey, queue_name=self.message_queue_name ) @@ -224,7 +224,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): anonymous_student_id + str(len(self.child_history))) - xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['callback_url'], + xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['construct_callback'](), lms_key=queuekey, queue_name=self.queue_name) diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py index 09c86baf27..aa8a077cc1 100644 --- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py +++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py @@ -183,7 +183,10 @@ class OpenEndedModuleTest(unittest.TestCase): self.test_system.location = self.location self.mock_xqueue = MagicMock() self.mock_xqueue.send_to_queue.return_value = (None, "Message") - self.test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue', + def constructed_callback(dispatch = "score_update"): + return dispatch + + self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue', 'waittime': 1} self.openendedmodule = OpenEndedModule(self.test_system, self.location, self.definition, self.descriptor, self.static_data, self.metadata) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 08df7bfb8c..0954f8d28c 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -181,12 +181,21 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours host=request.get_host(), proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http') ) - xqueue_callback_url += reverse('xqueue_callback', - kwargs=dict(course_id=course_id, - userid=str(user.id), - id=descriptor.location.url(), - dispatch='score_update'), - ) + + def make_xqueue_callback(dispatch = 'score_update'): + # Fully qualified callback URL for external queueing system + xqueue_callback_url = '{proto}://{host}'.format( + host=request.get_host(), + proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http') + ) + + xqueue_callback_url += reverse('xqueue_callback', + kwargs=dict(course_id=course_id, + userid=str(user.id), + id=descriptor.location.url(), + dispatch=dispatch), + ) + return xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. @@ -194,7 +203,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = {'interface': xqueue_interface, - 'callback_url': xqueue_callback_url, + 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } From 45d8086e1cd5ac4fb7a583a411a9b7bdda27491b Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 15 Mar 2013 11:40:22 -0400 Subject: [PATCH 03/17] Set up ajax to submit to XQueue. Add some unit tests to make sure this is working properly --- common/lib/capa/capa/inputtypes.py | 37 +++++++++-- common/lib/capa/capa/responsetypes.py | 5 +- common/lib/capa/capa/tests/__init__.py | 9 ++- common/lib/capa/capa/tests/test_inputtypes.py | 63 +++++++++++++++++++ 4 files changed, 105 insertions(+), 9 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index f49ad5b422..08c395692f 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -48,6 +48,8 @@ import pyparsing from .registry import TagRegistry from capa.chem import chemcalc +import xqueue_interface +from datetime import datetime log = logging.getLogger(__name__) @@ -639,6 +641,7 @@ class MatlabInput(CodeInput): # Check if problem has been queued self.queue_len = 0 + self.queuename = 'matlab' # Flag indicating that the problem has been queued, 'msg' is length of # queue if self.status == 'incomplete': @@ -650,20 +653,44 @@ class MatlabInput(CodeInput): def handle_ajax(self, dispatch, get): if dispatch == 'plot': - # put the data in the queue and ship it off - pass - elif dispatch == 'display': + return self.plot_data(get) + elif dispatch == 'xqueue_response': # render the response pass def plot_data(self, get): ''' send data via xqueue to the mathworks backend''' - # only send data if xqueue exists if self.system.xqueue is not None: - pass + # pull relevant info out of get + response = get['submission'] + + # construct xqueue headers + qinterface = self.system.xqueue['interface'] + qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) + callback_url = self.system.xqueue['construct_callback']('input_ajax') + anonymous_student_id = self.system.anonymous_student_id + queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + + anonymous_student_id + + self.id) + xheader = xqueue_interface.make_xheader( + lms_callback_url = callback_url, + lms_key = queuekey, + queue_name = self.queuename) + # construct xqueue body + student_info = {'anonymous_student_id': anonymous_student_id, + 'submission_time': qtime} + contents = {'grader_payload': self.plot_payload, + 'student_info': json.dumps(student_info), + 'student_response': response} + + (error, msg) = qinterface.send_to_queue(header=xheader, + body = json.dumps(contents)) + + return json.dumps({'success': error != 0, 'message': msg}) + return json.dumps({'success': False, 'message': 'Cannot connect to the queue'}) registry.register(MatlabInput) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index f997829cd0..bb202e6d6e 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1271,8 +1271,9 @@ class CodeResponse(LoncapaResponse): Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse: system.xqueue = { 'interface': XqueueInterface object, - 'callback_url': Per-StudentModule callback URL - where results are posted (string), + 'construct_callback': Per-StudentModule callback URL + constructor, defaults to using 'score_update' + as the correct dispatch (function), 'default_queuename': Default queuename to submit request (string) } diff --git a/common/lib/capa/capa/tests/__init__.py b/common/lib/capa/capa/tests/__init__.py index 89cb5a5ee9..7b1bffce62 100644 --- a/common/lib/capa/capa/tests/__init__.py +++ b/common/lib/capa/capa/tests/__init__.py @@ -2,7 +2,7 @@ import fs import fs.osfs import os -from mock import Mock +from mock import Mock, MagicMock import xml.sax.saxutils as saxutils @@ -16,6 +16,11 @@ def tst_render_template(template, context): """ return '
{0}
'.format(saxutils.escape(repr(context))) +def calledback_url(dispatch = 'score_update'): + return dispatch + +xqueue_interface = MagicMock() +xqueue_interface.send_to_queue.return_value = (1, 'Success!') test_system = Mock( ajax_url='courses/course_id/modx/a_location', @@ -26,7 +31,7 @@ test_system = Mock( user=Mock(), filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")), debug=True, - xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10}, + xqueue={'interface': xqueue_interface, 'construct_callback': calledback_url, 'default_queuename': 'testqueue', 'waittime': 10}, node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), anonymous_student_id='student' ) diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 360fd9f2f6..01801ac822 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -23,6 +23,7 @@ import xml.sax.saxutils as saxutils from . import test_system from capa import inputtypes +from mock import ANY # just a handy shortcut lookup_tag = inputtypes.registry.get_class_for_tag @@ -300,6 +301,68 @@ class CodeInputTest(unittest.TestCase): self.assertEqual(context, expected) +class MatlabTest(unittest.TestCase): + ''' + Test Matlab input types + ''' + def setUp(self): + self.rows = '10' + self.cols = '80' + self.tabsize = '4' + self.mode = "" + self.payload = "payload" + self.linenumbers = 'true' + self.xml = """ + + {payload} + + """.format(r = self.rows, + c = self.cols, + tabsize = self.tabsize, + m = self.mode, + payload = self.payload, + ln = self.linenumbers) + elt = etree.fromstring(self.xml) + state = {'value': 'print "good evening"', + 'status': 'incomplete', + 'feedback': {'message': '3'}, } + + self.input_class = lookup_tag('matlabinput') + self.the_input = self.input_class(test_system, elt, state) + + + def test_rendering(self): + context = self.the_input._get_render_context() + + expected = {'id': 'prob_1_2', + 'value': 'print "good evening"', + 'status': 'queued', + 'msg': self.input_class.submitted_msg, + 'mode': self.mode, + 'rows': self.rows, + 'cols': self.cols, + 'linenumbers': 'true', + 'hidden': '', + 'tabsize': int(self.tabsize), + 'queue_len': '3', + } + + self.assertEqual(context, expected) + + def test_plot_data(self): + get = {'submission': 'x = 1234;'} + response = json.loads(self.the_input.handle_ajax("plot", get)) + + test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY) + + + self.assertTrue(response['success']) + + + class SchematicTest(unittest.TestCase): ''' From 521c469a355a22d3125b9a6ae82c934b2dc5f10c Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 18 Mar 2013 17:14:16 -0400 Subject: [PATCH 04/17] Add the ability for input types to have their own state and add in a handler for ungraded responses via xqueue --- common/lib/capa/capa/capa_problem.py | 24 ++++++++ common/lib/capa/capa/inputtypes.py | 56 +++++++++++++++++-- .../lib/capa/capa/templates/matlabinput.html | 3 + common/lib/capa/capa/tests/test_inputtypes.py | 32 ++++++++++- common/lib/xmodule/xmodule/capa_module.py | 17 +++++- 5 files changed, 124 insertions(+), 8 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 42753fc90b..fbf911e500 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -111,6 +111,7 @@ class LoncapaProblem(object): if self.system is None: raise Exception() self.seed = seed + self.input_state = None if state: if 'seed' in state: @@ -121,11 +122,16 @@ class LoncapaProblem(object): self.correct_map.set_dict(state['correct_map']) if 'done' in state: self.done = state['done'] + if 'input_state' in state: + self.input_state = state['input_state'] # 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] + if not self.input_state: + self.input_state = {} + # Convert startouttext and endouttext to proper problem_text = re.sub("startouttext\s*/", "text", problem_text) problem_text = re.sub("endouttext\s*/", "/text", problem_text) @@ -188,6 +194,7 @@ class LoncapaProblem(object): return {'seed': self.seed, 'student_answers': self.student_answers, 'correct_map': self.correct_map.get_dict(), + 'input_state': self.input_state, 'done': self.done} def get_max_score(self): @@ -237,6 +244,19 @@ class LoncapaProblem(object): self.correct_map.set_dict(cmap.get_dict()) return cmap + def ungraded_response(self, xqueue_msg, queuekey): + ''' + Handle any responses from the xqueue that are not related to grading + + Does not return any value + ''' + # check against each inputtype + for the_input in self.inputs.values(): + # if the input type has an xqueue_response function, pass in the values + if hasattr(the_input, 'ungraded_response'): + the_input.ungraded_response(xqueue_msg, queuekey) + + def is_queued(self): ''' Returns True if any part of the problem has been submitted to an external queue @@ -527,11 +547,15 @@ class LoncapaProblem(object): value = "" if self.student_answers and problemid in self.student_answers: value = self.student_answers[problemid] + + if input_id not in self.input_state: + self.input_state[input_id] = {} # do the rendering state = {'value': value, 'status': status, 'id': input_id, + 'input_state': self.input_state[input_id], 'feedback': {'message': msg, 'hint': hint, 'hintmode': hintmode, }} diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 08c395692f..303619c820 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -134,6 +134,8 @@ class InputTypeBase(object): * 'id' -- the id of this input, typically "{problem-location}_{response-num}_{input-num}" * 'status' (answered, unanswered, unsubmitted) + * 'input_state' -- dictionary containing any inputtype-specific state + that has been preserved * 'feedback' (dictionary containing keys for hints, errors, or other feedback from previous attempt. Specifically 'message', 'hint', 'hintmode'. If 'hintmode' is 'always', the hint is always displayed.) @@ -160,6 +162,7 @@ class InputTypeBase(object): self.msg = feedback.get('message', '') self.hint = feedback.get('hint', '') self.hintmode = feedback.get('hintmode', None) + self.input_state = state.get('input_state', {}) # put hint above msg if it should be displayed if self.hintmode == 'always': @@ -643,8 +646,11 @@ class MatlabInput(CodeInput): self.queue_len = 0 self.queuename = 'matlab' # Flag indicating that the problem has been queued, 'msg' is length of + self.queue_msg = None # queue if self.status == 'incomplete': + if 'queue_msg' in self.input_state: + self.queue_msg = self.input_state['queue_msg'] self.status = 'queued' self.queue_len = self.msg self.msg = self.submitted_msg @@ -652,13 +658,47 @@ class MatlabInput(CodeInput): def handle_ajax(self, dispatch, get): + ''' Handle AJAX calls directed to this input''' if dispatch == 'plot': - return self.plot_data(get) - elif dispatch == 'xqueue_response': - # render the response - pass + return self._plot_data(get) - def plot_data(self, get): + def ungraded_response(self, queue_msg, queuekey): + ''' Handle any XQueue responses that have to be saved and rendered ''' + # check the queuekey against the saved queuekey + if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued' + and self.input_state['queuekey'] == queuekey): + msg = _parse_message(queue_msg) + # save the queue message so that it can be rendered later + self.input_state['queue_msg'] = msg + self.input_state['queued'] = 'dequeued' + + def _extra_context(self): + ''' Set up additional context variables''' + extra_context = {'queue_len': self.queue_len} + if self.queue_msg is not None: + extra_context['queue_msg'] = self.queue_msg + else: + extra_context['queue_msg'] = '' + return extra_context + + def _parse_data(self, queue_msg): + ''' + takes a queue_msg returned from the queue and parses it and returns + whatever is stored in msg + returns string msg + ''' + try: + result = json.loads(queue_msg) + except (TypeError, ValueError): + log.error("External message should be a JSON serialized dict." + " Received queue_msg = %s" % queue_msg) + raise + # TODO: needs more error checking + msg = result['msg'] + return msg + + + def _plot_data(self, get): ''' send data via xqueue to the mathworks backend''' # only send data if xqueue exists if self.system.xqueue is not None: @@ -668,7 +708,7 @@ class MatlabInput(CodeInput): # construct xqueue headers qinterface = self.system.xqueue['interface'] qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) - callback_url = self.system.xqueue['construct_callback']('input_ajax') + callback_url = self.system.xqueue['construct_callback']('ungraded_response') anonymous_student_id = self.system.anonymous_student_id queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + anonymous_student_id + @@ -678,6 +718,10 @@ class MatlabInput(CodeInput): lms_key = queuekey, queue_name = self.queuename) + # save the input state + self.input_state['queuekey'] = queuekey + self.input_state['queuestate'] = 'queued' + # construct xqueue body student_info = {'anonymous_student_id': anonymous_student_id, diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index ba516be249..07433f0a3a 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -29,6 +29,9 @@
${msg|n}
+
+ ${queue_msg|n} +
diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 01801ac822..97e27d5ffc 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -344,6 +344,35 @@ class MatlabTest(unittest.TestCase): 'mode': self.mode, 'rows': self.rows, 'cols': self.cols, + 'queue_msg': '', + 'linenumbers': 'true', + 'hidden': '', + 'tabsize': int(self.tabsize), + 'queue_len': '3', + } + + self.assertEqual(context, expected) + + + def test_rendering_with_state(self): + state = {'value': 'print "good evening"', + 'status': 'incomplete', + 'input_state': {'queue_msg': 'message'}, + 'feedback': {'message': '3'}, } + elt = etree.fromstring(self.xml) + + input_class = lookup_tag('matlabinput') + the_input = self.input_class(test_system, elt, state) + context = the_input._get_render_context() + + expected = {'id': 'prob_1_2', + 'value': 'print "good evening"', + 'status': 'queued', + 'msg': self.input_class.submitted_msg, + 'mode': self.mode, + 'rows': self.rows, + 'cols': self.cols, + 'queue_msg': 'message', 'linenumbers': 'true', 'hidden': '', 'tabsize': int(self.tabsize), @@ -358,8 +387,9 @@ class MatlabTest(unittest.TestCase): test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY) - self.assertTrue(response['success']) + self.assertTrue(self.the_input.input_state['queuekey'] is not None) + self.assertEqual(self.the_input.input_state['queuestate'], 'queued') diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index e66b1d3495..1bdd62f5b7 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -93,6 +93,7 @@ class CapaFields(object): rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings) data = String(help="XML data for the problem", scope=Scope.content) correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state, default={}) + input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.student_state, default={}) student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state) done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state) display_name = String(help="Display name for this module", scope=Scope.settings) @@ -188,6 +189,7 @@ class CapaModule(CapaFields, XModule): 'done': self.done, 'correct_map': self.correct_map, 'student_answers': self.student_answers, + 'input_state': self.input_state, 'seed': self.seed, } @@ -195,6 +197,7 @@ class CapaModule(CapaFields, XModule): lcp_state = self.lcp.get_state() self.done = lcp_state['done'] self.correct_map = lcp_state['correct_map'] + self.input_state = lcp_state['input_state'] self.student_answers = lcp_state['student_answers'] self.seed = lcp_state['seed'] @@ -443,7 +446,8 @@ class CapaModule(CapaFields, XModule): 'problem_save': self.save_problem, 'problem_show': self.get_answer, 'score_update': self.update_score, - 'input_ajax': self.lcp.handle_input_ajax + 'input_ajax': self.lcp.handle_input_ajax, + 'ungraded_response': self.handle_ungraded_response } if dispatch not in handlers: @@ -537,6 +541,17 @@ class CapaModule(CapaFields, XModule): return dict() # No AJAX return is needed + def handle_ungraded_response(self, get): + ''' + Get the XQueue response + ''' + queuekey = get['queuekey'] + score_msg = get['xqueue_body'] + # pass along the xqueue message to the problem + self.lcp.ungraded_response(score_msg, queuekey) + + self.set_state_from_lcp() + def get_answer(self, get): ''' For the "show answer" button. From 8649d67b9d6fe56d807a23d64d9ec8ab2e49b61a Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 19 Mar 2013 13:11:48 -0400 Subject: [PATCH 05/17] Force the progress bar update when we get a code response answer. --- common/lib/xmodule/xmodule/js/src/capa/display.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 4173f424b6..70704ab247 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -41,6 +41,11 @@ class @Problem @el.attr progress: response.progress_status @el.trigger('progressChanged') + forceUpdate: (response) => + @el.attr progress: response.progress_status + @el.trigger('progressChanged') + + queueing: => @queued_items = @$(".xqueue") @num_queued_items = @queued_items.length @@ -70,8 +75,8 @@ class @Problem @bind() @num_queued_items = @new_queued_items.length - @updateProgress response if @num_queued_items == 0 + @forceUpdate response delete window.queuePollerID else # TODO: Some logic to dynamically adjust polling rate based on queuelen From f4d68d77f671abf3e63e8788b68d6416b18d3287 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 19 Mar 2013 15:29:40 -0400 Subject: [PATCH 06/17] Add Javascript for new button and fix Python backend issues --- common/lib/capa/capa/capa_problem.py | 5 +-- common/lib/capa/capa/inputtypes.py | 24 +++++++--- .../lib/capa/capa/templates/matlabinput.html | 45 ++++++++++++++++++- common/lib/capa/capa/tests/__init__.py | 2 +- common/lib/capa/capa/tests/test_inputtypes.py | 4 +- common/lib/xmodule/xmodule/capa_module.py | 3 +- 6 files changed, 67 insertions(+), 16 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index fbf911e500..911f210812 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -548,14 +548,11 @@ class LoncapaProblem(object): if self.student_answers and problemid in self.student_answers: value = self.student_answers[problemid] - if input_id not in self.input_state: - self.input_state[input_id] = {} - # do the rendering state = {'value': value, 'status': status, 'id': input_id, - 'input_state': self.input_state[input_id], + 'input_state': self.input_state, 'feedback': {'message': msg, 'hint': hint, 'hintmode': hintmode, }} diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 303619c820..42865a01b5 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -162,7 +162,7 @@ class InputTypeBase(object): self.msg = feedback.get('message', '') self.hint = feedback.get('hint', '') self.hintmode = feedback.get('hintmode', None) - self.input_state = state.get('input_state', {}) + self.input_state_dict = state.get('input_state', {}) # put hint above msg if it should be displayed if self.hintmode == 'always': @@ -635,6 +635,11 @@ class MatlabInput(CodeInput): ''' Handle matlab-specific parsing ''' + # if we don't have state for this input type yet, make one + if self.id not in self.input_state_dict: + self.input_state_dict[self.id] = {} + + self.input_state = self.input_state_dict[self.id] xml = self.xml self.plot_payload = xml.findtext('./plot_payload') # if no student input yet, then use the default input given by the @@ -647,10 +652,13 @@ class MatlabInput(CodeInput): self.queuename = 'matlab' # Flag indicating that the problem has been queued, 'msg' is length of self.queue_msg = None + if 'queue_msg' in self.input_state: + self.queue_msg = self.input_state['queue_msg'] + if 'queued' in self.input_state and self.input_state['queuestate'] is not None: + self.status = 'queued' + self.queue_len = 1 # queue if self.status == 'incomplete': - if 'queue_msg' in self.input_state: - self.queue_msg = self.input_state['queue_msg'] self.status = 'queued' self.queue_len = self.msg self.msg = self.submitted_msg @@ -667,10 +675,11 @@ class MatlabInput(CodeInput): # check the queuekey against the saved queuekey if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued' and self.input_state['queuekey'] == queuekey): - msg = _parse_message(queue_msg) + msg = self._parse_data(queue_msg) # save the queue message so that it can be rendered later self.input_state['queue_msg'] = msg - self.input_state['queued'] = 'dequeued' + self.input_state['queuestate'] = None + self.input_state['queuekey'] = None def _extra_context(self): ''' Set up additional context variables''' @@ -733,8 +742,9 @@ class MatlabInput(CodeInput): (error, msg) = qinterface.send_to_queue(header=xheader, body = json.dumps(contents)) - return json.dumps({'success': error != 0, 'message': msg}) - return json.dumps({'success': False, 'message': 'Cannot connect to the queue'}) + + return {'success': error == 0, 'message': msg} + return {'success': False, 'message': 'Cannot connect to the queue'} registry.register(MatlabInput) diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index 07433f0a3a..cbfc4b119f 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -34,7 +34,7 @@
- +
diff --git a/common/lib/capa/capa/tests/__init__.py b/common/lib/capa/capa/tests/__init__.py index 7b1bffce62..72d82c683b 100644 --- a/common/lib/capa/capa/tests/__init__.py +++ b/common/lib/capa/capa/tests/__init__.py @@ -20,7 +20,7 @@ def calledback_url(dispatch = 'score_update'): return dispatch xqueue_interface = MagicMock() -xqueue_interface.send_to_queue.return_value = (1, 'Success!') +xqueue_interface.send_to_queue.return_value = (0, 'Success!') test_system = Mock( ajax_url='courses/course_id/modx/a_location', diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 97e27d5ffc..b9da9df03f 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -357,7 +357,7 @@ class MatlabTest(unittest.TestCase): def test_rendering_with_state(self): state = {'value': 'print "good evening"', 'status': 'incomplete', - 'input_state': {'queue_msg': 'message'}, + 'input_state': {'prob_1_2': {'queue_msg': 'message'}}, 'feedback': {'message': '3'}, } elt = etree.fromstring(self.xml) @@ -383,7 +383,7 @@ class MatlabTest(unittest.TestCase): def test_plot_data(self): get = {'submission': 'x = 1234;'} - response = json.loads(self.the_input.handle_ajax("plot", get)) + response = self.the_input.handle_ajax("plot", get) test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 1bdd62f5b7..a522c796bb 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -460,6 +460,7 @@ class CapaModule(CapaFields, XModule): 'progress_changed': after != before, 'progress_status': Progress.to_js_status_str(after), }) + self.set_state_from_lcp() return json.dumps(d, cls=ComplexEncoder) def is_past_due(self): @@ -549,8 +550,8 @@ class CapaModule(CapaFields, XModule): score_msg = get['xqueue_body'] # pass along the xqueue message to the problem self.lcp.ungraded_response(score_msg, queuekey) - self.set_state_from_lcp() + return dict() def get_answer(self, get): ''' From a2957cb3b72726e164824afea7be32ed453eaeba Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 19 Mar 2013 16:36:33 -0400 Subject: [PATCH 07/17] Add in some JS messages for when things go wrong. --- common/lib/capa/capa/inputtypes.py | 1 - .../lib/capa/capa/templates/matlabinput.html | 23 ++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 42865a01b5..5a47456fab 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -702,7 +702,6 @@ class MatlabInput(CodeInput): log.error("External message should be a JSON serialized dict." " Received queue_msg = %s" % queue_msg) raise - # TODO: needs more error checking msg = result['msg'] return msg diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index cbfc4b119f..e52a5297d1 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -60,9 +60,21 @@ $("#textbox_${id}").find('.CodeMirror-scroll').height(${int(13.5*eval(rows))}); + var gentle_alert = function (parent_elt, msg) { + if($(parent_elt).find('.capa_alert').length) { + $(parent_elt).find('.capa_alert').remove(); + } + var alert_elem = "
" + msg + "
"; + alert_elem = $(alert_elem).addClass('capa_alert'); + $(parent_elt).find('.action').after(alert_elem); + $(parent_elt).find('.capa_alert').css({opacity: 0}).animate({opacity: 1}, 700); + } + + // hook up the plot button var plot = function(event) { - url = $(this).closest('.problems-wrapper').data('url'); + var problem_elt = $(event.target).closest('.problems-wrapper'); + url = $(event.target).closest('.problems-wrapper').data('url'); input_id = "${id}"; // save the codemirror text to the textarea @@ -73,31 +85,30 @@ answer = input.serialize(); - // setup callback for + // setup callback for after we send information to plot var plot_callback = function(response) { if(response.success) { window.location.reload(); } else { - // TODO: show message + gentle_alert(problem_elt, msg); } } var save_callback = function(response) { if(response.success) { + // send information to the problem's plot functionality Problem.inputAjax(url, input_id, 'plot', {'submission': submission}, plot_callback); } else { - // TODO: show any messages + gentle_alert(problem_elt, msg); } } // save the answer $.postWithPrefix(url + '/problem_save', answer, save_callback); - - } $('#plot_${id}').click(plot); From 57f7acf86398698d1f54d15e4702092042aa7d3c Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 19 Mar 2013 17:10:38 -0400 Subject: [PATCH 08/17] Unbreak grading for capa problems Clean up some pylint errors --- common/lib/capa/capa/capa_problem.py | 8 +------- common/lib/capa/capa/inputtypes.py | 8 +++++++- .../lib/capa/capa/templates/matlabinput.html | 2 +- common/lib/xmodule/xmodule/capa_module.py | 19 ++++++++++++++++--- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 911f210812..f1fea4d8e3 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -16,7 +16,6 @@ This is used by capa_module. from __future__ import division from datetime import datetime -import json import logging import math import numpy @@ -32,8 +31,6 @@ from xml.sax.saxutils import unescape from copy import deepcopy import chem -import chem.chemcalc -import chem.chemtools import chem.miller import verifiers import verifiers.draganddrop @@ -70,9 +67,6 @@ global_context = {'random': random, 'scipy': scipy, 'calc': calc, 'eia': eia, - 'chemcalc': chem.chemcalc, - 'chemtools': chem.chemtools, - 'miller': chem.miller, 'draganddrop': verifiers.draganddrop} # These should be removed from HTML output, including all subelements @@ -371,7 +365,7 @@ class LoncapaProblem(object): dispatch = get['dispatch'] return self.inputs[input_id].handle_ajax(dispatch, get) else: - log.warning("Could not find matching input for id: %s" % problem_id) + log.warning("Could not find matching input for id: %s" % input_id) return {} diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 5a47456fab..a62e696b20 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -37,7 +37,6 @@ graded status as'status' # makes sense, but a bunch of problems have markup that assumes block. Bigger TODO: figure out a # general css and layout strategy for capa, document it, then implement it. -from collections import namedtuple import json import logging from lxml import etree @@ -623,6 +622,13 @@ registry.register(CodeInput) class MatlabInput(CodeInput): ''' InputType for handling Matlab code input + + Example: + + + %api_key=API_KEY + + ''' template = "matlabinput.html" tags = ['matlabinput'] diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index e52a5297d1..6c02e8e68e 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -44,7 +44,7 @@ % if linenumbers == 'true': lineNumbers: true, % endif - mode: "${mode}", + mode: "matlab", matchBrackets: true, lineWrapping: true, indentUnit: "${tabsize}", diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index a522c796bb..6ce8d3a805 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -446,7 +446,7 @@ class CapaModule(CapaFields, XModule): 'problem_save': self.save_problem, 'problem_show': self.get_answer, 'score_update': self.update_score, - 'input_ajax': self.lcp.handle_input_ajax, + 'input_ajax': self.handle_input_ajax, 'ungraded_response': self.handle_ungraded_response } @@ -460,7 +460,6 @@ class CapaModule(CapaFields, XModule): 'progress_changed': after != before, 'progress_status': Progress.to_js_status_str(after), }) - self.set_state_from_lcp() return json.dumps(d, cls=ComplexEncoder) def is_past_due(self): @@ -544,7 +543,10 @@ class CapaModule(CapaFields, XModule): def handle_ungraded_response(self, get): ''' - Get the XQueue response + Delivers a response to the capa problem where the expectation where this response does + not have relevant grading information + + No ajax return is needed, so an empty dict is returned ''' queuekey = get['queuekey'] score_msg = get['xqueue_body'] @@ -553,6 +555,17 @@ class CapaModule(CapaFields, XModule): self.set_state_from_lcp() return dict() + def handle_input_ajax(self, get): + ''' + Passes information down to the capa problem so that it can handle its own ajax calls + Returns the response from the capa problem + ''' + response = self.lcp.handle_input_ajax(get) + # save any state changes that may occur + self.set_state_from_lcp() + return response + + def get_answer(self, get): ''' For the "show answer" button. From 10c6e7615bbea19e0687e62e36b26d7515ea603c Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 20 Mar 2013 09:42:42 -0400 Subject: [PATCH 09/17] More polish for matlab input type --- common/lib/capa/capa/inputtypes.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index a62e696b20..0208f32503 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -657,8 +657,8 @@ class MatlabInput(CodeInput): self.queue_len = 0 self.queuename = 'matlab' # Flag indicating that the problem has been queued, 'msg' is length of - self.queue_msg = None - if 'queue_msg' in self.input_state: + self.queue_msg = '' + if 'queue_msg' in self.input_state and self.status in ['incomplete', 'unsubmitted']: self.queue_msg = self.input_state['queue_msg'] if 'queued' in self.input_state and self.input_state['queuestate'] is not None: self.status = 'queued' @@ -689,11 +689,10 @@ class MatlabInput(CodeInput): def _extra_context(self): ''' Set up additional context variables''' - extra_context = {'queue_len': self.queue_len} - if self.queue_msg is not None: - extra_context['queue_msg'] = self.queue_msg - else: - extra_context['queue_msg'] = '' + extra_context = { + 'queue_len': self.queue_len, + 'queue_msg': self.queue_msg + } return extra_context def _parse_data(self, queue_msg): @@ -747,7 +746,6 @@ class MatlabInput(CodeInput): (error, msg) = qinterface.send_to_queue(header=xheader, body = json.dumps(contents)) - return {'success': error == 0, 'message': msg} return {'success': False, 'message': 'Cannot connect to the queue'} From af1af8c6d1f57fb45435ef037253531ce8f5e551 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 20 Mar 2013 14:08:15 -0400 Subject: [PATCH 10/17] Address code review feedback: - improve docstrings - only pass in the state for a particular input and not the whole dictionary - refactor some common code - minor syntax cleanup --- common/lib/capa/capa/capa_problem.py | 43 +++--- common/lib/capa/capa/inputtypes.py | 136 ++++++++++-------- common/lib/capa/capa/responsetypes.py | 16 +-- common/lib/capa/capa/tests/test_inputtypes.py | 2 +- common/lib/xmodule/xmodule/capa_module.py | 20 ++- .../xmodule/tests/test_combined_open_ended.py | 2 +- lms/djangoapps/courseware/module_render.py | 2 +- 7 files changed, 126 insertions(+), 95 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index f1fea4d8e3..27f1066030 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -91,8 +91,12 @@ class LoncapaProblem(object): - problem_text (string): xml defining the problem - id (string): identifier for this problem; often a filename (no spaces) - - state (dict): student state - - seed (int): random number generator seed (int) + - state (dict): containing the following keys: + - 'seed' - (int) random number generator seed + - 'student_answers' - (dict) maps input id to the stored answer for that input + - 'correct_map' (CorrectMap) a map of each input to their 'correctness' + - 'done' - (bool) indicates whether or not this problem is considered done + - 'input_state' - (dict) maps input_id to a dictionary that holds the state for that input - system (ModuleSystem): ModuleSystem instance which provides OS, rendering, and user context @@ -104,27 +108,16 @@ class LoncapaProblem(object): self.system = system if self.system is None: raise Exception() - self.seed = seed - self.input_state = None - 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.set_dict(state['correct_map']) - if 'done' in state: - self.done = state['done'] - if 'input_state' in state: - self.input_state = state['input_state'] + state = state if state else {} + self.seed = seed if seed else state.get('seed', struct.unpack('i', os.urandom(4))[0]) + self.student_answers = state.get('student_answers', {}) + if 'correct_map' in state: + self.correct_map.set_dict(state['correct_map']) + self.done = state.get('done', False) + self.input_state = state.get('input_state', {}) - # 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] - if not self.input_state: - self.input_state = {} # Convert startouttext and endouttext to proper problem_text = re.sub("startouttext\s*/", "text", problem_text) @@ -240,13 +233,14 @@ class LoncapaProblem(object): def ungraded_response(self, xqueue_msg, queuekey): ''' - Handle any responses from the xqueue that are not related to grading + Handle any responses from the xqueue that do not contain grades + Will try to pass the queue message to all inputtypes that can handle ungraded responses Does not return any value ''' # check against each inputtype for the_input in self.inputs.values(): - # if the input type has an xqueue_response function, pass in the values + # if the input type has an ungraded function, pass in the values if hasattr(the_input, 'ungraded_response'): the_input.ungraded_response(xqueue_msg, queuekey) @@ -542,11 +536,14 @@ class LoncapaProblem(object): if self.student_answers and problemid in self.student_answers: value = self.student_answers[problemid] + if input_id not in self.input_state: + self.input_state[input_id] = {} + # do the rendering state = {'value': value, 'status': status, 'id': input_id, - 'input_state': self.input_state, + 'input_state': self.input_state[input_id], 'feedback': {'message': msg, 'hint': hint, 'hintmode': hintmode, }} diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 0208f32503..d5268fed89 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -161,7 +161,7 @@ class InputTypeBase(object): self.msg = feedback.get('message', '') self.hint = feedback.get('hint', '') self.hintmode = feedback.get('hintmode', None) - self.input_state_dict = state.get('input_state', {}) + self.input_state = state.get('input_state', {}) # put hint above msg if it should be displayed if self.hintmode == 'always': @@ -591,14 +591,14 @@ class CodeInput(InputTypeBase): Attribute('tabsize', 4, transform=int), ] - def setup(self): + def setup_code_response_rendering(self): """ Implement special logic: handle queueing state, and default input. """ # if no student input yet, then use the default input given by the # problem - if not self.value: - self.value = self.xml.text + if not self.value and self.xml.text: + self.value = self.xml.text.strip() # Check if problem has been queued self.queue_len = 0 @@ -609,6 +609,11 @@ class CodeInput(InputTypeBase): self.queue_len = self.msg self.msg = self.submitted_msg + + def setup(self): + ''' setup this input type ''' + self.setup_code_response_rendering() + def _extra_context(self): """Defined queue_len, add it """ return {'queue_len': self.queue_len, } @@ -623,8 +628,10 @@ class MatlabInput(CodeInput): ''' InputType for handling Matlab code input + TODO: API_KEY will go away once we have a way to specify it per-course Example: + Initial Text %api_key=API_KEY @@ -633,51 +640,56 @@ class MatlabInput(CodeInput): template = "matlabinput.html" tags = ['matlabinput'] - # pulled out for testing - submitted_msg = ("Submitted. As soon as your submission is" - " graded, this message will be replaced with the grader's feedback.") + plot_submitted_msg = ("Submitted. As soon as a response is returned, " + "this message will be replaced by that feedback.") def setup(self): ''' Handle matlab-specific parsing ''' - # if we don't have state for this input type yet, make one - if self.id not in self.input_state_dict: - self.input_state_dict[self.id] = {} + self.setup_code_response_rendering() - self.input_state = self.input_state_dict[self.id] xml = self.xml self.plot_payload = xml.findtext('./plot_payload') - # if no student input yet, then use the default input given by the - # problem - if not self.value: - self.value = self.xml.text # Check if problem has been queued - self.queue_len = 0 self.queuename = 'matlab' - # Flag indicating that the problem has been queued, 'msg' is length of self.queue_msg = '' if 'queue_msg' in self.input_state and self.status in ['incomplete', 'unsubmitted']: self.queue_msg = self.input_state['queue_msg'] if 'queued' in self.input_state and self.input_state['queuestate'] is not None: self.status = 'queued' self.queue_len = 1 - # queue - if self.status == 'incomplete': - self.status = 'queued' - self.queue_len = self.msg - self.msg = self.submitted_msg - + self.msg = self.plot_submitted_msg def handle_ajax(self, dispatch, get): - ''' Handle AJAX calls directed to this input''' + ''' + Handle AJAX calls directed to this input + + Args: + - dispatch (str) - indicates how we want this ajax call to be handled + - get (dict) - dictionary of key-value pairs that contain useful data + Returns: + + ''' + if dispatch == 'plot': return self._plot_data(get) + return {} def ungraded_response(self, queue_msg, queuekey): - ''' Handle any XQueue responses that have to be saved and rendered ''' + ''' + Handle the response from the XQueue + Stores the response in the input_state so it can be rendered later + + Args: + - queue_msg (str) - message returned from the queue. The message to be rendered + - queuekey (str) - a key passed to the queue. Will be matched up to verify that this is the response we're waiting for + + Returns: + nothing + ''' # check the queuekey against the saved queuekey if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued' and self.input_state['queuekey'] == queuekey): @@ -697,9 +709,11 @@ class MatlabInput(CodeInput): def _parse_data(self, queue_msg): ''' - takes a queue_msg returned from the queue and parses it and returns - whatever is stored in msg - returns string msg + Parses the message out of the queue message + Args: + queue_msg (str) - a JSON encoded string + Returns: + returns the value for the the key 'msg' in queue_msg ''' try: result = json.loads(queue_msg) @@ -712,42 +726,50 @@ class MatlabInput(CodeInput): def _plot_data(self, get): - ''' send data via xqueue to the mathworks backend''' + ''' + AJAX handler for the plot button + Args: + get (dict) - should have key 'submission' which contains the student submission + Returns: + dict - 'success' - whether or not we successfully queued this submission + - 'message' - message to be rendered in case of error + ''' # only send data if xqueue exists - if self.system.xqueue is not None: - # pull relevant info out of get - response = get['submission'] + if self.system.xqueue is None: + return {'success': False, 'message': 'Cannot connect to the queue'} - # construct xqueue headers - qinterface = self.system.xqueue['interface'] - qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) - callback_url = self.system.xqueue['construct_callback']('ungraded_response') - anonymous_student_id = self.system.anonymous_student_id - queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + - anonymous_student_id + - self.id) - xheader = xqueue_interface.make_xheader( - lms_callback_url = callback_url, - lms_key = queuekey, - queue_name = self.queuename) + # pull relevant info out of get + response = get['submission'] - # save the input state - self.input_state['queuekey'] = queuekey - self.input_state['queuestate'] = 'queued' + # construct xqueue headers + qinterface = self.system.xqueue['interface'] + qtime = datetime.strftime(datetime.utcnow(), xqueue_interface.dateformat) + callback_url = self.system.xqueue['construct_callback']('ungraded_response') + anonymous_student_id = self.system.anonymous_student_id + queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + + anonymous_student_id + + self.id) + xheader = xqueue_interface.make_xheader( + lms_callback_url = callback_url, + lms_key = queuekey, + queue_name = self.queuename) + + # save the input state + self.input_state['queuekey'] = queuekey + self.input_state['queuestate'] = 'queued' - # construct xqueue body - student_info = {'anonymous_student_id': anonymous_student_id, - 'submission_time': qtime} - contents = {'grader_payload': self.plot_payload, - 'student_info': json.dumps(student_info), - 'student_response': response} + # construct xqueue body + student_info = {'anonymous_student_id': anonymous_student_id, + 'submission_time': qtime} + contents = {'grader_payload': self.plot_payload, + 'student_info': json.dumps(student_info), + 'student_response': response} - (error, msg) = qinterface.send_to_queue(header=xheader, - body = json.dumps(contents)) + (error, msg) = qinterface.send_to_queue(header=xheader, + body = json.dumps(contents)) - return {'success': error == 0, 'message': msg} - return {'success': False, 'message': 'Cannot connect to the queue'} + return {'success': error == 0, 'message': msg} registry.register(MatlabInput) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index bb202e6d6e..8ab716735c 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1147,10 +1147,10 @@ def sympy_check2(): correct = [] messages = [] for input_dict in input_list: - correct.append('correct' if input_dict[ - 'ok'] else 'incorrect') - msg = self.clean_message_html(input_dict[ - 'msg']) if 'msg' in input_dict else None + correct.append('correct' + if input_dict['ok'] else 'incorrect') + msg = (self.clean_message_html(input_dict['msg']) + if 'msg' in input_dict else None) messages.append(msg) # Otherwise, we do not recognize the dictionary @@ -1164,8 +1164,8 @@ def sympy_check2(): # indicating whether all inputs should be marked # correct or incorrect else: - correct = ['correct'] * len( - idset) if ret else ['incorrect'] * len(idset) + n = len(idset) + correct = ['correct'] * n if ret else ['incorrect'] * n # build map giving "correct"ness of the answer(s) correct_map = CorrectMap() @@ -1174,8 +1174,8 @@ def sympy_check2(): correct_map.set_overall_message(overall_message) for k in range(len(idset)): - npoints = self.maxpoints[idset[ - k]] if correct[k] == 'correct' else 0 + npoints = (self.maxpoints[idset[k]] + if correct[k] == 'correct' else 0) correct_map.set(idset[k], correct[k], msg=messages[k], npoints=npoints) return correct_map diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index b9da9df03f..250cedd549 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -357,7 +357,7 @@ class MatlabTest(unittest.TestCase): def test_rendering_with_state(self): state = {'value': 'print "good evening"', 'status': 'incomplete', - 'input_state': {'prob_1_2': {'queue_msg': 'message'}}, + 'input_state': {'queue_msg': 'message'}, 'feedback': {'message': '3'}, } elt = etree.fromstring(self.xml) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 6ce8d3a805..da8b5b4f96 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -543,8 +543,16 @@ class CapaModule(CapaFields, XModule): def handle_ungraded_response(self, get): ''' - Delivers a response to the capa problem where the expectation where this response does - not have relevant grading information + Delivers a response from the XQueue to the capa problem + + The score of the problem will not be updated + + Args: + - get (dict) must contain keys: + queuekey - a key specific to this response + xqueue_body - the body of the response + Returns: + empty dictionary No ajax return is needed, so an empty dict is returned ''' @@ -557,8 +565,12 @@ class CapaModule(CapaFields, XModule): def handle_input_ajax(self, get): ''' - Passes information down to the capa problem so that it can handle its own ajax calls - Returns the response from the capa problem + Handle ajax calls meant for a particular input in the problem + + Args: + - get (dict) - data that should be passed to the input + Returns: + - dict containing the response from the input ''' response = self.lcp.handle_input_ajax(get) # save any state changes that may occur diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py index aa8a077cc1..55c31ded58 100644 --- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py +++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py @@ -183,7 +183,7 @@ class OpenEndedModuleTest(unittest.TestCase): self.test_system.location = self.location self.mock_xqueue = MagicMock() self.mock_xqueue.send_to_queue.return_value = (None, "Message") - def constructed_callback(dispatch = "score_update"): + def constructed_callback(dispatch="score_update"): return dispatch self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue', diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 0954f8d28c..973940d784 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -182,7 +182,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http') ) - def make_xqueue_callback(dispatch = 'score_update'): + def make_xqueue_callback(dispatch='score_update'): # Fully qualified callback URL for external queueing system xqueue_callback_url = '{proto}://{host}'.format( host=request.get_host(), From 204f89d4dcbf290a16609d372e7557dd0277e526 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 20 Mar 2013 17:16:04 -0400 Subject: [PATCH 11/17] Make sure we are still showing the message when we are queued as well. --- common/lib/capa/capa/inputtypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index d5268fed89..2febfbd5d2 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -655,7 +655,7 @@ class MatlabInput(CodeInput): # Check if problem has been queued self.queuename = 'matlab' self.queue_msg = '' - if 'queue_msg' in self.input_state and self.status in ['incomplete', 'unsubmitted']: + if 'queue_msg' in self.input_state and self.status in ['queued','incomplete', 'unsubmitted']: self.queue_msg = self.input_state['queue_msg'] if 'queued' in self.input_state and self.input_state['queuestate'] is not None: self.status = 'queued' From d763a6fc3ae6f531ed7ac3c976b5ebbfa3b70399 Mon Sep 17 00:00:00 2001 From: jmclaus Date: Thu, 21 Mar 2013 12:19:40 +0100 Subject: [PATCH 12/17] CSS from JSME doesn't affect surrounding content now --- common/static/js/capa/jsme/gwt/chrome/chrome.css | 3 +++ common/static/js/capa/jsme/gwt/chrome/chrome_rtl.css | 3 +++ common/static/js/capa/jsme/gwt/chrome/mosaic.css | 3 +++ common/static/js/capa/jsme/gwt/chrome/mosaic_rtl.css | 3 +++ common/static/js/capa/jsmolcalc/gwt/clean/clean.css | 3 +++ common/static/js/capa/jsmolcalc/gwt/clean/clean_rtl.css | 3 +++ 6 files changed, 18 insertions(+) diff --git a/common/static/js/capa/jsme/gwt/chrome/chrome.css b/common/static/js/capa/jsme/gwt/chrome/chrome.css index 9c7bcd627d..b8f084fe05 100644 --- a/common/static/js/capa/jsme/gwt/chrome/chrome.css +++ b/common/static/js/capa/jsme/gwt/chrome/chrome.css @@ -12,6 +12,8 @@ * } */ +/* Commented out the following that was messing up the CSS in sequential and had no use anyway when jsme is running inside edX platform. + body, table td, select { font-family: Arial Unicode MS, Arial, sans-serif; font-size: small; @@ -31,6 +33,7 @@ body { a, a:visited, a:hover { color: #0000AA; } +*/ /** * The reference theme can be used to determine when this style sheet has diff --git a/common/static/js/capa/jsme/gwt/chrome/chrome_rtl.css b/common/static/js/capa/jsme/gwt/chrome/chrome_rtl.css index 9d316660b1..602afd8c5d 100644 --- a/common/static/js/capa/jsme/gwt/chrome/chrome_rtl.css +++ b/common/static/js/capa/jsme/gwt/chrome/chrome_rtl.css @@ -12,6 +12,8 @@ * } */ +/* Commented out the following that was messing up the CSS in sequential and had no use anyway when jsme is running inside edX platform. + body, table td, select { font-family: Arial Unicode MS, Arial, sans-serif; font-size: small; @@ -31,6 +33,7 @@ body { a, a:visited, a:hover { color: #0000AA; } +*/ /** * The reference theme can be used to determine when this style sheet has diff --git a/common/static/js/capa/jsme/gwt/chrome/mosaic.css b/common/static/js/capa/jsme/gwt/chrome/mosaic.css index 9b6a242ea3..ccea6a69df 100644 --- a/common/static/js/capa/jsme/gwt/chrome/mosaic.css +++ b/common/static/js/capa/jsme/gwt/chrome/mosaic.css @@ -26,6 +26,8 @@ zoom: 1; } +/* Commented out the following that was messing up the CSS in sequential and had no use anyway when jsme is running inside edX platform. + body { font-family: arial,sans-serif; } @@ -45,6 +47,7 @@ a:visited { a:active { color:#ff0000; } +*/ /*** Button ***/ diff --git a/common/static/js/capa/jsme/gwt/chrome/mosaic_rtl.css b/common/static/js/capa/jsme/gwt/chrome/mosaic_rtl.css index dda9c913e9..9b910d66a1 100644 --- a/common/static/js/capa/jsme/gwt/chrome/mosaic_rtl.css +++ b/common/static/js/capa/jsme/gwt/chrome/mosaic_rtl.css @@ -26,6 +26,8 @@ zoom: 1; } +/* Commented out the following that was messing up the CSS in sequential and had no use anyway when jsme is running inside edX platform. + body { font-family: arial,sans-serif; } @@ -45,6 +47,7 @@ a:visited { a:active { color:#ff0000; } +*/ /*** Button ***/ diff --git a/common/static/js/capa/jsmolcalc/gwt/clean/clean.css b/common/static/js/capa/jsmolcalc/gwt/clean/clean.css index aa02d5385d..1800c0ee20 100644 --- a/common/static/js/capa/jsmolcalc/gwt/clean/clean.css +++ b/common/static/js/capa/jsmolcalc/gwt/clean/clean.css @@ -12,6 +12,8 @@ * } */ +/* Commented out the following that was messing up the CSS in sequential and had no use anyway when jsmolcalc is running inside edX platform. + body, table td, select, button { font-family: Arial Unicode MS, Arial, sans-serif; font-size: small; @@ -41,6 +43,7 @@ a:hover { select { background: white; } +*/ /** * The reference theme can be used to determine when this style sheet has diff --git a/common/static/js/capa/jsmolcalc/gwt/clean/clean_rtl.css b/common/static/js/capa/jsmolcalc/gwt/clean/clean_rtl.css index 7e2c695ccf..a80e7bd55f 100644 --- a/common/static/js/capa/jsmolcalc/gwt/clean/clean_rtl.css +++ b/common/static/js/capa/jsmolcalc/gwt/clean/clean_rtl.css @@ -12,6 +12,8 @@ * } */ +/* Commented out the following that was messing up the CSS in sequential and had no use anyway when jsmolcalc is running inside edX platform. + body, table td, select, button { font-family: Arial Unicode MS, Arial, sans-serif; font-size: small; @@ -41,6 +43,7 @@ a:hover { select { background: white; } +*/ /** * The reference theme can be used to determine when this style sheet has From ecf395d6929503c41cd479fe271f2309276c09db Mon Sep 17 00:00:00 2001 From: cahrens Date: Mon, 25 Mar 2013 10:39:35 -0400 Subject: [PATCH 13/17] Upadate link to static documentation. --- common/lib/xmodule/xmodule/templates/course/empty.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/templates/course/empty.yaml b/common/lib/xmodule/xmodule/templates/course/empty.yaml index 7b25c21ad6..89f1bfcf21 100644 --- a/common/lib/xmodule/xmodule/templates/course/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/course/empty.yaml @@ -89,7 +89,7 @@ metadata: {"short_description": "Download the Studio Documentation", "long_description": "Download the searchable Studio reference documentation in PDF form.", "is_checked": false, - "action_url": "http://help.edge.edx.org/help/assets/8ccd409f979c8639dd463e126eb840dc67f09098/Getting_Started_with_Studio.pdf", + "action_url": "http://files.edx.org/Getting_Started_with_Studio.pdf", "action_text": "Download Documentation", "action_external": true}] }, From cda0fa0aa5078c61a9bbf75d5e1eff267306ec5b Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 25 Mar 2013 11:08:51 -0400 Subject: [PATCH 14/17] Check for None specifically when setting a new seed. --- common/lib/capa/capa/capa_problem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 27f1066030..0b181e8558 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -91,6 +91,7 @@ class LoncapaProblem(object): - problem_text (string): xml defining the problem - id (string): identifier for this problem; often a filename (no spaces) + - seed (int): random number generator seed (int) - state (dict): containing the following keys: - 'seed' - (int) random number generator seed - 'student_answers' - (dict) maps input id to the stored answer for that input @@ -110,7 +111,7 @@ class LoncapaProblem(object): raise Exception() state = state if state else {} - self.seed = seed if seed else state.get('seed', struct.unpack('i', os.urandom(4))[0]) + self.seed = seed if seed is not None else state.get('seed', struct.unpack('i', os.urandom(4))[0]) self.student_answers = state.get('student_answers', {}) if 'correct_map' in state: self.correct_map.set_dict(state['correct_map']) From c39ff353ead444ea2cf29cfaed8d8accd1c81af3 Mon Sep 17 00:00:00 2001 From: cahrens Date: Mon, 25 Mar 2013 11:09:33 -0400 Subject: [PATCH 15/17] Don't assume input fields will be within a div. https://edx.lighthouseapp.com/projects/102637/tickets/232 --- cms/static/js/views/validating_view.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cms/static/js/views/validating_view.js b/cms/static/js/views/validating_view.js index 041e779030..c3ea57fd20 100644 --- a/cms/static/js/views/validating_view.js +++ b/cms/static/js/views/validating_view.js @@ -25,11 +25,14 @@ CMS.Views.ValidatingView = Backbone.View.extend({ for (var field in error) { var ele = this.$el.find('#' + this.fieldToSelectorMap[field]); this._cacheValidationErrors.push(ele); - if ($(ele).is('div')) { - // put error on the contained inputs - $(ele).find('input, textarea').addClass('error'); + var inputElements = 'input, textarea'; + if ($(ele).is(inputElements)) { + $(ele).addClass('error'); + } + else { + // put error on the contained inputs + $(ele).find(inputElements).addClass('error'); } - else $(ele).addClass('error'); $(ele).parent().append(this.errorTemplate({message : error[field]})); } }, From 4bda05d9eb1ef77e8456d393bc2a080904f7c65f Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 25 Mar 2013 11:22:56 -0400 Subject: [PATCH 16/17] Fix seed assignment priority and add clearer documentation --- common/lib/capa/capa/capa_problem.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 0b181e8558..68f80006f6 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -111,7 +111,14 @@ class LoncapaProblem(object): raise Exception() state = state if state else {} - self.seed = seed if seed is not None else state.get('seed', struct.unpack('i', os.urandom(4))[0]) + + # Set seed according to the following priority: + # 1. Contained in problem's state + # 2. Passed into capa_problem via constructor + # 3. Assign from the OS's random number generator + self.seed = state.get('seed', seed) + if self.seed is None: + self.seed = struct.unpack('i', os.urandom(4)) self.student_answers = state.get('student_answers', {}) if 'correct_map' in state: self.correct_map.set_dict(state['correct_map']) From a5a126ac938727d301a4399b6b76daa1e2728c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Mon, 25 Mar 2013 11:28:40 -0400 Subject: [PATCH 17/17] Fix incorrect date in test for course xmodule --- common/lib/xmodule/xmodule/tests/test_course_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index 59099b0dff..28095979ec 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_module.py +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -125,7 +125,7 @@ class IsNewCourseTestCase(unittest.TestCase): descriptor = self.get_dummy_course(start='2013-01-15T12:00') assert(descriptor.is_newish is True) - descriptor = self.get_dummy_course(start='2013-03-00T12:00') + descriptor = self.get_dummy_course(start='2013-03-01T12:00') assert(descriptor.is_newish is True) descriptor = self.get_dummy_course(start='2012-10-15T12:00')