From 31641252e7ebb25be4ed55af8cda9940ce42a406 Mon Sep 17 00:00:00 2001 From: kimth Date: Wed, 25 Jul 2012 13:28:52 -0400 Subject: [PATCH 01/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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 = $(" - -Lab 2A: Superposition Experiment - -

Note: This part of the lab is just to develop your intuition about -superposition. There are no responses that need to be checked. - -

Circuits with multiple sources can be hard to analyze as-is. For example, what is the voltage -between the two terminals on the right of Figure 1? - -
- -Figure 1. Example multi-source circuit -
- -

We can use superposition to make the analysis much easier. -The circuit in Figure 1 can be decomposed into two separate -subcircuits: one involving only the voltage source and one involving only the -current source. We'll analyze each circuit separately and combine the -results using superposition. Recall that to decompose a circuit for -analysis, we'll pick each source in turn and set all the other sources -to zero (i.e., voltage sources become short circuits and current -sources become open circuits). The circuit above has two sources, so -the decomposition produces two subcircuits, as shown in Figure 2. - -
-
- -(a) Subcircuit for analyzing contribution of voltage source - - -(b) Subcircuit for analyzing contribution of current source -
-
Figure 2. Decomposition of Figure 1 into subcircuits -
- -
Let's use the DC analysis capability of the schematic tool to see superposition -in action. The sliders below control the resistances of R1, R2, R3 and R4 in all -the diagrams. As you move the sliders, the schematic tool will adjust the appropriate -resistance, perform a DC analysis and display the node voltages on the diagrams. Here's -what you want to observe as you play with the sliders: - -
    -The voltage for a node in Figure 1 is the sum of the voltages for -that node in Figures 2(a) and 2(b), just as predicted by -superposition. (Note that due to round-off in the display of the -voltages, the sum of the displayed voltages in Figure 2 may only be within -.01 of the voltages displayed in Figure 1.) -
- -
-
- -
- - - - - - - - - - - - - - - - - -
R1 -
-
R2 -
-
R3 -
-
R4 -
-
-
-
diff --git a/common/test/data/toy/html/toylab.html b/common/test/data/toy/html/toylab.html new file mode 100644 index 0000000000..81df84bd63 --- /dev/null +++ b/common/test/data/toy/html/toylab.html @@ -0,0 +1,3 @@ +Lab 2A: Superposition Experiment + +

Isn't the toy course great?

From eb5989aa992b646e5335f43d0cd4aa0001218255 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 19 Jul 2012 19:22:03 -0400 Subject: [PATCH 28/81] Ready to implement path_to_location * Clean up test data for simple, toy courses * clean up test_mongo.py * write initial test for path_to_location * hook up view to use path_to_location Next: actually implement it :) --- .../xmodule/xmodule/modulestore/__init__.py | 23 +++++ .../xmodule/xmodule/modulestore/exceptions.py | 3 + .../lib/xmodule/xmodule/modulestore/mongo.py | 24 +++++- .../xmodule/modulestore/tests/test_mongo.py | 84 ++++++++++++------- common/test/data/simple/course.xml | 24 ++++++ common/test/data/simple/html/toylab.html | 3 + .../data/simple/problems/L1_Problem_1.xml | 43 ++++++++++ .../test/data/simple/problems/ps01-simple.xml | 62 ++++++++++++++ common/test/data/toy/course.xml | 4 +- lms/djangoapps/courseware/views.py | 11 ++- 10 files changed, 244 insertions(+), 37 deletions(-) create mode 100644 common/test/data/simple/course.xml create mode 100644 common/test/data/simple/html/toylab.html create mode 100644 common/test/data/simple/problems/L1_Problem_1.xml create mode 100644 common/test/data/simple/problems/ps01-simple.xml diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index 77dfacf372..279782b61a 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -253,3 +253,26 @@ class ModuleStore(object): in this modulestore. ''' raise NotImplementedError + + def path_to_location(self, location, course=None, chapter=None, section=None): + ''' + Try to find a course/chapter/section[/position] path to this location. + + raise ItemNotFoundError if the location doesn't exist. + + If course, chapter, section are not None, restrict search to paths with those + components as specified. + + raise NoPathToItem if the location exists, but isn't accessible via + a path that matches the course/chapter/section restrictions. + + In general, a location may be accessible via many paths. This method may + return any valid path. + + Return a tuple (course, chapter, section, position). + + If the section a sequence, position should be the position of this location + in that sequence. Otherwise, position should be None. + ''' + raise NotImplementedError + diff --git a/common/lib/xmodule/xmodule/modulestore/exceptions.py b/common/lib/xmodule/xmodule/modulestore/exceptions.py index a6dc99883f..d860a1d263 100644 --- a/common/lib/xmodule/xmodule/modulestore/exceptions.py +++ b/common/lib/xmodule/xmodule/modulestore/exceptions.py @@ -13,3 +13,6 @@ class InsufficientSpecificationError(Exception): class InvalidLocationError(Exception): pass + +class NoPathToItem(Exception): + pass diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 95c882824b..d2b68d03ef 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -251,7 +251,7 @@ class MongoModuleStore(ModuleStore): def update_metadata(self, location, metadata): """ - Set the children for the item specified by the location to + Set the metadata for the item specified by the location to metadata location: Something that can be passed to Location @@ -264,3 +264,25 @@ class MongoModuleStore(ModuleStore): {'_id': Location(location).dict()}, {'$set': {'metadata': metadata}} ) + + def path_to_location(self, location, course=None): + ''' + Try to find a course/chapter/section[/position] path to this location. + + raise ItemNotFoundError if the location doesn't exist. + + If course is not None, restrict search to paths in that course. + + raise NoPathToItem if the location exists, but isn't accessible via + a chapter/section path in the course(s) being searched. + + In general, a location may be accessible via many paths. This method may + return any valid path. + + Return a tuple (course, chapter, section, position). + + If the section a sequence, position should be the position of this location + in that sequence. Otherwise, position should be None. + ''' + raise NotImplementedError + diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index 55e573677f..493b8a385d 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -4,7 +4,7 @@ from nose.tools import assert_equals, assert_raises, assert_not_equals, with_set from path import path from xmodule.modulestore import Location -from xmodule.modulestore.exceptions import InvalidLocationError +from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.xml_importer import import_from_xml @@ -26,36 +26,60 @@ FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing loa DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor' -connection = None +class TestMongoModuleStore(object): -def setup(): - global connection - connection = pymongo.connection.Connection(HOST, PORT) + @classmethod + def setupClass(cls): + cls.connection = pymongo.connection.Connection(HOST, PORT) + cls.connection.drop_database(DB) + + @classmethod + def teardownClass(cls): + pass + + def setUp(self): + # connect to the db + self.store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, default_class=DEFAULT_CLASS) + # Explicitly list the courses to load (don't want the big one) + courses = ['toy', 'simple'] + import_from_xml(self.store, DATA_DIR, courses) + self.connection = TestMongoModuleStore.connection + def tearDown(self): + # Destroy the test db. + self.connection.drop_database(DB) + self.store = None - -def setup_func(): - # connect to the db - global store - store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, default_class=DEFAULT_CLASS) - print 'data_dir: {0}'.format(DATA_DIR) - import_from_xml(store, DATA_DIR) - -def teardown_func(): - global store - store = None - # Destroy the test db. - connection.drop_database(DB) - - -@with_setup(setup_func, teardown_func) -def test_init(): - '''Just make sure the db loads''' - pass - -@with_setup(setup_func, teardown_func) -def test_get_courses(): - '''Make sure the course objects loaded properly''' - courses = store.get_courses() - print courses + def test_init(self): + '''Just make sure the db loads''' + ids = list(self.connection[DB][COLLECTION].find({}, {'_id': True})) + print len(ids) + def test_get_courses(self): + '''Make sure the course objects loaded properly''' + courses = self.store.get_courses() + assert_equals(len(courses), 2) + courses.sort(key=lambda c: c.id) + assert_equals(courses[0].id, 'edX/simple/2012_Fall') + assert_equals(courses[1].id, 'edX/toy/2012_Fall') + + def Xtest_path_to_location(self): + '''Make sure that path_to_location works''' + should_work = ( + ("i4x://edX/toy/video/Welcome", ("toy", "Overview", None, None)), + ) + for location, expected in should_work: + assert_equals(self.store.path_to_location(location), expected) + + not_found = ( + "i4x://edX/toy/video/WelcomeX", + ) + for location in not_found: + assert_raises(ItemNotFoundError, self.store.path_to_location, location) + + no_path = ( + "i4x://edX/toy/video/Lost_Video", + ) + for location in not_found: + assert_raises(ItemNotFoundError, self.store.path_to_location, location) + diff --git a/common/test/data/simple/course.xml b/common/test/data/simple/course.xml new file mode 100644 index 0000000000..59a0a0c5bc --- /dev/null +++ b/common/test/data/simple/course.xml @@ -0,0 +1,24 @@ + + + + +
+ + + +
+
+
diff --git a/common/test/data/simple/html/toylab.html b/common/test/data/simple/html/toylab.html new file mode 100644 index 0000000000..81df84bd63 --- /dev/null +++ b/common/test/data/simple/html/toylab.html @@ -0,0 +1,3 @@ +Lab 2A: Superposition Experiment + +

Isn't the toy course great?

diff --git a/common/test/data/simple/problems/L1_Problem_1.xml b/common/test/data/simple/problems/L1_Problem_1.xml new file mode 100644 index 0000000000..2ba0617904 --- /dev/null +++ b/common/test/data/simple/problems/L1_Problem_1.xml @@ -0,0 +1,43 @@ + + +

+

Finger Exercise 1

+

+

+Here are two definitions:

+
    +
  1. +

    +Declarative knowledge refers to statements of fact.

    +
  2. +
  3. +

    +Imperative knowledge refers to 'how to' methods.

    +
  4. +
+

+Which of the following choices is correct?

+
    +
  1. +

    +Statement 1 is true, Statement 2 is false

    +
  2. +
  3. +

    +Statement 1 is false, Statement 2 is true

    +
  4. +
  5. +

    +Statement 1 and Statement 2 are both false

    +
  6. +
  7. +

    +Statement 1 and Statement 2 are both true

    +
  8. +
+

+ + + +

+
diff --git a/common/test/data/simple/problems/ps01-simple.xml b/common/test/data/simple/problems/ps01-simple.xml new file mode 100644 index 0000000000..e70d8f2c8d --- /dev/null +++ b/common/test/data/simple/problems/ps01-simple.xml @@ -0,0 +1,62 @@ +