Ike's changes to enable multicourse, new response types, etc.
This commit is contained in:
committed by
Piotr Mitros
parent
e45eb9fbe1
commit
ad44882835
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,6 +4,9 @@
|
||||
*.swp
|
||||
*.orig
|
||||
*.DS_Store
|
||||
:2e_*
|
||||
:2e#
|
||||
.AppleDouble
|
||||
database.sqlite
|
||||
courseware/static/js/mathjax/*
|
||||
db.newaskbot
|
||||
|
||||
@@ -25,7 +25,7 @@ from mako.template import Template
|
||||
|
||||
from util import contextualize_text
|
||||
import inputtypes
|
||||
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse
|
||||
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse,OptionResponse
|
||||
|
||||
import calc
|
||||
import eia
|
||||
@@ -40,8 +40,9 @@ response_types = {'numericalresponse':NumericalResponse,
|
||||
'multiplechoiceresponse':MultipleChoiceResponse,
|
||||
'truefalseresponse':TrueFalseResponse,
|
||||
'imageresponse':ImageResponse,
|
||||
'optionresponse':OptionResponse,
|
||||
}
|
||||
entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput']
|
||||
entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput','optioninput']
|
||||
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
|
||||
response_properties = ["responseparam", "answer"] # these get captured as student responses
|
||||
|
||||
@@ -186,6 +187,13 @@ class LoncapaProblem(object):
|
||||
if answer:
|
||||
answer_map[entry.get('id')] = contextualize_text(answer, self.context)
|
||||
|
||||
# include solutions from <solution>...</solution> stanzas
|
||||
# Tentative merge; we should figure out how we want to handle hints and solutions
|
||||
for entry in self.tree.xpath("//"+"|//".join(solution_types)):
|
||||
answer = etree.tostring(entry)
|
||||
if answer:
|
||||
answer_map[entry.get('id')] = answer
|
||||
|
||||
return answer_map
|
||||
|
||||
# ======= Private ========
|
||||
@@ -241,7 +249,24 @@ class LoncapaProblem(object):
|
||||
if self.student_answers and problemid in self.student_answers:
|
||||
value = self.student_answers[problemid]
|
||||
|
||||
return getattr(inputtypes, problemtree.tag)(problemtree, value, status) #TODO
|
||||
#### This code is a hack. It was merged to help bring two branches
|
||||
#### in sync, but should be replaced. msg should be passed in a
|
||||
#### response_type
|
||||
# prepare the response message, if it exists in correct_map
|
||||
if 'msg' in self.correct_map:
|
||||
msg = self.correct_map['msg']
|
||||
elif ('msg_%s' % problemid) in self.correct_map:
|
||||
msg = self.correct_map['msg_%s' % problemid]
|
||||
else:
|
||||
msg = ''
|
||||
|
||||
#if settings.DEBUG:
|
||||
# print "[courseware.capa.capa_problem.extract_html] msg = ",msg
|
||||
|
||||
# do the rendering
|
||||
#render_function = html_special_response[problemtree.tag]
|
||||
render_function = getattr(inputtypes, problemtree.tag)
|
||||
return render_function(problemtree, value, status, msg) # render the special response (textline, schematic,...)
|
||||
|
||||
tree=Element(problemtree.tag)
|
||||
for item in problemtree:
|
||||
@@ -287,6 +312,7 @@ class LoncapaProblem(object):
|
||||
answer_id = 1
|
||||
for entry in tree.xpath("|".join(['//'+response.tag+'[@id=$id]//'+x for x in (entry_types + solution_types)]),
|
||||
id=response_id_str):
|
||||
# assign one answer_id for each entry_type or solution_type
|
||||
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)
|
||||
|
||||
@@ -6,11 +6,16 @@
|
||||
Module containing the problem elements which render into input objects
|
||||
|
||||
- textline
|
||||
- textbox (change this to textarea?)
|
||||
- textbox (change this to textarea?)
|
||||
- schemmatic
|
||||
- choicegroup (for multiplechoice: checkbox, radio, or select option)
|
||||
- imageinput (for clickable image)
|
||||
- optioninput (for option list)
|
||||
|
||||
These are matched by *.html files templates/*.html which are mako templates with the actual html.
|
||||
|
||||
Each input type takes the xml tree as 'element', the previous answer as 'value', and the graded status as 'status'
|
||||
|
||||
'''
|
||||
|
||||
# TODO: rename "state" to "status" for all below
|
||||
@@ -18,6 +23,7 @@ These are matched by *.html files templates/*.html which are mako templates with
|
||||
# but it will turn into a dict containing both the answer and any associated message for the problem ID for the input element.
|
||||
|
||||
import re
|
||||
import shlex # for splitting quoted strings
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
@@ -27,9 +33,42 @@ from lxml import etree
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
#takes the xml tree as 'element', the student's previous answer as 'value', and the graded status as 'state'
|
||||
|
||||
def choicegroup(element, value, state, msg=""):
|
||||
def optioninput(element, value, status, msg=''):
|
||||
'''
|
||||
Select option input type.
|
||||
|
||||
Example:
|
||||
|
||||
<optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text>
|
||||
'''
|
||||
eid=element.get('id')
|
||||
options = element.get('options')
|
||||
if not options:
|
||||
raise Exception,"[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element)
|
||||
oset = shlex.shlex(options[1:-1])
|
||||
oset.quotes = "'"
|
||||
oset.whitespace = ","
|
||||
oset = [x[1:-1] for x in list(oset)]
|
||||
|
||||
# osetdict = dict([('option_%s_%s' % (eid,x),oset[x]) for x in range(len(oset)) ]) # make dict with IDs
|
||||
osetdict = dict([(oset[x],oset[x]) for x in range(len(oset)) ]) # make dict with key,value same
|
||||
if settings.DEBUG:
|
||||
print '[courseware.capa.inputtypes.optioninput] osetdict=',osetdict
|
||||
|
||||
context={'id':eid,
|
||||
'value':value,
|
||||
'state':status,
|
||||
'msg':msg,
|
||||
'options':osetdict,
|
||||
}
|
||||
|
||||
html=render_to_string("optioninput.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def choicegroup(element, value, status, msg=''):
|
||||
'''
|
||||
Radio button inputs: multiple choice or true/false
|
||||
|
||||
@@ -47,7 +86,7 @@ def choicegroup(element, value, state, msg=""):
|
||||
for choice in element:
|
||||
assert choice.tag =="choice", "only <choice> tags should be immediate children of a <choicegroup>"
|
||||
choices[choice.get("name")] = etree.tostring(choice[0]) # TODO: what if choice[0] has math tags in it?
|
||||
context={'id':eid, 'value':value, 'state':state, 'type':type, 'choices':choices}
|
||||
context={'id':eid, 'value':value, 'state':status, 'type':type, 'choices':choices}
|
||||
html=render_to_string("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
@@ -60,9 +99,9 @@ def textline(element, value, state, msg=""):
|
||||
return etree.XML(html)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# TODO: Make a wrapper for <formulainput>
|
||||
# TODO: Make an AJAX loop to confirm equation is okay in real-time as user types
|
||||
def jstextline(element, value, state, msg=""):
|
||||
|
||||
def js_textline(element, value, status, msg=''):
|
||||
## TODO: Code should follow PEP8 (4 spaces per indentation level)
|
||||
'''
|
||||
textline is used for simple one-line inputs, like formularesponse and symbolicresponse.
|
||||
'''
|
||||
@@ -72,7 +111,7 @@ def jstextline(element, value, state, msg=""):
|
||||
dojs = element.get('dojs') # dojs is used for client-side javascript display & return
|
||||
# when dojs=='math', a <span id=display_eid>`{::}`</span>
|
||||
# and a hidden textarea with id=input_eid_fromjs will be output
|
||||
context = {'id':eid, 'value':value, 'state':state, 'count':count, 'size': size,
|
||||
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size,
|
||||
'dojs':dojs,
|
||||
'msg':msg,
|
||||
}
|
||||
@@ -81,7 +120,7 @@ def jstextline(element, value, state, msg=""):
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
## TODO: Make a wrapper for <codeinput>
|
||||
def textbox(element, value, state, msg=''):
|
||||
def textbox(element, value, status, msg=''):
|
||||
'''
|
||||
The textbox is used for code input. The message is the return HTML string from
|
||||
evaluating the code, eg error messages, and output from the code tests.
|
||||
@@ -91,12 +130,12 @@ def textbox(element, value, state, msg=''):
|
||||
eid=element.get('id')
|
||||
count = int(eid.split('_')[-2])-1 # HACK
|
||||
size = element.get('size')
|
||||
context = {'id':eid, 'value':value, 'state':state, 'count':count, 'size': size, 'msg':msg}
|
||||
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg}
|
||||
html=render_to_string("textbox.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
def schematic(element, value, state):
|
||||
def schematic(element, value, status, msg=''):
|
||||
eid = element.get('id')
|
||||
height = element.get('height')
|
||||
width = element.get('width')
|
||||
@@ -120,7 +159,7 @@ def schematic(element, value, state):
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
### TODO: Move out of inputtypes
|
||||
def math(element, value, state, msg=''):
|
||||
def math(element, value, status, msg=''):
|
||||
'''
|
||||
This is not really an input type. It is a convention from Lon-CAPA, used for
|
||||
displaying a math equation.
|
||||
@@ -134,21 +173,27 @@ def math(element, value, state, msg=''):
|
||||
|
||||
TODO: use shorter tags (but this will require converting problem XML files!)
|
||||
'''
|
||||
mathstr = element.text[1:-1]
|
||||
if '\\displaystyle' in mathstr:
|
||||
isinline = False
|
||||
mathstr = mathstr.replace('\\displaystyle','')
|
||||
else:
|
||||
isinline = True
|
||||
mathstr = re.sub('\$(.*)\$','[mathjaxinline]\\1[/mathjaxinline]',element.text)
|
||||
mtag = 'mathjax'
|
||||
if not '\\displaystyle' in mathstr: mtag += 'inline'
|
||||
else: mathstr = mathstr.replace('\\displaystyle','')
|
||||
mathstr = mathstr.replace('mathjaxinline]','%s]'%mtag)
|
||||
|
||||
html=render_to_string("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail})
|
||||
#if '\\displaystyle' in mathstr:
|
||||
# isinline = False
|
||||
# mathstr = mathstr.replace('\\displaystyle','')
|
||||
#else:
|
||||
# isinline = True
|
||||
# html=render_to_string("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail})
|
||||
|
||||
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr,element.tail)
|
||||
xhtml = etree.XML(html)
|
||||
# xhtml.tail = element.tail # don't forget to include the tail!
|
||||
return xhtml
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def solution(element, value, state, msg=''):
|
||||
def solution(element, value, status, msg=''):
|
||||
'''
|
||||
This is not really an input type. It is just a <span>...</span> which is given an ID,
|
||||
that is used for displaying an extended answer (a problem "solution") after "show answers"
|
||||
@@ -159,7 +204,7 @@ def solution(element, value, state, msg=''):
|
||||
size = element.get('size')
|
||||
context = {'id':eid,
|
||||
'value':value,
|
||||
'state':state,
|
||||
'state':status,
|
||||
'size': size,
|
||||
'msg':msg,
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful So
|
||||
import calc
|
||||
import eia
|
||||
|
||||
from util import contextualize_text
|
||||
|
||||
def compare_with_tolerance(v1, v2, tol):
|
||||
''' Compare v1 to v2 with maximum tolerance tol
|
||||
tol is relative if it ends in %; otherwise, it is absolute
|
||||
@@ -61,6 +63,8 @@ class GenericResponse(object):
|
||||
|
||||
#Every response type needs methods "grade" and "get_answers"
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class MultipleChoiceResponse(GenericResponse):
|
||||
'''
|
||||
Example:
|
||||
@@ -84,6 +88,7 @@ class MultipleChoiceResponse(GenericResponse):
|
||||
self.correct_choices = [choice.get('name') for choice in self.correct_choices]
|
||||
self.context = context
|
||||
|
||||
self.answer_field = xml.find('choicegroup') # assumes only ONE choicegroup within this response
|
||||
self.answer_id = xml.xpath('//*[@id=$id]//choicegroup/@id',
|
||||
id=xml.get('id'))
|
||||
if not len(self.answer_id) == 1:
|
||||
@@ -100,9 +105,14 @@ class MultipleChoiceResponse(GenericResponse):
|
||||
return {self.answer_id:self.correct_choices}
|
||||
|
||||
def preprocess_response(self):
|
||||
'''
|
||||
Initialize name attributes in <choice> stanzas in the <choicegroup> in this response.
|
||||
'''
|
||||
i=0
|
||||
for response in self.xml.xpath("choicegroup"):
|
||||
response.set("type", "MultipleChoice")
|
||||
rtype = response.get('type')
|
||||
if rtype not in ["MultipleChoice"]:
|
||||
response.set("type", "MultipleChoice") # force choicegroup to be MultipleChoice if not valid
|
||||
for choice in list(response):
|
||||
if choice.get("name") == None:
|
||||
choice.set("name", "choice_"+str(i))
|
||||
@@ -131,6 +141,42 @@ class TrueFalseResponse(MultipleChoiceResponse):
|
||||
|
||||
return {self.answer_id : 'incorrect'}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class OptionResponse(GenericResponse):
|
||||
'''
|
||||
Example:
|
||||
|
||||
<optionresponse direction="vertical" randomize="yes">
|
||||
<optioninput options="('Up','Down')" correct="Up"><text>The location of the sky</text></optioninput>
|
||||
<optioninput options="('Up','Down')" correct="Down"><text>The location of the earth</text></optioninput>
|
||||
</optionresponse>
|
||||
|
||||
TODO: handle direction and randomize
|
||||
|
||||
'''
|
||||
def __init__(self, xml, context):
|
||||
self.xml = xml
|
||||
self.answer_fields = xml.findall('optioninput')
|
||||
if settings.DEBUG:
|
||||
print '[courseware.capa.responsetypes.OR.init] answer_fields=%s' % (self.answer_fields)
|
||||
self.context = context
|
||||
|
||||
def grade(self, student_answers):
|
||||
cmap = {}
|
||||
amap = self.get_answers()
|
||||
for aid in amap:
|
||||
if aid in student_answers and student_answers[aid]==amap[aid]:
|
||||
cmap[aid] = 'correct'
|
||||
else:
|
||||
cmap[aid] = 'incorrect'
|
||||
return cmap
|
||||
|
||||
def get_answers(self):
|
||||
amap = dict([(af.get('id'),af.get('correct')) for af in self.answer_fields])
|
||||
return amap
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class NumericalResponse(GenericResponse):
|
||||
def __init__(self, xml, context):
|
||||
@@ -219,43 +265,153 @@ def sympy_check2():
|
||||
self.answer_ids = xml.xpath('//*[@id=$id]//textline/@id',
|
||||
id=xml.get('id'))
|
||||
self.context = context
|
||||
answer_list = xml.xpath('//*[@id=$id]//answer',
|
||||
id=xml.get('id'))
|
||||
if len(answer_list):
|
||||
answer=answer_list[0]
|
||||
else:
|
||||
raise Exception("Invalid custom response -- no checker code")
|
||||
|
||||
answer_src = answer.get('src')
|
||||
if answer_src != None:
|
||||
self.code = open(settings.DATA_DIR+'src/'+answer_src).read()
|
||||
else:
|
||||
self.code = answer.text
|
||||
# if <customresponse> has an "expect" attribute then save that
|
||||
self.expect = xml.get('expect')
|
||||
self.myid = xml.get('id')
|
||||
|
||||
# the <answer>...</answer> stanza should be local to the current <customresponse>. So try looking there first.
|
||||
self.code = None
|
||||
answer = None
|
||||
try:
|
||||
answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0]
|
||||
except IndexError,err:
|
||||
# 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 <script>...</script> stanza instead
|
||||
cfn = xml.get('cfn')
|
||||
if cfn:
|
||||
if settings.DEBUG: print "[courseware.capa.responsetypes] cfn = ",cfn
|
||||
if cfn in context:
|
||||
self.code = context[cfn]
|
||||
else:
|
||||
print "can't find cfn in context = ",context
|
||||
|
||||
if not self.code:
|
||||
if not answer:
|
||||
# raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid
|
||||
print "[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid
|
||||
self.code = ''
|
||||
else:
|
||||
answer_src = answer.get('src')
|
||||
if answer_src != None:
|
||||
self.code = open(settings.DATA_DIR+'src/'+answer_src).read()
|
||||
else:
|
||||
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 "_").
|
||||
'''
|
||||
from capa_problem import global_context
|
||||
submission = [student_answers[k] for k in sorted(self.answer_ids)]
|
||||
self.context.update({'submission':submission})
|
||||
exec self.code in global_context, self.context
|
||||
return zip(sorted(self.answer_ids), self.context['correct'])
|
||||
|
||||
def getkey2(dict,key,default):
|
||||
"""utilify function: get dict[key] if key exists, or return default"""
|
||||
if dict.has_key(key):
|
||||
return dict[key]
|
||||
return default
|
||||
|
||||
idset = sorted(self.answer_ids) # ordered list of answer id's
|
||||
submission = [student_answers[k] for k in idset] # ordered list of answers
|
||||
fromjs = [ getkey2(student_answers,k+'_fromjs',None) for k in idset ] # ordered list of fromjs_XXX responses (if exists)
|
||||
|
||||
# if there is only one box, and it's empty, then don't evaluate
|
||||
if len(idset)==1 and not submission[0]:
|
||||
return {idset[0]:'no_answer_entered'}
|
||||
|
||||
gctxt = self.context['global_context']
|
||||
|
||||
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
|
||||
'fromjs':fromjs, # 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
|
||||
'testdat':'hello world',
|
||||
})
|
||||
|
||||
# exec the check function
|
||||
if type(self.code)==str:
|
||||
try:
|
||||
exec self.code in self.context['global_context'], self.context
|
||||
except Exception,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
|
||||
try:
|
||||
answer_given = submission[0] if (len(idset)==1) else submission
|
||||
if fn.func_code.co_argcount>=4: # does it want four arguments (the answers dict, myname)?
|
||||
ret = fn(self.expect,answer_given,student_answers,self.answer_ids[0])
|
||||
elif fn.func_code.co_argcount>=3: # does it want a third argument (the answers dict)?
|
||||
ret = fn(self.expect,answer_given,student_answers)
|
||||
else:
|
||||
ret = fn(self.expect,answer_given)
|
||||
except Exception,err:
|
||||
print "oops in customresponse (cfn) error %s" % err
|
||||
# print "context = ",self.context
|
||||
print traceback.format_exc()
|
||||
if settings.DEBUG: print "[courseware.capa.responsetypes.customresponse.grade] ret = ",ret
|
||||
if type(ret)==dict:
|
||||
correct[0] = 'correct' if ret['ok'] else 'incorrect'
|
||||
msg = ret['msg']
|
||||
|
||||
if 1:
|
||||
# try to clean up message html
|
||||
msg = '<html>'+msg+'</html>'
|
||||
msg = etree.tostring(fromstring_bs(msg),pretty_print=True)
|
||||
msg = msg.replace(' ','')
|
||||
#msg = re.sub('<html>(.*)</html>','\\1',msg,flags=re.M|re.DOTALL) # python 2.7
|
||||
msg = re.sub('(?ms)<html>(.*)</html>','\\1',msg)
|
||||
|
||||
messages[0] = msg
|
||||
else:
|
||||
correct[0] = 'correct' if ret else 'incorrect'
|
||||
|
||||
# build map giving "correct"ness of the answer(s)
|
||||
#correct_map = dict(zip(idset, self.context['correct']))
|
||||
correct_map = {}
|
||||
for k in range(len(idset)):
|
||||
correct_map[idset[k]] = correct[k]
|
||||
correct_map['msg_%s' % idset[k]] = messages[k]
|
||||
return correct_map
|
||||
|
||||
def get_answers(self):
|
||||
# Since this is explicitly specified in the problem, this will
|
||||
# be handled by capa_problem
|
||||
'''
|
||||
Give correct answer expected for this response.
|
||||
|
||||
capa_problem handles correct_answers from entry objects like textline, and that
|
||||
is what should be used when this response has multiple entry objects.
|
||||
|
||||
but for simplicity, if an "expect" attribute was given by the content author
|
||||
ie <customresponse expect="foo" ...> then return it now.
|
||||
'''
|
||||
if len(self.answer_ids)>1:
|
||||
return {}
|
||||
if self.expect:
|
||||
return {self.answer_ids[0] : self.expect}
|
||||
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',
|
||||
@@ -471,10 +627,6 @@ class ImageResponse(GenericResponse):
|
||||
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'
|
||||
|
||||
@@ -24,7 +24,9 @@ try: # This lets us do __name__ == ='__main__'
|
||||
from student.models import UserTestGroup
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from util.cache import cache
|
||||
from multicourse import multicourse_settings
|
||||
except:
|
||||
print "Could not import/content_parser"
|
||||
settings = None
|
||||
|
||||
''' This file will eventually form an abstraction layer between the
|
||||
@@ -181,7 +183,7 @@ def course_xml_process(tree):
|
||||
propogate_downward_tag(tree, "rerandomize")
|
||||
return tree
|
||||
|
||||
def course_file(user):
|
||||
def course_file(user,coursename=None):
|
||||
''' Given a user, return course.xml'''
|
||||
|
||||
if user.is_authenticated():
|
||||
@@ -189,6 +191,11 @@ def course_file(user):
|
||||
else:
|
||||
filename = 'guest_course.xml'
|
||||
|
||||
# if a specific course is specified, then use multicourse to get the right path to the course XML directory
|
||||
if coursename and settings.ENABLE_MULTICOURSE:
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename)
|
||||
filename = xp + filename # prefix the filename with the path
|
||||
|
||||
groups = user_groups(user)
|
||||
options = {'dev_content':settings.DEV_CONTENT,
|
||||
'groups' : groups}
|
||||
@@ -210,13 +217,24 @@ def course_file(user):
|
||||
|
||||
return tree
|
||||
|
||||
def section_file(user, section):
|
||||
''' Given a user and the name of a section, return that section
|
||||
def section_file(user, section, coursename=None, dironly=False):
|
||||
'''
|
||||
Given a user and the name of a section, return that section.
|
||||
This is done specific to each course.
|
||||
If dironly=True then return the sections directory.
|
||||
'''
|
||||
filename = section+".xml"
|
||||
|
||||
if filename not in os.listdir(settings.DATA_DIR + '/sections/'):
|
||||
print filename+" not in "+str(os.listdir(settings.DATA_DIR + '/sections/'))
|
||||
# if a specific course is specified, then use multicourse to get the right path to the course XML directory
|
||||
xp = ''
|
||||
if coursename and settings.ENABLE_MULTICOURSE: xp = multicourse_settings.get_course_xmlpath(coursename)
|
||||
|
||||
dirname = settings.DATA_DIR + xp + '/sections/'
|
||||
|
||||
if dironly: return dirname
|
||||
|
||||
if filename not in os.listdir(dirname):
|
||||
print filename+" not in "+str(os.listdir(dirname))
|
||||
return None
|
||||
|
||||
options = {'dev_content':settings.DEV_CONTENT,
|
||||
@@ -226,7 +244,7 @@ def section_file(user, section):
|
||||
return tree
|
||||
|
||||
|
||||
def module_xml(user, module, id_tag, module_id):
|
||||
def module_xml(user, module, id_tag, module_id, coursename=None):
|
||||
''' Get XML for a module based on module and module_id. Assumes
|
||||
module occurs once in courseware XML file or hidden section. '''
|
||||
# Sanitize input
|
||||
@@ -239,14 +257,15 @@ def module_xml(user, module, id_tag, module_id):
|
||||
id_tag=id_tag,
|
||||
id=module_id)
|
||||
#result_set=doc.xpathEval(xpath_search)
|
||||
doc = course_file(user)
|
||||
section_list = (s[:-4] for s in os.listdir(settings.DATA_DIR+'/sections') if s[-4:]=='.xml')
|
||||
doc = course_file(user,coursename)
|
||||
sdirname = section_file(user,'',coursename,True) # get directory where sections information is stored
|
||||
section_list = (s[:-4] for s in os.listdir(sdirname) if s[-4:]=='.xml')
|
||||
|
||||
result_set=doc.xpath(xpath_search)
|
||||
if len(result_set)<1:
|
||||
for section in section_list:
|
||||
try:
|
||||
s = section_file(user, section)
|
||||
s = section_file(user, section, coursename)
|
||||
except etree.XMLSyntaxError:
|
||||
ex= sys.exc_info()
|
||||
raise ContentException("Malformed XML in " + section+ "("+str(ex[1].msg)+")")
|
||||
|
||||
@@ -67,7 +67,7 @@ course_settings = Settings()
|
||||
|
||||
|
||||
|
||||
def grade_sheet(student):
|
||||
def grade_sheet(student,coursename=None):
|
||||
"""
|
||||
This pulls a summary of all problems in the course. It returns a dictionary with two datastructures:
|
||||
|
||||
@@ -77,7 +77,7 @@ def grade_sheet(student):
|
||||
|
||||
- grade_summary is the output from the course grader. More information on the format is in the docstring for CourseGrader.
|
||||
"""
|
||||
dom=content_parser.course_file(student)
|
||||
dom=content_parser.course_file(student,coursename)
|
||||
course = dom.xpath('//course/@name')[0]
|
||||
xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course)
|
||||
|
||||
@@ -103,7 +103,7 @@ def grade_sheet(student):
|
||||
scores=[]
|
||||
if len(problems)>0:
|
||||
for p in problems:
|
||||
(correct,total) = get_score(student, p, response_by_id)
|
||||
(correct,total) = get_score(student, p, response_by_id, coursename=coursename)
|
||||
|
||||
if settings.GENERATE_PROFILE_SCORES:
|
||||
if total > 1:
|
||||
@@ -167,7 +167,7 @@ def aggregate_scores(scores, section_name = "summary"):
|
||||
return all_total, graded_total
|
||||
|
||||
|
||||
def get_score(user, problem, cache):
|
||||
def get_score(user, problem, cache, coursename=None):
|
||||
## HACK: assumes max score is fixed per problem
|
||||
id = problem.get('id')
|
||||
correct = 0.0
|
||||
@@ -196,7 +196,7 @@ def get_score(user, problem, cache):
|
||||
## HACK 1: We shouldn't specifically reference capa_module
|
||||
## HACK 2: Backwards-compatibility: This should be written when a grade is saved, and removed from the system
|
||||
from module_render import I4xSystem
|
||||
system = I4xSystem(None, None, None)
|
||||
system = I4xSystem(None, None, None, coursename=coursename)
|
||||
total=float(courseware.modules.capa_module.Module(system, etree.tostring(problem), "id").max_score())
|
||||
response.max_grade = total
|
||||
response.save()
|
||||
|
||||
@@ -6,9 +6,9 @@ from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from courseware.content_parser import course_file
|
||||
import courseware.module_render
|
||||
import courseware.modules
|
||||
from mitx.courseware.content_parser import course_file
|
||||
import mitx.courseware.module_render
|
||||
import mitx.courseware.modules
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Does basic validity tests on course.xml."
|
||||
@@ -25,15 +25,15 @@ class Command(BaseCommand):
|
||||
check = False
|
||||
print "Confirming all modules render. Nothing should print during this step. "
|
||||
for module in course.xpath('//problem|//html|//video|//vertical|//sequential|/tab'):
|
||||
module_class = courseware.modules.modx_modules[module.tag]
|
||||
module_class=mitx.courseware.modules.modx_modules[module.tag]
|
||||
# TODO: Abstract this out in render_module.py
|
||||
try:
|
||||
module_class(etree.tostring(module),
|
||||
module.get('id'),
|
||||
ajax_url='',
|
||||
state=None,
|
||||
track_function = lambda x,y,z:None,
|
||||
render_function = lambda x: {'content':'','destroy_js':'','init_js':'','type':'video'})
|
||||
instance=module_class(etree.tostring(module),
|
||||
module.get('id'),
|
||||
ajax_url='',
|
||||
state=None,
|
||||
track_function = lambda x,y,z:None,
|
||||
render_function = lambda x: {'content':'','destroy_js':'','init_js':'','type':'video'})
|
||||
except:
|
||||
print "==============> Error in ", etree.tostring(module)
|
||||
check = False
|
||||
|
||||
@@ -22,6 +22,11 @@ import courseware.modules
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
class I4xSystem(object):
|
||||
'''
|
||||
This is an abstraction such that x_modules can function independent
|
||||
of the courseware (e.g. import into other types of courseware, LMS,
|
||||
or if we want to have a sandbox server for user-contributed content)
|
||||
'''
|
||||
def __init__(self, ajax_url, track_function, render_function, filestore=None):
|
||||
self.ajax_url = ajax_url
|
||||
self.track_function = track_function
|
||||
@@ -29,6 +34,10 @@ class I4xSystem(object):
|
||||
self.filestore = OSFS(settings.DATA_DIR)
|
||||
self.render_function = render_function
|
||||
self.exception404 = Http404
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
def __str__(self):
|
||||
return str(self.__dict__)
|
||||
|
||||
def object_cache(cache, user, module_type, module_id):
|
||||
# We don't look up on user -- all queries include user
|
||||
@@ -50,6 +59,7 @@ def make_track_function(request):
|
||||
def f(event_type, event):
|
||||
return track.views.server_track(request, event_type, event, page='x_module')
|
||||
return f
|
||||
|
||||
def grade_histogram(module_id):
|
||||
''' Print out a histogram of grades on a given problem.
|
||||
Part of staff member debug info.
|
||||
@@ -83,6 +93,10 @@ def render_x_module(user, request, xml_module, module_object_preload):
|
||||
else:
|
||||
state = smod.state
|
||||
|
||||
# get coursename if stored
|
||||
if 'coursename' in request.session: coursename = request.session['coursename']
|
||||
else: coursename = None
|
||||
|
||||
# Create a new instance
|
||||
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module_type+'/'+module_id+'/'
|
||||
system = I4xSystem(track_function = make_track_function(request),
|
||||
@@ -104,6 +118,7 @@ def render_x_module(user, request, xml_module, module_object_preload):
|
||||
state=instance.get_state())
|
||||
smod.save()
|
||||
module_object_preload.append(smod)
|
||||
|
||||
# Grab content
|
||||
content = instance.get_html()
|
||||
init_js = instance.get_init_js()
|
||||
|
||||
@@ -21,6 +21,7 @@ from mitxmako.shortcuts import render_to_string
|
||||
from x_module import XModule
|
||||
from courseware.capa.capa_problem import LoncapaProblem, StudentInputError
|
||||
import courseware.content_parser as content_parser
|
||||
from multicourse import multicourse_settings
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
@@ -115,18 +116,19 @@ class Module(XModule):
|
||||
if len(explain) == 0:
|
||||
explain = False
|
||||
|
||||
html=render_to_string('problem.html',
|
||||
{'problem' : content,
|
||||
'id' : self.item_id,
|
||||
'check_button' : check_button,
|
||||
'reset_button' : reset_button,
|
||||
'save_button' : save_button,
|
||||
'answer_available' : self.answer_available(),
|
||||
'ajax_url' : self.ajax_url,
|
||||
'attempts_used': self.attempts,
|
||||
'attempts_allowed': self.max_attempts,
|
||||
'explain': explain
|
||||
})
|
||||
context = {'problem' : content,
|
||||
'id' : self.item_id,
|
||||
'check_button' : check_button,
|
||||
'reset_button' : reset_button,
|
||||
'save_button' : save_button,
|
||||
'answer_available' : self.answer_available(),
|
||||
'ajax_url' : self.ajax_url,
|
||||
'attempts_used': self.attempts,
|
||||
'attempts_allowed': self.max_attempts,
|
||||
'explain': explain,
|
||||
}
|
||||
|
||||
html=render_to_string('problem.html', context)
|
||||
if encapsulate:
|
||||
html = '<div id="main_{id}">'.format(id=self.item_id)+html+"</div>"
|
||||
|
||||
@@ -193,7 +195,12 @@ class Module(XModule):
|
||||
seed = 1
|
||||
else:
|
||||
seed = None
|
||||
self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, state, seed = seed)
|
||||
try:
|
||||
fp = self.filestore.open(self.filename)
|
||||
except Exception,err:
|
||||
print '[courseware.capa.capa_module.Module.init] error %s: cannot open file %s' % (err,self.filename)
|
||||
raise Exception,err
|
||||
self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed)
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
@@ -306,7 +313,7 @@ class Module(XModule):
|
||||
except:
|
||||
self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state)
|
||||
traceback.print_exc()
|
||||
raise
|
||||
raise Exception,"error in capa_module"
|
||||
return json.dumps({'success':'Unknown Error'})
|
||||
|
||||
self.attempts = self.attempts + 1
|
||||
|
||||
63
djangoapps/courseware/test_files/optionresponse.xml
Normal file
63
djangoapps/courseware/test_files/optionresponse.xml
Normal file
@@ -0,0 +1,63 @@
|
||||
<problem>
|
||||
<text>
|
||||
<p>
|
||||
Why do bicycles benefit from having larger wheels when going up a bump as shown in the picture? <br/>
|
||||
Assume that for both bicycles:<br/>
|
||||
1.) The tires have equal air pressure.<br/>
|
||||
2.) The bicycles never leave the contact with the bump.<br/>
|
||||
3.) The bicycles have the same mass. The bicycle tires (regardless of size) have the same mass.<br/>
|
||||
</p>
|
||||
</text>
|
||||
<optionresponse texlayout="horizontal" max="10" randomize="yes">
|
||||
<ul>
|
||||
<li>
|
||||
<text>
|
||||
<p>The bicycles with larger wheels have more time to go over the bump. This decreases the magnitude of the force needed to lift the bicycle.</p>
|
||||
</text>
|
||||
<optioninput name="Foil1" location="random" options="('True','False')" correct="True">
|
||||
</optioninput>
|
||||
</li>
|
||||
<li>
|
||||
<text>
|
||||
<p>The bicycles with larger wheels always have a smaller vertical displacement regardless of speed.</p>
|
||||
</text>
|
||||
<optioninput name="Foil2" location="random" options="('True','False')" correct="False">
|
||||
</optioninput>
|
||||
</li>
|
||||
<li>
|
||||
<text>
|
||||
<p>The bicycles with larger wheels experience a force backward with less magnitude for the same amount of time.</p>
|
||||
</text>
|
||||
<optioninput name="Foil3" location="random" options="('True','False')" correct="False">
|
||||
</optioninput>
|
||||
</li>
|
||||
<li>
|
||||
<text>
|
||||
<p>The bicycles with larger wheels experience a force backward with less magnitude for a greater amount of time.</p>
|
||||
</text>
|
||||
<optioninput name="Foil4" location="random" options="('True','False')" correct="True">
|
||||
</optioninput>
|
||||
</li>
|
||||
<li>
|
||||
<text>
|
||||
<p>The bicycles with larger wheels have more kinetic energy turned into gravitational potential energy.</p>
|
||||
</text>
|
||||
<optioninput name="Foil5" location="random" options="('True','False')" correct="False">
|
||||
</optioninput>
|
||||
</li>
|
||||
<li>
|
||||
<text>
|
||||
<p>The bicycles with larger wheels have more rotational kinetic energy, so the horizontal velocity of the biker changes less.</p>
|
||||
</text>
|
||||
<optioninput name="Foil6" location="random" options="('True','False')" correct="False">
|
||||
</optioninput>
|
||||
</li>
|
||||
</ul>
|
||||
<hintgroup showoncorrect="no">
|
||||
<text>
|
||||
<br/>
|
||||
<br/>
|
||||
</text>
|
||||
</hintgroup>
|
||||
</optionresponse>
|
||||
</problem>
|
||||
@@ -63,6 +63,9 @@ class ModelsTest(unittest.TestCase):
|
||||
exception_happened = True
|
||||
self.assertTrue(exception_happened)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# tests of capa_problem inputtypes
|
||||
|
||||
class MultiChoiceTest(unittest.TestCase):
|
||||
def test_MC_grade(self):
|
||||
multichoice_file = os.path.dirname(__file__)+"/test_files/multichoice.xml"
|
||||
@@ -93,6 +96,38 @@ class MultiChoiceTest(unittest.TestCase):
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
|
||||
false_answers = {'1_2_1':['choice_foil1', 'choice_foil2', 'choice_foil3']}
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
|
||||
|
||||
class ImageResponseTest(unittest.TestCase):
|
||||
def test_ir_grade(self):
|
||||
imageresponse_file = os.path.dirname(__file__)+"/test_files/imageresponse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(imageresponse_file), '1')
|
||||
correct_answers = {'1_2_1':'(490,11)-(556,98)',
|
||||
'1_2_2':'(242,202)-(296,276)'}
|
||||
test_answers = {'1_2_1':'[500,20]',
|
||||
'1_2_2':'[250,300]',
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_1'], 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_2'], 'incorrect')
|
||||
|
||||
class OptionResponseTest(unittest.TestCase):
|
||||
'''
|
||||
Run this with
|
||||
|
||||
python manage.py test courseware.OptionResponseTest
|
||||
'''
|
||||
def test_or_grade(self):
|
||||
optionresponse_file = os.path.dirname(__file__)+"/test_files/optionresponse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(optionresponse_file), '1')
|
||||
correct_answers = {'1_2_1':'True',
|
||||
'1_2_2':'False'}
|
||||
test_answers = {'1_2_1':'True',
|
||||
'1_2_2':'True',
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_1'], 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_2'], 'incorrect')
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Grading tests
|
||||
|
||||
class GradesheetTest(unittest.TestCase):
|
||||
|
||||
@@ -118,7 +153,7 @@ class GradesheetTest(unittest.TestCase):
|
||||
all, graded = aggregate_scores(scores)
|
||||
self.assertAlmostEqual(all, Score(earned=5, possible=15, graded=False, section="summary"))
|
||||
self.assertAlmostEqual(graded, Score(earned=5, possible=10, graded=True, section="summary"))
|
||||
|
||||
|
||||
class GraderTest(unittest.TestCase):
|
||||
|
||||
empty_gradesheet = {
|
||||
|
||||
@@ -16,6 +16,7 @@ from lxml import etree
|
||||
from module_render import render_module, make_track_function, I4xSystem
|
||||
from models import StudentModule
|
||||
from student.models import UserProfile
|
||||
from multicourse import multicourse_settings
|
||||
|
||||
import courseware.content_parser as content_parser
|
||||
import courseware.modules
|
||||
@@ -33,11 +34,16 @@ template_imports={'urllib':urllib}
|
||||
def gradebook(request):
|
||||
if 'course_admin' not in content_parser.user_groups(request.user):
|
||||
raise Http404
|
||||
|
||||
# TODO: This should be abstracted out. We repeat this logic many times.
|
||||
if 'coursename' in request.session: coursename = request.session['coursename']
|
||||
else: coursename = None
|
||||
|
||||
student_objects = User.objects.all()[:100]
|
||||
student_info = [{'username' :s.username,
|
||||
'id' : s.id,
|
||||
'email': s.email,
|
||||
'grade_info' : grades.grade_sheet(s),
|
||||
'grade_info' : grades.grade_sheet(s,coursename),
|
||||
'realname' : UserProfile.objects.get(user = s).name
|
||||
} for s in student_objects]
|
||||
|
||||
@@ -59,6 +65,9 @@ def profile(request, student_id = None):
|
||||
|
||||
user_info = UserProfile.objects.get(user=student) # request.user.profile_cache #
|
||||
|
||||
if 'coursename' in request.session: coursename = request.session['coursename']
|
||||
else: coursename = None
|
||||
|
||||
context={'name':user_info.name,
|
||||
'username':student.username,
|
||||
'location':user_info.location,
|
||||
@@ -67,7 +76,7 @@ def profile(request, student_id = None):
|
||||
'format_url_params' : content_parser.format_url_params,
|
||||
'csrf':csrf(request)['csrf_token']
|
||||
}
|
||||
context.update(grades.grade_sheet(student))
|
||||
context.update(grades.grade_sheet(student,coursename))
|
||||
|
||||
return render_to_response('profile.html', context)
|
||||
|
||||
@@ -77,7 +86,7 @@ def render_accordion(request,course,chapter,section):
|
||||
if not course:
|
||||
course = "6.002 Spring 2012"
|
||||
|
||||
toc=content_parser.toc_from_xml(content_parser.course_file(request.user), chapter, section)
|
||||
toc=content_parser.toc_from_xml(content_parser.course_file(request.user,course), chapter, section)
|
||||
active_chapter=1
|
||||
for i in range(len(toc)):
|
||||
if toc[i]['active']:
|
||||
@@ -98,8 +107,11 @@ def render_section(request, section):
|
||||
if not settings.COURSEWARE_ENABLED:
|
||||
return redirect('/')
|
||||
|
||||
if 'coursename' in request.session: coursename = request.session['coursename']
|
||||
else: coursename = None
|
||||
|
||||
# try:
|
||||
dom = content_parser.section_file(user, section)
|
||||
dom = content_parser.section_file(user, section, coursename)
|
||||
#except:
|
||||
# raise Http404
|
||||
|
||||
@@ -128,13 +140,21 @@ def render_section(request, section):
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def index(request, course="6.002 Spring 2012", chapter="Using the System", section="Hints"):
|
||||
def index(request, course=None, chapter="Using the System", section="Hints"):
|
||||
''' Displays courseware accordion, and any associated content.
|
||||
'''
|
||||
user = request.user
|
||||
if not settings.COURSEWARE_ENABLED:
|
||||
return redirect('/')
|
||||
|
||||
if course==None:
|
||||
if not settings.ENABLE_MULTICOURSE:
|
||||
course = "6.002 Spring 2012"
|
||||
elif 'coursename' in request.session:
|
||||
course = request.session['coursename']
|
||||
else:
|
||||
course = settings.COURSE_DEFAULT
|
||||
|
||||
# Fixes URLs -- we don't get funny encoding characters from spaces
|
||||
# so they remain readable
|
||||
## TODO: Properly replace underscores
|
||||
@@ -142,16 +162,18 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti
|
||||
chapter=chapter.replace("_"," ")
|
||||
section=section.replace("_"," ")
|
||||
|
||||
# HACK: Force course to 6.002 for now
|
||||
# Without this, URLs break
|
||||
if course!="6.002 Spring 2012":
|
||||
# use multicourse module to determine if "course" is valid
|
||||
#if course!=settings.COURSE_NAME.replace('_',' '):
|
||||
if not multicourse_settings.is_valid_course(course):
|
||||
return redirect('/')
|
||||
|
||||
#import logging
|
||||
#log = logging.getLogger("mitx")
|
||||
#log.info( "DEBUG: "+str(user) )
|
||||
|
||||
dom = content_parser.course_file(user)
|
||||
request.session['coursename'] = course # keep track of current course being viewed in django's request.session
|
||||
|
||||
dom = content_parser.course_file(user,course) # also pass course to it, for course-specific XML path
|
||||
dom_module = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]/*[1]",
|
||||
course=course, chapter=chapter, section=section)
|
||||
if len(dom_module) == 0:
|
||||
@@ -179,6 +201,7 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti
|
||||
context={'init':module['init_js'],
|
||||
'accordion':accordion,
|
||||
'content':module['content'],
|
||||
'COURSE_TITLE':multicourse_settings.get_course_title(course),
|
||||
'csrf':csrf(request)['csrf_token']}
|
||||
|
||||
result = render_to_response('courseware.html', context)
|
||||
@@ -206,8 +229,12 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
|
||||
|
||||
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module+'/'+id+'/'
|
||||
|
||||
# get coursename if stored
|
||||
if 'coursename' in request.session: coursename = request.session['coursename']
|
||||
else: coursename = None
|
||||
|
||||
# Grab the XML corresponding to the request from course.xml
|
||||
xml = content_parser.module_xml(request.user, module, 'id', id)
|
||||
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
|
||||
|
||||
# Create the module
|
||||
system = I4xSystem(track_function = make_track_function(request),
|
||||
@@ -229,3 +256,98 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
|
||||
s.save()
|
||||
# Return whatever the module wanted to return to the client/caller
|
||||
return HttpResponse(ajax_return)
|
||||
|
||||
def quickedit(request, id=None):
|
||||
'''
|
||||
quick-edit capa problem.
|
||||
|
||||
Maybe this should be moved into capa/views.py
|
||||
Or this should take a "module" argument, and the quickedit moved into capa_module.
|
||||
'''
|
||||
print "WARNING: UNDEPLOYABLE CODE. FOR DEV USE ONLY."
|
||||
print "In deployed use, this will only edit on one server"
|
||||
print "We need a setting to disable for production where there is"
|
||||
print "a load balanacer"
|
||||
if not request.user.is_staff():
|
||||
return redirect('/')
|
||||
|
||||
# get coursename if stored
|
||||
if 'coursename' in request.session: coursename = request.session['coursename']
|
||||
else: coursename = None
|
||||
|
||||
def get_lcp(coursename,id):
|
||||
# Grab the XML corresponding to the request from course.xml
|
||||
module = 'problem'
|
||||
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
|
||||
|
||||
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module+'/'+id+'/'
|
||||
|
||||
# Create the module (instance of capa_module.Module)
|
||||
system = I4xSystem(track_function = make_track_function(request),
|
||||
render_function = None,
|
||||
ajax_url = ajax_url,
|
||||
filestore = None,
|
||||
coursename = coursename,
|
||||
role = 'staff' if request.user.is_staff else 'student', # TODO: generalize this
|
||||
)
|
||||
instance=courseware.modules.get_module_class(module)(system,
|
||||
xml,
|
||||
id,
|
||||
state=None)
|
||||
lcp = instance.lcp
|
||||
pxml = lcp.tree
|
||||
pxmls = etree.tostring(pxml,pretty_print=True)
|
||||
|
||||
return instance, pxmls
|
||||
|
||||
instance, pxmls = get_lcp(coursename,id)
|
||||
|
||||
# if there was a POST, then process it
|
||||
msg = ''
|
||||
if 'qesubmit' in request.POST:
|
||||
action = request.POST['qesubmit']
|
||||
if "Revert" in action:
|
||||
msg = "Reverted to original"
|
||||
elif action=='Change Problem':
|
||||
key = 'quickedit_%s' % id
|
||||
if not key in request.POST:
|
||||
msg = "oops, missing code key=%s" % key
|
||||
else:
|
||||
newcode = request.POST[key]
|
||||
|
||||
# see if code changed
|
||||
if str(newcode)==str(pxmls) or '<?xml version="1.0"?>\n'+str(newcode)==str(pxmls):
|
||||
msg = "No changes"
|
||||
else:
|
||||
# check new code
|
||||
isok = False
|
||||
try:
|
||||
newxml = etree.fromstring(newcode)
|
||||
isok = True
|
||||
except Exception,err:
|
||||
msg = "Failed to change problem: XML error \"<font color=red>%s</font>\"" % err
|
||||
|
||||
if isok:
|
||||
filename = instance.lcp.fileobject.name
|
||||
fp = open(filename,'w') # TODO - replace with filestore call?
|
||||
fp.write(newcode)
|
||||
fp.close()
|
||||
msg = "<font color=green>Problem changed!</font> (<tt>%s</tt>)" % filename
|
||||
instance, pxmls = get_lcp(coursename,id)
|
||||
|
||||
lcp = instance.lcp
|
||||
|
||||
# get the rendered problem HTML
|
||||
phtml = instance.get_problem_html()
|
||||
|
||||
context = {'id':id,
|
||||
'msg' : msg,
|
||||
'lcp' : lcp,
|
||||
'filename' : lcp.fileobject.name,
|
||||
'pxmls' : pxmls,
|
||||
'phtml' : phtml,
|
||||
'init_js':instance.get_init_js(),
|
||||
}
|
||||
|
||||
result = render_to_response('quickedit.html', context)
|
||||
return result
|
||||
|
||||
0
djangoapps/multicourse/__init__.py
Normal file
0
djangoapps/multicourse/__init__.py
Normal file
73
djangoapps/multicourse/multicourse_settings.py
Normal file
73
djangoapps/multicourse/multicourse_settings.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# multicourse/multicourse_settings.py
|
||||
#
|
||||
# central module for providing fixed settings (course name, number, title)
|
||||
# for multiple courses. Loads this information from django.conf.settings
|
||||
#
|
||||
# Allows backward compatibility with settings configurations without
|
||||
# multiple courses specified.
|
||||
#
|
||||
# The central piece of configuration data is the dict COURSE_SETTINGS, with
|
||||
# keys being the COURSE_NAME (spaces ok), and the value being a dict of
|
||||
# parameter,value pairs. The required parameters are:
|
||||
#
|
||||
# - number : course number (used in the simplewiki pages)
|
||||
# - title : humanized descriptive course title
|
||||
#
|
||||
# Optional parameters:
|
||||
#
|
||||
# - xmlpath : path (relative to data directory) for this course (defaults to "")
|
||||
#
|
||||
# If COURSE_SETTINGS does not exist, then fallback to 6.002_Spring_2012 default,
|
||||
# for now.
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# load course settings
|
||||
|
||||
if hasattr(settings,'COURSE_SETTINGS'): # in the future, this could be replaced by reading an XML file
|
||||
COURSE_SETTINGS = settings.COURSE_SETTINGS
|
||||
|
||||
elif hasattr(settings,'COURSE_NAME'): # backward compatibility
|
||||
COURSE_SETTINGS = {settings.COURSE_NAME: {'number': settings.COURSE_NUMBER,
|
||||
'title': settings.COURSE_TITLE,
|
||||
},
|
||||
}
|
||||
else: # default to 6.002_Spring_2012
|
||||
COURSE_SETTINGS = {'6.002_Spring_2012': {'number': '6.002x',
|
||||
'title': 'Circuits and Electronics',
|
||||
},
|
||||
}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# wrapper functions around course settings
|
||||
|
||||
def get_course_settings(coursename):
|
||||
if not coursename:
|
||||
if hasattr(settings,'COURSE_DEFAULT'):
|
||||
coursename = settings.COURSE_DEFAULT
|
||||
else:
|
||||
coursename = '6.002_Spring_2012'
|
||||
if coursename in COURSE_SETTINGS: return COURSE_SETTINGS[coursename]
|
||||
coursename = coursename.replace(' ','_')
|
||||
if coursename in COURSE_SETTINGS: return COURSE_SETTINGS[coursename]
|
||||
return None
|
||||
|
||||
def is_valid_course(coursename):
|
||||
return not (get_course_settings==None)
|
||||
|
||||
def get_course_property(coursename,property):
|
||||
cs = get_course_settings(coursename)
|
||||
if not cs: return '' # raise exception instead?
|
||||
if property in cs: return cs[property]
|
||||
return '' # default
|
||||
|
||||
def get_course_xmlpath(coursename):
|
||||
return get_course_property(coursename,'xmlpath')
|
||||
|
||||
def get_course_title(coursename):
|
||||
return get_course_property(coursename,'title')
|
||||
|
||||
def get_course_number(coursename):
|
||||
return get_course_property(coursename,'number')
|
||||
|
||||
1
djangoapps/multicourse/views.py
Normal file
1
djangoapps/multicourse/views.py
Normal file
@@ -0,0 +1 @@
|
||||
# multicourse/views.py
|
||||
@@ -9,6 +9,8 @@ from django.utils import simplejson
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from multicourse import multicourse_settings
|
||||
|
||||
from models import Revision, Article, CreateArticleForm, RevisionFormWithTitle, RevisionForm
|
||||
import wiki_settings
|
||||
|
||||
@@ -17,6 +19,11 @@ def view(request, wiki_url):
|
||||
if err:
|
||||
return err
|
||||
|
||||
if 'coursename' in request.session: coursename = request.session['coursename']
|
||||
else: coursename = None
|
||||
|
||||
course_number = multicourse_settings.get_course_number(coursename)
|
||||
|
||||
perm_err = check_permissions(request, article, check_read=True, check_deleted=True)
|
||||
if perm_err:
|
||||
return perm_err
|
||||
@@ -25,7 +32,7 @@ def view(request, wiki_url):
|
||||
'wiki_write': article.can_write_l(request.user),
|
||||
'wiki_attachments_write': article.can_attach(request.user),
|
||||
'wiki_current_revision_deleted' : not (article.current_revision.deleted == 0),
|
||||
'wiki_title' : article.title + " - MITX 6.002x Wiki"
|
||||
'wiki_title' : article.title + " - MITX %s Wiki" % course_number
|
||||
}
|
||||
d.update(csrf(request))
|
||||
return render_to_response('simplewiki_view.html', d)
|
||||
|
||||
0
djangoapps/ssl_auth/__init__.py
Normal file
0
djangoapps/ssl_auth/__init__.py
Normal file
281
djangoapps/ssl_auth/ssl_auth.py
Executable file
281
djangoapps/ssl_auth/ssl_auth.py
Executable file
@@ -0,0 +1,281 @@
|
||||
"""
|
||||
User authentication backend for ssl (no pw required)
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import User, check_password
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth.middleware import RemoteUserMiddleware
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
import os, string, re
|
||||
from random import choice
|
||||
|
||||
from student.models import UserProfile
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def ssl_dn_extract_info(dn):
|
||||
'''
|
||||
Extract username, email address (may be anyuser@anydomain.com) and full name
|
||||
from the SSL DN string. Return (user,email,fullname) if successful, and None
|
||||
otherwise.
|
||||
'''
|
||||
ss = re.search('/emailAddress=(.*)@([^/]+)',dn)
|
||||
if ss:
|
||||
user = ss.group(1)
|
||||
email = "%s@%s" % (user,ss.group(2))
|
||||
else:
|
||||
return None
|
||||
ss = re.search('/CN=([^/]+)/',dn)
|
||||
if ss:
|
||||
fullname = ss.group(1)
|
||||
else:
|
||||
return None
|
||||
return (user,email,fullname)
|
||||
|
||||
def check_nginx_proxy(request):
|
||||
'''
|
||||
Check for keys in the HTTP header (META) to se if we are behind an ngix reverse proxy.
|
||||
If so, get user info from the SSL DN string and return that, as (user,email,fullname)
|
||||
'''
|
||||
m = request.META
|
||||
if m.has_key('HTTP_X_REAL_IP'): # we're behind a nginx reverse proxy, which has already done ssl auth
|
||||
if not m.has_key('HTTP_SSL_CLIENT_S_DN'):
|
||||
return None
|
||||
dn = m['HTTP_SSL_CLIENT_S_DN']
|
||||
return ssl_dn_extract_info(dn)
|
||||
return None
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def get_ssl_username(request):
|
||||
x = check_nginx_proxy(request)
|
||||
if x:
|
||||
return x[0]
|
||||
env = request._req.subprocess_env
|
||||
if env.has_key('SSL_CLIENT_S_DN_Email'):
|
||||
email = env['SSL_CLIENT_S_DN_Email']
|
||||
user = email[:email.index('@')]
|
||||
return user
|
||||
return None
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class NginxProxyHeaderMiddleware(RemoteUserMiddleware):
|
||||
'''
|
||||
Django "middleware" function for extracting user information from HTTP request.
|
||||
|
||||
'''
|
||||
# this field is generated by nginx's reverse proxy
|
||||
header = 'HTTP_SSL_CLIENT_S_DN' # specify the request.META field to use
|
||||
|
||||
def process_request(self, request):
|
||||
# AuthenticationMiddleware is required so that request.user exists.
|
||||
if not hasattr(request, 'user'):
|
||||
raise ImproperlyConfigured(
|
||||
"The Django remote user auth middleware requires the"
|
||||
" authentication middleware to be installed. Edit your"
|
||||
" MIDDLEWARE_CLASSES setting to insert"
|
||||
" 'django.contrib.auth.middleware.AuthenticationMiddleware'"
|
||||
" before the RemoteUserMiddleware class.")
|
||||
|
||||
#raise ImproperlyConfigured('[ProxyHeaderMiddleware] request.META=%s' % repr(request.META))
|
||||
|
||||
try:
|
||||
username = request.META[self.header] # try the nginx META key first
|
||||
except KeyError:
|
||||
try:
|
||||
env = request._req.subprocess_env # else try the direct apache2 SSL key
|
||||
if env.has_key('SSL_CLIENT_S_DN'):
|
||||
username = env['SSL_CLIENT_S_DN']
|
||||
else:
|
||||
raise ImproperlyConfigured('no ssl key, env=%s' % repr(env))
|
||||
username = ''
|
||||
except:
|
||||
# If specified header doesn't exist then return (leaving
|
||||
# request.user set to AnonymousUser by the
|
||||
# AuthenticationMiddleware).
|
||||
return
|
||||
# If the user is already authenticated and that user is the user we are
|
||||
# getting passed in the headers, then the correct user is already
|
||||
# persisted in the session and we don't need to continue.
|
||||
|
||||
#raise ImproperlyConfigured('[ProxyHeaderMiddleware] username=%s' % username)
|
||||
|
||||
if request.user.is_authenticated():
|
||||
if request.user.username == self.clean_username(username, request):
|
||||
#raise ImproperlyConfigured('%s already authenticated (%s)' % (username,request.user.username))
|
||||
return
|
||||
# We are seeing this user for the first time in this session, attempt
|
||||
# to authenticate the user.
|
||||
#raise ImproperlyConfigured('calling auth.authenticate, remote_user=%s' % username)
|
||||
user = auth.authenticate(remote_user=username)
|
||||
if user:
|
||||
# User is valid. Set request.user and persist user in the session
|
||||
# by logging the user in.
|
||||
request.user = user
|
||||
if settings.DEBUG: print "[ssl_auth.ssl_auth.NginxProxyHeaderMiddleware] logging in user=%s" % user
|
||||
auth.login(request, user)
|
||||
|
||||
def clean_username(self,username,request):
|
||||
'''
|
||||
username is the SSL DN string - extract the actual username from it and return
|
||||
'''
|
||||
info = ssl_dn_extract_info(username)
|
||||
if not info:
|
||||
return None
|
||||
(username,email,fullname) = info
|
||||
return username
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class SSLLoginBackend(ModelBackend):
|
||||
'''
|
||||
Django authentication back-end which auto-logs-in a user based on having
|
||||
already authenticated with an MIT certificate (SSL).
|
||||
'''
|
||||
def authenticate(self, username=None, password=None, remote_user=None):
|
||||
|
||||
# remote_user is from the SSL_DN string. It will be non-empty only when
|
||||
# the user has already passed the server authentication, which means
|
||||
# matching with the certificate authority.
|
||||
if not remote_user:
|
||||
# no remote_user, so check username (but don't auto-create user)
|
||||
if not username:
|
||||
return None
|
||||
return None # pass on to another authenticator backend
|
||||
#raise ImproperlyConfigured("in SSLLoginBackend, username=%s, remote_user=%s" % (username,remote_user))
|
||||
try:
|
||||
user = User.objects.get(username=username) # if user already exists don't create it
|
||||
return user
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
return None
|
||||
|
||||
#raise ImproperlyConfigured("in SSLLoginBackend, username=%s, remote_user=%s" % (username,remote_user))
|
||||
#if not os.environ.has_key('HTTPS'):
|
||||
# return None
|
||||
#if not os.environ.get('HTTPS')=='on': # only use this back-end if HTTPS on
|
||||
# return None
|
||||
|
||||
def GenPasswd(length=8, chars=string.letters + string.digits):
|
||||
return ''.join([choice(chars) for i in range(length)])
|
||||
|
||||
# convert remote_user to user, email, fullname
|
||||
info = ssl_dn_extract_info(remote_user)
|
||||
#raise ImproperlyConfigured("[SSLLoginBackend] looking up %s" % repr(info))
|
||||
if not info:
|
||||
#raise ImproperlyConfigured("[SSLLoginBackend] remote_user=%s, info=%s" % (remote_user,info))
|
||||
return None
|
||||
(username,email,fullname) = info
|
||||
|
||||
try:
|
||||
user = User.objects.get(username=username) # if user already exists don't create it
|
||||
except User.DoesNotExist:
|
||||
raise "User does not exist. Not creating user; potential schema consistency issues"
|
||||
#raise ImproperlyConfigured("[SSLLoginBackend] creating %s" % repr(info))
|
||||
user = User(username=username, password=GenPasswd()) # create new User
|
||||
user.is_staff = False
|
||||
user.is_superuser = False
|
||||
# get first, last name from fullname
|
||||
name = fullname
|
||||
if not name.count(' '):
|
||||
user.first_name = " "
|
||||
user.last_name = name
|
||||
mn = ''
|
||||
else:
|
||||
user.first_name = name[:name.find(' ')]
|
||||
ml = name[name.find(' '):].strip()
|
||||
if ml.count(' '):
|
||||
user.last_name = ml[ml.rfind(' '):]
|
||||
mn = ml[:ml.rfind(' ')]
|
||||
else:
|
||||
user.last_name = ml
|
||||
mn = ''
|
||||
# set email
|
||||
user.email = email
|
||||
# cleanup last name
|
||||
user.last_name = user.last_name.strip()
|
||||
# save
|
||||
user.save()
|
||||
|
||||
# auto-create user profile
|
||||
up = UserProfile(user=user)
|
||||
up.name = fullname
|
||||
up.save()
|
||||
|
||||
#tui = user.get_profile()
|
||||
#tui.middle_name = mn
|
||||
#tui.role = 'Misc'
|
||||
#tui.section = None # no section assigned at first
|
||||
#tui.save()
|
||||
# return None
|
||||
return user
|
||||
|
||||
def get_user(self, user_id):
|
||||
#if not os.environ.has_key('HTTPS'):
|
||||
# return None
|
||||
#if not os.environ.get('HTTPS')=='on': # only use this back-end if HTTPS on
|
||||
# return None
|
||||
try:
|
||||
return User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# OLD!
|
||||
|
||||
class AutoLoginBackend:
|
||||
def authenticate(self, username=None, password=None):
|
||||
raise ImproperlyConfigured("in AutoLoginBackend, username=%s" % username)
|
||||
if not os.environ.has_key('HTTPS'):
|
||||
return None
|
||||
if not os.environ.get('HTTPS')=='on':# only use this back-end if HTTPS on
|
||||
return None
|
||||
|
||||
def GenPasswd(length=8, chars=string.letters + string.digits):
|
||||
return ''.join([choice(chars) for i in range(length)])
|
||||
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
user = User(username=username, password=GenPasswd())
|
||||
user.is_staff = False
|
||||
user.is_superuser = False
|
||||
# get first, last name
|
||||
name = os.environ.get('SSL_CLIENT_S_DN_CN').strip()
|
||||
if not name.count(' '):
|
||||
user.first_name = " "
|
||||
user.last_name = name
|
||||
mn = ''
|
||||
else:
|
||||
user.first_name = name[:name.find(' ')]
|
||||
ml = name[name.find(' '):].strip()
|
||||
if ml.count(' '):
|
||||
user.last_name = ml[ml.rfind(' '):]
|
||||
mn = ml[:ml.rfind(' ')]
|
||||
else:
|
||||
user.last_name = ml
|
||||
mn = ''
|
||||
# get email
|
||||
user.email = os.environ.get('SSL_CLIENT_S_DN_Email')
|
||||
# save
|
||||
user.save()
|
||||
tui = user.get_profile()
|
||||
tui.middle_name = mn
|
||||
tui.role = 'Misc'
|
||||
tui.section = None# no section assigned at first
|
||||
tui.save()
|
||||
# return None
|
||||
return user
|
||||
|
||||
def get_user(self, user_id):
|
||||
if not os.environ.has_key('HTTPS'):
|
||||
return None
|
||||
if not os.environ.get('HTTPS')=='on':# only use this back-end if HTTPS on
|
||||
return None
|
||||
try:
|
||||
return User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
3
lib/loncapa/__init__.py
Normal file
3
lib/loncapa/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from loncapa_check import *
|
||||
17
lib/loncapa/loncapa_check.py
Normal file
17
lib/loncapa/loncapa_check.py
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# File: mitx/lib/loncapa/loncapa_check.py
|
||||
#
|
||||
# Python functions which duplicate the standard comparison functions available to LON-CAPA problems.
|
||||
# Used in translating LON-CAPA problems to i4x problem specification language.
|
||||
|
||||
import random
|
||||
|
||||
def lc_random(lower,upper,stepsize):
|
||||
'''
|
||||
like random.randrange but lower and upper can be non-integer
|
||||
'''
|
||||
nstep = int((upper-lower)/(1.0*stepsize))
|
||||
choices = [lower+x*stepsize for x in range(nstep)]
|
||||
return random.choice(choices)
|
||||
|
||||
@@ -34,6 +34,9 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
|
||||
context_dictionary.update(d)
|
||||
if context:
|
||||
context_dictionary.update(context)
|
||||
## HACK
|
||||
## We should remove this, and possible set COURSE_TITLE in the middleware from the session.
|
||||
if 'COURSE_TITLE' not in context_dictionary: context_dictionary['COURSE_TITLE'] = ''
|
||||
# fetch and render template
|
||||
template = middleware.lookup[namespace].get_template(template_name)
|
||||
return template.render(**context_dictionary)
|
||||
|
||||
@@ -2,7 +2,6 @@ import datetime
|
||||
import json
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.context_processors import csrf
|
||||
@@ -61,3 +60,9 @@ def send_feedback(request):
|
||||
def info(request):
|
||||
''' Info page (link from main header) '''
|
||||
return render_to_response("info.html", {})
|
||||
|
||||
def mitxhome(request):
|
||||
''' Home page (link from main header). List of courses. '''
|
||||
if settings.ENABLE_MULTICOURSE:
|
||||
return render_to_response("mitxhome.html", {})
|
||||
return info(request)
|
||||
|
||||
22
settings.py
22
settings.py
@@ -8,6 +8,7 @@ import djcelery
|
||||
### Dark code. Should be enabled in local settings for devel.
|
||||
|
||||
ENABLE_MULTICOURSE = False # set to False to disable multicourse display (see lib.util.views.mitxhome)
|
||||
QUICKEDIT = False
|
||||
|
||||
###
|
||||
|
||||
@@ -20,19 +21,11 @@ COURSE_TITLE = "Circuits and Electronics"
|
||||
|
||||
COURSE_DEFAULT = '6.002_Spring_2012'
|
||||
|
||||
COURSE_LIST = {'6.002_Spring_2012': {'number' : '6.002x',
|
||||
'title' : 'Circuits and Electronics',
|
||||
'datapath': '6002x/',
|
||||
},
|
||||
'8.02_Spring_2013': {'number' : '8.02x',
|
||||
'title' : 'Electricity & Magnetism',
|
||||
'datapath': '802x/',
|
||||
},
|
||||
'8.01_Spring_2013': {'number' : '8.01x',
|
||||
'title' : 'Mechanics',
|
||||
'datapath': '801x/',
|
||||
},
|
||||
}
|
||||
COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x',
|
||||
'title' : 'Circuits and Electronics',
|
||||
'xmlpath': '6002x/',
|
||||
}
|
||||
}
|
||||
|
||||
ROOT_URLCONF = 'urls'
|
||||
|
||||
@@ -150,6 +143,7 @@ MIDDLEWARE_CLASSES = (
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'track.middleware.TrackMiddleware',
|
||||
'mitxmako.middleware.MakoMiddleware',
|
||||
#'ssl_auth.ssl_auth.NginxProxyHeaderMiddleware', # ssl authentication behind nginx proxy
|
||||
#'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
|
||||
# Uncommenting the following will prevent csrf token from being re-set if you
|
||||
@@ -179,6 +173,8 @@ INSTALLED_APPS = (
|
||||
'util',
|
||||
'masquerade',
|
||||
'django_jasmine',
|
||||
#'ssl_auth', ## Broken. Disabled for now.
|
||||
'multicourse', # multiple courses
|
||||
# Uncomment the next line to enable the admin:
|
||||
# 'django.contrib.admin',
|
||||
# Uncomment the next line to enable admin documentation:
|
||||
|
||||
95
templates/quickedit.html
Normal file
95
templates/quickedit.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="${ settings.LIB_URL }jquery.treeview.css" type="text/css" media="all" />
|
||||
<link rel="stylesheet" href="/static/css/codemirror.css" type="text/css" media="all" />
|
||||
|
||||
<script type="text/javascript" src="${ settings.LIB_URL }jquery-1.6.2.min.js"></script>
|
||||
<script type="text/javascript" src="${ settings.LIB_URL }jquery-ui-1.8.16.custom.min.js"></script>
|
||||
<script type="text/javascript" src="${ settings.LIB_URL }codemirror-compressed.js"></script>
|
||||
<script type="text/javascript" src="/static/js/schematic.js"></script>
|
||||
<%include file="mathjax_include.html" />
|
||||
|
||||
<script>
|
||||
function postJSON(url, data, callback) {
|
||||
$.ajax({type:'POST',
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
data: data,
|
||||
success: callback,
|
||||
headers : {'X-CSRFToken':'none'} // getCookie('csrftoken')}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="/static/js/html5shiv.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style type="text/css">
|
||||
.CodeMirror {border-style: solid;
|
||||
border-width: 1px;}
|
||||
.CodeMirror-scroll {
|
||||
height: 500;
|
||||
width: 100%
|
||||
}
|
||||
</style>
|
||||
|
||||
## -----------------------------------------------------------------------------
|
||||
## information and i4x PSL code
|
||||
|
||||
<hr width="100%">
|
||||
<h2>QuickEdit</h2>
|
||||
<hr width="100%">
|
||||
<ul>
|
||||
<li>File = ${filename}</li>
|
||||
<li>ID = ${id}</li>
|
||||
</ul>
|
||||
|
||||
<form method="post">
|
||||
<textarea rows="40" cols="160" name="quickedit_${id}" id="quickedit_${id}">${pxmls|h}</textarea>
|
||||
<br/>
|
||||
<input type="submit" value="Change Problem" name="qesubmit" />
|
||||
<input type="submit" value="Revert to original" name="qesubmit" />
|
||||
</form>
|
||||
|
||||
<span>${msg|n}</span>
|
||||
|
||||
## -----------------------------------------------------------------------------
|
||||
## rendered problem display
|
||||
|
||||
<script>
|
||||
// height: auto;
|
||||
// overflow-y: hidden;
|
||||
// overflow-x: auto;
|
||||
|
||||
$(function(){
|
||||
var cm = CodeMirror.fromTextArea(document.getElementById("quickedit_${id}"),
|
||||
{ 'mode': {name: "xml", alignCDATA: true},
|
||||
lineNumbers: true
|
||||
});
|
||||
|
||||
// $('.my-wymeditor').wymeditor();
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<hr width="100%">
|
||||
|
||||
<script>
|
||||
${init_js}
|
||||
</script>
|
||||
|
||||
<style type="text/css">
|
||||
.staff {display:none;}
|
||||
}
|
||||
</style>
|
||||
|
||||
<form>
|
||||
${phtml}
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
6
urls.py
6
urls.py
@@ -69,6 +69,12 @@ if settings.COURSEWARE_ENABLED:
|
||||
url(r'^calculate$', 'util.views.calculate'),
|
||||
)
|
||||
|
||||
if settings.ENABLE_MULTICOURSE:
|
||||
urlpatterns += (url(r'^mitxhome$', 'util.views.mitxhome'),)
|
||||
|
||||
if settings.QUICKEDIT:
|
||||
urlpatterns += (url(r'^quickedit/(?P<id>[^/]*)$', 'courseware.views.quickedit'),)
|
||||
|
||||
if settings.ASKBOT_ENABLED:
|
||||
urlpatterns += (url(r'^%s' % settings.ASKBOT_URL, include('askbot.urls')), \
|
||||
url(r'^admin/', include(admin.site.urls)), \
|
||||
|
||||
Reference in New Issue
Block a user