From 31641252e7ebb25be4ed55af8cda9940ce42a406 Mon Sep 17 00:00:00 2001 From: kimth Date: Wed, 25 Jul 2012 13:28:52 -0400 Subject: [PATCH 01/11] Typos in and some additional comments --- common/lib/capa/capa/responsetypes.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 21a3083c13..f2fdb64888 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -789,6 +789,8 @@ class CodeResponse(LoncapaResponse): In contrast to ExternalResponse, CodeResponse has following behavior: 1) Goes through a queueing system 2) Does not do external request for 'get_answers' + The XML definition of a CodeResponse is meant to be identical to that of ExternalResponse. Simply replace the + tag 'externalresponse' with 'coderesponse' ''' response_tag = 'coderesponse' @@ -797,7 +799,7 @@ class CodeResponse(LoncapaResponse): def setup_response(self): xml = self.xml - self.url = xml.get('url', "http://ec2-50-16-59-149.compute-1.amazonaws.com/xqueue/submit/") # FIXME -- hardcoded url + self.url = xml.get('url', "http://ec2-50-17-86-200.compute-1.amazonaws.com/xqueue/submit/") # FIXME -- hardcoded url answer = xml.find('answer') if answer is not None: @@ -859,7 +861,6 @@ class CodeResponse(LoncapaResponse): msg = 'Error in CodeResponse %s: cannot parse response from xworker r.text=%s' % (err, score_msg) raise Exception(err) - # The following process is lifted directly from ExternalResponse ad = rxml.find('awarddetail').text admap = {'EXACT_ANS': 'correct', # TODO: handle other loncapa responses 'WRONG_FORMAT': 'incorrect', @@ -1020,7 +1021,7 @@ main() raise Exception('Error: no response from external server url=%s' % self.url) try: - rxml = etree.fromstring(r.text) # response is XML; prase it + rxml = etree.fromstring(r.text) # response is XML; parse it except Exception as err: msg = 'Error %s - cannot parse response from external server r.text=%s' % (err,r.text) log.error(msg) From 4680b9f9e559fc60b5b8d4bc6c9d43f99308916c Mon Sep 17 00:00:00 2001 From: kimth Date: Wed, 25 Jul 2012 16:11:21 -0400 Subject: [PATCH 02/11] Fix cursor/text mismatch in Code/External-Response styling --- lms/static/css/codemirror.css | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lms/static/css/codemirror.css b/lms/static/css/codemirror.css index 2d79f4aa79..c28dfae339 100644 --- a/lms/static/css/codemirror.css +++ b/lms/static/css/codemirror.css @@ -5,7 +5,7 @@ .CodeMirror-scroll { overflow: auto; - height: 300px; + height: 480px; /* This is needed to prevent an IE[67] bug where the scrolled content is visible outside of the scrolling box. */ position: relative; @@ -38,12 +38,18 @@ border-radius: 0; border-width: 0; margin: 0; padding: 0; background: transparent; font-family: inherit; - font-size: inherit; + font-size: 16px; padding: 0; margin: 0; white-space: pre; + line-height: 22px; word-wrap: normal; } +/* THK: This is to prevent global 'span' from defining .CodeMirror style */ +.CodeMirror pre span { + font: inherit; +} + .CodeMirror-wrap pre { word-wrap: break-word; white-space: pre-wrap; From 57810bb564bc0a7896fc3d19bec09318323966e7 Mon Sep 17 00:00:00 2001 From: kimth Date: Wed, 25 Jul 2012 16:53:07 -0400 Subject: [PATCH 03/11] Consolidate textbox styling into codemirror.css --- common/lib/capa/capa/templates/textbox.html | 7 ------- lms/static/css/codemirror.css | 1 + 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/common/lib/capa/capa/templates/textbox.html b/common/lib/capa/capa/templates/textbox.html index dbdb597cf9..73cb1def60 100644 --- a/common/lib/capa/capa/templates/textbox.html +++ b/common/lib/capa/capa/templates/textbox.html @@ -27,9 +27,6 @@
- - - - diff --git a/lms/static/css/codemirror.css b/lms/static/css/codemirror.css index c28dfae339..daee5f6ab7 100644 --- a/lms/static/css/codemirror.css +++ b/lms/static/css/codemirror.css @@ -1,4 +1,5 @@ .CodeMirror { + border: 2px solid black; line-height: 1em; font-family: monospace; } From 1255ed130f169474079ea06692abc40b5686d1e6 Mon Sep 17 00:00:00 2001 From: kimth Date: Thu, 26 Jul 2012 09:15:07 -0400 Subject: [PATCH 04/11] Adjust comments --- common/lib/capa/capa/responsetypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index f2fdb64888..551ee6d40c 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -789,8 +789,8 @@ class CodeResponse(LoncapaResponse): In contrast to ExternalResponse, CodeResponse has following behavior: 1) Goes through a queueing system 2) Does not do external request for 'get_answers' - The XML definition of a CodeResponse is meant to be identical to that of ExternalResponse. Simply replace the - tag 'externalresponse' with 'coderesponse' + The XML definition of a CodeResponse is identical to that of ExternalResponse. Simply replace + the tag 'externalresponse' with 'coderesponse' ''' response_tag = 'coderesponse' From 8e8ac35d46038d8b1b29a808e5cdd002ae6e395f Mon Sep 17 00:00:00 2001 From: kimth Date: Thu, 26 Jul 2012 10:11:49 -0400 Subject: [PATCH 05/11] restored responsetypes.py from master --- common/lib/capa/capa/responsetypes.py | 1318 +++++++++++++++++++++++++ 1 file changed, 1318 insertions(+) create mode 100644 common/lib/capa/capa/responsetypes.py diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py new file mode 100644 index 0000000000..08a4521bbb --- /dev/null +++ b/common/lib/capa/capa/responsetypes.py @@ -0,0 +1,1318 @@ +# +# File: courseware/capa/responsetypes.py +# +''' +Problem response evaluation. Handles checking of student responses, of a variety of types. + +Used by capa_problem.py +''' + +# standard library imports +import hashlib +import inspect +import json +import logging +import numbers +import numpy +import random +import re +import requests +import time +import traceback +import abc + +# specific library imports +from calc import evaluator, UndefinedVariable +from correctmap import CorrectMap +from util import * +from lxml import etree +from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? + +log = logging.getLogger('mitx.' + __name__) + +#----------------------------------------------------------------------------- +# Exceptions + + +class LoncapaProblemError(Exception): + ''' + Error in specification of a problem + ''' + pass + + +class ResponseError(Exception): + ''' + Error for failure in processing a response + ''' + pass + + +class StudentInputError(Exception): + pass + +#----------------------------------------------------------------------------- +# +# Main base class for CAPA responsetypes + + +class LoncapaResponse(object): + ''' + Base class for CAPA responsetypes. Each response type (ie a capa question, + which is part of a capa problem) is represented as a subclass, + which should provide the following methods: + + - get_score : evaluate the given student answers, and return a CorrectMap + - get_answers : provide a dict of the expected answers for this problem + + Each subclass must also define the following attributes: + + - response_tag : xhtml tag identifying this response (used in auto-registering) + + In addition, these methods are optional: + + - get_max_score : if defined, this is called to obtain the maximum score possible for this question + - setup_response : find and note the answer input field IDs for the response; called by __init__ + - check_hint_condition : check to see if the student's answers satisfy a particular condition for a hint to be displayed + - render_html : render this Response as HTML (must return XHTML compliant string) + - __unicode__ : unicode representation of this Response + + Each response type may also specify the following attributes: + + - max_inputfields : (int) maximum number of answer input fields (checked in __init__ if not None) + - allowed_inputfields : list of allowed input fields (each a string) for this Response + - required_attributes : list of required attributes (each a string) on the main response XML stanza + - hint_tag : xhtml tag identifying hint associated with this response inside hintgroup + + ''' + __metaclass__ = abc.ABCMeta # abc = Abstract Base Class + + response_tag = None + hint_tag = None + + max_inputfields = None + allowed_inputfields = [] + required_attributes = [] + + def __init__(self, xml, inputfields, context, system=None): + ''' + Init is passed the following arguments: + + - xml : ElementTree of this Response + - inputfields : ordered list of ElementTrees for each input entry field in this Response + - context : script processor context + - system : ModuleSystem instance which provides OS, rendering, and user context + + ''' + self.xml = xml + self.inputfields = inputfields + self.context = context + self.system = system + + 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', '') + 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', '') + 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', '') + raise LoncapaProblemError(msg) + + self.answer_ids = [x.get('id') for x in self.inputfields] # ordered list of answer_id values for this response + if self.max_inputfields == 1: + self.answer_id = self.answer_ids[0] # for convenience + + self.default_answer_map = {} # dict for default answer map (provided in input elements) + for entry in self.inputfields: + answer = entry.get('correct_answer') + if answer: + self.default_answer_map[entry.get('id')] = contextualize_text(answer, self.context) + + if hasattr(self, 'setup_response'): + self.setup_response() + + def render_html(self, renderer): + ''' + Return XHTML Element tree representation of this Response. + + Arguments: + + - renderer : procedure which produces HTML given an ElementTree + ''' + tree = etree.Element('span') # render ourself as a + our content + for item in self.xml: + item_xhtml = renderer(item) # call provided procedure to do the rendering + if item_xhtml is not None: tree.append(item_xhtml) + tree.tail = self.xml.tail + return tree + + def evaluate_answers(self, student_answers, old_cmap): + ''' + Called by capa_problem.LoncapaProblem to evaluate student answers, and to + generate hints (if any). + + Returns the new CorrectMap, with (correctness,msg,hint,hintmode) for each answer_id. + ''' + new_cmap = self.get_score(student_answers) + self.get_hints(student_answers, new_cmap, old_cmap) + # log.debug('new_cmap = %s' % new_cmap) + return new_cmap + + def get_hints(self, student_answers, new_cmap, old_cmap): + ''' + Generate adaptive hints for this problem based on student answers, the old CorrectMap, + and the new CorrectMap produced by get_score. + + Does not return anything. + + Modifies new_cmap, by adding hints to answer_id entries as appropriate. + ''' + hintgroup = self.xml.find('hintgroup') + if hintgroup is None: return + + # hint specified by function? + hintfn = hintgroup.get('hintfn') + if hintfn: + ''' + Hint is determined by a function defined in the + ''' + snippets = [{'snippet': ''' + +
+ Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\) + In the space provided below write an algebraic expression for \(I(t)\). +
+ +
+ + correct=['correct'] + try: + r = str(submission[0]) + except ValueError: + correct[0] ='incorrect' + r = '0' + if not(r=="IS*u(t-t0)"): + correct[0] ='incorrect' + +
'''}, + {'snippet': ''' + + + + + '''}] + + response_tag = 'customresponse' + allowed_inputfields = ['textline', 'textbox'] + + def setup_response(self): + xml = self.xml + + # if has an "expect" (or "answer") attribute then save that + self.expect = xml.get('expect') or xml.get('answer') + self.myid = xml.get('id') + + log.debug('answer_ids=%s' % self.answer_ids) + + # the ... stanza should be local to the current . So try looking there first. + self.code = None + answer = None + try: + answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0] + except IndexError: + # print "xml = ",etree.tostring(xml,pretty_print=True) + + # if we have a "cfn" attribute then look for the function specified by cfn, in the problem context + # ie the comparison function is defined in the stanza instead + cfn = xml.get('cfn') + if cfn: + log.debug("cfn = %s" % cfn) + if cfn in self.context: + self.code = self.context[cfn] + else: + msg = "%s: can't find cfn %s in context" % (unicode(self), cfn) + msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '') + raise LoncapaProblemError(msg) + + if not self.code: + if answer is None: + # raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid + log.error("[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid) + self.code = '' + else: + answer_src = answer.get('src') + if answer_src is not None: + self.code = self.system.filesystem.open('src/' + answer_src).read() + else: + self.code = answer.text + + def get_score(self, student_answers): + ''' + student_answers is a dict with everything from request.POST, but with the first part + of each key removed (the string before the first "_"). + ''' + + log.debug('%s: student_answers=%s' % (unicode(self), student_answers)) + + idset = sorted(self.answer_ids) # ordered list of answer id's + try: + submission = [student_answers[k] for k in idset] # ordered list of answers + except Exception as err: + msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers + msg += '\n idset = %s, error = %s' % (idset, err) + log.error(msg) + raise Exception(msg) + + # global variable in context which holds the Presentation MathML from dynamic math input + dynamath = [student_answers.get(k + '_dynamath', None) for k in idset] # ordered list of dynamath responses + + # if there is only one box, and it's empty, then don't evaluate + if len(idset) == 1 and not submission[0]: + return CorrectMap(idset[0], 'incorrect', msg='No answer entered!') + + correct = ['unknown'] * len(idset) + messages = [''] * len(idset) + + # put these in the context of the check function evaluator + # note that this doesn't help the "cfn" version - only the exec version + self.context.update({'xml': self.xml, # our subtree + 'response_id': self.myid, # my ID + 'expect': self.expect, # expected answer (if given as attribute) + 'submission': submission, # ordered list of student answers from entry boxes in our subtree + 'idset': idset, # ordered list of ID's of all entry boxes in our subtree + 'dynamath': dynamath, # ordered list of all javascript inputs in our subtree + 'answers': student_answers, # dict of student's responses, with keys being entry box IDs + 'correct': correct, # the list to be filled in by the check function + 'messages': messages, # the list of messages to be filled in by the check function + 'options': self.xml.get('options'), # any options to be passed to the cfn + 'testdat': 'hello world', + }) + + # pass self.system.debug to cfn + self.context['debug'] = self.system.DEBUG + + # exec the check function + if type(self.code) == str: + try: + exec self.code in self.context['global_context'], self.context + except Exception as err: + print "oops in customresponse (code) error %s" % err + print "context = ", self.context + print traceback.format_exc() + else: # self.code is not a string; assume its a function + + # this is an interface to the Tutor2 check functions + fn = self.code + ret = None + log.debug(" submission = %s" % submission) + try: + answer_given = submission[0] if (len(idset) == 1) else submission + # handle variable number of arguments in check function, for backwards compatibility + # with various Tutor2 check functions + args = [self.expect, answer_given, student_answers, self.answer_ids[0]] + argspec = inspect.getargspec(fn) + nargs = len(argspec.args) - len(argspec.defaults or []) + kwargs = {} + for argname in argspec.args[nargs:]: + kwargs[argname] = self.context[argname] if argname in self.context else None + + log.debug('[customresponse] answer_given=%s' % answer_given) + log.debug('nargs=%d, args=%s, kwargs=%s' % (nargs, args, kwargs)) + + ret = fn(*args[:nargs], **kwargs) + except Exception as err: + log.error("oops in customresponse (cfn) error %s" % err) + # print "context = ",self.context + log.error(traceback.format_exc()) + raise Exception("oops in customresponse (cfn) error %s" % err) + log.debug("[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret) + if type(ret) == dict: + correct = ['correct'] * len(idset) if ret['ok'] else ['incorrect'] * len(idset) + msg = ret['msg'] + + if 1: + # try to clean up message html + msg = '' + msg + '' + msg = msg.replace('<', '<') + #msg = msg.replace('<','<') + msg = etree.tostring(fromstring_bs(msg, convertEntities=None), pretty_print=True) + #msg = etree.tostring(fromstring_bs(msg),pretty_print=True) + msg = msg.replace(' ', '') + #msg = re.sub('(.*)','\\1',msg,flags=re.M|re.DOTALL) # python 2.7 + msg = re.sub('(?ms)(.*)', '\\1', msg) + + messages[0] = msg + else: + correct = ['correct'] * len(idset) if ret else ['incorrect'] * len(idset) + + # build map giving "correct"ness of the answer(s) + correct_map = CorrectMap() + for k in range(len(idset)): + correct_map.set(idset[k], correct[k], msg=messages[k]) + return correct_map + + def get_answers(self): + ''' + Give correct answer expected for this response. + + use default_answer_map from entry elements (eg textline), + when this response has multiple entry objects. + + but for simplicity, if an "expect" attribute was given by the content author + ie then that. + ''' + if len(self.answer_ids) > 1: + return self.default_answer_map + if self.expect: + return {self.answer_ids[0]: self.expect} + return self.default_answer_map + +#----------------------------------------------------------------------------- + + +class SymbolicResponse(CustomResponse): + """ + Symbolic math response checking, using symmath library. + """ + snippets = [{'snippet': ''' + Compute \[ \exp\left(-i \frac{\theta}{2} \left[ \begin{matrix} 0 & 1 \\ 1 & 0 \end{matrix} \right] \right) \] + and give the resulting \(2\times 2\) matrix:
+ + + +
+ Your input should be typed in as a list of lists, eg [[1,2],[3,4]]. +
+
'''}] + + response_tag = 'symbolicresponse' + + def setup_response(self): + self.xml.set('cfn', 'symmath_check') + code = "from symmath import *" + exec code in self.context, self.context + CustomResponse.setup_response(self) + +#----------------------------------------------------------------------------- + + +class CodeResponse(LoncapaResponse): + ''' + Grade student code using an external server, called 'xqueue' + In contrast to ExternalResponse, CodeResponse has following behavior: + 1) Goes through a queueing system + 2) Does not do external request for 'get_answers' + ''' + + response_tag = 'coderesponse' + allowed_inputfields = ['textline', 'textbox'] + max_inputfields = 1 + + def setup_response(self): + xml = self.xml + self.url = xml.get('url', "http://ec2-50-16-59-149.compute-1.amazonaws.com/xqueue/submit/") # FIXME -- hardcoded url + + answer = xml.find('answer') + if answer is not None: + answer_src = answer.get('src') + if answer_src is not None: + self.code = self.system.filesystem.open('src/' + answer_src).read() + else: + self.code = answer.text + else: # no stanza; get code from + + +
+ Give an equation for the relativistic energy of an object with mass m. +
+ + + + + + '''}] + + response_tag = 'formularesponse' + hint_tag = 'formulahint' + allowed_inputfields = ['textline'] + required_attributes = ['answer'] + max_inputfields = 1 + + def setup_response(self): + xml = self.xml + context = self.context + self.correct_answer = contextualize_text(xml.get('answer'), context) + self.samples = contextualize_text(xml.get('samples'), context) + try: + 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.00001' + + ts = xml.get('type') + if ts is None: + typeslist = [] + else: + typeslist = ts.split(',') + if 'ci' in typeslist: # Case insensitive + self.case_sensitive = False + elif 'cs' in typeslist: # Case sensitive + self.case_sensitive = True + else: # Default + self.case_sensitive = False + + def get_score(self, student_answers): + given = student_answers[self.answer_id] + correctness = self.check_formula(self.correct_answer, given, self.samples) + return CorrectMap(self.answer_id, correctness) + + def check_formula(self, expected, given, samples): + variables = samples.split('@')[0].split(',') + numsamples = int(samples.split('@')[1].split('#')[1]) + sranges = zip(*map(lambda x: map(float, x.split(",")), + samples.split('@')[1].split('#')[0].split(':'))) + + ranges = dict(zip(variables, sranges)) + for i in range(numsamples): + instructor_variables = self.strip_dict(dict(self.context)) + student_variables = dict() + for var in ranges: # ranges give numerical ranges for testing + value = random.uniform(*ranges[var]) + instructor_variables[str(var)] = value + student_variables[str(var)] = value + #log.debug('formula: instructor_vars=%s, expected=%s' % (instructor_variables,expected)) + instructor_result = evaluator(instructor_variables, dict(), expected, cs=self.case_sensitive) + try: + #log.debug('formula: student_vars=%s, given=%s' % (student_variables,given)) + student_result = evaluator(student_variables, + dict(), + given, + cs=self.case_sensitive) + except UndefinedVariable as uv: + log.debug('formularesponse: undefined variable in given=%s' % given) + raise StudentInputError(uv.message + " not permitted in answer") + except Exception as err: + #traceback.print_exc() + log.debug('formularesponse: error %s in formula' % err) + raise StudentInputError("Error in formula") + if numpy.isnan(student_result) or numpy.isinf(student_result): + return "incorrect" + if not compare_with_tolerance(student_result, instructor_result, self.tolerance): + return "incorrect" + return "correct" + + def strip_dict(self, d): + ''' Takes a dict. Returns an identical dict, with all non-word + keys and all non-numeric values stripped out. All values also + converted to float. Used so we can safely use Python contexts. + ''' + d = dict([(k, numpy.complex(d[k])) for k in d if type(k) == str and \ + k.isalnum() and \ + isinstance(d[k], numbers.Number)]) + return d + + def check_hint_condition(self, hxml_set, student_answers): + given = student_answers[self.answer_id] + hints_to_show = [] + for hxml in hxml_set: + samples = hxml.get('samples') + name = hxml.get('name') + correct_answer = contextualize_text(hxml.get('answer'), self.context) + try: + correctness = self.check_formula(correct_answer, given, samples) + except Exception: + correctness = 'incorrect' + if correctness == 'correct': + hints_to_show.append(name) + log.debug('hints_to_show = %s' % hints_to_show) + return hints_to_show + + def get_answers(self): + return {self.answer_id: self.correct_answer} + +#----------------------------------------------------------------------------- + + +class SchematicResponse(LoncapaResponse): + + response_tag = 'schematicresponse' + allowed_inputfields = ['schematic'] + + def setup_response(self): + xml = self.xml + answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0] + answer_src = answer.get('src') + if answer_src is not None: + self.code = self.system.filestore.open('src/' + answer_src).read() # Untested; never used + else: + self.code = answer.text + + def get_score(self, student_answers): + from capa_problem import global_context + submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)] + self.context.update({'submission': submission}) + exec self.code in global_context, self.context + cmap = CorrectMap() + cmap.set_dict(dict(zip(sorted(self.answer_ids), self.context['correct']))) + return cmap + + def get_answers(self): + # use answers provided in input elements + return self.default_answer_map + +#----------------------------------------------------------------------------- + + +class ImageResponse(LoncapaResponse): + """ + Handle student response for image input: the input is a click on an image, + which produces an [x,y] coordinate pair. The click is correct if it falls + within a region specified. This region is nominally a rectangle. + + Lon-CAPA requires that each has a inside it. That + doesn't make sense to me (Ike). Instead, let's have it such that + should contain one or more stanzas. Each should specify + a rectangle, given as an attribute, defining the correct answer. + """ + snippets = [{'snippet': ''' + + + '''}] + + response_tag = 'imageresponse' + allowed_inputfields = ['imageinput'] + + def setup_response(self): + self.ielements = self.inputfields + self.answer_ids = [ie.get('id') for ie in self.ielements] + + def get_score(self, student_answers): + correct_map = CorrectMap() + expectedset = self.get_answers() + + for aid in self.answer_ids: # loop through IDs of fields in our stanza + given = student_answers[aid] # this should be a string of the form '[x,y]' + + # parse expected answer + # TODO: Compile regexp on file load + m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', expectedset[aid].strip().replace(' ', '')) + if not m: + msg = 'Error in problem specification! cannot parse rectangle in %s' % (etree.tostring(self.ielements[aid], + pretty_print=True)) + raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg) + (llx, lly, urx, ury) = [int(x) for x in m.groups()] + + # parse given answer + m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', '')) + if not m: + raise Exception('[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (aid, given)) + (gx, gy) = [int(x) for x in m.groups()] + + # answer is correct if (x,y) is within the specified rectangle + if (llx <= gx <= urx) and (lly <= gy <= ury): + correct_map.set(aid, 'correct') + else: + correct_map.set(aid, 'incorrect') + return correct_map + + def get_answers(self): + return dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements]) + +#----------------------------------------------------------------------------- +# TEMPORARY: List of all response subclasses +# FIXME: To be replaced by auto-registration + +__all__ = [CodeResponse, NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse] From 848e4e5801219087834822627ee7310c74db9eea Mon Sep 17 00:00:00 2001 From: kimth Date: Thu, 26 Jul 2012 10:27:59 -0400 Subject: [PATCH 06/11] Pull clean codemirror.css from master --- .../js/vendor/CodeMirror/codemirror.css | 119 ------------------ 1 file changed, 119 deletions(-) delete mode 100644 common/static/js/vendor/CodeMirror/codemirror.css diff --git a/common/static/js/vendor/CodeMirror/codemirror.css b/common/static/js/vendor/CodeMirror/codemirror.css deleted file mode 100644 index daee5f6ab7..0000000000 --- a/common/static/js/vendor/CodeMirror/codemirror.css +++ /dev/null @@ -1,119 +0,0 @@ -.CodeMirror { - border: 2px solid black; - line-height: 1em; - font-family: monospace; -} - -.CodeMirror-scroll { - overflow: auto; - height: 480px; - /* This is needed to prevent an IE[67] bug where the scrolled content - is visible outside of the scrolling box. */ - position: relative; - outline: none; -} - -.CodeMirror-gutter { - position: absolute; left: 0; top: 0; - z-index: 10; - background-color: #f7f7f7; - border-right: 1px solid #eee; - min-width: 2em; - height: 100%; -} -.CodeMirror-gutter-text { - color: #aaa; - text-align: right; - padding: .4em .2em .4em .4em; - white-space: pre !important; -} -.CodeMirror-lines { - padding: .4em; - white-space: pre; -} - -.CodeMirror pre { - -moz-border-radius: 0; - -webkit-border-radius: 0; - -o-border-radius: 0; - border-radius: 0; - border-width: 0; margin: 0; padding: 0; background: transparent; - font-family: inherit; - font-size: 16px; - padding: 0; margin: 0; - white-space: pre; - line-height: 22px; - word-wrap: normal; -} - -/* THK: This is to prevent global 'span' from defining .CodeMirror style */ -.CodeMirror pre span { - font: inherit; -} - -.CodeMirror-wrap pre { - word-wrap: break-word; - white-space: pre-wrap; -} -.CodeMirror-wrap .CodeMirror-scroll { - overflow-x: hidden; -} - -.CodeMirror textarea { - outline: none !important; -} - -.CodeMirror pre.CodeMirror-cursor { - z-index: 10; - position: absolute; - visibility: hidden; - border-left: 1px solid black; - border-right:none; - width:0; -} -.CodeMirror pre.CodeMirror-cursor.CodeMirror-overwrite {} -.CodeMirror-focused pre.CodeMirror-cursor { - visibility: visible; -} - -div.CodeMirror-selected { background: #d9d9d9; } -.CodeMirror-focused div.CodeMirror-selected { background: #d7d4f0; } - -.CodeMirror-searching { - background: #ffa; - background: rgba(255, 255, 0, .4); -} - -/* Default theme */ - -.cm-s-default span.cm-keyword {color: #708;} -.cm-s-default span.cm-atom {color: #219;} -.cm-s-default span.cm-number {color: #164;} -.cm-s-default span.cm-def {color: #00f;} -.cm-s-default span.cm-variable {color: black;} -.cm-s-default span.cm-variable-2 {color: #05a;} -.cm-s-default span.cm-variable-3 {color: #085;} -.cm-s-default span.cm-property {color: black;} -.cm-s-default span.cm-operator {color: black;} -.cm-s-default span.cm-comment {color: #a50;} -.cm-s-default span.cm-string {color: #a11;} -.cm-s-default span.cm-string-2 {color: #f50;} -.cm-s-default span.cm-meta {color: #555;} -.cm-s-default span.cm-error {color: #f00;} -.cm-s-default span.cm-qualifier {color: #555;} -.cm-s-default span.cm-builtin {color: #30a;} -.cm-s-default span.cm-bracket {color: #cc7;} -.cm-s-default span.cm-tag {color: #170;} -.cm-s-default span.cm-attribute {color: #00c;} -.cm-s-default span.cm-header {color: #a0a;} -.cm-s-default span.cm-quote {color: #090;} -.cm-s-default span.cm-hr {color: #999;} -.cm-s-default span.cm-link {color: #00c;} - -span.cm-header, span.cm-strong {font-weight: bold;} -span.cm-em {font-style: italic;} -span.cm-emstrong {font-style: italic; font-weight: bold;} -span.cm-link {text-decoration: underline;} - -div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} -div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} From 6631848b53c4daa08f89c040eb1344a98124d745 Mon Sep 17 00:00:00 2001 From: kimth Date: Thu, 26 Jul 2012 10:49:10 -0400 Subject: [PATCH 07/11] CodeMirror fix in the new scss org --- common/lib/capa/capa/templates/textbox.html | 7 ++ .../js/vendor/CodeMirror/codemirror.css | 112 ++++++++++++++++++ lms/static/sass/base/_base.scss | 4 + 3 files changed, 123 insertions(+) create mode 100644 common/static/js/vendor/CodeMirror/codemirror.css diff --git a/common/lib/capa/capa/templates/textbox.html b/common/lib/capa/capa/templates/textbox.html index 73cb1def60..aa87682e59 100644 --- a/common/lib/capa/capa/templates/textbox.html +++ b/common/lib/capa/capa/templates/textbox.html @@ -38,4 +38,11 @@ }); }); + diff --git a/common/static/js/vendor/CodeMirror/codemirror.css b/common/static/js/vendor/CodeMirror/codemirror.css new file mode 100644 index 0000000000..2d79f4aa79 --- /dev/null +++ b/common/static/js/vendor/CodeMirror/codemirror.css @@ -0,0 +1,112 @@ +.CodeMirror { + line-height: 1em; + font-family: monospace; +} + +.CodeMirror-scroll { + overflow: auto; + height: 300px; + /* This is needed to prevent an IE[67] bug where the scrolled content + is visible outside of the scrolling box. */ + position: relative; + outline: none; +} + +.CodeMirror-gutter { + position: absolute; left: 0; top: 0; + z-index: 10; + background-color: #f7f7f7; + border-right: 1px solid #eee; + min-width: 2em; + height: 100%; +} +.CodeMirror-gutter-text { + color: #aaa; + text-align: right; + padding: .4em .2em .4em .4em; + white-space: pre !important; +} +.CodeMirror-lines { + padding: .4em; + white-space: pre; +} + +.CodeMirror pre { + -moz-border-radius: 0; + -webkit-border-radius: 0; + -o-border-radius: 0; + border-radius: 0; + border-width: 0; margin: 0; padding: 0; background: transparent; + font-family: inherit; + font-size: inherit; + padding: 0; margin: 0; + white-space: pre; + word-wrap: normal; +} + +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; +} +.CodeMirror-wrap .CodeMirror-scroll { + overflow-x: hidden; +} + +.CodeMirror textarea { + outline: none !important; +} + +.CodeMirror pre.CodeMirror-cursor { + z-index: 10; + position: absolute; + visibility: hidden; + border-left: 1px solid black; + border-right:none; + width:0; +} +.CodeMirror pre.CodeMirror-cursor.CodeMirror-overwrite {} +.CodeMirror-focused pre.CodeMirror-cursor { + visibility: visible; +} + +div.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused div.CodeMirror-selected { background: #d7d4f0; } + +.CodeMirror-searching { + background: #ffa; + background: rgba(255, 255, 0, .4); +} + +/* Default theme */ + +.cm-s-default span.cm-keyword {color: #708;} +.cm-s-default span.cm-atom {color: #219;} +.cm-s-default span.cm-number {color: #164;} +.cm-s-default span.cm-def {color: #00f;} +.cm-s-default span.cm-variable {color: black;} +.cm-s-default span.cm-variable-2 {color: #05a;} +.cm-s-default span.cm-variable-3 {color: #085;} +.cm-s-default span.cm-property {color: black;} +.cm-s-default span.cm-operator {color: black;} +.cm-s-default span.cm-comment {color: #a50;} +.cm-s-default span.cm-string {color: #a11;} +.cm-s-default span.cm-string-2 {color: #f50;} +.cm-s-default span.cm-meta {color: #555;} +.cm-s-default span.cm-error {color: #f00;} +.cm-s-default span.cm-qualifier {color: #555;} +.cm-s-default span.cm-builtin {color: #30a;} +.cm-s-default span.cm-bracket {color: #cc7;} +.cm-s-default span.cm-tag {color: #170;} +.cm-s-default span.cm-attribute {color: #00c;} +.cm-s-default span.cm-header {color: #a0a;} +.cm-s-default span.cm-quote {color: #090;} +.cm-s-default span.cm-hr {color: #999;} +.cm-s-default span.cm-link {color: #00c;} + +span.cm-header, span.cm-strong {font-weight: bold;} +span.cm-em {font-style: italic;} +span.cm-emstrong {font-style: italic; font-weight: bold;} +span.cm-link {text-decoration: underline;} + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} diff --git a/lms/static/sass/base/_base.scss b/lms/static/sass/base/_base.scss index 6e4881865e..ba0f47de52 100644 --- a/lms/static/sass/base/_base.scss +++ b/lms/static/sass/base/_base.scss @@ -43,6 +43,10 @@ p { span { font: normal 1em/1.6em $sans-serif; } +/* Fix for CodeMirror: prevent top-level span from affecting deeply-embedded span in CodeMirror */ +.CodeMirror span { + font: inherit; +} p + p, ul + p, ol + p { margin-top: 20px; From 2671801f93529f18e6a84e783d84417b5ef1291b Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Thu, 26 Jul 2012 14:55:42 -0400 Subject: [PATCH 08/11] Add optional hide_from_toc attribute to XML elements to support Tutorials (which don't show up in the navigation). --- common/lib/xmodule/xmodule/x_module.py | 3 ++- common/lib/xmodule/xmodule/xml_module.py | 2 +- lms/djangoapps/courseware/module_render.py | 4 ++- lms/templates/accordion.html | 30 ++++++++++++---------- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 7fd0b04f94..996d31a83d 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -104,7 +104,8 @@ class HTMLSnippet(object): """ Return the html used to display this snippet """ - raise NotImplementedError("get_html() must be provided by specific modules") + raise NotImplementedError("get_html() must be provided by specific modules - not present in {0}" + .format(self.__class__)) class XModule(HTMLSnippet): diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index eeceeb3c1c..a7fc686e45 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -88,7 +88,7 @@ class XmlDescriptor(XModuleDescriptor): # The attributes will be removed from the definition xml passed # to definition_from_xml, and from the xml returned by definition_to_xml metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize', - 'start', 'due', 'graded', 'name', 'slug') + 'start', 'due', 'graded', 'name', 'slug', 'hide_from_toc') # A dictionary mapping xml attribute names to functions of the value # that return the metadata key and value diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 3f111c2953..0a089af33b 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -56,11 +56,13 @@ def toc_for_course(user, request, course, active_chapter, active_section): active = (chapter.metadata.get('display_name') == active_chapter and section.metadata.get('display_name') == active_section) + hide_from_toc = section.metadata.get('hide_from_toc', 'false').lower() == 'true' sections.append({'name': section.metadata.get('display_name'), 'format': section.metadata.get('format', ''), 'due': section.metadata.get('due', ''), - 'active': active}) + 'active': active, + 'hide_from_toc' : hide_from_toc}) chapters.append({'name': chapter.metadata.get('display_name'), 'sections': sections, diff --git a/lms/templates/accordion.html b/lms/templates/accordion.html index d9e6cee61e..f67126c614 100644 --- a/lms/templates/accordion.html +++ b/lms/templates/accordion.html @@ -3,20 +3,22 @@ <%def name="make_chapter(chapter)">

${chapter['name']}

- - + + % for chapter in toc: ${make_chapter(chapter)} From c4c8bc2fbea328fb030b188754278cfeb7e8f056 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Thu, 26 Jul 2012 16:34:40 -0400 Subject: [PATCH 09/11] Adding the ability for javascript from problems to execute more intelligently; for example, errors from javascript included in the returned html will show up in the console when using this mechanism. --- .../lib/xmodule/xmodule/js/src/capa/display.coffee | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index b5a41e3da5..4ee8257e36 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -31,8 +31,20 @@ class @Problem else $.postWithPrefix "#{@url}/problem_get", (response) => @el.html(response.html) + @executeProblemScripts() @bind() + executeProblemScripts: -> + @el.find(".script_placeholder").each (index, placeholder) -> + s = $("