diff --git a/djangoapps/courseware/capa/capa_problem.py b/djangoapps/courseware/capa/capa_problem.py index 0f9b5dacab..565e82a577 100644 --- a/djangoapps/courseware/capa/capa_problem.py +++ b/djangoapps/courseware/capa/capa_problem.py @@ -25,6 +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,OptionResponse import calc @@ -166,7 +167,7 @@ class LoncapaProblem(object): problems_simple = self.extract_problems(self.tree) for response in problems_simple: grader = response_types[response.tag](response, self.context, self.system) - results = grader.grade(answers) # call the responsetype instance to do the actual grading + results = grader.get_score(answers) # call the responsetype instance to do the actual grading self.correct_map.update(results) return self.correct_map @@ -239,7 +240,7 @@ class LoncapaProblem(object): # used to be # if problemtree.tag in html_special_response: - if hasattr(inputtypes, problemtree.tag): + if problemtree.tag in inputtypes.get_input_xml_tags(): # status is currently the answer for the problem ID for the input element, # but it will turn into a dict containing both the answer and any associated message # for the problem ID for the input element. @@ -266,9 +267,17 @@ class LoncapaProblem(object): # 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,...) + # This should be broken out into a helper function + # that handles all input objects + render_object = inputtypes.SimpleInput(system = self.system, + xml = problemtree, + state = {'value':value, + 'status': status, + 'id':problemtree.get('id'), + 'feedback':{'message':msg} + }, + use = 'capa_input') + return render_object.get_html() #function(problemtree, value, status, msg) # render the special response (textline, schematic,...) tree=Element(problemtree.tag) for item in problemtree: diff --git a/djangoapps/courseware/capa/inputtypes.py b/djangoapps/courseware/capa/inputtypes.py index ae8809c066..d50e41a5a3 100644 --- a/djangoapps/courseware/capa/inputtypes.py +++ b/djangoapps/courseware/capa/inputtypes.py @@ -32,8 +32,121 @@ from lxml import etree from mitxmako.shortcuts import render_to_string +def get_input_xml_tags(): + ''' Eventually, this will be for all registered input types ''' + return SimpleInput.get_xml_tags() + +class SimpleInput():# XModule + ''' Type for simple inputs -- plain HTML with a form element + State is a dictionary with optional keys: + * Value + * ID + * Status (answered, unanswered, unsubmitted) + * Feedback (dictionary containing keys for hints, errors, or other + feedback from previous attempt) + ''' + + xml_tags = {} ## Maps tags to functions + + @classmethod + def get_xml_tags(c): + return c.xml_tags.keys() + + @classmethod + def get_uses(c): + return ['capa_input', 'capa_transform'] + + def get_html(self): + return self.xml_tags[self.tag](self.xml, self.value, self.status, self.msg) + + def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'): + self.xml = xml + self.tag = xml.tag + if not state: + state = {} + ## ID should only come from one place. + ## If it comes from multiple, we use state first, XML second, and parameter + ## third. Since we don't make this guarantee, we can swap this around in + ## the future if there's a more logical order. + if item_id: + self.id = item_id + if xml.get('id'): + self.id = xml.get('id') + if 'id' in state: + self.id = state['id'] + self.system = system + + self.value = '' + if 'value' in state: + self.value = state['value'] + + self.msg = '' + if 'feedback' in state and 'message' in state['feedback']: + self.msg = state['feedback']['message'] + + self.status = 'unanswered' + if 'status' in state: + self.status = state['status'] + +## TODO +# class SimpleTransform(): +# ''' Type for simple XML to HTML transforms. Examples: +# * Math tags, which go from LON-CAPA-style m-tags to MathJAX +# ''' +# xml_tags = {} ## Maps tags to functions + +# @classmethod +# def get_xml_tags(c): +# return c.xml_tags.keys() + +# @classmethod +# def get_uses(c): +# return ['capa_transform'] + +# def get_html(self): +# return self.xml_tags[self.tag](self.xml, self.value, self.status, self.msg) + +# def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'): +# self.xml = xml +# self.tag = xml.tag +# if not state: +# state = {} +# if item_id: +# self.id = item_id +# if xml.get('id'): +# self.id = xml.get('id') +# if 'id' in state: +# self.id = state['id'] +# self.system = system + +# self.value = '' +# if 'value' in state: +# self.value = state['value'] + +# self.msg = '' +# if 'feedback' in state and 'message' in state['feedback']: +# self.msg = state['feedback']['message'] + +# self.status = 'unanswered' +# if 'status' in state: +# self.status = state['status'] + + +def register_render_function(fn, names=None, cls=SimpleInput): + if names == None: + SimpleInput.xml_tags[fn.__name__] = fn + else: + raise NotImplementedError + def wrapped(): + return fn + return wrapped + + + + #----------------------------------------------------------------------------- +@register_render_function def optioninput(element, value, status, msg=''): ''' Select option input type. @@ -67,7 +180,7 @@ def optioninput(element, value, status, msg=''): return etree.XML(html) #----------------------------------------------------------------------------- - +@register_render_function def choicegroup(element, value, status, msg=''): ''' Radio button inputs: multiple choice or true/false @@ -90,6 +203,7 @@ def choicegroup(element, value, status, msg=''): html=render_to_string("choicegroup.html", context) return etree.XML(html) +@register_render_function def textline(element, value, state, msg=""): eid=element.get('id') count = int(eid.split('_')[-2])-1 # HACK @@ -100,6 +214,7 @@ def textline(element, value, state, msg=""): #----------------------------------------------------------------------------- +@register_render_function def js_textline(element, value, status, msg=''): ''' Plan: We will inspect element to figure out type @@ -125,6 +240,7 @@ def js_textline(element, value, status, msg=''): #----------------------------------------------------------------------------- ## 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 @@ -140,6 +256,7 @@ def textbox(element, value, status, msg=''): return etree.XML(html) #----------------------------------------------------------------------------- +@register_render_function def schematic(element, value, status, msg=''): eid = element.get('id') height = element.get('height') @@ -164,6 +281,7 @@ def schematic(element, value, status, msg=''): #----------------------------------------------------------------------------- ### TODO: Move out of inputtypes +@register_render_function def math(element, value, status, msg=''): ''' This is not really an input type. It is a convention from Lon-CAPA, used for @@ -198,6 +316,7 @@ def math(element, value, status, msg=''): #----------------------------------------------------------------------------- +@register_render_function def solution(element, value, status, msg=''): ''' This is not really an input type. It is just a ... which is given an ID, @@ -218,6 +337,7 @@ def solution(element, value, status, msg=''): #----------------------------------------------------------------------------- +@register_render_function def imageinput(element, value, status, msg=''): ''' Clickable image as an input field. Element should specify the image source, height, and width, eg @@ -253,4 +373,3 @@ def imageinput(element, value, status, msg=''): print '[courseware.capa.inputtypes.imageinput] context=',context html=render_to_string("imageinput.html", context) return etree.XML(html) - diff --git a/djangoapps/courseware/capa/responsetypes.py b/djangoapps/courseware/capa/responsetypes.py index 190f52e7b6..380ab85c56 100644 --- a/djangoapps/courseware/capa/responsetypes.py +++ b/djangoapps/courseware/capa/responsetypes.py @@ -47,10 +47,10 @@ def compare_with_tolerance(v1, v2, tol): return abs(v1-v2) <= tolerance class GenericResponse(object): - __metaclass__=abc.ABCMeta + __metaclass__=abc.ABCMeta # abc = Abstract Base Class @abc.abstractmethod - def grade(self, student_answers): + def get_score(self, student_answers): pass @abc.abstractmethod @@ -61,7 +61,7 @@ class GenericResponse(object): def preprocess_response(self): pass -#Every response type needs methods "grade" and "get_answers" +#Every response type needs methods "get_score" and "get_answers" #----------------------------------------------------------------------------- @@ -95,7 +95,7 @@ class MultipleChoiceResponse(GenericResponse): raise Exception("should have exactly one choice group per multiplechoicceresponse") self.answer_id=self.answer_id[0] - def grade(self, student_answers): + def get_score(self, student_answers): if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices: return {self.answer_id:'correct'} else: @@ -132,7 +132,7 @@ class TrueFalseResponse(MultipleChoiceResponse): else: choice.set("name", "choice_"+choice.get("name")) - def grade(self, student_answers): + def get_score(self, student_answers): correct = set(self.correct_choices) answers = set(student_answers.get(self.answer_id, [])) @@ -162,7 +162,7 @@ class OptionResponse(GenericResponse): print '[courseware.capa.responsetypes.OR.init] answer_fields=%s' % (self.answer_fields) self.context = context - def grade(self, student_answers): + def get_score(self, student_answers): cmap = {} amap = self.get_answers() for aid in amap: @@ -194,7 +194,7 @@ class NumericalResponse(GenericResponse): except Exception, err: self.answer_id = None - def grade(self, student_answers): + def get_score(self, student_answers): ''' Display HTML for a numeric response ''' student_answer = student_answers[self.answer_id] try: @@ -300,7 +300,7 @@ def sympy_check2(): else: self.code = answer.text - def grade(self, student_answers): + def get_score(self, student_answers): ''' student_answers is a dict with everything from request.POST, but with the first part of each key removed (the string before the first "_"). @@ -363,7 +363,7 @@ def sympy_check2(): print "oops in customresponse (cfn) error %s" % err # print "context = ",self.context print traceback.format_exc() - if settings.DEBUG: print "[courseware.capa.responsetypes.customresponse.grade] ret = ",ret + if settings.DEBUG: print "[courseware.capa.responsetypes.customresponse.get_score] ret = ",ret if type(ret)==dict: correct[0] = 'correct' if ret['ok'] else 'incorrect' msg = ret['msg'] @@ -428,7 +428,7 @@ class ExternalResponse(GenericResponse): self.tests = xml.get('answer') - def grade(self, student_answers): + def get_score(self, student_answers): submission = [student_answers[k] for k in sorted(self.answer_ids)] self.context.update({'submission':submission}) @@ -504,7 +504,7 @@ class FormulaResponse(GenericResponse): self.case_sensitive = False - def grade(self, student_answers): + def get_score(self, student_answers): variables=self.samples.split('@')[0].split(',') numsamples=int(self.samples.split('@')[1].split('#')[1]) sranges=zip(*map(lambda x:map(float, x.split(",")), @@ -566,7 +566,7 @@ class SchematicResponse(GenericResponse): else: self.code = answer.text - def grade(self, student_answers): + def get_score(self, student_answers): from capa_problem import global_context submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)] self.context.update({'submission':submission}) @@ -605,7 +605,7 @@ class ImageResponse(GenericResponse): self.ielements = xml.findall('imageinput') self.answer_ids = [ie.get('id') for ie in self.ielements] - def grade(self, student_answers): + def get_score(self, student_answers): correct_map = {} expectedset = self.get_answers() diff --git a/djangoapps/courseware/capa/util.py b/djangoapps/courseware/capa/util.py index b85073d78c..d042aa21d3 100644 --- a/djangoapps/courseware/capa/util.py +++ b/djangoapps/courseware/capa/util.py @@ -1,6 +1,7 @@ def contextualize_text(text, context): # private ''' Takes a string with variables. E.g. $a+$b. Does a substitution of those variables from the context ''' + if not text: return text for key in sorted(context, lambda x,y:cmp(len(y),len(x))): text=text.replace('$'+key, str(context[key])) return text diff --git a/djangoapps/courseware/module_render.py b/djangoapps/courseware/module_render.py index 210cddb0b2..fc7240286f 100644 --- a/djangoapps/courseware/module_render.py +++ b/djangoapps/courseware/module_render.py @@ -13,8 +13,8 @@ from fs.osfs import OSFS from django.conf import settings from mitxmako.shortcuts import render_to_string - from models import StudentModule +from multicourse import multicourse_settings import courseware.modules @@ -31,6 +31,8 @@ class I4xSystem(object): self.track_function = track_function if not filestore: self.filestore = OSFS(settings.DATA_DIR) + else: + self.filestore = filestore self.render_function = render_function self.exception404 = Http404 def __repr__(self): @@ -95,15 +97,15 @@ def render_x_module(user, request, xml_module, module_object_preload): state = smod.state # get coursename if stored - if 'coursename' in request.session: coursename = request.session['coursename'] - else: coursename = None + coursename = multicourse_settings.get_coursename_from_request(request) + xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course # Create a new instance ajax_url = settings.MITX_ROOT_URL + '/modx/'+module_type+'/'+module_id+'/' system = I4xSystem(track_function = make_track_function(request), render_function = lambda x: render_module(user, request, x, module_object_preload), ajax_url = ajax_url, - filestore = None + filestore = OSFS(settings.DATA_DIR + xp), ) instance=module_class(system, etree.tostring(xml_module), diff --git a/djangoapps/courseware/views.py b/djangoapps/courseware/views.py index 56409b65a5..857bdd2e03 100644 --- a/djangoapps/courseware/views.py +++ b/djangoapps/courseware/views.py @@ -2,6 +2,8 @@ import logging import urllib import json +from fs.osfs import OSFS + from django.conf import settings from django.core.context_processors import csrf from django.contrib.auth.models import User @@ -38,9 +40,7 @@ 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 + coursename = multicourse_settings.get_coursename_from_request(request) student_objects = User.objects.all()[:100] student_info = [{'username' :s.username, @@ -68,8 +68,7 @@ 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 + coursename = multicourse_settings.get_coursename_from_request(request) context={'name':user_info.name, 'username':student.username, @@ -110,8 +109,7 @@ def render_section(request, section): if not settings.COURSEWARE_ENABLED: return redirect('/') - if 'coursename' in request.session: coursename = request.session['coursename'] - else: coursename = None + coursename = multicourse_settings.get_coursename_from_request(request) try: dom = content_parser.section_file(user, section, coursename) @@ -251,8 +249,8 @@ 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 + coursename = multicourse_settings.get_coursename_from_request(request) + xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course # Grab the XML corresponding to the request from course.xml try: @@ -269,7 +267,7 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): system = I4xSystem(track_function = make_track_function(request), render_function = None, ajax_url = ajax_url, - filestore = None + filestore = OSFS(settings.DATA_DIR + xp), ) try: @@ -307,12 +305,12 @@ def quickedit(request, id=None): 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 request.user.is_staff: return redirect('/') # get coursename if stored - if 'coursename' in request.session: coursename = request.session['coursename'] - else: coursename = None + 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 @@ -325,9 +323,8 @@ def quickedit(request, id=None): 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 + 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, diff --git a/djangoapps/multicourse/multicourse_settings.py b/djangoapps/multicourse/multicourse_settings.py index 99c9ef8620..7ed9ec4efe 100644 --- a/djangoapps/multicourse/multicourse_settings.py +++ b/djangoapps/multicourse/multicourse_settings.py @@ -42,6 +42,11 @@ else: # default to 6.002_Spring_2012 #----------------------------------------------------------------------------- # wrapper functions around course settings +def get_coursename_from_request(request): + if 'coursename' in request.session: coursename = request.session['coursename'] + else: coursename = None + return coursename + def get_course_settings(coursename): if not coursename: if hasattr(settings,'COURSE_DEFAULT'): diff --git a/djangoapps/multicourse/views.py b/djangoapps/multicourse/views.py index d0662b710e..15c4a7a382 100644 --- a/djangoapps/multicourse/views.py +++ b/djangoapps/multicourse/views.py @@ -1 +1,24 @@ -# multicourse/views.py +import datetime +import json +import sys + +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 multicourse import multicourse_settings + +def mitxhome(request): + ''' Home page (link from main header). List of courses. ''' + if settings.ENABLE_MULTICOURSE: + context = {'courseinfo' : multicourse_settings.COURSE_SETTINGS} + return render_to_response("mitxhome.html", context) + return info(request) + diff --git a/lib/util/views.py b/lib/util/views.py index a071208e92..941e4082c5 100644 --- a/lib/util/views.py +++ b/lib/util/views.py @@ -61,12 +61,6 @@ 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) - # From http://djangosnippets.org/snippets/1042/ def parse_accept_header(accept): """Parse the Accept header *accept*, returning a list with pairs of diff --git a/templates/mathjax_include.html b/templates/mathjax_include.html new file mode 100644 index 0000000000..a22f52fdc7 --- /dev/null +++ b/templates/mathjax_include.html @@ -0,0 +1,89 @@ +## +## File: templates/mathjax_include.html +## +## Advanced mathjax using 2.0-latest CDN for Dynamic Math +## +## This enables ASCIIMathJAX, and is used by js_textbox + + + <%block name="headextra"/> + + + + + diff --git a/templates/mitxhome.html b/templates/mitxhome.html index af821c1c84..182446ed35 100644 --- a/templates/mitxhome.html +++ b/templates/mitxhome.html @@ -28,10 +28,10 @@ $(document).ready(function(){

Courses available:

+ % for coursename, info in courseinfo.items(): +
  • ${info['title']} (${coursename})
  • + % endfor + diff --git a/templates/optioninput.html b/templates/optioninput.html new file mode 100644 index 0000000000..c23f433082 --- /dev/null +++ b/templates/optioninput.html @@ -0,0 +1,25 @@ +
    + + + + + + % if state == 'unsubmitted': + + % elif state == 'correct': + + % elif state == 'incorrect': + + % elif state == 'incomplete': + + % endif +
    diff --git a/templates/problem.html b/templates/problem.html index 750fe7f0cc..af712f67ac 100644 --- a/templates/problem.html +++ b/templates/problem.html @@ -3,6 +3,15 @@ % if problem['weight']: : ${ problem['weight'] } points % endif +% if settings.QUICKEDIT: + +
    +
    +
    +
    +Quick +Edit Problem
    +% endif
    diff --git a/urls.py b/urls.py index aef852b99d..4f9b0912ab 100644 --- a/urls.py +++ b/urls.py @@ -70,7 +70,7 @@ if settings.COURSEWARE_ENABLED: ) if settings.ENABLE_MULTICOURSE: - urlpatterns += (url(r'^mitxhome$', 'util.views.mitxhome'),) + urlpatterns += (url(r'^mitxhome$', 'multicourse.views.mitxhome'),) if settings.QUICKEDIT: urlpatterns += (url(r'^quickedit/(?P[^/]*)$', 'courseware.views.quickedit'),)