Integrating some of Ike's courseware changes
This commit is contained in:
committed by
Piotr Mitros
parent
756dc97897
commit
232e758c7a
@@ -1,3 +1,12 @@
|
||||
#
|
||||
# File: courseware/capa/capa_problem.py
|
||||
#
|
||||
'''
|
||||
Main module which shows problems (of "capa" type).
|
||||
|
||||
This is used by capa_module.
|
||||
'''
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import math
|
||||
@@ -10,12 +19,13 @@ import struct
|
||||
|
||||
from lxml import etree
|
||||
from lxml.etree import Element
|
||||
from xml.sax.saxutils import escape, unescape
|
||||
|
||||
from mako.template import Template
|
||||
|
||||
from util import contextualize_text
|
||||
import inputtypes
|
||||
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse
|
||||
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse
|
||||
|
||||
import calc
|
||||
import eia
|
||||
@@ -26,19 +36,27 @@ response_types = {'numericalresponse':NumericalResponse,
|
||||
'formularesponse':FormulaResponse,
|
||||
'customresponse':CustomResponse,
|
||||
'schematicresponse':SchematicResponse,
|
||||
'externalresponse':ExternalResponse,
|
||||
'multiplechoiceresponse':MultipleChoiceResponse,
|
||||
'truefalseresponse':TrueFalseResponse}
|
||||
entry_types = ['textline', 'schematic', 'choicegroup']
|
||||
response_properties = ["responseparam", "answer"]
|
||||
'truefalseresponse':TrueFalseResponse,
|
||||
'imageresponse':ImageResponse,
|
||||
}
|
||||
entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput']
|
||||
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
|
||||
response_properties = ["responseparam", "answer"] # these get captured as student responses
|
||||
|
||||
# How to convert from original XML to HTML
|
||||
# We should do this with xlst later
|
||||
html_transforms = {'problem': {'tag':'div'},
|
||||
"numericalresponse": {'tag':'span'},
|
||||
"customresponse": {'tag':'span'},
|
||||
"externalresponse": {'tag':'span'},
|
||||
"schematicresponse": {'tag':'span'},
|
||||
"formularesponse": {'tag':'span'},
|
||||
"multiplechoiceresponse": {'tag':'span'},
|
||||
"text": {'tag':'span'}}
|
||||
"text": {'tag':'span'},
|
||||
"math": {'tag':'span'},
|
||||
}
|
||||
|
||||
global_context={'random':random,
|
||||
'numpy':numpy,
|
||||
@@ -50,7 +68,15 @@ global_context={'random':random,
|
||||
# These should be removed from HTML output, including all subelements
|
||||
html_problem_semantics = ["responseparam", "answer", "script"]
|
||||
# These should be removed from HTML output, but keeping subelements
|
||||
html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text"]
|
||||
html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text","externalresponse"]
|
||||
|
||||
# removed in MC
|
||||
## These should be transformed
|
||||
#html_special_response = {"textline":textline.render,
|
||||
# "schematic":schematic.render,
|
||||
# "textbox":textbox.render,
|
||||
# "solution":solution.render,
|
||||
# }
|
||||
|
||||
class LoncapaProblem(object):
|
||||
def __init__(self, fileobject, id, state=None, seed=None):
|
||||
@@ -80,6 +106,7 @@ class LoncapaProblem(object):
|
||||
|
||||
## Parse XML file
|
||||
file_text = fileobject.read()
|
||||
self.fileobject = fileobject # save it, so we can use for debugging information later
|
||||
# Convert startouttext and endouttext to proper <text></text>
|
||||
# TODO: Do with XML operations
|
||||
file_text = re.sub("startouttext\s*/","text",file_text)
|
||||
@@ -102,6 +129,9 @@ class LoncapaProblem(object):
|
||||
'done':self.done}
|
||||
|
||||
def get_max_score(self):
|
||||
'''
|
||||
TODO: multiple points for programming problems.
|
||||
'''
|
||||
sum = 0
|
||||
for et in entry_types:
|
||||
sum = sum + self.tree.xpath('count(//'+et+')')
|
||||
@@ -120,27 +150,39 @@ class LoncapaProblem(object):
|
||||
'total':self.get_max_score()}
|
||||
|
||||
def grade_answers(self, answers):
|
||||
'''
|
||||
Grade student responses. Called by capa_module.check_problem.
|
||||
answers is a dict of all the entries from request.POST, but with the first part
|
||||
of each key removed (the string before the first "_").
|
||||
|
||||
Thus, for example, input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123
|
||||
'''
|
||||
self.student_answers = answers
|
||||
context=self.extract_context(self.tree)
|
||||
self.correct_map = dict()
|
||||
problems_simple = self.extract_problems(self.tree)
|
||||
for response in problems_simple:
|
||||
grader = response_types[response.tag](response, self.context)
|
||||
results = grader.grade(answers)
|
||||
results = grader.grade(answers) # call the responsetype instance to do the actual grading
|
||||
self.correct_map.update(results)
|
||||
return self.correct_map
|
||||
|
||||
def get_question_answers(self):
|
||||
'''
|
||||
Make a dict of (id,correct_answer) entries, for all the problems.
|
||||
Called by "show answers" button JSON request (see capa_module)
|
||||
'''
|
||||
context=self.extract_context(self.tree)
|
||||
answer_map = dict()
|
||||
problems_simple = self.extract_problems(self.tree)
|
||||
problems_simple = self.extract_problems(self.tree) # purified (flat) XML tree of just response queries
|
||||
for response in problems_simple:
|
||||
responder = response_types[response.tag](response, self.context)
|
||||
responder = response_types[response.tag](response, self.context) # instance of numericalresponse, customresponse,...
|
||||
results = responder.get_answers()
|
||||
answer_map.update(results)
|
||||
answer_map.update(results) # dict of (id,correct_answer)
|
||||
|
||||
# example for the following: <textline size="5" correct_answer="saturated" />
|
||||
for entry in problems_simple.xpath("//"+"|//".join(response_properties+entry_types)):
|
||||
answer = entry.get('correct_answer')
|
||||
answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline
|
||||
if answer:
|
||||
answer_map[entry.get('id')] = contextualize_text(answer, self.context)
|
||||
|
||||
@@ -149,11 +191,28 @@ class LoncapaProblem(object):
|
||||
# ======= Private ========
|
||||
|
||||
def extract_context(self, tree, seed = struct.unpack('i', os.urandom(4))[0]): # private
|
||||
''' Problem XML goes to Python execution context. Runs everything in script tags '''
|
||||
'''
|
||||
Extract content of <script>...</script> from the problem.xml file, and exec it in the
|
||||
context of this problem. Provides ability to randomize problems, and also set
|
||||
variables for problem answer checking.
|
||||
|
||||
Problem XML goes to Python execution context. Runs everything in script tags
|
||||
'''
|
||||
random.seed(self.seed)
|
||||
context = dict()
|
||||
for script in tree.xpath('/problem/script'):
|
||||
exec script.text in global_context, context
|
||||
### IKE: Why do we need these two lines?
|
||||
context = {'global_context':global_context} # save global context in here also
|
||||
global_context['context'] = context # and put link to local context in the global one
|
||||
|
||||
#for script in tree.xpath('/problem/script'):
|
||||
for script in tree.findall('.//script'):
|
||||
code = script.text
|
||||
XMLESC = {"'": "'", """: '"'}
|
||||
code = unescape(code,XMLESC)
|
||||
try:
|
||||
exec code in global_context, context
|
||||
except Exception,err:
|
||||
print "[courseware.capa.capa_problem.extract_context] error %s" % err
|
||||
print "in doing exec of this code:",code
|
||||
return context
|
||||
|
||||
def get_html(self):
|
||||
@@ -165,14 +224,22 @@ class LoncapaProblem(object):
|
||||
if problemtree.tag in html_problem_semantics:
|
||||
return
|
||||
|
||||
problemid = problemtree.get('id') # my ID
|
||||
|
||||
# used to be
|
||||
# if problemtree.tag in html_special_response:
|
||||
|
||||
if hasattr(inputtypes, problemtree.tag):
|
||||
# status is currently the answer for the problem ID for the input element,
|
||||
# but it will turn into a dict containing both the answer and any associated message
|
||||
# for the problem ID for the input element.
|
||||
status = "unsubmitted"
|
||||
if problemtree.get('id') in self.correct_map:
|
||||
if problemid in self.correct_map:
|
||||
status = self.correct_map[problemtree.get('id')]
|
||||
|
||||
value = ""
|
||||
if self.student_answers and problemtree.get('id') in self.student_answers:
|
||||
value = self.student_answers[problemtree.get('id')]
|
||||
if self.student_answers and problemid in self.student_answers:
|
||||
value = self.student_answers[problemid]
|
||||
|
||||
return getattr(inputtypes, problemtree.tag)(problemtree, value, status) #TODO
|
||||
|
||||
@@ -203,7 +270,8 @@ class LoncapaProblem(object):
|
||||
return [tree]
|
||||
|
||||
def preprocess_problem(self, tree, correct_map=dict(), answer_map=dict()): # private
|
||||
''' Assign IDs to all the responses
|
||||
'''
|
||||
Assign IDs to all the responses
|
||||
Assign sub-IDs to all entries (textline, schematic, etc.)
|
||||
Annoted correctness and value
|
||||
In-place transformation
|
||||
@@ -217,13 +285,20 @@ class LoncapaProblem(object):
|
||||
response.attrib['state'] = correct
|
||||
response_id = response_id + 1
|
||||
answer_id = 1
|
||||
for entry in tree.xpath("|".join(['//'+response.tag+'[@id=$id]//'+x for x in entry_types]),
|
||||
for entry in tree.xpath("|".join(['//'+response.tag+'[@id=$id]//'+x for x in (entry_types + solution_types)]),
|
||||
id=response_id_str):
|
||||
entry.attrib['response_id'] = str(response_id)
|
||||
entry.attrib['answer_id'] = str(answer_id)
|
||||
entry.attrib['id'] = "%s_%i_%i"%(self.problem_id, response_id, answer_id)
|
||||
answer_id=answer_id+1
|
||||
|
||||
# <solution>...</solution> may not be associated with any specific response; give IDs for those separately
|
||||
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
|
||||
solution_id = 1
|
||||
for solution in tree.findall('.//solution'):
|
||||
solution.attrib['id'] = "%s_solution_%i"%(self.problem_id, solution_id)
|
||||
solution_id += 1
|
||||
|
||||
def extract_problems(self, problem_tree):
|
||||
''' Remove layout from the problem, and give a purified XML tree of just the problems '''
|
||||
problem_tree=copy.deepcopy(problem_tree)
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
#
|
||||
# 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 json
|
||||
import math
|
||||
import numbers
|
||||
import numpy
|
||||
import random
|
||||
import re
|
||||
import requests
|
||||
import scipy
|
||||
import traceback
|
||||
import copy
|
||||
import abc
|
||||
|
||||
# specific library imports
|
||||
from calc import evaluator, UndefinedVariable
|
||||
from django.conf import settings
|
||||
from util import contextualize_text
|
||||
from lxml import etree
|
||||
from lxml.etree import Element
|
||||
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
|
||||
|
||||
# local imports
|
||||
import calc
|
||||
import eia
|
||||
|
||||
# TODO: Should be the same object as in capa_problem
|
||||
global_context={'random':random,
|
||||
'numpy':numpy,
|
||||
'math':math,
|
||||
'scipy':scipy,
|
||||
'calc':calc,
|
||||
'eia':eia}
|
||||
|
||||
|
||||
def compare_with_tolerance(v1, v2, tol):
|
||||
''' Compare v1 to v2 with maximum tolerance tol
|
||||
@@ -56,6 +63,21 @@ class GenericResponse(object):
|
||||
#Every response type needs methods "grade" and "get_answers"
|
||||
|
||||
class MultipleChoiceResponse(GenericResponse):
|
||||
'''
|
||||
Example:
|
||||
|
||||
<multiplechoiceresponse direction="vertical" randomize="yes">
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice location="random" name="1" correct="false"><span>`a+b`<br/></span></choice>
|
||||
<choice location="random" name="2" correct="true"><span><math>a+b^2</math><br/></span></choice>
|
||||
<choice location="random" name="3" correct="false"><math>a+b+c</math></choice>
|
||||
<choice location="bottom" name="4" correct="false"><math>a+b+d</math></choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
TODO: handle direction and randomize
|
||||
|
||||
'''
|
||||
def __init__(self, xml, context):
|
||||
self.xml = xml
|
||||
self.correct_choices = xml.xpath('//*[@id=$id]//choice[@correct="true"]',
|
||||
@@ -115,11 +137,17 @@ class NumericalResponse(GenericResponse):
|
||||
def __init__(self, xml, context):
|
||||
self.xml = xml
|
||||
self.correct_answer = contextualize_text(xml.get('answer'), context)
|
||||
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
|
||||
id=xml.get('id'))[0]
|
||||
self.tolerance = contextualize_text(self.tolerance_xml, context)
|
||||
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
|
||||
id=xml.get('id'))[0]
|
||||
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,err:
|
||||
self.tolerance = 0
|
||||
try:
|
||||
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
|
||||
id=xml.get('id'))[0]
|
||||
except Exception, err:
|
||||
self.answer_id = None
|
||||
|
||||
def grade(self, student_answers):
|
||||
''' Display HTML for a numeric response '''
|
||||
@@ -140,7 +168,50 @@ class NumericalResponse(GenericResponse):
|
||||
def get_answers(self):
|
||||
return {self.answer_id:self.correct_answer}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class CustomResponse(GenericResponse):
|
||||
'''
|
||||
Custom response. The python code to be run should be in <answer>...</answer>. Example:
|
||||
|
||||
<customresponse>
|
||||
<startouttext/>
|
||||
<br/>
|
||||
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)\).
|
||||
<br/>
|
||||
<textline size="5" correct_answer="IS*u(t-t0)" />
|
||||
<endouttext/>
|
||||
<answer type="loncapa/python">
|
||||
correct=['correct']
|
||||
try:
|
||||
r = str(submission[0])
|
||||
except ValueError:
|
||||
correct[0] ='incorrect'
|
||||
r = '0'
|
||||
if not(r=="IS*u(t-t0)"):
|
||||
correct[0] ='incorrect'
|
||||
</answer>
|
||||
</customresponse>
|
||||
|
||||
Alternatively, the check function can be defined in <script>...</script> Example:
|
||||
|
||||
<script type="loncapa/python"><![CDATA[
|
||||
|
||||
def sympy_check2():
|
||||
messages[0] = '%s:%s' % (submission[0],fromjs[0].replace('<','<'))
|
||||
#messages[0] = str(answers)
|
||||
correct[0] = 'correct'
|
||||
|
||||
]]>
|
||||
</script>
|
||||
|
||||
<customresponse cfn="sympy_check2" type="cs" expect="2.27E-39" dojs="math" size="30" answer="2.27E-39">
|
||||
<textline size="40" dojs="math" />
|
||||
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
|
||||
</customresponse>
|
||||
|
||||
'''
|
||||
def __init__(self, xml, context):
|
||||
self.xml = xml
|
||||
## CRITICAL TODO: Should cover all entrytypes
|
||||
@@ -163,6 +234,10 @@ class CustomResponse(GenericResponse):
|
||||
self.code = answer.text
|
||||
|
||||
def grade(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 "_").
|
||||
'''
|
||||
submission = [student_answers[k] for k in sorted(self.answer_ids)]
|
||||
self.context.update({'submission':submission})
|
||||
exec self.code in global_context, self.context
|
||||
@@ -173,19 +248,92 @@ class CustomResponse(GenericResponse):
|
||||
# be handled by capa_problem
|
||||
return {}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class ExternalResponse(GenericResponse):
|
||||
'''
|
||||
Grade the student's input using an external server.
|
||||
|
||||
Typically used by coding problems.
|
||||
'''
|
||||
def __init__(self, xml, context):
|
||||
self.xml = xml
|
||||
self.answer_ids = xml.xpath('//*[@id=$id]//textbox/@id|//*[@id=$id]//textline/@id',
|
||||
id=xml.get('id'))
|
||||
self.context = context
|
||||
answer = xml.xpath('//*[@id=$id]//answer',
|
||||
id=xml.get('id'))[0]
|
||||
|
||||
answer_src = answer.get('src')
|
||||
if answer_src != None:
|
||||
self.code = open(settings.DATA_DIR+'src/'+answer_src).read()
|
||||
else:
|
||||
self.code = answer.text
|
||||
|
||||
self.tests = xml.get('answer')
|
||||
|
||||
def grade(self, student_answers):
|
||||
submission = [student_answers[k] for k in sorted(self.answer_ids)]
|
||||
self.context.update({'submission':submission})
|
||||
|
||||
xmlstr = etree.tostring(self.xml, pretty_print=True)
|
||||
|
||||
payload = {'xml': xmlstr,
|
||||
### Question: Is this correct/what we want? Shouldn't this be a json.dumps?
|
||||
'LONCAPA_student_response': ''.join(submission),
|
||||
'LONCAPA_correct_answer': self.tests,
|
||||
'processor' : self.code,
|
||||
}
|
||||
|
||||
# call external server; TODO: get URL from settings.py
|
||||
r = requests.post("http://eecs1.mit.edu:8889/pyloncapa",data=payload)
|
||||
|
||||
rxml = etree.fromstring(r.text) # response is XML; prase it
|
||||
ad = rxml.find('awarddetail').text
|
||||
admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses
|
||||
'WRONG_FORMAT': 'incorrect',
|
||||
}
|
||||
self.context['correct'] = ['correct']
|
||||
if ad in admap:
|
||||
self.context['correct'][0] = admap[ad]
|
||||
|
||||
# self.context['correct'] = ['correct','correct']
|
||||
correct_map = dict(zip(sorted(self.answer_ids), self.context['correct']))
|
||||
|
||||
# TODO: separate message for each answer_id?
|
||||
correct_map['msg'] = rxml.find('message').text.replace(' ',' ') # store message in correct_map
|
||||
|
||||
return correct_map
|
||||
|
||||
def get_answers(self):
|
||||
# Since this is explicitly specified in the problem, this will
|
||||
# be handled by capa_problem
|
||||
return {}
|
||||
|
||||
class StudentInputError(Exception):
|
||||
pass
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class FormulaResponse(GenericResponse):
|
||||
def __init__(self, xml, context):
|
||||
self.xml = xml
|
||||
self.correct_answer = contextualize_text(xml.get('answer'), context)
|
||||
self.samples = contextualize_text(xml.get('samples'), context)
|
||||
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
|
||||
id=xml.get('id'))[0]
|
||||
self.tolerance = contextualize_text(self.tolerance_xml, context)
|
||||
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
|
||||
id=xml.get('id'))[0]
|
||||
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,err:
|
||||
self.tolerance = 0
|
||||
|
||||
try:
|
||||
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
|
||||
id=xml.get('id'))[0]
|
||||
except Exception, err:
|
||||
self.answer_id = None
|
||||
raise Exception, "[courseware.capa.responsetypes.FormulaResponse] Error: missing answer_id!!"
|
||||
|
||||
self.context = context
|
||||
ts = xml.get('type')
|
||||
if ts == None:
|
||||
@@ -211,7 +359,7 @@ class FormulaResponse(GenericResponse):
|
||||
for i in range(numsamples):
|
||||
instructor_variables = self.strip_dict(dict(self.context))
|
||||
student_variables = dict()
|
||||
for var in ranges:
|
||||
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
|
||||
@@ -246,6 +394,8 @@ class FormulaResponse(GenericResponse):
|
||||
def get_answers(self):
|
||||
return {self.answer_id:self.correct_answer}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class SchematicResponse(GenericResponse):
|
||||
def __init__(self, xml, context):
|
||||
self.xml = xml
|
||||
@@ -270,3 +420,68 @@ class SchematicResponse(GenericResponse):
|
||||
# Since this is explicitly specified in the problem, this will
|
||||
# be handled by capa_problem
|
||||
return {}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class ImageResponse(GenericResponse):
|
||||
"""
|
||||
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 <imageresponse> has a <foilgroup> inside it. That
|
||||
doesn't make sense to me (Ike). Instead, let's have it such that <imageresponse>
|
||||
should contain one or more <imageinput> stanzas. Each <imageinput> should specify
|
||||
a rectangle, given as an attribute, defining the correct answer.
|
||||
|
||||
Example:
|
||||
|
||||
<imageresponse>
|
||||
<imageinput src="image1.jpg" width="200" height="100" rectangle="(10,10)-(20,30)" />
|
||||
<imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" />
|
||||
</imageresponse>
|
||||
|
||||
"""
|
||||
def __init__(self, xml, context):
|
||||
self.xml = xml
|
||||
self.context = context
|
||||
self.ielements = xml.findall('imageinput')
|
||||
self.answer_ids = [ie.get('id') for ie in self.ielements]
|
||||
|
||||
def grade(self, student_answers):
|
||||
correct_map = {}
|
||||
expectedset = self.get_answers()
|
||||
|
||||
for aid in self.answer_ids: # loop through IDs of <imageinput> 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)' % (err,aid,given)
|
||||
(gx,gy) = [int(x) for x in m.groups()]
|
||||
|
||||
if settings.DEBUG:
|
||||
print "[capamodule.capa.responsetypes.imageinput] llx,lly,urx,ury=",(llx,lly,urx,ury)
|
||||
print "[capamodule.capa.responsetypes.imageinput] gx,gy=",(gx,gy)
|
||||
|
||||
# answer is correct if (x,y) is within the specified rectangle
|
||||
if (llx <= gx <= urx) and (lly <= gy <= ury):
|
||||
correct_map[aid] = 'correct'
|
||||
else:
|
||||
correct_map[aid] = 'incorrect'
|
||||
if settings.DEBUG:
|
||||
print "[capamodule.capa.responsetypes.imageinput] correct_map=",correct_map
|
||||
return correct_map
|
||||
|
||||
def get_answers(self):
|
||||
return dict([(ie.get('id'),ie.get('rectangle')) for ie in self.ielements])
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
'''
|
||||
courseware/content_parser.py
|
||||
|
||||
This file interfaces between all courseware modules and the top-level course.xml file for a course.
|
||||
|
||||
Does some caching (to be explained).
|
||||
|
||||
'''
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
15
djangoapps/courseware/test_files/imageresponse.xml
Normal file
15
djangoapps/courseware/test_files/imageresponse.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<problem>
|
||||
<text><p>
|
||||
Two skiers are on frictionless black diamond ski slopes.
|
||||
Hello</p></text>
|
||||
|
||||
<imageresponse max="1" loncapaid="11">
|
||||
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98)"/>
|
||||
<text>Click on the image where the top skier will stop momentarily if the top skier starts from rest.</text>
|
||||
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(242,202)-(296,276)"/>
|
||||
<text>Click on the image where the lower skier will stop momentarily if the lower skier starts from rest.</text>
|
||||
<hintgroup showoncorrect="no">
|
||||
<text><p>Use conservation of energy.</p></text>
|
||||
</hintgroup>
|
||||
</imageresponse>
|
||||
</problem>
|
||||
0
djangoapps/ssl_auth/__init__.py
Normal file
0
djangoapps/ssl_auth/__init__.py
Normal file
112
static/css/codemirror.css
Normal file
112
static/css/codemirror.css
Normal file
@@ -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;}
|
||||
1
static/js/codemirror-compressed.js
Normal file
1
static/js/codemirror-compressed.js
Normal file
File diff suppressed because one or more lines are too long
24
static/js/imageinput.js
Normal file
24
static/js/imageinput.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Simple image input
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// click on image, return coordinates
|
||||
// put a dot at location of click, on imag
|
||||
|
||||
// window.image_input_click = function(id,event){
|
||||
|
||||
function image_input_click(id,event){
|
||||
iidiv = document.getElementById("imageinput_"+id);
|
||||
pos_x = event.offsetX?(event.offsetX):event.pageX-document.iidiv.offsetLeft;
|
||||
pos_y = event.offsetY?(event.offsetY):event.pageY-document.iidiv.offsetTop;
|
||||
result = "[" + pos_x + "," + pos_y + "]";
|
||||
cx = (pos_x-15) +"px";
|
||||
cy = (pos_y-15) +"px" ;
|
||||
// alert(result);
|
||||
document.getElementById("cross_"+id).style.left = cx;
|
||||
document.getElementById("cross_"+id).style.top = cy;
|
||||
document.getElementById("cross_"+id).style.visibility = "visible" ;
|
||||
document.getElementById("input_"+id).value =result;
|
||||
}
|
||||
3
templates/solutionspan.html
Normal file
3
templates/solutionspan.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<section class="solution-span">
|
||||
<span id="solution_${id}"></span>
|
||||
</section>
|
||||
34
templates/textbox.html
Normal file
34
templates/textbox.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<section class="text-input">
|
||||
<textarea rows="30" cols="80" name="input_${id}" id="input_${id}">${value|h}</textarea>
|
||||
|
||||
<span id="answer_${id}"></span>
|
||||
|
||||
% if state == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
|
||||
% elif state == 'correct':
|
||||
<span class="correct" id="status_${id}"></span>
|
||||
% elif state == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
% elif state == 'incomplete':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
% endif
|
||||
<br/>
|
||||
<span class="debug">(${state})</span>
|
||||
<br/>
|
||||
<span class="debug">${msg|n}</span>
|
||||
<br/>
|
||||
|
||||
<br/>
|
||||
<script>
|
||||
// Note: We need to make the area follow the CodeMirror for this to
|
||||
// work.
|
||||
$(function(){
|
||||
var cm = CodeMirror.fromTextArea(document.getElementById("input_${id}"),
|
||||
{'mode':"python"});
|
||||
});
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.CodeMirror {border-style: solid;
|
||||
border-width: 1px;}
|
||||
</style>
|
||||
</section>
|
||||
Reference in New Issue
Block a user