From 32a8ed7be32c1451a3fc5b591f5ee86585162d96 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 31 Oct 2012 16:54:00 -0400 Subject: [PATCH] adding in self assessment grader --- common/lib/xmodule/xmodule/capa_module.py | 116 ++++++------ .../js/src/selfassessment/display.coffee | 24 --- .../xmodule/xmodule/self_assessment_module.py | 168 ++++-------------- 3 files changed, 91 insertions(+), 217 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 31106a4aa8..76158093b6 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -29,10 +29,10 @@ TIMEDELTA_REGEX = re.compile(r'^((?P\d+?) day(?:s?))?(\s)?((?P\d+?) def only_one(lst, default="", process=lambda x: x): """ -If lst is empty, returns default -If lst has a single element, applies process to that element and returns it -Otherwise, raises an exeception -""" + If lst is empty, returns default + If lst has a single element, applies process to that element and returns it + Otherwise, raises an exeception + """ if len(lst) == 0: return default elif len(lst) == 1: @@ -43,14 +43,14 @@ Otherwise, raises an exeception def parse_timedelta(time_str): """ -time_str: A string with the following components: - day[s] (optional) - hour[s] (optional) - minute[s] (optional) - second[s] (optional) + time_str: A string with the following components: + day[s] (optional) + hour[s] (optional) + minute[s] (optional) + second[s] (optional) -Returns a datetime.timedelta parsed from the string -""" + Returns a datetime.timedelta parsed from the string + """ parts = TIMEDELTA_REGEX.match(time_str) if not parts: return @@ -71,9 +71,9 @@ class ComplexEncoder(json.JSONEncoder): class CapaModule(XModule): ''' -An XModule implementing LonCapa format problems, implemented by way of -capa.capa_problem.LoncapaProblem -''' + An XModule implementing LonCapa format problems, implemented by way of + capa.capa_problem.LoncapaProblem + ''' icon_class = 'problem' js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'), @@ -175,9 +175,9 @@ capa.capa_problem.LoncapaProblem @property def rerandomize(self): """ -Property accessor that returns self.metadata['rerandomize'] in a -canonical form -""" + Property accessor that returns self.metadata['rerandomize'] in a + canonical form + """ rerandomize = self.metadata.get('rerandomize', 'always') if rerandomize in ("", "always", "true"): return "always" @@ -203,7 +203,7 @@ canonical form def get_progress(self): ''' For now, just return score / max_score -''' + ''' d = self.get_score() score = d['score'] total = d['total'] @@ -224,7 +224,7 @@ canonical form def get_problem_html(self, encapsulate=True): '''Return html for the problem. Adds check, reset, save buttons -as necessary based on the problem config and state.''' + as necessary based on the problem config and state.''' try: html = self.lcp.get_html() @@ -358,14 +358,14 @@ as necessary based on the problem config and state.''' def handle_ajax(self, dispatch, get): ''' -This is called by courseware.module_render, to handle an AJAX call. -"get" is request.POST. + This is called by courseware.module_render, to handle an AJAX call. + "get" is request.POST. -Returns a json dictionary: -{ 'progress_changed' : True/False, -'progress' : 'none'/'in_progress'/'done', - } -''' + Returns a json dictionary: + { 'progress_changed' : True/False, + 'progress' : 'none'/'in_progress'/'done', + } + ''' handlers = { 'problem_get': self.get_problem, 'problem_check': self.check_problem, @@ -398,7 +398,7 @@ Returns a json dictionary: def answer_available(self): ''' Is the user allowed to see an answer? -''' + ''' if self.show_answer == '': return False @@ -425,14 +425,14 @@ Returns a json dictionary: def update_score(self, get): """ -Delivers grading response (e.g. from asynchronous code checking) to -the capa problem, so its score can be updated + Delivers grading response (e.g. from asynchronous code checking) to + the capa problem, so its score can be updated -'get' must have a field 'response' which is a string that contains the -grader's response + 'get' must have a field 'response' which is a string that contains the + grader's response -No ajax return is needed. Return empty dict. -""" + No ajax return is needed. Return empty dict. + """ queuekey = get['queuekey'] score_msg = get['xqueue_body'] self.lcp.update_score(score_msg, queuekey) @@ -441,10 +441,10 @@ No ajax return is needed. Return empty dict. def get_answer(self, get): ''' -For the "show answer" button. + For the "show answer" button. -Returns the answers: {'answers' : answers} -''' + Returns the answers: {'answers' : answers} + ''' event_info = dict() event_info['problem_id'] = self.location.url() self.system.track_function('show_answer', event_info) @@ -469,18 +469,18 @@ Returns the answers: {'answers' : answers} # Figure out if we should move these to capa_problem? def get_problem(self, get): ''' Return results of get_problem_html, as a simple dict for json-ing. -{ 'html': } + { 'html': } -Used if we want to reconfirm we have the right thing e.g. after -several AJAX calls. -''' + Used if we want to reconfirm we have the right thing e.g. after + several AJAX calls. + ''' return {'html': self.get_problem_html(encapsulate=False)} @staticmethod def make_dict_of_responses(get): '''Make dictionary of student responses (aka "answers") -get is POST dictionary. -''' + get is POST dictionary. + ''' answers = dict() for key in get: # e.g. input_resistor_1 ==> resistor_1 @@ -500,11 +500,11 @@ get is POST dictionary. def check_problem(self, get): ''' Checks whether answers to a problem are correct, and -returns a map of correct/incorrect answers: + returns a map of correct/incorrect answers: -{'success' : bool, -'contents' : html} -''' + {'success' : bool, + 'contents' : html} + ''' event_info = dict() event_info['state'] = self.lcp.get_state() event_info['problem_id'] = self.location.url() @@ -567,18 +567,18 @@ returns a map of correct/incorrect answers: # 'success' will always be incorrect event_info['correct_map'] = correct_map.get_dict() event_info['success'] = success -event_info['attempts'] = self.attempts -self.system.track_function('save_problem_check', event_info) + event_info['attempts'] = self.attempts + self.system.track_function('save_problem_check', event_info) -if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback - self.system.psychometrics_handler(self.get_instance_state()) + if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback + self.system.psychometrics_handler(self.get_instance_state()) -# render problem into HTML -html = self.get_problem_html(encapsulate=False) + # render problem into HTML + html = self.get_problem_html(encapsulate=False) -return {'success': success, - 'contents': html, - } + return {'success': success, + 'contents': html, + } def save_problem(self, get): ''' @@ -654,9 +654,9 @@ def reset_problem(self, get): class CapaDescriptor(RawDescriptor): """ -Module implementing problems in the LON-CAPA format, -as implemented by capa.capa_problem -""" + Module implementing problems in the LON-CAPA format, + as implemented by capa.capa_problem + """ module_class = CapaModule diff --git a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee index da91c44af7..5e16182553 100644 --- a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee @@ -125,30 +125,6 @@ class @Problem @gentle_alert saveMessage @updateProgress response - refreshMath: (event, element) => - element = event.target unless element - elid = element.id.replace(/^input_/,'') - target = "display_" + elid - - # MathJax preprocessor is loaded by 'setupInputTypes' - preprocessor_tag = "inputtype_" + elid - mathjax_preprocessor = @inputtypeDisplays[preprocessor_tag] - - if jax = MathJax.Hub.getAllJax(target)[0] - eqn = $(element).val() - if mathjax_preprocessor - eqn = mathjax_preprocessor(eqn) - MathJax.Hub.Queue(['Text', jax, eqn], [@updateMathML, jax, element]) - - return # Explicit return for CoffeeScript - - updateMathML: (jax, element) => - try - $("##{element.id}_dynamath").val(jax.root.toMathML '') - catch exception - throw exception unless exception.restart - MathJax.Callback.After [@refreshMath, jax], exception.restart - refreshAnswers: => @$('input.schematic').each (index, element) -> element.schematic.update_value() diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 4473b7d430..b6e8a667b4 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -57,9 +57,10 @@ class SelfAssessmentModule(XModule): dom2 = etree.fromstring(definition['data']) self.attempts = 0 - self.max_attempts = None + self.max_attempts = 1 self.max_attempts = self.metadata.get('attempts', None) + if self.max_attempts is not None: self.max_attempts = int(self.max_attempts) @@ -70,7 +71,7 @@ class SelfAssessmentModule(XModule): self.name = only_one(dom2.xpath('/problem/@name')) - self.rubric=etree.tostring(only_one(dom2.xpath("/rubric"))) + self.rubric=etree.tostring(only_one(dom2.xpath("/rubric/html"))) self.problem=etree.tostring(only_one(dom2.xpath("/problem"))) self.lcp = LoncapaProblem(self.problem, self.location.html_id(), @@ -255,10 +256,7 @@ class SelfAssessmentModule(XModule): handlers = { 'problem_get': self.get_problem, 'problem_check': self.check_problem, - 'problem_reset': self.reset_problem, 'problem_save': self.save_problem, - 'problem_show': self.get_answer, - 'score_update': self.update_score, } if dispatch not in handlers: @@ -274,7 +272,7 @@ class SelfAssessmentModule(XModule): return json.dumps(d, cls=ComplexEncoder) def closed(self): - ''' Is the student still allowed to submit answers? ''' + ''' Is the student still allowed to submit answers? ''' if self.attempts == self.max_attempts: return True if self.close_date is not None and datetime.datetime.utcnow() > self.close_date: @@ -319,9 +317,10 @@ class SelfAssessmentModule(XModule): ''' return {'html': self.get_problem_html(encapsulate=False)} - @staticmethod + @staticmethod def make_dict_of_responses(get): - '''Make dictionary of student responses (aka "answers") + ''' + Make dictionary of student responses (aka "answers") get is POST dictionary. ''' answers = dict() @@ -355,9 +354,9 @@ class SelfAssessmentModule(XModule): answers = self.make_dict_of_responses(get) event_info['answers'] = convert_files_to_filenames(answers) - parsed_answer=False - if(answer[answers.keys()[0]]=="True"): - parsed_answer=True + parsed_answer="incorrect" + if(answer[answers.keys()[1]]=="Correct"): + parsed_answer="correct" # Too late. Cannot submit if self.closed(): @@ -445,137 +444,36 @@ class SelfAssessmentModule(XModule): return {'success': True} -class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): +class SelfAssessmentDescriptor(RawDescriptor): """ Module for putting raw html in a course """ - mako_template = "widgets/html-edit.html" - module_class = HtmlModule - filename_extension = "xml" - template_dir_name = "html" - stores_state=True - has_score=True + module_class = SelfAssessmentModule - js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/edit.coffee')]} - js_module_name = "HTMLEditingDescriptor" + stores_state = True + has_score = True + template_dir_name = 'selfassessment' - # VS[compat] TODO (cpennington): Delete this method once all fall 2012 course - # are being edited in the cms + # Capa modules have some additional metadata: + # TODO (vshnayder): do problems have any other metadata? Do they + # actually use type and points? + metadata_attributes = RawDescriptor.metadata_attributes + ('type', 'points') + + # VS[compat] + # TODO (cpennington): Delete this method once all fall 2012 course are being + # edited in the cms @classmethod def backcompat_paths(cls, path): - if path.endswith('.html.xml'): - path = path[:-9] + '.html' # backcompat--look for html instead of xml - if path.endswith('.html.html'): - path = path[:-5] # some people like to include .html in filenames.. - candidates = [] - while os.sep in path: - candidates.append(path) - _, _, path = path.partition(os.sep) + return [ + 'problems/' + path[8:], + path[8:], + ] - # also look for .html versions instead of .xml - nc = [] - for candidate in candidates: - if candidate.endswith('.xml'): - nc.append(candidate[:-4] + '.html') - return candidates + nc + def __init__(self, *args, **kwargs): + super(CapaDescriptor, self).__init__(*args, **kwargs) - # NOTE: html descriptors are special. We do not want to parse and - # export them ourselves, because that can break things (e.g. lxml - # adds body tags when it exports, but they should just be html - # snippets that will be included in the middle of pages. - - @classmethod - def load_definition(cls, xml_object, system, location): - '''Load a descriptor from the specified xml_object: - - If there is a filename attribute, load it as a string, and - log a warning if it is not parseable by etree.HTMLParser. - - If there is not a filename attribute, the definition is the body - of the xml_object, without the root tag (do not want in the - middle of a page) - ''' - filename = xml_object.get('filename') - if filename is None: - definition_xml = copy.deepcopy(xml_object) - cls.clean_metadata_from_xml(definition_xml) - return {'data': stringify_children(definition_xml)} + weight_string = self.metadata.get('weight', None) + if weight_string: + self.weight = float(weight_string) else: - # html is special. cls.filename_extension is 'xml', but - # if 'filename' is in the definition, that means to load - # from .html - # 'filename' in html pointers is a relative path - # (not same as 'html/blah.html' when the pointer is in a directory itself) - pointer_path = "{category}/{url_path}".format(category='html', - url_path=name_to_pathname(location.name)) - base = path(pointer_path).dirname() - #log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename)) - filepath = "{base}/{name}.html".format(base=base, name=filename) - #log.debug("looking for html file for {0} at {1}".format(location, filepath)) - - - - # VS[compat] - # TODO (cpennington): If the file doesn't exist at the right path, - # give the class a chance to fix it up. The file will be written out - # again in the correct format. This should go away once the CMS is - # online and has imported all current (fall 2012) courses from xml - if not system.resources_fs.exists(filepath): - candidates = cls.backcompat_paths(filepath) - #log.debug("candidates = {0}".format(candidates)) - for candidate in candidates: - if system.resources_fs.exists(candidate): - filepath = candidate - break - - try: - with system.resources_fs.open(filepath) as file: - html = file.read() - # Log a warning if we can't parse the file, but don't error - if not check_html(html): - msg = "Couldn't parse html in {0}.".format(filepath) - log.warning(msg) - system.error_tracker("Warning: " + msg) - - definition = {'data': html} - - # TODO (ichuang): remove this after migration - # for Fall 2012 LMS migration: keep filename (and unmangled filename) - definition['filename'] = [ filepath, filename ] - - return definition - - except (ResourceNotFoundError) as err: - msg = 'Unable to load file contents at path {0}: {1} '.format( - filepath, err) - # add more info and re-raise - raise Exception(msg), None, sys.exc_info()[2] - - # TODO (vshnayder): make export put things in the right places. - - def definition_to_xml(self, resource_fs): - '''If the contents are valid xml, write them to filename.xml. Otherwise, - write just to filename.xml, and the html - string to filename.html. - ''' - try: - return etree.fromstring(self.definition['data']) - except etree.XMLSyntaxError: - pass - - # Not proper format. Write html to file, return an empty tag - pathname = name_to_pathname(self.url_name) - pathdir = path(pathname).dirname() - filepath = u'{category}/{pathname}.html'.format(category=self.category, - pathname=pathname) - - resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True) - with resource_fs.open(filepath, 'w') as file: - file.write(self.definition['data']) - - # write out the relative name - relname = path(pathname).basename() - - elt = etree.Element('html') - elt.set("filename", relname) - return elt + self.weight = None \ No newline at end of file