diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 8f6482ccf0..c296e30f3d 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -49,7 +49,7 @@ def xpath(xml, query_string, **args): We should remove this with the move to lxml. We should also use lxml argument passing. ''' doc = etree.fromstring(xml) - print type(doc) + #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 @@ -60,7 +60,7 @@ def xpath(xml, query_string, **args): return x args=dict( ((k, escape(args[k])) for k in args) ) - print args + #print args results = doc.xpath(query_string.format(**args)) return results @@ -86,14 +86,20 @@ def item(l, default="", process=lambda x:x): def id_tag(course): ''' Tag all course elements with unique IDs ''' - default_ids = {'video':'youtube', + old_ids = {'video':'youtube', 'problem':'filename', 'sequential':'id', 'html':'filename', 'vertical':'id', 'tab':'id', - 'schematic':'id'} - + 'schematic':'id', + 'book' : 'id'} + import courseware.modules + default_ids = courseware.modules.get_default_ids() + + #print default_ids, old_ids + #print default_ids == old_ids + # Tag elements with unique IDs elements = course.xpath("|".join(['//'+c for c in default_ids])) for elem in elements: @@ -143,7 +149,7 @@ def course_file(user): filename = UserProfile.objects.get(user=user).courseware data_template = template_lookup.get_template(filename) - options = {'dev_content':True} + options = {'dev_content':settings.DEV_CONTENT} tree = etree.XML(data_template.render(**options)) id_tag(tree) diff --git a/courseware/management/commands/check_course.py b/courseware/management/commands/check_course.py index 7ca8ef8242..b020529ffb 100644 --- a/courseware/management/commands/check_course.py +++ b/courseware/management/commands/check_course.py @@ -41,6 +41,7 @@ class Command(BaseCommand): if os.path.exists(sections_dir): print "Checking all section includes are valid XML" for f in os.listdir(sections_dir): + print f etree.parse(sections_dir+'/'+f) else: print "Skipping check of include files -- no section includes dir ("+sections_dir+")" diff --git a/courseware/module_render.py b/courseware/module_render.py index 3c863684be..1f4e784b2e 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -26,24 +26,10 @@ import track.views import courseware.content_parser as content_parser -import courseware.modules.capa_module -import courseware.modules.html_module -import courseware.modules.schematic_module -import courseware.modules.seq_module -import courseware.modules.vertical_module -import courseware.modules.video_module +import courseware.modules log = logging.getLogger("mitx.courseware") -## 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, - 'tab':courseware.modules.seq_module.SequentialModule, - 'schematic':courseware.modules.schematic_module.SchematicModule} - def object_cache(cache, user, module_type, module_id): # We don't look up on user -- all queries include user # Additional lookup would require a DB hit the way Django @@ -74,18 +60,18 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): ajax_url = '/modx/'+module+'/'+id+'/' - id_tag=modx_modules[module].id_attribute + id_tag=courseware.modules.get_module_class(module.id_attribute) # Grab the XML corresponding to the request from course.xml xml = content_parser.module_xml(content_parser.course_file(request.user), module, id_tag, id) # Create the module - instance=modx_modules[module](xml, - s.module_id, - ajax_url=ajax_url, - state=s.state, - track_function = make_track_function(request), - render_function = None) + instance=courseware.modules.get_module_class(module)(xml, + s.module_id, + ajax_url=ajax_url, + state=s.state, + track_function = make_track_function(request), + render_function = None) # Let the module handle the AJAX ajax_return=instance.handle_ajax(dispatch, request.POST) # Save the state back to the database @@ -100,7 +86,7 @@ def render_x_module(user, request, xml_module, module_object_preload): ''' Generic module for extensions. This renders to HTML. ''' # Check if problem has an instance in DB module_type=xml_module.tag - module_class=modx_modules[module_type] + module_class=courseware.modules.get_module_class(module_type) module_id=xml_module.get('id') #module_class.id_attribute) or "" # Grab state from database diff --git a/courseware/modules/__init__.py b/courseware/modules/__init__.py index e69de29bb2..4c528144d2 100644 --- a/courseware/modules/__init__.py +++ b/courseware/modules/__init__.py @@ -0,0 +1,61 @@ +import os +import os.path + +from django.conf import settings + +import capa_module +import html_module +import schematic_module +import seq_module +import template_module +import vertical_module +import video_module + +from courseware import content_parser + +# Import all files in modules directory, excluding backups (# and . in name) +# and __init__ +# +# Stick them in a list +# modx_module_list = [] + +# for f in os.listdir(os.path.dirname(__file__)): +# if f!='__init__.py' and \ +# f[-3:] == ".py" and \ +# "." not in f[:-3] \ +# and '#' not in f: +# mod_path = 'courseware.modules.'+f[:-3] +# mod = __import__(mod_path, fromlist = "courseware.modules") +# if 'Module' in mod.__dict__: +# modx_module_list.append(mod) + +#print modx_module_list +modx_module_list = [capa_module, html_module, schematic_module, seq_module, template_module, vertical_module, video_module] +#print modx_module_list + +# Convert list to a dictionary for lookup by tag +modx_modules = {} +for module in modx_module_list: + for tag in module.Module.get_xml_tags(): + modx_modules[tag] = module.Module + +def get_module_class(tag): + ''' Given an XML tag (e.g. 'video'), return + the associated module (e.g. video_module.Module). + ''' + return modx_modules[tag] + +def get_module_id(tag): + ''' Given an XML tag (e.g. 'video'), return + the default ID for that module (e.g. 'youtube_id') + ''' + return modx_modules[tag].id_attribute + +def get_valid_tags(): + return modx_modules.keys() + +def get_default_ids(): + tags = get_valid_tags() + ids = map(get_module_id, tags) + return dict(zip(tags, ids)) + diff --git a/courseware/modules/capa_module.py b/courseware/modules/capa_module.py index 3161664a9e..378c322c42 100644 --- a/courseware/modules/capa_module.py +++ b/courseware/modules/capa_module.py @@ -26,15 +26,18 @@ import courseware.content_parser as content_parser log = logging.getLogger("mitx.courseware") -class LoncapaModule(XModule): +class Module(XModule): ''' Interface between capa_problem and x_module. Originally a hack meant to be refactored out, but it seems to be serving a useful prupose now. We can e.g .destroy and create the capa_problem on a reset. ''' - xml_tags = ["problem"] + id_attribute = "filename" + @classmethod + def get_xml_tags(c): + return ["problem"] def get_state(self): state = self.lcp.get_state() diff --git a/courseware/modules/html_module.py b/courseware/modules/html_module.py index f1ce14ec7e..92e29df938 100644 --- a/courseware/modules/html_module.py +++ b/courseware/modules/html_module.py @@ -7,14 +7,15 @@ from mitxmako.shortcuts import render_to_response, render_to_string from x_module import XModule from lxml import etree -class HtmlModule(XModule): +class Module(XModule): id_attribute = 'filename' def get_state(self): return json.dumps({ }) - def get_xml_tags(): - return "html" + @classmethod + def get_xml_tags(c): + return ["html"] def get_html(self): if self.filename==None: diff --git a/courseware/modules/schematic_module.py b/courseware/modules/schematic_module.py index 0312321b4d..e253f1acc6 100644 --- a/courseware/modules/schematic_module.py +++ b/courseware/modules/schematic_module.py @@ -6,14 +6,15 @@ from mitxmako.shortcuts import render_to_response, render_to_string from x_module import XModule -class SchematicModule(XModule): +class Module(XModule): id_attribute = 'id' def get_state(self): return json.dumps({ }) - def get_xml_tags(): - return "schematic" + @classmethod + def get_xml_tags(c): + return ["schematic"] def get_html(self): return ''.format(item_id=self.item_id) diff --git a/courseware/modules/seq_module.py b/courseware/modules/seq_module.py index 33c4088486..6ec74feace 100644 --- a/courseware/modules/seq_module.py +++ b/courseware/modules/seq_module.py @@ -13,7 +13,7 @@ from x_module import XModule # OBSOLETE: This obsoletes 'type' class_priority = ['video', 'problem'] -class SequentialModule(XModule): +class Module(XModule): ''' Layout module which lays out content in a temporal sequence ''' id_attribute = 'id' @@ -21,7 +21,8 @@ class SequentialModule(XModule): def get_state(self): return json.dumps({ 'position':self.position }) - def get_xml_tags(): + @classmethod + def get_xml_tags(c): return ["sequential", 'tab'] def get_html(self): diff --git a/courseware/modules/template_module.py b/courseware/modules/template_module.py new file mode 100644 index 0000000000..11778348e3 --- /dev/null +++ b/courseware/modules/template_module.py @@ -0,0 +1,29 @@ +import json +import os + +## TODO: Abstract out from Django +from django.conf import settings +from mitxmako.shortcuts import render_to_response, render_to_string + +from x_module import XModule +from lxml import etree + +class Module(XModule): + def get_state(self): + return json.dumps({ }) + + @classmethod + def get_xml_tags(c): + tags = os.listdir(settings.DATA_DIR+'/custom_tags') + return tags + + def get_html(self): + return self.html + + def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None): + XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) + xmltree = etree.fromstring(xml) + filename = xmltree.tag + params = dict(xmltree.items()) +# print params + self.html = render_to_string('custom_tags/'+filename, params) diff --git a/courseware/modules/vertical_module.py b/courseware/modules/vertical_module.py index 6939c75cc9..c068cb9a76 100644 --- a/courseware/modules/vertical_module.py +++ b/courseware/modules/vertical_module.py @@ -7,14 +7,15 @@ from mitxmako.shortcuts import render_to_response, render_to_string from x_module import XModule from lxml import etree -class VerticalModule(XModule): +class Module(XModule): id_attribute = 'id' def get_state(self): return json.dumps({ }) - def get_xml_tags(): - return "vertical" + @classmethod + def get_xml_tags(c): + return ["vertical"] def get_html(self): return render_to_string('vert_module.html',{'items':self.contents}) diff --git a/courseware/modules/video_module.py b/courseware/modules/video_module.py index b7e1e211dc..2063c18953 100644 --- a/courseware/modules/video_module.py +++ b/courseware/modules/video_module.py @@ -11,8 +11,8 @@ from x_module import XModule log = logging.getLogger("mitx.courseware.modules") -class VideoModule(XModule): - #id_attribute = 'youtube' +class Module(XModule): + id_attribute = 'youtube' video_time = 0 def handle_ajax(self, dispatch, get): @@ -28,9 +28,10 @@ class VideoModule(XModule): log.debug(u"STATE POSITION {0}".format(self.position)) return json.dumps({ 'position':self.position }) - def get_xml_tags(): + @classmethod + def get_xml_tags(c): '''Tags in the courseware file guaranteed to correspond to the module''' - return "video" + return ["video"] def video_list(self): l = self.youtube.split(',') diff --git a/courseware/modules/x_module.py b/courseware/modules/x_module.py index 4b45c4c7fc..4a379253c4 100644 --- a/courseware/modules/x_module.py +++ b/courseware/modules/x_module.py @@ -8,9 +8,10 @@ class XModule(object): Initialized on access with __init__, first time with state=None, and then with state ''' - id_attribute='name' # An attribute guaranteed to be unique + id_attribute='id' # An attribute guaranteed to be unique - def get_xml_tags(): + @classmethod + def get_xml_tags(c): ''' Tags in the courseware file guaranteed to correspond to the module ''' return [] diff --git a/courseware/tests.py b/courseware/tests.py index 501deb776c..66a7ef181e 100644 --- a/courseware/tests.py +++ b/courseware/tests.py @@ -1,16 +1,36 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". +import unittest -Replace this with more appropriate tests for your application. -""" +import numpy -from django.test import TestCase +import courseware.modules +import courseware.capa.calc as calc +class ModelsTest(unittest.TestCase): + def setUp(self): + pass -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) + def test_get_module_class(self): + vc = courseware.modules.get_module_class('video') + vc_str = "" + self.assertEqual(str(vc), vc_str) + video_id = courseware.modules.get_default_ids()['video'] + self.assertEqual(video_id, 'youtube') + + def test_calc(self): + variables={'R1':2.0, 'R3':4.0} + functions={'sin':numpy.sin, 'cos':numpy.cos} + + self.assertEqual(calc.evaluator(variables, functions, "10000||sin(7+5)-6k"), 4000.0) + self.assertEqual(calc.evaluator({'R1': 2.0, 'R3':4.0}, {}, "13"), 13) + self.assertEqual(calc.evaluator(variables, functions, "13"), 13) + self.assertEqual(calc.evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5"), 5) + self.assertEqual(calc.evaluator({},{}, "-1"), -1) + self.assertEqual(calc.evaluator({},{}, "-0.33"), -.33) + self.assertEqual(calc.evaluator({},{}, "-.33"), -.33) + exception_happened = False + try: + evaluator({},{}, "5+7 QWSEKO") + except: + exception_happened = True + self.assertTrue(exception_happened) +