diff --git a/README b/README
new file mode 100644
index 0000000000..6860772c35
--- /dev/null
+++ b/README
@@ -0,0 +1,2 @@
+This branch (re-)adds dynamic math and symbolicresponse.
+Test cases included.
diff --git a/djangoapps/courseware/admin.py b/djangoapps/courseware/admin.py
new file mode 100644
index 0000000000..cda4fbb788
--- /dev/null
+++ b/djangoapps/courseware/admin.py
@@ -0,0 +1,9 @@
+'''
+django admin pages for courseware model
+'''
+
+from courseware.models import *
+from django.contrib import admin
+from django.contrib.auth.models import User
+
+admin.site.register(StudentModule)
diff --git a/djangoapps/courseware/capa/capa_problem.py b/djangoapps/courseware/capa/capa_problem.py
index 4c74d5315f..3c918651ba 100644
--- a/djangoapps/courseware/capa/capa_problem.py
+++ b/djangoapps/courseware/capa/capa_problem.py
@@ -26,7 +26,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,OptionResponse
+from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse,OptionResponse, SymbolicResponse
import calc
import eia
@@ -42,6 +42,7 @@ response_types = {'numericalresponse':NumericalResponse,
'truefalseresponse':TrueFalseResponse,
'imageresponse':ImageResponse,
'optionresponse':OptionResponse,
+ 'symbolicresponse':SymbolicResponse,
}
entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput','optioninput']
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
@@ -55,6 +56,7 @@ html_transforms = {'problem': {'tag':'div'},
"externalresponse": {'tag':'span'},
"schematicresponse": {'tag':'span'},
"formularesponse": {'tag':'span'},
+ "symbolicresponse": {'tag':'span'},
"multiplechoiceresponse": {'tag':'span'},
"text": {'tag':'span'},
"math": {'tag':'span'},
@@ -70,7 +72,7 @@ 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","externalresponse"]
+html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text","externalresponse",'symbolicresponse']
# removed in MC
## These should be transformed
@@ -109,6 +111,8 @@ class LoncapaProblem(object):
self.seed=struct.unpack('i', os.urandom(4))[0]
## Parse XML file
+ if getattr(system,'DEBUG',False):
+ log.info("[courseware.capa.capa_problem.lcp.init] fileobject = %s" % fileobject)
file_text = fileobject.read()
self.fileobject = fileobject # save it, so we can use for debugging information later
# Convert startouttext and endouttext to proper
@@ -210,20 +214,26 @@ class LoncapaProblem(object):
Problem XML goes to Python execution context. Runs everything in script tags
'''
random.seed(self.seed)
- ### 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
+ context.update(global_context) # initialize context to have stuff in global_context
+ context['__builtins__'] = globals()['__builtins__'] # put globals there also
+ context['the_lcp'] = self # pass instance of LoncapaProblem in
#for script in tree.xpath('/problem/script'):
for script in tree.findall('.//script'):
+ stype = script.get('type')
+ if stype:
+ if 'javascript' in stype: continue # skip javascript
+ if 'perl' in stype: continue # skip perl
+ # TODO: evaluate only python
code = script.text
XMLESC = {"'": "'", """: '"'}
code = unescape(code,XMLESC)
try:
- exec code in global_context, context
+ exec code in context, context # use "context" for global context; thus defs in code are global within code
except Exception,err:
- print "[courseware.capa.capa_problem.extract_context] error %s" % err
- print "in doing exec of this code:",code
+ log.exception("[courseware.capa.capa_problem.extract_context] error %s" % err)
+ log.exception("in doing exec of this code: %s" % code)
return context
def get_html(self):
diff --git a/djangoapps/courseware/capa/inputtypes.py b/djangoapps/courseware/capa/inputtypes.py
index e535ec400e..2298935c58 100644
--- a/djangoapps/courseware/capa/inputtypes.py
+++ b/djangoapps/courseware/capa/inputtypes.py
@@ -197,63 +197,77 @@ def choicegroup(element, value, status, msg=''):
type="radio"
choices={}
for choice in element:
- assert choice.tag =="choice", "only tags should be immediate children of a "
- choices[choice.get("name")] = etree.tostring(choice[0]) # TODO: what if choice[0] has math tags in it?
+ if not choice.tag=='choice':
+ raise Exception,"[courseware.capa.inputtypes.choicegroup] Error only tags should be immediate children of a , found %s instead" % choice.tag
+ ctext = ""
+ ctext += ''.join([etree.tostring(x) for x in choice]) # TODO: what if choice[0] has math tags in it?
+ ctext += choice.text # TODO: fix order?
+ choices[choice.get("name")] = ctext
context={'id':eid, 'value':value, 'state':status, 'type':type, 'choices':choices}
html=render_to_string("choicegroup.html", context)
return etree.XML(html)
@register_render_function
def textline(element, value, state, msg=""):
+ '''
+ Simple text line input, with optional size specification.
+ '''
+ if element.get('math') or element.get('dojs'): # 'dojs' flag is temporary, for backwards compatibility with 8.02x
+ return SimpleInput.xml_tags['textline_dynamath'](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}
+ context = {'id':eid, 'value':value, 'state':state, 'count':count, 'size': size, 'msg': msg}
html=render_to_string("textinput.html", context)
return etree.XML(html)
#-----------------------------------------------------------------------------
@register_render_function
-def js_textline(element, value, status, msg=''):
- '''
- Plan: We will inspect element to figure out type
- '''
- # TODO: Make a wrapper for
- # TODO: Make an AJAX loop to confirm equation is okay in real-time as user types
- ## TODO: Code should follow PEP8 (4 spaces per indentation level)
- '''
- textline is used for simple one-line inputs, like formularesponse and symbolicresponse.
- '''
- eid=element.get('id')
- count = int(eid.split('_')[-2])-1 # HACK
- size = element.get('size')
- dojs = element.get('dojs') # dojs is used for client-side javascript display & return
- # when dojs=='math', a `{::}`
- # and a hidden textarea with id=input_eid_fromjs will be output
- context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size,
- 'dojs':dojs,
- 'msg':msg,
- }
- html=render_to_string("jstext.html", context)
- return etree.XML(html)
+def textline_dynamath(element, value, status, msg=''):
+ '''
+ Text line input with dynamic math display (equation rendered on client in real time during input).
+ '''
+ # TODO: Make a wrapper for
+ # TODO: Make an AJAX loop to confirm equation is okay in real-time as user types
+ ## TODO: Code should follow PEP8 (4 spaces per indentation level)
+ '''
+ textline is used for simple one-line inputs, like formularesponse and symbolicresponse.
+ uses a `{::}`
+ and a hidden textarea with id=input_eid_fromjs for the mathjax rendering and return.
+ '''
+ eid=element.get('id')
+ count = int(eid.split('_')[-2])-1 # HACK
+ size = element.get('size')
+ context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size,
+ 'msg':msg,
+ }
+ html=render_to_string("textinput_dynamath.html", context)
+ return etree.XML(html)
#-----------------------------------------------------------------------------
## TODO: Make a wrapper for
@register_render_function
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.
+ '''
+ 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.
- TODO: make this use rows and cols attribs, not size
- '''
- eid=element.get('id')
- count = int(eid.split('_')[-2])-1 # HACK
- size = element.get('size')
- context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg}
- html=render_to_string("textbox.html", context)
- return etree.XML(html)
+ '''
+ eid=element.get('id')
+ count = int(eid.split('_')[-2])-1 # HACK
+ size = element.get('size')
+ rows = element.get('rows') or '30'
+ cols = element.get('cols') or '80'
+ mode = element.get('mode') or 'python' # mode for CodeMirror, eg "python" or "xml"
+ linenumbers = element.get('linenumbers') # for CodeMirror
+ if not value: value = element.text # if no student input yet, then use the default input given by the problem
+ context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg,
+ 'mode':mode, 'linenumbers':linenumbers,
+ 'rows':rows, 'cols':cols,
+ }
+ html=render_to_string("textbox.html", context)
+ return etree.XML(html)
#-----------------------------------------------------------------------------
@register_render_function
diff --git a/djangoapps/courseware/capa/responsetypes.py b/djangoapps/courseware/capa/responsetypes.py
index d35d526b30..8dff5fbda6 100644
--- a/djangoapps/courseware/capa/responsetypes.py
+++ b/djangoapps/courseware/capa/responsetypes.py
@@ -8,7 +8,9 @@ Used by capa_problem.py
'''
# standard library imports
+import inspect
import json
+import logging
import math
import numbers
import numpy
@@ -34,6 +36,8 @@ import eia
from util import contextualize_text
+log = logging.getLogger("mitx.courseware")
+
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
@@ -140,16 +144,13 @@ class TrueFalseResponse(MultipleChoiceResponse):
class OptionResponse(GenericResponse):
'''
- Example:
-
-
+ TODO: handle direction and randomize
+ '''
+ snippets = [{'snippet': '''
The location of the sky
The location of the earth
-
+ '''}]
- TODO: handle direction and randomize
-
- '''
def __init__(self, xml, context, system=None):
self.xml = xml
self.answer_fields = xml.findall('optioninput')
@@ -176,6 +177,10 @@ class OptionResponse(GenericResponse):
class NumericalResponse(GenericResponse):
def __init__(self, xml, context, system=None):
self.xml = xml
+ if not xml.get('answer'):
+ msg = "Error in problem specification: numericalresponse missing required answer attribute\n"
+ msg += "See XML source line %s" % getattr(xml,'sourceline','')
+ raise Exception,msg
self.correct_answer = contextualize_text(xml.get('answer'), context)
try:
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
@@ -212,9 +217,9 @@ class NumericalResponse(GenericResponse):
class CustomResponse(GenericResponse):
'''
- Custom response. The python code to be run should be in .... Example:
+ Custom response. The python code to be run should be in ...
+ or in a
'''
-
snippets = [{'snippet': '''
@@ -233,12 +238,8 @@ class CustomResponse(GenericResponse):
if not(r=="IS*u(t-t0)"):
correct[0] ='incorrect'
- '''}]
-
-
- '''Footnote: the check function can also be defined in Example:
-
- stanza instead
cfn = xml.get('cfn')
if cfn:
- if settings.DEBUG: print "[courseware.capa.responsetypes] cfn = ",cfn
+ if settings.DEBUG: log.info("[courseware.capa.responsetypes] cfn = %s" % cfn)
if cfn in context:
self.code = context[cfn]
else:
@@ -310,8 +315,16 @@ def sympy_check2():
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)
+ try:
+ submission = [student_answers[k] for k in idset] # ordered list of answers
+ except Exception, err:
+ msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers
+ msg += '\n idset = %s, error = %s' % (idset,err)
+ log.error(msg)
+ raise Exception,msg
+
+ # global variable in context which holds the Presentation MathML from dynamic math input
+ dynamath = [ student_answers.get(k+'_dynamath',None) for k in idset ] # ordered list of dynamath responses
# if there is only one box, and it's empty, then don't evaluate
if len(idset)==1 and not submission[0]:
@@ -329,13 +342,18 @@ def sympy_check2():
'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
+ 'dynamath':dynamath, # ordered list of all javascript inputs in our subtree
'answers':student_answers, # dict of student's responses, with keys being entry box IDs
'correct':correct, # the list to be filled in by the check function
'messages':messages, # the list of messages to be filled in by the check function
+ 'options':self.xml.get('options'), # any options to be passed to the cfn
'testdat':'hello world',
})
+ # pass self.system.debug to cfn
+ # if hasattr(self.system,'debug'): self.context['debug'] = self.system.debug
+ self.context['debug'] = settings.DEBUG
+
# exec the check function
if type(self.code)==str:
try:
@@ -348,34 +366,48 @@ def sympy_check2():
# this is an interface to the Tutor2 check functions
fn = self.code
+ ret = None
+ if settings.DEBUG: log.info(" submission = %s" % submission)
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)
+ # handle variable number of arguments in check function, for backwards compatibility
+ # with various Tutor2 check functions
+ args = [self.expect,answer_given,student_answers,self.answer_ids[0]]
+ argspec = inspect.getargspec(fn)
+ nargs = len(argspec.args)-len(argspec.defaults or [])
+ kwargs = {}
+ for argname in argspec.args[nargs:]:
+ kwargs[argname] = self.context[argname] if argname in self.context else None
+
+ if settings.DEBUG:
+ log.debug('[courseware.capa.responsetypes.customresponse] answer_given=%s' % answer_given)
+ log.info('nargs=%d, args=%s, kwargs=%s' % (nargs,args,kwargs))
+
+ ret = fn(*args[:nargs],**kwargs)
except Exception,err:
- print "oops in customresponse (cfn) error %s" % err
+ log.error("oops in customresponse (cfn) error %s" % err)
# print "context = ",self.context
- print traceback.format_exc()
- if settings.DEBUG: print "[courseware.capa.responsetypes.customresponse.get_score] ret = ",ret
+ log.error(traceback.format_exc())
+ raise Exception,"oops in customresponse (cfn) error %s" % err
+ if settings.DEBUG: log.info("[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret)
if type(ret)==dict:
- correct[0] = 'correct' if ret['ok'] else 'incorrect'
+ correct = ['correct']*len(idset) if ret['ok'] else ['incorrect']*len(idset)
msg = ret['msg']
if 1:
# try to clean up message html
msg = ''+msg+''
- msg = etree.tostring(fromstring_bs(msg),pretty_print=True)
+ msg = msg.replace('<','<')
+ #msg = msg.replace('<','<')
+ msg = etree.tostring(fromstring_bs(msg,convertEntities=None),pretty_print=True)
+ #msg = etree.tostring(fromstring_bs(msg),pretty_print=True)
msg = msg.replace('
','')
#msg = re.sub('(.*)','\\1',msg,flags=re.M|re.DOTALL) # python 2.7
msg = re.sub('(?ms)(.*)','\\1',msg)
messages[0] = msg
else:
- correct[0] = 'correct' if ret else 'incorrect'
+ correct = ['correct']*len(idset) if ret else ['incorrect']*len(idset)
# build map giving "correct"ness of the answer(s)
#correct_map = dict(zip(idset, self.context['correct']))
@@ -403,14 +435,78 @@ def sympy_check2():
#-----------------------------------------------------------------------------
-class ExternalResponse(GenericResponse):
+class SymbolicResponse(CustomResponse):
"""
- Grade the student's input using an external server.
+ Symbolic math response checking, using symmath library.
+ """
+ snippets = [{'snippet': '''
+ Compute \[ \exp\left(-i \frac{\theta}{2} \left[ \begin{matrix} 0 & 1 \\ 1 & 0 \end{matrix} \right] \right) \]
+ and give the resulting \(2\times 2\) matrix:
+
+
+
+
+ Your input should be typed in as a list of lists, eg [[1,2],[3,4]].
+
+ '''}]
+ def __init__(self, xml, context, system=None):
+ xml.set('cfn','symmath_check')
+ code = "from symmath import *"
+ exec code in context,context
+ CustomResponse.__init__(self,xml,context,system)
+
+
+#-----------------------------------------------------------------------------
+
+class ExternalResponse(GenericResponse):
+ '''
+ Grade the students input using an external server.
Typically used by coding problems.
- """
+
+ '''
+ snippets = [{'snippet': '''
+
+
+
+ '''}]
+
def __init__(self, xml, context, system=None):
self.xml = xml
+ self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
self.answer_ids = xml.xpath('//*[@id=$id]//textbox/@id|//*[@id=$id]//textline/@id',
id=xml.get('id'))
self.context = context
@@ -423,25 +519,66 @@ class ExternalResponse(GenericResponse):
else:
self.code = answer.text
- self.tests = xml.get('answer')
+ self.tests = xml.get('tests')
- def get_score(self, student_answers):
- submission = [student_answers[k] for k in sorted(self.answer_ids)]
- self.context.update({'submission':submission})
+ def do_external_request(self,cmd,extra_payload):
+ '''
+ Perform HTTP request / post to external server.
+ cmd = remote command to perform (str)
+ extra_payload = dict of extra stuff to post.
+
+ Return XML tree of response (from response body)
+ '''
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,
+ 'edX_cmd' : cmd,
+ 'edX_tests': self.tests,
'processor' : self.code,
}
+ payload.update(extra_payload)
- # call external server; TODO: get URL from settings.py
- r = requests.post("http://eecs1.mit.edu:8889/pyloncapa",data=payload)
+ try:
+ r = requests.post(self.url,data=payload) # call external server
+ except Exception,err:
+ msg = 'Error %s - cannot connect to external server url=%s' % (err,self.url)
+ log.error(msg)
+ raise Exception, msg
+
+ if settings.DEBUG: log.info('response = %s' % r.text)
+
+ if (not r.text ) or (not r.text.strip()):
+ raise Exception,'Error: no response from external server url=%s' % self.url
+
+ try:
+ rxml = etree.fromstring(r.text) # response is XML; prase it
+ except Exception,err:
+ msg = 'Error %s - cannot parse response from external server r.text=%s' % (err,r.text)
+ log.error(msg)
+ raise Exception, msg
+
+ return rxml
+
+ def get_score(self, student_answers):
+ try:
+ submission = [student_answers[k] for k in sorted(self.answer_ids)]
+ except Exception,err:
+ log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err,self.answer_ids,student_answers))
+ raise Exception,err
+
+ self.context.update({'submission':submission})
+
+ extra_payload = {'edX_student_response': json.dumps(submission)}
+
+ try:
+ rxml = self.do_external_request('get_score',extra_payload)
+ except Exception, err:
+ log.error('Error %s' % err)
+ if settings.DEBUG:
+ correct_map = dict(zip(sorted(self.answer_ids), ['incorrect'] * len(self.answer_ids) ))
+ correct_map['msg_%s' % self.answer_ids[0]] = '%s' % str(err).replace('<','<')
+ return correct_map
- 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',
@@ -453,15 +590,29 @@ class ExternalResponse(GenericResponse):
# 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
+ # store message in correct_map
+ correct_map['msg_%s' % self.answer_ids[0]] = rxml.find('message').text.replace(' ',' ')
return correct_map
def get_answers(self):
- # Since this is explicitly specified in the problem, this will
- # be handled by capa_problem
- return {}
+ '''
+ Use external server to get expected answers
+ '''
+ try:
+ rxml = self.do_external_request('get_answers',{})
+ exans = json.loads(rxml.find('expected').text)
+ except Exception,err:
+ log.error('Error %s' % err)
+ if settings.DEBUG:
+ msg = '%s' % str(err).replace('<','<')
+ exans = [''] * len(self.answer_ids)
+ exans[0] = msg
+
+ if not (len(exans)==len(self.answer_ids)):
+ log.error('Expected %d answers from external server, only got %d!' % (len(self.answer_ids),len(exans)))
+ raise Exception,'Short response from external server'
+ return dict(zip(self.answer_ids,exans))
class StudentInputError(Exception):
pass
@@ -469,6 +620,27 @@ class StudentInputError(Exception):
#-----------------------------------------------------------------------------
class FormulaResponse(GenericResponse):
+ '''
+ Checking of symbolic math response using numerical sampling.
+ '''
+ snippets = [{'snippet': '''
+
+
+
+
+
+ Give an equation for the relativistic energy of an object with mass m.
+
+
+
+
+
+
+ '''}]
+
def __init__(self, xml, context, system=None):
self.xml = xml
self.correct_answer = contextualize_text(xml.get('answer'), context)
@@ -587,15 +759,12 @@ class ImageResponse(GenericResponse):
doesn't make sense to me (Ike). Instead, let's have it such that
should contain one or more stanzas. Each should specify
a rectangle, given as an attribute, defining the correct answer.
-
- Example:
-
-
+ """
+ snippets = [{'snippet': '''
-
+ '''}]
- """
def __init__(self, xml, context, system=None):
self.xml = xml
self.context = context
@@ -621,7 +790,7 @@ class ImageResponse(GenericResponse):
# 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)
+ raise Exception,'[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (aid,given)
(gx,gy) = [int(x) for x in m.groups()]
# answer is correct if (x,y) is within the specified rectangle
diff --git a/djangoapps/courseware/content_parser.py b/djangoapps/courseware/content_parser.py
index 06c07bff64..a24c14039e 100644
--- a/djangoapps/courseware/content_parser.py
+++ b/djangoapps/courseware/content_parser.py
@@ -203,6 +203,10 @@ def course_file(user,coursename=None):
else:
tree_string = None
+ if settings.DEBUG:
+ log.info('[courseware.content_parser.course_file] filename=%s, cache_key=%s' % (filename,cache_key))
+ # print '[courseware.content_parser.course_file] tree_string = ',tree_string
+
if not tree_string:
tree = course_xml_process(etree.XML(render_to_string(filename, options, namespace = 'course')))
tree_string = etree.tostring(tree)
@@ -231,7 +235,7 @@ def section_file(user, section, coursename=None, dironly=False):
if dironly: return dirname
if filename not in os.listdir(dirname):
- print filename+" not in "+str(os.listdir(dirname))
+ log.error(filename+" not in "+str(os.listdir(dirname)))
return None
options = {'dev_content':settings.DEV_CONTENT,
@@ -271,9 +275,14 @@ def module_xml(user, module, id_tag, module_id, coursename=None):
break
if len(result_set)>1:
- print "WARNING: Potentially malformed course file", module, module_id
+ log.error("WARNING: Potentially malformed course file", module, module_id)
if len(result_set)==0:
+ if settings.DEBUG:
+ log.error('[courseware.content_parser.module_xml] cannot find %s in course.xml tree' % xpath_search)
+ log.error('tree = %s' % etree.tostring(doc,pretty_print=True))
return None
+ if settings.DEBUG:
+ log.info('[courseware.content_parser.module_xml] found %s' % result_set)
return etree.tostring(result_set[0])
#return result_set[0].serialize()
diff --git a/djangoapps/courseware/module_render.py b/djangoapps/courseware/module_render.py
index 250a4ea055..2640ccfd1e 100644
--- a/djangoapps/courseware/module_render.py
+++ b/djangoapps/courseware/module_render.py
@@ -35,8 +35,16 @@ class I4xSystem(object):
self.filestore = OSFS(settings.DATA_DIR)
else:
self.filestore = filestore
+ if settings.DEBUG:
+ log.info("[courseware.module_render.I4xSystem] filestore path = %s" % filestore)
self.render_function = render_function
self.exception404 = Http404
+ self.DEBUG = settings.DEBUG
+
+ def get(self,attr): # uniform access to attributes (like etree)
+ return self.__dict__.get(attr)
+ def set(self,attr,val): # uniform access to attributes (like etree)
+ self.__dict__[attr] = val
def __repr__(self):
return repr(self.__dict__)
def __str__(self):
@@ -80,8 +88,7 @@ def grade_histogram(module_id):
return []
return grades
-
-def get_module(user, request, xml_module, module_object_preload):
+def get_module(user, request, xml_module, module_object_preload, position=None):
module_type=xml_module.tag
module_class=courseware.modules.get_module_class(module_type)
module_id=xml_module.get('id') #module_class.id_attribute) or ""
@@ -110,10 +117,11 @@ def get_module(user, request, xml_module, module_object_preload):
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module_type+'/'+module_id+'/'
system = I4xSystem(track_function = make_track_function(request),
- render_function = lambda x: render_x_module(user, request, x, module_object_preload),
+ render_function = lambda x: render_x_module(user, request, x, module_object_preload, position),
ajax_url = ajax_url,
filestore = OSFS(data_root),
)
+ system.set('position',position) # pass URL specified position along to module, through I4xSystem
instance=module_class(system,
etree.tostring(xml_module),
module_id,
@@ -131,12 +139,30 @@ def get_module(user, request, xml_module, module_object_preload):
return (instance, smod, module_type)
-def render_x_module(user, request, xml_module, module_object_preload):
- ''' Generic module for extensions. This renders to HTML. '''
+def render_x_module(user, request, xml_module, module_object_preload, position=None):
+ ''' Generic module for extensions. This renders to HTML.
+
+ modules include sequential, vertical, problem, video, html
+
+ Note that modules can recurse. problems, video, html, can be inside sequential or vertical.
+
+ Arguments:
+
+ - user : current django User
+ - request : current django HTTPrequest
+ - xml_module : lxml etree of xml subtree for the current module
+ - module_object_preload : list of StudentModule objects, one of which may match this module type and id
+ - position : extra information from URL for user-specified position within module
+
+ Returns:
+
+ - dict which is context for HTML rendering of the specified module
+
+ '''
if xml_module==None :
return {"content":""}
- (instance, smod, module_type) = get_module(user, request, xml_module, module_object_preload)
+ (instance, smod, module_type) = get_module(user, request, xml_module, module_object_preload, position)
# Grab content
content = instance.get_html()
@@ -156,9 +182,8 @@ def render_x_module(user, request, xml_module, module_object_preload):
return content
-
def modx_dispatch(request, module=None, dispatch=None, id=None):
- ''' Generic view for extensions. '''
+ ''' Generic view for extensions. This is where AJAX calls go.'''
if not request.user.is_authenticated():
return redirect('/')
@@ -191,7 +216,7 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
try:
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
except:
- log.exception("Unable to load module during ajax call")
+ log.exception("Unable to load module during ajax call. module=%s, dispatch=%s, id=%s" % (module, dispatch, id))
if accepts(request, 'text/html'):
return render_to_response("module-error.html", {})
else:
@@ -228,4 +253,3 @@ 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)
-
diff --git a/djangoapps/courseware/modules/capa_module.py b/djangoapps/courseware/modules/capa_module.py
index ba02bd854e..67af821159 100644
--- a/djangoapps/courseware/modules/capa_module.py
+++ b/djangoapps/courseware/modules/capa_module.py
@@ -25,6 +25,8 @@ from multicourse import multicourse_settings
log = logging.getLogger("mitx.courseware")
+#-----------------------------------------------------------------------------
+
class ComplexEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, complex):
@@ -127,7 +129,7 @@ class Module(XModule):
html=render_to_string('problem.html', context)
if encapsulate:
- html = ''.format(id=self.item_id)+html+"
"
+ html = ''.format(id=self.item_id,ajax_url=self.ajax_url)+html+"
"
return html
@@ -197,9 +199,27 @@ class Module(XModule):
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, system=self.system)
+ log.exception('[courseware.capa.capa_module.Module.init] error %s: cannot open file %s' % (err,self.filename))
+ if self.DEBUG:
+ # create a dummy problem instead of failing
+ fp = StringIO.StringIO('Problem file %s is missing' % self.filename)
+ fp.name = "StringIO"
+ else:
+ raise
+ try:
+ self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed, system=self.system)
+ except Exception,err:
+ msg = '[courseware.capa.capa_module.Module.init] error %s: cannot create LoncapaProblem %s' % (err,self.filename)
+ log.exception(msg)
+ if self.DEBUG:
+ msg = '%s
' % msg.replace('<','<')
+ msg += '%s
' % traceback.format_exc().replace('<','<')
+ # create a dummy problem with error message instead of failing
+ fp = StringIO.StringIO('Problem file %s has an error:%s' % (self.filename,msg))
+ fp.name = "StringIO"
+ self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed, system=self.system)
+ else:
+ raise
def handle_ajax(self, dispatch, get):
'''
@@ -328,8 +348,15 @@ class Module(XModule):
self.tracker('save_problem_check', event_info)
+ try:
+ html = self.get_problem_html(encapsulate=False)
+ except Exception,err:
+ log.error('failed to generate html')
+ raise Exception,err
+
return json.dumps({'success': success,
- 'contents': self.get_problem_html(encapsulate=False)})
+ 'contents': html,
+ })
def save_problem(self, get):
event_info = dict()
diff --git a/djangoapps/courseware/modules/html_module.py b/djangoapps/courseware/modules/html_module.py
index bf60174e18..9543107be0 100644
--- a/djangoapps/courseware/modules/html_module.py
+++ b/djangoapps/courseware/modules/html_module.py
@@ -1,10 +1,14 @@
import json
+import logging
from mitxmako.shortcuts import render_to_response, render_to_string
from x_module import XModule, XModuleDescriptor
from lxml import etree
+log = logging.getLogger("mitx.courseware")
+
+#-----------------------------------------------------------------------------
class ModuleDescriptor(XModuleDescriptor):
pass
@@ -28,7 +32,10 @@ class Module(XModule):
filename="html/"+self.filename
return self.filestore.open(filename).read()
except: # For backwards compatibility. TODO: Remove
- return render_to_string(self.filename, {'id': self.item_id})
+ if self.DEBUG:
+ log.info('[courseware.modules.html_module] filename=%s' % self.filename)
+ #return render_to_string(self.filename, {'id': self.item_id})
+ return render_to_string(self.filename, {'id': self.item_id},namespace='course')
def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state)
diff --git a/djangoapps/courseware/modules/seq_module.py b/djangoapps/courseware/modules/seq_module.py
index 3604fa0a6a..de80e2bf05 100644
--- a/djangoapps/courseware/modules/seq_module.py
+++ b/djangoapps/courseware/modules/seq_module.py
@@ -31,7 +31,16 @@ class Module(XModule):
self.render()
return self.content
- def handle_ajax(self, dispatch, get):
+ def get_init_js(self):
+ self.render()
+ return self.init_js
+
+ def get_destroy_js(self):
+ self.render()
+ return self.destroy_js
+
+ def handle_ajax(self, dispatch, get): # TODO: bounds checking
+ ''' get = request.POST instance '''
if dispatch=='goto_position':
self.position = int(get['position'])
return json.dumps({'success':True})
@@ -83,4 +92,8 @@ class Module(XModule):
state = json.loads(state)
if 'position' in state: self.position = int(state['position'])
+ # if position is specified in system, then use that instead
+ if system.get('position'):
+ self.position = int(system.get('position'))
+
self.rendered = False
diff --git a/djangoapps/courseware/modules/x_module.py b/djangoapps/courseware/modules/x_module.py
index 93489fab04..035b3819d1 100644
--- a/djangoapps/courseware/modules/x_module.py
+++ b/djangoapps/courseware/modules/x_module.py
@@ -55,6 +55,7 @@ class XModule(object):
self.json = json
self.item_id = item_id
self.state = state
+ self.DEBUG = False
self.__xmltree = etree.fromstring(xml) # PRIVATE
@@ -65,6 +66,7 @@ class XModule(object):
self.tracker = system.track_function
self.filestore = system.filestore
self.render_function = system.render_function
+ self.DEBUG = system.DEBUG
self.system = system
### Functions used in the LMS
diff --git a/djangoapps/courseware/test_files/symbolicresponse.xml b/djangoapps/courseware/test_files/symbolicresponse.xml
new file mode 100644
index 0000000000..4dc2bc9d7b
--- /dev/null
+++ b/djangoapps/courseware/test_files/symbolicresponse.xml
@@ -0,0 +1,29 @@
+
+
+Example: Symbolic Math Response Problem
+
+
+A symbolic math response problem presents one or more symbolic math
+input fields for input. Correctness of input is evaluated based on
+the symbolic properties of the expression entered. The student enters
+text, but sees a proper symbolic rendition of the entered formula, in
+real time, next to the input box.
+
+
+This is a correct answer which may be entered below:
+cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]
+
+
+ Compute [mathjax] U = \exp\left( i \theta \left[ \begin{matrix} 0 & 1 \\ 1 & 0 \end{matrix} \right] \right) [/mathjax]
+ and give the resulting \(2 \times 2\) matrix.
+ Your input should be typed in as a list of lists, eg [[1,2],[3,4]].
+ [mathjax]U=[/mathjax]
+
+
+
+
+
+
+
diff --git a/djangoapps/courseware/tests.py b/djangoapps/courseware/tests.py
index fabb79e4e7..6f44cb3f7e 100644
--- a/djangoapps/courseware/tests.py
+++ b/djangoapps/courseware/tests.py
@@ -1,3 +1,10 @@
+#
+# unittests for courseware
+#
+# Note: run this using a like like this:
+#
+# django-admin.py test --settings=envs.test_ike --pythonpath=. courseware
+
import unittest
import os
@@ -127,6 +134,94 @@ class ImageResponseTest(unittest.TestCase):
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 SymbolicResponseTest(unittest.TestCase):
+ def test_sr_grade(self):
+ symbolicresponse_file = os.path.dirname(__file__)+"/test_files/symbolicresponse.xml"
+ test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file), '1', system=i4xs)
+ correct_answers = {'1_2_1':'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]',
+ '1_2_1_dynamath': '''
+
+''',
+ }
+ wrong_answers = {'1_2_1':'2',
+ '1_2_1_dynamath':'''
+ ''',
+ }
+ self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct')
+ self.assertEquals(test_lcp.grade_answers(wrong_answers)['1_2_1'], 'incorrect')
+
class OptionResponseTest(unittest.TestCase):
'''
Run this with
diff --git a/djangoapps/courseware/views.py b/djangoapps/courseware/views.py
index 8b34e8b26d..3185edbe98 100644
--- a/djangoapps/courseware/views.py
+++ b/djangoapps/courseware/views.py
@@ -11,6 +11,7 @@ from django.http import Http404
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
#from django.views.decorators.csrf import ensure_csrf_cookie
+from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control
from lxml import etree
@@ -145,9 +146,23 @@ def render_section(request, section):
return result
+@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
-def index(request, course=None, chapter="Using the System", section="Hints"):
- ''' Displays courseware accordion, and any associated content.
+def index(request, course=None, chapter="Using the System", section="Hints",position=None):
+ ''' Displays courseware accordion, and any associated content.
+
+ Arguments:
+
+ - request : HTTP request
+ - course : coursename (str)
+ - chapter : chapter name (str)
+ - section : section name (str)
+ - position : position in module, eg of module (str)
+
+ Returns:
+
+ - HTTPresponse
+
'''
user = request.user
if not settings.COURSEWARE_ENABLED:
@@ -176,12 +191,13 @@ def index(request, course=None, chapter="Using the System", section="Hints"):
request.session['coursename'] = course # keep track of current course being viewed in django's request.session
try:
+ # this is the course.xml etree
dom = content_parser.course_file(user,course) # also pass course to it, for course-specific XML path
except:
log.exception("Unable to parse courseware xml")
return render_to_response('courseware-error.html', {})
- #dom_module = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]/*[1]",
+ # this is the module's parent's etree
dom_module = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]",
course=course, chapter=chapter, section=section)
@@ -197,6 +213,7 @@ def index(request, course=None, chapter="Using the System", section="Hints"):
elif module_wrapper.get("src"):
module = content_parser.section_file(user=user, section=module_wrapper.get("src"), coursename=course)
else:
+ # this is the module's etree
module = etree.XML(etree.tostring(module_wrapper[0])) # Copy the element out of the tree
module_ids = []
@@ -217,7 +234,7 @@ def index(request, course=None, chapter="Using the System", section="Hints"):
}
try:
- module = render_x_module(user, request, module, module_object_preload)
+ module_context = render_x_module(user, request, module, module_object_preload, position)
except:
log.exception("Unable to load module")
context.update({
@@ -227,104 +244,48 @@ def index(request, course=None, chapter="Using the System", section="Hints"):
return render_to_response('courseware.html', context)
context.update({
- 'init': module.get('init_js', ''),
- 'content': module['content'],
+ 'init': module_context.get('init_js', ''),
+ 'content': module_context['content'],
})
result = render_to_response('courseware.html', context)
return result
-
-def quickedit(request, id=None):
+def jump_to(request, probname=None):
'''
- quick-edit capa problem.
+ Jump to viewing a specific problem. The problem is specified by a problem name - currently the filename (minus .xml)
+ of the problem. Maybe this should change to a more generic tag, eg "name" given as an attribute in .
+
+ We do the jump by (1) reading course.xml to find the first instance of with the given filename, then
+ (2) finding the parent element of the problem, then (3) rendering that parent element with a specific computed position
+ value (if it is ).
- 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 CONTENT 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
coursename = multicourse_settings.get_coursename_from_request(request)
- xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
- 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 = OSFS(settings.DATA_DIR + xp),
- #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)
+ # begin by getting course.xml tree
+ xml = content_parser.course_file(request.user,coursename)
- return instance, pxmls
+ # look for problem of given name
+ pxml = xml.xpath('//problem[@filename="%s"]' % probname)
+ if pxml: pxml = pxml[0]
- instance, pxmls = get_lcp(coursename,id)
+ # get the parent element
+ parent = pxml.getparent()
- # 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]
+ # figure out chapter and section names
+ chapter = None
+ section = None
+ branch = parent
+ for k in range(4): # max depth of recursion
+ if branch.tag=='section': section = branch.get('name')
+ if branch.tag=='chapter': chapter = branch.get('name')
+ branch = branch.getparent()
- # see if code changed
- if str(newcode)==str(pxmls) or '\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 \"%s\"" % err
-
- if isok:
- filename = instance.lcp.fileobject.name
- fp = open(filename,'w') # TODO - replace with filestore call?
- fp.write(newcode)
- fp.close()
- msg = "Problem changed! (%s)" % filename
- instance, pxmls = get_lcp(coursename,id)
+ position = None
+ if parent.tag=='sequential':
+ position = parent.index(pxml)+1 # position in sequence
+
+ return index(request,course=coursename,chapter=chapter,section=section,position=position)
- 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
diff --git a/djangoapps/multicourse/multicourse_settings.py b/djangoapps/multicourse/multicourse_settings.py
index 8aea42da9a..1adc7b0c01 100644
--- a/djangoapps/multicourse/multicourse_settings.py
+++ b/djangoapps/multicourse/multicourse_settings.py
@@ -78,3 +78,4 @@ def get_course_title(coursename):
def get_course_number(coursename):
return get_course_property(coursename,'number')
+
diff --git a/djangoapps/multicourse/views.py b/djangoapps/multicourse/views.py
index 15c4a7a382..ef9fca7443 100644
--- a/djangoapps/multicourse/views.py
+++ b/djangoapps/multicourse/views.py
@@ -17,6 +17,8 @@ from multicourse import multicourse_settings
def mitxhome(request):
''' Home page (link from main header). List of courses. '''
+ if settings.DEBUG:
+ print "[djangoapps.multicourse.mitxhome] MITX_ROOT_URL = " + settings.MITX_ROOT_URL
if settings.ENABLE_MULTICOURSE:
context = {'courseinfo' : multicourse_settings.COURSE_SETTINGS}
return render_to_response("mitxhome.html", context)
diff --git a/djangoapps/ssl_auth/ssl_auth.py b/djangoapps/ssl_auth/ssl_auth.py
index 8c96d4b9a6..df3029da93 100755
--- a/djangoapps/ssl_auth/ssl_auth.py
+++ b/djangoapps/ssl_auth/ssl_auth.py
@@ -173,7 +173,8 @@ class SSLLoginBackend(ModelBackend):
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"
+ if not settings.DEBUG:
+ 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
diff --git a/envs/common.py b/envs/common.py
index d6a9d0c889..31b8e9936f 100644
--- a/envs/common.py
+++ b/envs/common.py
@@ -35,7 +35,8 @@ PERFSTATS = False
# Features
MITX_FEATURES = {
- 'SAMPLE' : False
+ 'SAMPLE' : False,
+ 'USE_DJANGO_PIPELINE' : True,
}
# Used for A/B testing
diff --git a/envs/dev_ike.py b/envs/dev_ike.py
new file mode 100644
index 0000000000..dd6ffa9176
--- /dev/null
+++ b/envs/dev_ike.py
@@ -0,0 +1,80 @@
+"""
+This config file runs the simplest dev environment using sqlite, and db-based
+sessions. Assumes structure:
+
+/envroot/
+ /db # This is where it'll write the database file
+ /mitx # The location of this repo
+ /log # Where we're going to write log files
+"""
+
+import socket
+
+if 'eecs1' in socket.gethostname():
+ MITX_ROOT_URL = '/mitx2'
+
+from envs.common import *
+from envs.logsettings import get_logger_config
+from dev import *
+
+if 'eecs1' in socket.gethostname():
+ MITX_ROOT_URL = '/mitx2'
+
+#-----------------------------------------------------------------------------
+# ichuang
+
+DEBUG = True
+ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome)
+QUICKEDIT = True
+
+MITX_FEATURES['USE_DJANGO_PIPELINE'] = False
+
+COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x',
+ 'title' : 'Circuits and Electronics',
+ 'xmlpath': '/6002x/',
+ 'active' : True,
+ },
+ '8.02_Spring_2013': {'number' : '8.02x',
+ 'title' : 'Electricity & Magnetism',
+ 'xmlpath': '/802x/',
+ 'active' : True,
+ },
+ '8.01_Spring_2013': {'number' : '8.01x',
+ 'title' : 'Mechanics',
+ 'xmlpath': '/801x/',
+ 'active' : False,
+ },
+ '6.189_Spring_2013': {'number' : '6.189x',
+ 'title' : 'IAP Python Programming',
+ 'xmlpath': '/6189-pytutor/',
+ 'active' : True,
+ },
+ '8.01_Summer_2012': {'number' : '8.01x',
+ 'title' : 'Mechanics',
+ 'xmlpath': '/801x-summer/',
+ 'active': True,
+ },
+ 'edx4edx': {'number' : 'edX.01',
+ 'title' : 'edx4edx: edX Author Course',
+ 'xmlpath': '/edx4edx/',
+ 'active' : True,
+ },
+ }
+
+#-----------------------------------------------------------------------------
+
+MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + (
+ 'ssl_auth.ssl_auth.NginxProxyHeaderMiddleware', # ssl authentication behind nginx proxy
+ )
+
+AUTHENTICATION_BACKENDS = (
+ 'ssl_auth.ssl_auth.SSLLoginBackend',
+ 'django.contrib.auth.backends.ModelBackend',
+ )
+
+INSTALLED_APPS = INSTALLED_APPS + (
+ 'ssl_auth',
+ )
+
+LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/'
+LOGIN_URL = MITX_ROOT_URL + '/'
diff --git a/envs/test_ike.py b/envs/test_ike.py
new file mode 100644
index 0000000000..2d319ff281
--- /dev/null
+++ b/envs/test_ike.py
@@ -0,0 +1,89 @@
+"""
+This config file runs the simplest dev environment using sqlite, and db-based
+sessions. Assumes structure:
+
+/envroot/
+ /db # This is where it'll write the database file
+ /mitx # The location of this repo
+ /log # Where we're going to write log files
+"""
+from envs.common import *
+from envs.logsettings import get_logger_config
+import os
+
+DEBUG = True
+
+INSTALLED_APPS = [
+ app
+ for app
+ in INSTALLED_APPS
+ if not app.startswith('askbot')
+]
+
+# Nose Test Runner
+INSTALLED_APPS += ['django_nose']
+#NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html', '--cover-inclusive']
+NOSE_ARGS = ['--cover-erase', '--with-xunit', '--cover-html', '--cover-inclusive']
+for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
+ NOSE_ARGS += ['--cover-package', app]
+TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
+
+# Local Directories
+TEST_ROOT = path("test_root")
+COURSES_ROOT = TEST_ROOT / "data"
+DATA_DIR = COURSES_ROOT
+MAKO_TEMPLATES['course'] = [DATA_DIR]
+MAKO_TEMPLATES['sections'] = [DATA_DIR / 'sections']
+MAKO_TEMPLATES['custom_tags'] = [DATA_DIR / 'custom_tags']
+MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
+ DATA_DIR / 'info',
+ DATA_DIR / 'problems']
+
+LOGGING = get_logger_config(TEST_ROOT / "log",
+ logging_env="dev",
+ tracking_filename="tracking.log",
+ debug=True)
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': PROJECT_ROOT / "db" / "mitx.db",
+ }
+}
+
+CACHES = {
+ # This is the cache used for most things. Askbot will not work without a
+ # functioning cache -- it relies on caching to load its settings in places.
+ # In staging/prod envs, the sessions also live here.
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ 'LOCATION': 'mitx_loc_mem_cache',
+ 'KEY_FUNCTION': 'util.memcache.safe_key',
+ },
+
+ # The general cache is what you get if you use our util.cache. It's used for
+ # things like caching the course.xml file for different A/B test groups.
+ # We set it to be a DummyCache to force reloading of course.xml in dev.
+ # In staging environments, we would grab VERSION from data uploaded by the
+ # push process.
+ 'general': {
+ 'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
+ 'KEY_PREFIX': 'general',
+ 'VERSION': 4,
+ 'KEY_FUNCTION': 'util.memcache.safe_key',
+ }
+}
+
+# Dummy secret key for dev
+SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
+
+############################ FILE UPLOADS (ASKBOT) #############################
+DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
+MEDIA_ROOT = PROJECT_ROOT / "uploads"
+MEDIA_URL = "/static/uploads/"
+STATICFILES_DIRS.append(("uploads", MEDIA_ROOT))
+FILE_UPLOAD_TEMP_DIR = PROJECT_ROOT / "uploads"
+FILE_UPLOAD_HANDLERS = (
+ 'django.core.files.uploadhandler.MemoryFileUploadHandler',
+ 'django.core.files.uploadhandler.TemporaryFileUploadHandler',
+)
diff --git a/lib/dogfood/__init__.py b/lib/dogfood/__init__.py
new file mode 100644
index 0000000000..d00d8ea793
--- /dev/null
+++ b/lib/dogfood/__init__.py
@@ -0,0 +1 @@
+from check import *
diff --git a/lib/dogfood/check.py b/lib/dogfood/check.py
new file mode 100644
index 0000000000..574a01e0b0
--- /dev/null
+++ b/lib/dogfood/check.py
@@ -0,0 +1,61 @@
+#!/usr/bin/python
+
+from random import choice
+import string
+import traceback
+
+from django.conf import settings
+import courseware.capa.capa_problem as lcp
+from dogfood.views import update_problem
+
+def GenID(length=8, chars=string.letters + string.digits):
+ return ''.join([choice(chars) for i in range(length)])
+
+randomid = GenID()
+
+def check_problem_code(ans,the_lcp,correct_answers,false_answers):
+ """
+ ans = student's answer
+ the_lcp = LoncapaProblem instance
+
+ returns dict {'ok':is_ok,'msg': message with iframe}
+ """
+ pfn = "dog%s" % randomid
+ pfn += the_lcp.problem_id.replace('filename','') # add problem ID to dogfood problem name
+ update_problem(pfn,ans,filestore=the_lcp.system.filestore)
+ msg = '
'
+ msg += '' % (settings.MITX_ROOT_URL,pfn)
+ msg += '
'
+
+ endmsg = """Note: if the code text box disappears after clicking on "Check",
+ please type something in the box to make it refresh properly. This is a
+ bug with Chrome; it does not happen with Firefox. It is being fixed.
+
"""
+
+ is_ok = True
+ if (not correct_answers) or (not false_answers):
+ ret = {'ok':is_ok,
+ 'msg': msg+endmsg,
+ }
+ return ret
+
+ try:
+ # check correctness
+ fp = the_lcp.system.filestore.open('problems/%s.xml' % pfn)
+ test_lcp = lcp.LoncapaProblem(fp, '1', system=the_lcp.system)
+
+ if not (test_lcp.grade_answers(correct_answers)['1_2_1']=='correct'):
+ is_ok = False
+ if (test_lcp.grade_answers(false_answers)['1_2_1']=='correct'):
+ is_ok = False
+ except Exception,err:
+ is_ok = False
+ msg += "Error: %s
" % str(err).replace('<','<')
+ msg += "%s
" % traceback.format_exc().replace('<','<')
+
+ ret = {'ok':is_ok,
+ 'msg': msg+endmsg,
+ }
+ return ret
+
+
diff --git a/lib/dogfood/views.py b/lib/dogfood/views.py
new file mode 100644
index 0000000000..ca9c89c0dc
--- /dev/null
+++ b/lib/dogfood/views.py
@@ -0,0 +1,297 @@
+'''
+dogfood.py
+
+For using mitx / edX / i4x in checking itself.
+
+df_capa_problem: accepts an XML file for a problem, and renders it.
+'''
+import logging
+import datetime
+import re
+import os # FIXME - use OSFS instead
+
+from fs.osfs import OSFS
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.context_processors import csrf
+from django.core.mail import send_mail
+from django.http import Http404
+from django.http import HttpResponse
+from django.shortcuts import redirect
+from mitxmako.shortcuts import render_to_response, render_to_string
+
+import courseware.capa.calc
+import track.views
+from lxml import etree
+
+
+from courseware.module_render import make_track_function, I4xSystem
+from courseware.models import StudentModule
+from multicourse import multicourse_settings
+from student.models import UserProfile
+from util.cache import cache
+from util.views import accepts
+
+import courseware.content_parser as content_parser
+import courseware.modules
+
+log = logging.getLogger("mitx.courseware")
+
+etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False,
+ remove_comments = True))
+
+DOGFOOD_COURSENAME = 'edx_dogfood' # FIXME - should not be here; maybe in settings
+
+def update_problem(pfn,pxml,coursename=None,overwrite=True,filestore=None):
+ '''
+ update problem with filename pfn, and content (xml) pxml.
+ '''
+ if not filestore:
+ if not coursename: coursename = DOGFOOD_COURSENAME
+ xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
+ pfn2 = settings.DATA_DIR + xp + 'problems/%s.xml' % pfn
+ fp = open(pfn2,'w')
+ else:
+ pfn2 = 'problems/%s.xml' % pfn
+ fp = filestore.open(pfn2,'w')
+
+ if os.path.exists(pfn2) and not overwrite: return # don't overwrite if already exists and overwrite=False
+ pxmls = pxml if type(pxml) in [str,unicode] else etree.tostring(pxml,pretty_print=True)
+ fp.write(pxmls)
+ fp.close()
+
+def df_capa_problem(request, id=None):
+ '''
+ dogfood capa problem.
+
+ Accepts XML for a problem, inserts it into the dogfood course.xml.
+ Returns rendered problem.
+ '''
+ # "WARNING: UNDEPLOYABLE CODE. FOR DEV USE ONLY."
+
+ if settings.DEBUG:
+ print '[lib.dogfood.df_capa_problem] id=%s' % id
+
+ if not 'coursename' in request.session:
+ coursename = DOGFOOD_COURSENAME
+ else:
+ coursename = request.session['coursename']
+
+ xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
+
+ # Grab the XML corresponding to the request from course.xml
+ module = 'problem'
+
+ try:
+ xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
+ except Exception,err:
+ print "[lib.dogfood.df_capa_problem] error in calling content_parser: %s" % err
+ xml = None
+
+ # if problem of given ID does not exist, then create it
+ # do this only if course.xml has a section named "DogfoodProblems"
+ if not xml:
+ m = re.match('filename([A-Za-z0-9_]+)$',id) # extract problem filename from ID given
+ if not m:
+ raise Exception,'[lib.dogfood.df_capa_problem] Illegal problem id %s' % id
+ pfn = m.group(1)
+ print '[lib.dogfood.df_capa_problem] creating new problem pfn=%s' % pfn
+
+ # add problem to course.xml
+ fn = settings.DATA_DIR + xp + 'course.xml'
+ xml = etree.parse(fn)
+ seq = xml.find('chapter/section[@name="DogfoodProblems"]/sequential') # assumes simplistic course.xml structure!
+ if seq==None:
+ raise Exception,"[lib.dogfood.views.df_capa_problem] missing DogfoodProblems section in course.xml!"
+ newprob = etree.Element('problem')
+ newprob.set('type','lecture')
+ newprob.set('showanswer','attempted')
+ newprob.set('rerandomize','never')
+ newprob.set('title',pfn)
+ newprob.set('filename',pfn)
+ newprob.set('name',pfn)
+ seq.append(newprob)
+ fp = open(fn,'w')
+ fp.write(etree.tostring(xml,pretty_print=True)) # write new XML
+ fp.close()
+
+ # now create new problem file
+ # update_problem(pfn,'\n\nThis is a new problem\n\n\n',coursename,overwrite=False)
+
+ # reset cache entry
+ user = request.user
+ groups = content_parser.user_groups(user)
+ options = {'dev_content':settings.DEV_CONTENT,
+ 'groups' : groups}
+ filename = xp + 'course.xml'
+ cache_key = filename + "_processed?dev_content:" + str(options['dev_content']) + "&groups:" + str(sorted(groups))
+ print '[lib.dogfood.df_capa_problem] cache_key = %s' % cache_key
+ #cache.delete(cache_key)
+ tree = content_parser.course_xml_process(xml) # add ID tags
+ cache.set(cache_key,etree.tostring(tree),60)
+ # settings.DEFAULT_GROUPS.append('dev') # force content_parser.course_file to not use cache
+
+ xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
+ if not xml:
+ print "[lib.dogfood.df_capa_problem] problem xml not found!"
+
+ # add problem ID to list so that is_staff check can be bypassed
+ request.session['dogfood_id'] = id
+
+ # hand over to quickedit to do the rest
+ return quickedit(request,id=id,qetemplate='dogfood.html',coursename=coursename)
+
+def quickedit(request, id=None, qetemplate='quickedit.html',coursename=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.
+
+ id is passed in from url resolution
+ qetemplate is used by dogfood.views.dj_capa_problem, to override normal template
+ '''
+ 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:
+ if not ('dogfood_id' in request.session and request.session['dogfood_id']==id):
+ return redirect('/')
+
+ if id=='course.xml':
+ return quickedit_git_reload(request)
+
+ # get coursename if stored
+ if not coursename:
+ coursename = multicourse_settings.get_coursename_from_request(request)
+ xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
+
+ 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 = OSFS(settings.DATA_DIR + xp),
+ #role = 'staff' if request.user.is_staff else 'student', # TODO: generalize this
+ )
+ instance=courseware.modules.get_module_class(module)(system,
+ xml,
+ id,
+ state=None)
+ log.info('ajax_url = ' + instance.ajax_url)
+
+ # create empty student state for this problem, if not previously existing
+ s = StudentModule.objects.filter(student=request.user,
+ module_id=id)
+ if len(s) == 0 or s is None:
+ smod=StudentModule(student=request.user,
+ module_type = 'problem',
+ module_id=id,
+ state=instance.get_state())
+ smod.save()
+
+ 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 '\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 \"%s\"" % err
+
+ if isok:
+ filename = instance.lcp.fileobject.name
+ fp = open(filename,'w') # TODO - replace with filestore call?
+ fp.write(newcode)
+ fp.close()
+ msg = "Problem changed! (%s)" % filename
+ instance, pxmls = get_lcp(coursename,id)
+
+ lcp = instance.lcp
+
+ # get the rendered problem HTML
+ phtml = instance.get_html()
+ # phtml = instance.get_problem_html()
+ # init_js = instance.get_init_js()
+ # destory_js = instance.get_destroy_js()
+
+ context = {'id':id,
+ 'msg' : msg,
+ 'lcp' : lcp,
+ 'filename' : lcp.fileobject.name,
+ 'pxmls' : pxmls,
+ 'phtml' : phtml,
+ "destroy_js":'',
+ 'init_js':'',
+ 'csrf':csrf(request)['csrf_token'],
+ }
+
+ result = render_to_response(qetemplate, context)
+ return result
+
+def quickedit_git_reload(request):
+ '''
+ reload course.xml and all courseware files for this course, from the git repo.
+ assumes the git repo has already been setup.
+ staff only.
+ '''
+ if not request.user.is_staff:
+ return redirect('/')
+
+ # get coursename if stored
+ coursename = multicourse_settings.get_coursename_from_request(request)
+ xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
+
+ msg = ""
+ if 'cancel' in request.POST:
+ return redirect("/courseware")
+
+ if 'gitupdate' in request.POST:
+ import os # FIXME - put at top?
+ cmd = "cd ../data%s; git reset --hard HEAD; git pull origin %s" % (xp,xp.replace('/',''))
+ msg += 'cmd: %s
' % cmd
+ ret = os.popen(cmd).read()
+ msg += '%s
' % ret.replace('<','<')
+ msg += "git update done!
"
+
+ context = {'id':id,
+ 'msg' : msg,
+ 'coursename' : coursename,
+ 'csrf':csrf(request)['csrf_token'],
+ }
+
+ result = render_to_response("gitupdate.html", context)
+ return result
diff --git a/lib/symmath/__init__.py b/lib/symmath/__init__.py
new file mode 100644
index 0000000000..6a5632c001
--- /dev/null
+++ b/lib/symmath/__init__.py
@@ -0,0 +1,2 @@
+from formula import *
+from symmath_check import *
diff --git a/lib/sympy_check/formula.py b/lib/symmath/formula.py
similarity index 84%
rename from lib/sympy_check/formula.py
rename to lib/symmath/formula.py
index 44bd020c4e..5eba1329cb 100644
--- a/lib/sympy_check/formula.py
+++ b/lib/symmath/formula.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#
# File: formula.py
-# Date: 04-May-12
+# Date: 04-May-12 (creation)
# Author: I. Chuang
#
# flexible python representation of a symbolic mathematical formula.
@@ -30,7 +30,7 @@ from lxml import etree
import requests
from copy import deepcopy
-print "[lib.sympy_check.formula] Warning: Dark code. Needs review before enabling in prod."
+print "[lib.symmath.formula] Warning: Dark code. Needs review before enabling in prod."
os.environ['PYTHONIOENCODING'] = 'utf-8'
@@ -143,11 +143,12 @@ class formula(object):
Representation of a mathematical formula object. Accepts mathml math expression for constructing,
and can produce sympy translation. The formula may or may not include an assignment (=).
'''
- def __init__(self,expr,asciimath=''):
+ def __init__(self,expr,asciimath='',options=None):
self.expr = expr.strip()
self.asciimath = asciimath
self.the_cmathml = None
self.the_sympy = None
+ self.options = options
def is_presentation_mathml(self):
return 'Error! Cannot process pmathml