import copy from fs.errors import ResourceNotFoundError import itertools import json import logging from lxml import etree from lxml.html import rewrite_links from path import path import os import sys from pkg_resources import resource_string from .capa_module import only_one, ComplexEncoder from .editing_module import EditingDescriptor from .html_checker import check_html from progress import Progress from .stringify import stringify_children from .x_module import XModule from .xml_module import XmlDescriptor from xmodule.modulestore import Location import openendedchild from combined_open_ended_rubric import CombinedOpenEndedRubric log = logging.getLogger("mitx.courseware") class SelfAssessmentModule(openendedchild.OpenEndedChild): """ A Self Assessment module that allows students to write open-ended responses, submit, then see a rubric and rate themselves. Persists student supplied hints, answers, and assessment judgment (currently only correct/incorrect). Parses xml definition file--see below for exact format. Sample XML format: What hint about this problem would you give to someone? Save Succcesful. Thanks for participating! """ def setup_response(self, system, location, definition, descriptor): """ Sets up the module @param system: Modulesystem @param location: location, to let the module know where it is. @param definition: XML definition of the module. @param descriptor: SelfAssessmentDescriptor @return: None """ self.submit_message = definition['submitmessage'] self.hint_prompt = definition['hintprompt'] self.prompt = stringify_children(self.prompt) self.rubric = stringify_children(self.rubric) def get_html(self, system): """ Gets context and renders HTML that represents the module @param system: Modulesystem @return: Rendered HTML """ #set context variables and render template if self.state != self.INITIAL: latest = self.latest_answer() previous_answer = latest if latest is not None else '' else: previous_answer = '' context = { 'prompt': self.prompt, 'previous_answer': previous_answer, 'ajax_url': system.ajax_url, 'initial_rubric': self.get_rubric_html(system), 'initial_hint': self.get_hint_html(system), 'initial_message': self.get_message_html(), 'state': self.state, 'allow_reset': self._allow_reset(), 'child_type': 'selfassessment', } html = system.render_template('self_assessment_prompt.html', context) return html def handle_ajax(self, dispatch, get, system): """ 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', } """ handlers = { 'save_answer': self.save_answer, 'save_assessment': self.save_assessment, 'save_post_assessment': self.save_hint, } if dispatch not in handlers: return 'Error' before = self.get_progress() d = handlers[dispatch](get, system) after = self.get_progress() d.update({ 'progress_changed': after != before, 'progress_status': Progress.to_js_status_str(after), }) return json.dumps(d, cls=ComplexEncoder) def get_rubric_html(self, system): """ Return the appropriate version of the rubric, based on the state. """ if self.state == self.INITIAL: return '' rubric_html = CombinedOpenEndedRubric.render_rubric(self.rubric) # we'll render it context = {'rubric': rubric_html, 'max_score': self._max_score, } if self.state == self.ASSESSING: context['read_only'] = False elif self.state in (self.POST_ASSESSMENT, self.DONE): context['read_only'] = True else: raise ValueError("Illegal state '%r'" % self.state) return system.render_template('self_assessment_rubric.html', context) def get_hint_html(self, system): """ Return the appropriate version of the hint view, based on state. """ if self.state in (self.INITIAL, self.ASSESSING): return '' if self.state == self.DONE: # display the previous hint latest = self.latest_post_assessment() hint = latest if latest is not None else '' else: hint = '' context = {'hint_prompt': self.hint_prompt, 'hint': hint} if self.state == self.POST_ASSESSMENT: context['read_only'] = False elif self.state == self.DONE: context['read_only'] = True else: raise ValueError("Illegal state '%r'" % self.state) return system.render_template('self_assessment_hint.html', context) def get_message_html(self): """ Return the appropriate version of the message view, based on state. """ if self.state != self.DONE: return "" return """
{0}
""".format(self.submit_message) def save_answer(self, get, system): """ After the answer is submitted, show the rubric. Args: get: the GET dictionary passed to the ajax request. Should contain a key 'student_answer' Returns: Dictionary with keys 'success' and either 'error' (if not success), or 'rubric_html' (if success). """ # Check to see if attempts are less than max if self.attempts > self.max_attempts: # If too many attempts, prevent student from saving answer and # seeing rubric. In normal use, students shouldn't see this because # they won't see the reset button once they're out of attempts. return { 'success': False, 'error': 'Too many attempts.' } if self.state != self.INITIAL: return self.out_of_sync_error(get) # add new history element with answer and empty score and hint. self.new_history_entry(get['student_answer']) self.change_state(self.ASSESSING) return { 'success': True, 'rubric_html': self.get_rubric_html(system) } def save_assessment(self, get, system): """ Save the assessment. If the student said they're right, don't ask for a hint, and go straight to the done state. Otherwise, do ask for a hint. Returns a dict { 'success': bool, 'state': state, 'hint_html': hint_html OR 'message_html': html and 'allow_reset', 'error': error-msg}, with 'error' only present if 'success' is False, and 'hint_html' or 'message_html' only if success is true """ if self.state != self.ASSESSING: return self.out_of_sync_error(get) try: score = int(get['assessment']) except ValueError: return {'success': False, 'error': "Non-integer score value"} self.record_latest_score(score) d = {'success': True, } if score == self.max_score(): self.change_state(self.DONE) d['message_html'] = self.get_message_html() d['allow_reset'] = self._allow_reset() else: self.change_state(self.POST_ASSESSMENT) d['hint_html'] = self.get_hint_html(system) d['state'] = self.state return d def save_hint(self, get, system): ''' Save the hint. Returns a dict { 'success': bool, 'message_html': message_html, 'error': error-msg, 'allow_reset': bool}, with the error key only present if success is False and message_html only if True. ''' if self.state != self.POST_ASSESSMENT: # Note: because we only ask for hints on wrong answers, may not have # the same number of hints and answers. return self.out_of_sync_error(get) self.record_latest_post_assessment(get['hint']) self.change_state(self.DONE) return {'success': True, 'message_html': self.get_message_html(), 'allow_reset': self._allow_reset()} class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): """ Module for adding self assessment questions to courses """ mako_template = "widgets/html-edit.html" module_class = SelfAssessmentModule filename_extension = "xml" stores_state = True has_score = True template_dir_name = "selfassessment" js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]} js_module_name = "HTMLEditingDescriptor" @classmethod def definition_from_xml(cls, xml_object, system): """ Pull out the rubric, prompt, and submitmessage into a dictionary. Returns: { 'submitmessage': 'some-html' 'hintprompt': 'some-html' } """ expected_children = ['submitmessage', 'hintprompt'] for child in expected_children: if len(xml_object.xpath(child)) != 1: raise ValueError("Self assessment definition must include exactly one '{0}' tag".format(child)) def parse(k): """Assumes that xml_object has child k""" return stringify_children(xml_object.xpath(k)[0]) return {'submitmessage': parse('submitmessage'), 'hintprompt': parse('hintprompt'), } def definition_to_xml(self, resource_fs): '''Return an xml element representing this definition.''' elt = etree.Element('selfassessment') def add_child(k): child_str = '<{tag}>{body}'.format(tag=k, body=self.definition[k]) child_node = etree.fromstring(child_str) elt.append(child_node) for child in ['submitmessage', 'hintprompt']: add_child(child) return elt