Commit ad448828 by Isaac Chuang Committed by Piotr Mitros

Ike's changes to enable multicourse, new response types, etc.

parent e45eb9fb
......@@ -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
html=render_to_string("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail})
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)
#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,
}
......
......@@ -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
......
<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
# 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')
# 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)
......
#!/usr/bin/python
from loncapa_check import *
#!/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)
......
......@@ -3,7 +3,6 @@ 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
from django.core.mail import send_mail
......@@ -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)
......@@ -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 &amp; 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:
......
<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>
......@@ -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)), \
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment