diff --git a/courseware/capa/capa_problem.py b/courseware/capa/capa_problem.py index bedd58a33b..75a14ec82c 100644 --- a/courseware/capa/capa_problem.py +++ b/courseware/capa/capa_problem.py @@ -11,11 +11,12 @@ import calc, eia from util import contextualize_text from inputtypes import textline, schematic -from responsetypes import numericalresponse, formularesponse, customresponse +from responsetypes import numericalresponse, formularesponse, customresponse, schematicresponse response_types = {'numericalresponse':numericalresponse, 'formularesponse':formularesponse, - 'customresponse':customresponse} + 'customresponse':customresponse, + 'schematicresponse':schematicresponse} entry_types = ['textline', 'schematic'] response_properties = ["responseparam", "answer"] # How to convert from original XML to HTML @@ -23,6 +24,7 @@ response_properties = ["responseparam", "answer"] html_transforms = {'problem': {'tag':'div'}, "numericalresponse": {'tag':'span'}, "customresponse": {'tag':'span'}, + "schematicresponse": {'tag':'span'}, "formularesponse": {'tag':'span'}, "text": {'tag':'span'}} @@ -36,7 +38,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", "formularesponse", "text"] +html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text"] # These should be transformed html_special_response = {"textline":textline.render, "schematic":schematic.render} diff --git a/courseware/capa/content_parser.py b/courseware/capa/content_parser.py deleted file mode 100644 index 5b0e197bc5..0000000000 --- a/courseware/capa/content_parser.py +++ /dev/null @@ -1,117 +0,0 @@ -try: - from django.conf import settings - from auth.models import UserProfile -except: - settings = None - -from xml.dom.minidom import parse, parseString - -from lxml import etree - -''' This file will eventually form an abstraction layer between the -course XML file and the rest of the system. - -TODO: Shift everything from xml.dom.minidom to XPath (or XQuery) -''' - -def xpath(xml, query_string, **args): - ''' Safe xpath query into an xml tree: - * xml is the tree. - * query_string is the query - * args are the parameters. Substitute for {params}. - We should remove this with the move to lxml. - We should also use lxml argument passing. ''' - doc = etree.fromstring(xml) - print type(doc) - def escape(x): - # TODO: This should escape the string. For now, we just assume it's made of valid characters. - # Couldn't figure out how to escape for lxml in a few quick Googles - valid_chars="".join(map(chr, range(ord('a'),ord('z')+1)+range(ord('A'),ord('Z')+1)+range(ord('0'), ord('9')+1)))+"_ " - for e in x: - if e not in valid_chars: - raise Exception("Invalid char in xpath expression. TODO: Escape") - return x - - args=dict( ((k, escape(args[k])) for k in args) ) - print args - results = doc.xpath(query_string.format(**args)) - return results - -def xpath_remove(tree, path): - ''' Remove all items matching path from lxml tree. Works in - place.''' - items = tree.xpath(path) - for item in items: - item.getparent().remove(item) - return tree - -if __name__=='__main__': - print xpath('', '/{search}/problem[@name="{name}"]', search='html', name="Bob") - -def item(l, default="", process=lambda x:x): - if len(l)==0: - return default - elif len(l)==1: - return process(l[0]) - else: - raise Exception('Malformed XML') - - -def course_file(user): - # TODO: Cache. Also, return the libxml2 object. - return settings.DATA_DIR+UserProfile.objects.get(user=user).courseware - -def module_xml(coursefile, module, id_tag, module_id): - ''' Get XML for a module based on module and module_id. Assumes - module occurs once in courseware XML file.. ''' - doc = etree.parse(coursefile) - - # Sanitize input - if not module.isalnum(): - raise Exception("Module is not alphanumeric") - if not module_id.isalnum(): - raise Exception("Module ID is not alphanumeric") - xpath_search='//*/{module}[(@{id_tag} = "{id}") or (@id = "{id}")]'.format(module=module, - id_tag=id_tag, - id=module_id) - #result_set=doc.xpathEval(xpath_search) - result_set=doc.xpath(xpath_search) - if len(result_set)>1: - print "WARNING: Potentially malformed course file", module, module_id - if len(result_set)==0: - return None - return etree.tostring(result_set[0]) - #return result_set[0].serialize() - -def toc_from_xml(coursefile, active_chapter, active_section): - dom=parse(coursefile) - - course = dom.getElementsByTagName('course')[0] - name=course.getAttribute("name") - chapters = course.getElementsByTagName('chapter') - ch=list() - for c in chapters: - if c.getAttribute("name") == 'hidden': - continue - sections=list() - for s in c.getElementsByTagName('section'): - sections.append({'name':s.getAttribute("name"), - 'time':s.getAttribute("time"), - 'format':s.getAttribute("format"), - 'due':s.getAttribute("due"), - 'active':(c.getAttribute("name")==active_chapter and \ - s.getAttribute("name")==active_section)}) - ch.append({'name':c.getAttribute("name"), - 'sections':sections, - 'active':(c.getAttribute("name")==active_chapter)}) - return ch - -def dom_select(dom, element_type, element_name): - if dom==None: - return None - elements=dom.getElementsByTagName(element_type) - for e in elements: - if e.getAttribute("name")==element_name: - return e - return None - diff --git a/courseware/capa/inputtypes.py b/courseware/capa/inputtypes.py index be0bdbcfbd..db98363872 100644 --- a/courseware/capa/inputtypes.py +++ b/courseware/capa/inputtypes.py @@ -17,7 +17,19 @@ class schematic(object): eid = element.get('id') height = element.get('height') width = element.get('width') - context = {'id':eid, 'value':value, 'state':state, 'width':width, 'height':height} + parts = element.get('parts') + analyses = element.get('analyses') + initial_value = element.get('initial_value') + context = { + 'id':eid, + 'value':value, + 'initial_value':initial_value, + 'state':state, + 'width':width, + 'height':height, + 'parts':parts, + 'analyses':analyses, + } html=render_to_string("schematicinput.html", context) return etree.XML(html) diff --git a/courseware/capa/responsetypes.py b/courseware/capa/responsetypes.py index 61ed2b30cb..3e3cefd8ab 100644 --- a/courseware/capa/responsetypes.py +++ b/courseware/capa/responsetypes.py @@ -1,4 +1,4 @@ -import random, numpy, math, scipy +import random, numpy, math, scipy, json from util import contextualize_text from calc import evaluator import random, math @@ -63,7 +63,6 @@ class customresponse(object): # be handled by capa_problem return {} - class formularesponse(object): def __init__(self, xml, context): self.xml = xml @@ -114,3 +113,28 @@ class formularesponse(object): def get_answers(self): return {self.answer_id:self.correct_answer} + +class schematicresponse(object): + def __init__(self, xml, context): + self.xml = xml + self.answer_ids = xml.xpath('//*[@id=$id]//schematic/@id', + id=xml.get('id')) + self.context = context + answer = xml.xpath('//*[@id=$id]//answer', + id=xml.get('id'))[0] + answer_src = answer.get('src') + if answer_src != None: + self.code = open(settings.DATA_DIR+'src/'+answer_src).read() + else: + self.code = answer.text + + def grade(self, student_answers): + submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)] + self.context.update({'submission':submission}) + exec self.code in global_context, self.context + return zip(sorted(self.answer_ids), self.context['correct']) + + def get_answers(self): + # Since this is explicitly specified in the problem, this will + # be handled by capa_problem + return {} diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 5d91eb8d06..c5c0600f9d 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -1,9 +1,13 @@ -from django.conf import settings -from xml.dom.minidom import parse, parseString +try: + from django.conf import settings + from auth.models import UserProfile +except: + settings = None from lxml import etree -from auth.models import UserProfile +import json +import hashlib ''' This file will eventually form an abstraction layer between the course XML file and the rest of the system. @@ -11,11 +15,18 @@ course XML file and the rest of the system. TODO: Shift everything from xml.dom.minidom to XPath (or XQuery) ''' +def fasthash(string): + m = hashlib.new("md4") + m.update(string) + return "id"+m.hexdigest() + def xpath(xml, query_string, **args): ''' Safe xpath query into an xml tree: * xml is the tree. * query_string is the query - * args are the parameters. Substitute for {params}. ''' + * args are the parameters. Substitute for {params}. + We should remove this with the move to lxml. + We should also use lxml argument passing. ''' doc = etree.fromstring(xml) print type(doc) def escape(x): @@ -32,8 +43,17 @@ def xpath(xml, query_string, **args): results = doc.xpath(query_string.format(**args)) return results +def xpath_remove(tree, path): + ''' Remove all items matching path from lxml tree. Works in + place.''' + items = tree.xpath(path) + for item in items: + item.getparent().remove(item) + return tree + if __name__=='__main__': - print xpath('', '/{search}/problem[@name="{name}"]', search='html', name="Bob") + print xpath('', '/{search}/problem[@name="{name}"]', + search='html', name="Bob") def item(l, default="", process=lambda x:x): if len(l)==0: @@ -42,16 +62,39 @@ def item(l, default="", process=lambda x:x): return process(l[0]) else: raise Exception('Malformed XML') + +def id_tag(course): + ''' Tag all course elements with unique IDs ''' + default_ids = {'video':'youtube', + 'problem':'filename', + 'sequential':'id', + 'html':'filename', + 'vertical':'id', + 'tab':'id', + 'schematic':'id'} + # Tag elements with unique IDs + elements = course.xpath("|".join(['//'+c for c in default_ids])) + for elem in elements: + if elem.get('id'): + pass + elif elem.get(default_ids[elem.tag]): + new_id = elem.get(default_ids[elem.tag]) # Convert to alphanumeric + new_id = "".join([a for a in new_id if a.isalnum()]) + elem.set('id', new_id) + else: + elem.set('id', fasthash(etree.tostring(elem))) def course_file(user): - # TODO: Cache. Also, return the libxml2 object. - return settings.DATA_DIR+UserProfile.objects.get(user=user).courseware + # TODO: Cache. + tree = etree.parse(settings.DATA_DIR+UserProfile.objects.get(user=user).courseware) + id_tag(tree) + return tree def module_xml(coursefile, module, id_tag, module_id): ''' Get XML for a module based on module and module_id. Assumes module occurs once in courseware XML file.. ''' - doc = etree.parse(coursefile) + doc = coursefile # Sanitize input if not module.isalnum(): @@ -70,35 +113,24 @@ def module_xml(coursefile, module, id_tag, module_id): return etree.tostring(result_set[0]) #return result_set[0].serialize() -def toc_from_xml(coursefile, active_chapter, active_section): - dom=parse(coursefile) +def toc_from_xml(dom, active_chapter, active_section): + name = dom.xpath('//course/@name')[0] - course = dom.getElementsByTagName('course')[0] - name=course.getAttribute("name") - chapters = course.getElementsByTagName('chapter') + chapters = dom.xpath('//course[@name=$name]/chapter', name=name) ch=list() for c in chapters: - if c.getAttribute("name") == 'hidden': + if c.get('name') == 'hidden': continue sections=list() - for s in c.getElementsByTagName('section'): - sections.append({'name':s.getAttribute("name"), - 'time':s.getAttribute("time"), - 'format':s.getAttribute("format"), - 'due':s.getAttribute("due"), - 'active':(c.getAttribute("name")==active_chapter and \ - s.getAttribute("name")==active_section)}) - ch.append({'name':c.getAttribute("name"), + for s in dom.xpath('//course[@name=$name]/chapter[@name=$chname]/section', name=name, chname=c.get('name')): + sections.append({'name':s.get("name") or "", + 'time':s.get("time") or "", + 'format':s.get("format") or "", + 'due':s.get("due") or "", + 'active':(c.get("name")==active_chapter and \ + s.get("name")==active_section)}) + ch.append({'name':c.get("name"), 'sections':sections, - 'active':(c.getAttribute("name")==active_chapter)}) + 'active':(c.get("name")==active_chapter)}) return ch -def dom_select(dom, element_type, element_name): - if dom==None: - return None - elements=dom.getElementsByTagName(element_type) - for e in elements: - if e.getAttribute("name")==element_name: - return e - return None - diff --git a/courseware/module_render.py b/courseware/module_render.py index 58574df190..9375facc4c 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -16,10 +16,12 @@ from django.http import Http404 import urllib -import capa_module -import video_module -import html_module -import schematic_module +import courseware.modules.capa_module +import courseware.modules.video_module +import courseware.modules.vertical_module +import courseware.modules.html_module +import courseware.modules.schematic_module +import courseware.modules.seq_module from models import StudentModule @@ -29,12 +31,18 @@ from django.conf import settings import content_parser +import sys + +from lxml import etree import uuid -modx_modules={'problem':capa_module.LoncapaModule, - 'video':video_module.VideoModule, - 'html':html_module.HtmlModule, - 'schematic':schematic_module.SchematicModule} +## TODO: Add registration mechanism +modx_modules={'problem':courseware.modules.capa_module.LoncapaModule, + 'video':courseware.modules.video_module.VideoModule, + 'html':courseware.modules.html_module.HtmlModule, + 'vertical':courseware.modules.vertical_module.VerticalModule, + 'sequential':courseware.modules.seq_module.SequentialModule, + 'schematic':courseware.modules.schematic_module.SchematicModule} def make_track_function(request): def f(event_type, event): @@ -44,11 +52,12 @@ def make_track_function(request): def modx_dispatch(request, module=None, dispatch=None, id=None): ''' Generic view for extensions. ''' # Grab the student information for the module from the database + print module, request.user, id s = StudentModule.objects.filter(module_type=module, student=request.user, module_id=id) if len(s) == 0: - print "ls404" + print "ls404", module, request.user, id raise Http404 s=s[0] @@ -67,83 +76,25 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): s.module_id, ajax_url=ajax_url, state=s.state, - track_function = make_track_function(request)) + track_function = make_track_function(request), + render_function = render_module, + meta = request) # Let the module handle the AJAX ajax_return=instance.handle_ajax(dispatch, request.POST) # Save the state back to the database s.state=instance.get_state() - s.grade=instance.get_score()['score'] + if instance.get_score() != None: + s.grade=instance.get_score()['score'] s.save() # Return whatever the module wanted to return to the client/caller return HttpResponse(ajax_return) -def vertical_module(request, module): - ''' Layout module which lays out content vertically. - ''' - contents=[(e.getAttribute("name"),render_module(request, e)) \ - for e in module.childNodes \ - if e.nodeType==1] - init_js="".join([e[1]['init_js'] for e in contents if 'init_js' in e[1]]) - destroy_js="".join([e[1]['destroy_js'] for e in contents if 'destroy_js' in e[1]]) - - return {'init_js':init_js, - 'destroy_js':destroy_js, - 'content':render_to_string('vert_module.html',{'items':contents}), - 'type':'vertical'} - -def seq_module(request, module): - ''' Layout module which lays out content in a temporal sequence - ''' - def j(m): - # jsonify contents so it can be embedded in a js array - # We also need to split tags so they don't break - # mid-string - if 'init_js' not in m: m['init_js']="" - if 'type' not in m: m['init_js']="" - content=json.dumps(m['content']) - content=content.replace('', '<"+"/script>') - - return {'content':content, - "destroy_js":m['destroy_js'], - 'init_js':m['init_js'], - 'type':m['type']} - contents=[(e.getAttribute("name"),j(render_module(request, e))) \ - for e in module.childNodes \ - if e.nodeType==1] - - js="" - - iid=uuid.uuid1().hex - - params={'items':contents, - 'id':"seq"} - - # TODO/BUG: Destroy JavaScript should only be called for the active view - # This calls it for all the views - # - # To fix this, we'd probably want to have some way of assigning unique - # IDs to sequences. - destroy_js="".join([e[1]['destroy_js'] for e in contents if 'destroy_js' in e[1]]) - - if module.nodeName == 'sequential': - return {'init_js':js+render_to_string('seq_module.js',params), - "destroy_js":destroy_js, - 'content':render_to_string('seq_module.html',params), - 'type':'sequential'} - if module.nodeName == 'tab': - params['id'] = 'tab' - return {'init_js':js+render_to_string('tab_module.js',params), - "destroy_js":destroy_js, - 'content':render_to_string('tab_module.html',params), - 'type':'tab'} - - def render_x_module(request, xml_module): ''' Generic module for extensions. This renders to HTML. ''' # Check if problem has an instance in DB - module_type=xml_module.nodeName + module_type=xml_module.tag module_class=modx_modules[module_type] - module_id=xml_module.getAttribute(module_class.id_attribute) + module_id=xml_module.get('id') #module_class.id_attribute) or "" # Grab state from database s = StudentModule.objects.filter(student=request.user, @@ -157,11 +108,13 @@ def render_x_module(request, xml_module): # Create a new instance ajax_url = '/modx/'+module_type+'/'+module_id+'/' - instance=module_class(xml_module.toxml(), + instance=module_class(etree.tostring(xml_module), module_id, ajax_url=ajax_url, state=state, - track_function = make_track_function(request)) + track_function = make_track_function(request), + render_function = render_module, + meta = request) # If instance wasn't already in the database, create it if len(s) == 0: @@ -179,20 +132,8 @@ def render_x_module(request, xml_module): return content -module_types={'video':render_x_module, - 'html':render_x_module, - 'tab':seq_module, - 'vertical':vertical_module, - 'sequential':seq_module, - 'problem':render_x_module, - 'schematic':render_x_module - } - def render_module(request, module): ''' Generic dispatch for internal modules. ''' - if module==None: + if module==None : return {"content":""} - if str(module.localName) in module_types: - return module_types[module.localName](request, module) - print "rm404" - raise Http404 + return render_x_module(request, module) diff --git a/courseware/modules/__init__.py b/courseware/modules/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/courseware/capa_module.py b/courseware/modules/capa_module.py similarity index 98% rename from courseware/capa_module.py rename to courseware/modules/capa_module.py index e13ee453fe..591f9c7516 100644 --- a/courseware/capa_module.py +++ b/courseware/modules/capa_module.py @@ -2,14 +2,14 @@ import random, numpy, math, scipy, sys, StringIO, os, struct, json from x_module import XModule import sys -from capa.capa_problem import LoncapaProblem +from courseware.capa.capa_problem import LoncapaProblem from django.http import Http404 import dateutil import dateutil.parser import datetime -import content_parser +import courseware.content_parser as content_parser from lxml import etree @@ -108,8 +108,8 @@ class LoncapaModule(XModule): return html - def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None): - XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function) + def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None, meta = None): + XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) self.attempts = 0 self.max_attempts = None diff --git a/courseware/html_module.py b/courseware/modules/html_module.py similarity index 90% rename from courseware/html_module.py rename to courseware/modules/html_module.py index 853a322236..ce26ea0045 100644 --- a/courseware/html_module.py +++ b/courseware/modules/html_module.py @@ -25,8 +25,8 @@ class HtmlModule(XModule): textlist=[i for i in textlist if type(i)==str] return "".join(textlist) - def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None): - XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function) + def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None, meta = None): + XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) xmltree=etree.fromstring(xml) self.filename = None filename_l=xmltree.xpath("/html/@filename") diff --git a/courseware/schematic_module.py b/courseware/modules/schematic_module.py similarity index 88% rename from courseware/schematic_module.py rename to courseware/modules/schematic_module.py index 5a7fa390a0..b97e80541b 100644 --- a/courseware/schematic_module.py +++ b/courseware/modules/schematic_module.py @@ -18,6 +18,6 @@ class SchematicModule(XModule): def get_html(self): return ''.format(item_id=self.item_id) - def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None): - XModule.__init__(self, xml, item_id, ajax_url, track_url, state) + def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, render_function = None, meta = None): + XModule.__init__(self, xml, item_id, ajax_url, track_url, state, render_function) diff --git a/courseware/modules/seq_module.py b/courseware/modules/seq_module.py new file mode 100644 index 0000000000..5845c23a55 --- /dev/null +++ b/courseware/modules/seq_module.py @@ -0,0 +1,87 @@ +from x_module import XModule +from lxml import etree +from django.http import Http404 + +import json + +## TODO: Abstract out from Django +from django.conf import settings +from djangomako.shortcuts import render_to_response, render_to_string + +class SequentialModule(XModule): + ''' Layout module which lays out content in a temporal sequence + ''' + id_attribute = 'id' + + def get_state(self): + return json.dumps({ 'position':self.position }) + + def get_xml_tags(): + return ["sequential", 'tab'] + + def get_html(self): + return self.content + + def get_init_js(self): + return self.init_js + + def get_destroy_js(self): + return self.destroy_js + + def handle_ajax(self, dispatch, get): + print "GET", get + print "DISPATCH", dispatch + if dispatch=='goto_position': + self.position = int(get['position']) + return json.dumps({'success':True}) + raise Http404() + + def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None, meta = None): + XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) + xmltree=etree.fromstring(xml) + + self.position = 1 + + if state!=None: + state = json.loads(state) + if 'position' in state: self.position = int(state['position']) + + def j(m): + ''' jsonify contents so it can be embedded in a js array + We also need to split tags so they don't break + mid-string''' + if 'init_js' not in m: m['init_js']="" + if 'type' not in m: m['init_js']="" + content=json.dumps(m['content']) + content=content.replace('', '<"+"/script>') + + return {'content':content, + "destroy_js":m['destroy_js'], + 'init_js':m['init_js'], + 'type':m['type']} + + contents=[(e.get("name"),j(render_function(meta, e))) \ + for e in xmltree] + + js="" + + params={'items':contents, + 'id':item_id, + 'position': self.position} + + # TODO/BUG: Destroy JavaScript should only be called for the active view + # This calls it for all the views + # + # To fix this, we'd probably want to have some way of assigning unique + # IDs to sequences. + destroy_js="".join([e[1]['destroy_js'] for e in contents if 'destroy_js' in e[1]]) + + if xmltree.tag == 'sequential': + self.init_js=js+render_to_string('seq_module.js',params) + self.destroy_js=destroy_js + self.content=render_to_string('seq_module.html',params) + if xmltree.tag == 'tab': + params['id'] = 'tab' + self.init_js=js+render_to_string('tab_module.js',params) + self.destroy_js=destroy_js + self.content=render_to_string('tab_module.html',params) diff --git a/courseware/modules/vertical_module.py b/courseware/modules/vertical_module.py new file mode 100644 index 0000000000..b5ad1be368 --- /dev/null +++ b/courseware/modules/vertical_module.py @@ -0,0 +1,34 @@ +from x_module import XModule +from lxml import etree + +import json + +## TODO: Abstract out from Django +from django.conf import settings +from djangomako.shortcuts import render_to_response, render_to_string + +class VerticalModule(XModule): + id_attribute = 'id' + + def get_state(self): + return json.dumps({ }) + + def get_xml_tags(): + return "vertical" + + def get_html(self): + return render_to_string('vert_module.html',{'items':self.contents}) + + def get_init_js(self): + return self.init_js_text + + def get_destroy_js(self): + return self.destroy_js_text + + def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None, meta = None): + XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) + xmltree=etree.fromstring(xml) + self.contents=[(e.get("name"),self.render_function(meta, e)) \ + for e in xmltree] + self.init_js_text="".join([e[1]['init_js'] for e in self.contents if 'init_js' in e[1]]) + self.destroy_js_text="".join([e[1]['destroy_js'] for e in self.contents if 'destroy_js' in e[1]]) diff --git a/courseware/video_module.py b/courseware/modules/video_module.py similarity index 53% rename from courseware/video_module.py rename to courseware/modules/video_module.py index 0fdb54f119..6fadc17941 100644 --- a/courseware/video_module.py +++ b/courseware/modules/video_module.py @@ -1,4 +1,5 @@ from x_module import XModule +from lxml import etree import json @@ -7,47 +8,55 @@ from django.conf import settings from djangomako.shortcuts import render_to_response, render_to_string class VideoModule(XModule): - id_attribute = 'youtube' + #id_attribute = 'youtube' video_time = 0 def handle_ajax(self, dispatch, get): - if dispatch == 'time': - self.video_time = int(get['time']) - print self.video_time - - return json.dumps("True") + print "GET", get + print "DISPATCH", dispatch + if dispatch=='goto_position': + self.position = int(float(get['position'])) + print "NEW POSITION", self.position + return json.dumps({'success':True}) + raise Http404() def get_state(self): - return json.dumps({ 'time':self.video_time }) + print "STATE POSITION", self.position + return json.dumps({ 'position':self.position }) def get_xml_tags(): ''' Tags in the courseware file guaranteed to correspond to the module ''' return "video" def video_list(self): - l=self.item_id.split(',') + l=self.youtube.split(',') l=[i.split(":") for i in l] return json.dumps(dict(l)) def get_html(self): return render_to_string('video.html',{'streams':self.video_list(), 'id':self.item_id, - 'video_time':self.video_time}) + 'position':self.position}) def get_init_js(self): ''' JavaScript code to be run when problem is shown. Be aware that this may happen several times on the same page (e.g. student switching tabs). Common functions should be put in the main course .js files for now. ''' + print "INIT POSITION", self.position return render_to_string('video_init.js',{'streams':self.video_list(), 'id':self.item_id, - 'video_time':self.video_time}) + 'position':self.position}) def get_destroy_js(self): - return "videoDestroy();" + return "videoDestroy(\""+self.item_id+"\");" - def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None): - XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function) - print state - if state!=None and "time" not in json.loads(state): - self.video_time = 0 + def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None, meta = None): + XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) + self.youtube = etree.XML(xml).get('youtube') + self.position = 0 + if state!=None: + state = json.loads(state) + if 'position' in state: self.position = int(float(state['position'])) + print "POOSITION IN STATE" + print "LOAD POSITION", self.position diff --git a/courseware/x_module.py b/courseware/modules/x_module.py similarity index 78% rename from courseware/x_module.py rename to courseware/modules/x_module.py index 2b79519d20..75ff038f23 100644 --- a/courseware/x_module.py +++ b/courseware/modules/x_module.py @@ -39,11 +39,13 @@ class XModule(object): get is a dictionary-like object ''' return "" - def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None): + def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None, meta = None): ''' In most cases, you must pass state or xml''' - self.xml=xml - self.item_id=item_id - self.ajax_url=ajax_url - self.track_url=track_url - self.state=state - self.tracker=track_function + self.xml = xml + self.item_id = item_id + self.ajax_url = ajax_url + self.track_url = track_url + self.state = state + self.tracker = track_function + self.render_function = render_function + self.meta = meta diff --git a/courseware/views.py b/courseware/views.py index 03a7a05965..be93b8131e 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -1,7 +1,6 @@ from django.http import HttpResponse from django.template import Context, loader from djangomako.shortcuts import render_to_response, render_to_string -from xml.dom.minidom import parse, parseString import json, os, sys from django.core.context_processors import csrf @@ -14,11 +13,6 @@ import StringIO from django.http import Http404 -import urllib - -import capa_module -import video_module - from models import StudentModule import urllib @@ -31,6 +25,11 @@ import uuid from module_render import * +from lxml import etree + +etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, + remove_comments = True)) + template_imports={'urllib':urllib} def profile(request): @@ -39,20 +38,23 @@ def profile(request): if not request.user.is_authenticated(): return redirect('/') - dom=parse(content_parser.course_file(request.user)) + dom=content_parser.course_file(request.user) hw=[] - course = dom.getElementsByTagName('course')[0] - chapters = course.getElementsByTagName('chapter') + course = dom.xpath('//course/@name')[0] + chapters = dom.xpath('//course[@name=$course]/chapter', course=course) responses=StudentModule.objects.filter(student=request.user) for c in chapters: - for s in c.getElementsByTagName('section'): - problems=s.getElementsByTagName('problem') + chname=c.get('name') + for s in dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section', + course=course, chname=chname): + problems=dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section[@name=$section]//problem', + course=course, chname=chname, section=s.get('name')) scores=[] if len(problems)>0: for p in problems: - id = p.getAttribute('filename') + id = p.get('filename') correct = 0 for response in responses: if response.module_id == id: @@ -60,11 +62,11 @@ def profile(request): correct=response.grade else: correct=0 - total=capa_module.LoncapaModule(p.toxml(), "id").max_score() # TODO: Add state. Not useful now, but maybe someday problems will have randomized max scores? + total=courseware.modules.capa_module.LoncapaModule(etree.tostring(p), "id").max_score() # TODO: Add state. Not useful now, but maybe someday problems will have randomized max scores? scores.append((int(correct),total)) - score={'course':course.getAttribute('name'), - 'section':s.getAttribute("name"), - 'chapter':c.getAttribute("name"), + score={'course':course, + 'section':s.get("name"), + 'chapter':c.get("name"), 'scores':scores, } hw.append(score) @@ -109,6 +111,7 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti # Fixes URLs -- we don't get funny encoding characters from spaces # so they remain readable + ## TODO: Properly replace underscores course=course.replace("_"," ") chapter=chapter.replace("_"," ") section=section.replace("_"," ") @@ -118,15 +121,13 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti if course!="6.002 Spring 2012": return redirect('/') - cf = content_parser.course_file(request.user) - dom=parse(cf) - dom_course=content_parser.dom_select(dom, 'course', course) - dom_chapter=content_parser.dom_select(dom_course, 'chapter', chapter) - dom_section=content_parser.dom_select(dom_chapter, 'section', section) - if dom_section!=None: - module=[e for e in dom_section.childNodes if e.nodeType==1][0] + dom = content_parser.course_file(request.user) + 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: + module = None else: - module=None + module = dom_module[0] accordion=render_accordion(request, course, chapter, section) @@ -135,6 +136,8 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti if 'init_js' not in module: module['init_js']='' + + context={'init':accordion['init_js']+module['init_js'], 'accordion':accordion['content'], 'content':module['content'], diff --git a/settings_new_askbot.py b/settings_new_askbot.py deleted file mode 100644 index 865b0b2c80..0000000000 --- a/settings_new_askbot.py +++ /dev/null @@ -1,256 +0,0 @@ -import os -import sys - -import djcelery - -COURSEWARE_ENABLED = True -ASKBOT_ENABLED = True - -CSRF_COOKIE_DOMAIN = '127.0.0.1' - -# Defaults to be overridden -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' -SITE_NAME = "localhost:8000" - -DEFAULT_FROM_EMAIL = 'registration@mitx.mit.edu' -DEFAULT_FEEDBACK_EMAIL = 'feedback@mitx.mit.edu' - -GENERATE_RANDOM_USER_CREDENTIALS = False - -WIKI_REQUIRE_LOGIN_EDIT = True -WIKI_REQUIRE_LOGIN_VIEW = True - -PERFSTATS = False - -HTTPS = 'on' - -MEDIA_URL = '' -MEDIA_ROOT = '' - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( - ('Piotr Mitros', 'pmitros@csail.mit.edu'), -) - -MANAGERS = ADMINS - -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -TIME_ZONE = 'America/Chicago' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale -USE_L10N = True - -STATIC_URL = '/static/' - -# URL prefix for admin static files -- CSS, JavaScript and images. -# Make sure to use a trailing slash. -# Examples: "http://foo.com/static/admin/", "/static/admin/". -ADMIN_MEDIA_PREFIX = '/static/admin/' - -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'track.middleware.TrackMiddleware', - 'djangomako.middleware.MakoMiddleware', - #'debug_toolbar.middleware.DebugToolbarMiddleware', -) - -ROOT_URLCONF = 'mitx.urls' - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'courseware', - 'auth', - 'django.contrib.humanize', - 'static_template_view', - 'staticbook', - 'simplewiki', - 'track', - 'circuit', - 'perfstats', - # Uncomment the next line to enable the admin: - # 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', -) - -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} - -#TRACK_DIR = None -DEBUG_TRACK_LOG = False -# Maximum length of a tracking string. We don't want e.g. a file upload in our log -TRACK_MAX_EVENT = 1000 -# Maximum length of log file before starting a new one. -MAXLOG = 500 - -# Our parent dir (mitx_all) is the BASE_DIR -BASE_DIR = os.path.abspath(os.path.join(__file__, "..", "..")) - -# Make sure we execute correctly regardless of where we're called from -execfile(os.path.join(BASE_DIR, "settings.py")) - - -if PERFSTATS : - MIDDLEWARE_CLASSES = ( 'perfstats.middleware.ProfileMiddleware',) + MIDDLEWARE_CLASSES - -if 'TRACK_DIR' not in locals(): - TRACK_DIR = BASE_DIR+'/track_dir/' -if 'STATIC_ROOT' not in locals(): - STATIC_ROOT = BASE_DIR+'/staticroot/' -if 'DATA_DIR' not in locals(): - DATA_DIR = BASE_DIR+'/data/' -if 'TEXTBOOK_DIR' not in locals(): - TEXTBOOK_DIR = BASE_DIR+'/textbook/' - -if 'TEMPLATE_DIRS' not in locals(): - TEMPLATE_DIRS = ( - BASE_DIR+'/templates/', - DATA_DIR+'/templates', - TEXTBOOK_DIR, - ) - -if 'STATICFILES_DIRS' not in locals(): - STATICFILES_DIRS = ( - BASE_DIR+'/3rdParty/static', - BASE_DIR+'/static', - ) - -if 'ASKBOT_EXTRA_SKINS_DIR' not in locals(): - ASKBOT_EXTRA_SKINS_DIR = BASE_DIR+'/askbot-devel/askbot/skins' -if 'ASKBOT_DIR' not in locals(): - ASKBOT_DIR = BASE_DIR+'/askbot-devel' - -sys.path.append(ASKBOT_DIR) -import askbot -import site - -STATICFILES_DIRS = STATICFILES_DIRS + ( ASKBOT_DIR+'/askbot/skins',) - -# Needed for Askbot -# Critical TODO: Move to S3 -MEDIA_URL = '/discussion/upfiles/' -MEDIA_ROOT = ASKBOT_DIR+'/askbot/upfiles' - -ASKBOT_ROOT = os.path.dirname(askbot.__file__) - -site.addsitedir(os.path.join(os.path.dirname(askbot.__file__), 'deps')) -TEMPLATE_LOADERS = TEMPLATE_LOADERS + ('askbot.skins.loaders.filesystem_load_template_source',) - -MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + ( - 'askbot.middleware.anon_user.ConnectToSessionMessagesMiddleware', - 'askbot.middleware.forum_mode.ForumModeMiddleware', - 'askbot.middleware.cancel.CancelActionMiddleware', - 'django.middleware.transaction.TransactionMiddleware', - #'debug_toolbar.middleware.DebugToolbarMiddleware', - 'askbot.middleware.view_log.ViewLogMiddleware', - 'askbot.middleware.spaceless.SpacelessMiddleware', - # 'askbot.middleware.pagesize.QuestionsPageSizeMiddleware', -) - -FILE_UPLOAD_TEMP_DIR = os.path.join(os.path.dirname(__file__), 'tmp').replace('\\','/') -FILE_UPLOAD_HANDLERS = ( - 'django.core.files.uploadhandler.MemoryFileUploadHandler', - 'django.core.files.uploadhandler.TemporaryFileUploadHandler', -) -ASKBOT_ALLOWED_UPLOAD_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff') -ASKBOT_MAX_UPLOAD_FILE_SIZE = 1024 * 1024 #result in bytes -# ASKBOT_FILE_UPLOAD_DIR = os.path.join(os.path.dirname(__file__), 'askbot', 'upfiles') -DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' - -PROJECT_ROOT = os.path.dirname(__file__) - -TEMPLATE_CONTEXT_PROCESSORS = ( - 'django.core.context_processors.request', - 'askbot.context.application_settings', - #'django.core.context_processors.i18n', - 'askbot.user_messages.context_processors.user_messages',#must be before auth - 'django.core.context_processors.auth', #this is required for admin - 'django.core.context_processors.csrf', #necessary for csrf protection -) - -INSTALLED_APPS = INSTALLED_APPS + ( - 'django.contrib.sitemaps', - 'django.contrib.admin', - 'south', - 'askbot.deps.livesettings', - 'askbot', - #'keyedcache', # TODO: Main askbot tree has this installed, but we get intermittent errors if we include it. - 'robots', - 'django_countries', - 'djcelery', - 'djkombu', - 'followit', -) - -CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True -ASKBOT_URL = 'discussion/' -LOGIN_REDIRECT_URL = '/' -LOGIN_URL = '/' - -# ASKBOT_UPLOADED_FILES_URL = '%s%s' % (ASKBOT_URL, 'upfiles/') -ALLOW_UNICODE_SLUGS = False -ASKBOT_USE_STACKEXCHANGE_URLS = False #mimic url scheme of stackexchange -ASKBOT_CSS_DEVEL = True - -# Celery Settings -BROKER_TRANSPORT = "djkombu.transport.DatabaseTransport" -CELERY_ALWAYS_EAGER = True - -djcelery.setup_loader() diff --git a/settings_new_askbot.py b/settings_new_askbot.py new file mode 120000 index 0000000000..57b3227b1f --- /dev/null +++ b/settings_new_askbot.py @@ -0,0 +1 @@ +settings_old_askbot.py \ No newline at end of file diff --git a/simplewiki/views.py b/simplewiki/views.py index 697a40fd2b..3fbd1f3b9a 100644 --- a/simplewiki/views.py +++ b/simplewiki/views.py @@ -48,7 +48,7 @@ def root_redirect(request): try: root = Article.get_root() except: - err = not_found(request, 'mainpage') + err = not_found(request, '/') return err return HttpResponseRedirect(reverse('wiki_view', args=(root.get_url()))) @@ -92,7 +92,7 @@ def create(request, wiki_url): #except ShouldHaveExactlyOneRootSlug, (e): except: if Article.objects.filter(parent=None).count() > 0: - return HttpResponseRedirect(reverse('wiki_view', args=('',))) + return HttpResponseRedirect(reverse('wiki_view', args=('/',))) # Root not found... path = [] url_path = [""] @@ -380,7 +380,7 @@ def fetch_from_url(request, url): try: root = Article.get_root() except: - err = not_found(request, '') + err = not_found(request, '/') return (article, path, err) if url_path and root.slug == url_path[0]: diff --git a/static_template_view/views.py b/static_template_view/views.py index b290f84473..5579e20c86 100644 --- a/static_template_view/views.py +++ b/static_template_view/views.py @@ -8,7 +8,12 @@ from django.shortcuts import redirect from django.core.context_processors import csrf #valid_templates=['index.html', 'staff.html', 'info.html', 'credits.html'] -valid_templates=['mitx_global.html', 'index.html'] +valid_templates=['mitx_global.html', + 'index.html', + 'tos.html', + 'privacy.html', + 'honor.html', + 'copyright.html'] def index(request, template): csrf_token = csrf(request)['csrf_token']