From e3f12607cdd2ba17a5c87f5a9b70067cf7f6aec0 Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Wed, 27 Feb 2013 01:44:07 -0500 Subject: [PATCH] add grading for annotationinput --- common/lib/capa/capa/inputtypes.py | 32 +++--- common/lib/capa/capa/responsetypes.py | 102 +++++++++++++++++- .../capa/capa/templates/annotationinput.html | 4 +- common/static/js/capa/annotationinput.js | 28 ++++- 4 files changed, 142 insertions(+), 24 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 37cd2a8fa4..f7788e90c9 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -988,27 +988,25 @@ class AnnotationInput(InputTypeBase): self.value = 'null' def _find_options(self): - options = [] - index = 0 - for option in self.xml.findall('./options/option'): - options.append({ + ''' Returns an array of dicts where each dict represents an option. ''' + elements = self.xml.findall('./options/option') + return [{ 'id': index, 'description': option.text, - 'score': option.get('score', 0) - }) - index += 1 - return options + 'choice': option.get('choice') + } for (index, option) in enumerate(elements) ] - def _unpack_value(self): - unpacked_value = json.loads(self.value) - if type(unpacked_value) != dict: - unpacked_value = {} + def _unpack(self, json_value): + ''' Unpacks the json input state into a dict. ''' + d = json.loads(json_value) + if type(d) != dict: + d = {} - comment_value = unpacked_value.get('comment', '') + comment_value = d.get('comment', '') if not isinstance(comment_value, basestring): comment_value = '' - options_value = unpacked_value.get('options', []) + options_value = d.get('options', []) if not isinstance(options_value, list): options_value = [] @@ -1027,9 +1025,9 @@ class AnnotationInput(InputTypeBase): 'options': self.options, 'return_to_annotation': self.return_to_annotation, 'debug': self.debug - } - unpacked_value = self._unpack_value() - extra_context.update(unpacked_value) + } + + extra_context.update(self._unpack(self.value)) return extra_context diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 529b409a96..1c3f179e52 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1843,6 +1843,105 @@ class ImageResponse(LoncapaResponse): dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements])) #----------------------------------------------------------------------------- +class AnnotationResponse(LoncapaResponse): + + response_tag = 'annotationresponse' + allowed_inputfields = ['annotationinput'] + max_inputfields = 1 + default_scoring = { 'incorrect': 0, 'partial': 1, 'correct': 2 } + + def setup_response(self): + xml = self.xml + self.points_map = self._get_points_map() + self.answer_map = self._get_answer_map() + + def _get_points_map(self): + ''' Returns a dict of option->scoring for each input. ''' + scoring = self.default_scoring + choices = dict(zip(scoring.keys(), scoring.keys())) + points_map = {} + + for inputfield in self.inputfields: + option_map = dict([(option['id'], { + 'correctness': choices.get(option['choice']), + 'points': scoring.get(option['choice']) + }) for option in self._find_options(inputfield) ]) + + points_map[inputfield.get('id')] = option_map + + return points_map + + def _get_answer_map(self): + ''' Returns a dict of answers for each input.''' + answer_map = {} + for inputfield in self.inputfields: + correct_option = self._find_option_with_choice(inputfield, 'correct') + answer_map[inputfield.get('id')] = correct_option['description'] + return answer_map + + def _find_options(self, inputfield): + ''' Returns an array of dicts where each dict represents an option. ''' + elements = inputfield.findall('./options/option') + return [{ + 'id': index, + 'description': option.text, + 'choice': option.get('choice') + } for (index, option) in enumerate(elements) ] + + def _find_option_with_choice(self, inputfield, choice): + ''' Returns the option with the given choice value, otherwise None. ''' + for option in self._find_options(inputfield): + if option['choice'] == choice: + return option + + def _unpack(self, json_value): + ''' Unpacks a student response value submitted as JSON.''' + d = json.loads(json_value) + if type(d) != dict: + d = {} + + comment_value = d.get('comment', '') + if not isinstance(d, basestring): + comment_value = '' + + options_value = d.get('options', []) + if not isinstance(options_value, list): + options_value = [] + + return { + 'options_value': options_value, + 'comment_value': comment_value + } + + def _get_submitted_option(self, student_answer): + ''' Return the single option that was selected, otherwise None.''' + value = self._unpack(student_answer) + options = value['options_value'] + if len(options) == 1: + return options[0] + return None + + def get_score(self, student_answers): + ''' Returns a CorrectMap for the student answer, which may include + partially correct answers.''' + student_answer = student_answers[self.answer_id] + student_option = self._get_submitted_option(student_answer) + + scoring = self.points_map[self.answer_id] + is_valid = student_option is not None and student_option in scoring.keys() + + (correctness, points) = ('incorrect', None) + if is_valid: + correctness = scoring[student_option]['correctness'] + points = scoring[student_option]['points'] + + return CorrectMap(self.answer_id, correctness=correctness, npoints=points) + + def get_answers(self): + return self.answer_map + +#----------------------------------------------------------------------------- + # TEMPORARY: List of all response subclasses # FIXME: To be replaced by auto-registration @@ -1859,4 +1958,5 @@ __all__ = [CodeResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse, - JavascriptResponse] + JavascriptResponse, + AnnotationResponse] diff --git a/common/lib/capa/capa/templates/annotationinput.html b/common/lib/capa/capa/templates/annotationinput.html index dce0434555..997b51b224 100644 --- a/common/lib/capa/capa/templates/annotationinput.html +++ b/common/lib/capa/capa/templates/annotationinput.html @@ -24,7 +24,7 @@ % if debug:
Rendered with value:
-
${value}
+
${value|h}
Current input value:
@@ -38,6 +38,8 @@ % elif status == 'correct': + % elif status == 'partial': + Partially Correct % elif status == 'incorrect': % elif status == 'incomplete': diff --git a/common/static/js/capa/annotationinput.js b/common/static/js/capa/annotationinput.js index 47b8ad342f..4353fd262a 100644 --- a/common/static/js/capa/annotationinput.js +++ b/common/static/js/capa/annotationinput.js @@ -1,13 +1,16 @@ (function () { - var debug = true; + var debug = false; var module = { debug: debug, inputSelector: '.annotation-input', tagSelector: '.tag', + tagsSelector: '.tags', commentSelector: 'textarea.comment', valueSelector: 'input.value', // stash tag selections and comment here as a JSON string... + singleSelect: true, + init: function() { var that = this; @@ -38,15 +41,30 @@ target_value = $(e.target).data('id'); if(!$(target_el).hasClass('selected')) { - current_value.options.push(target_value); + if(this.singleSelect) { + current_value.options = [target_value] + } else { + current_value.options.push(target_value); + } } else { - target_index = current_value.options.indexOf(target_value); - if(target_index !== -1) { - current_value.options.splice(target_index, 1); + if(this.singleSelect) { + current_value.options = [] + } else { + target_index = current_value.options.indexOf(target_value); + if(target_index !== -1) { + current_value.options.splice(target_index, 1); + } } } this.storeValue(value_el, current_value); + + if(this.singleSelect) { + $(target_el).closest(this.tagsSelector) + .find(this.tagSelector) + .not(target_el) + .removeClass('selected') + } $(target_el).toggleClass('selected'); }, findValueEl: function(target_el) {