From 1b4d400cdd331522aca8e26fa8a6326f80a2acdb Mon Sep 17 00:00:00 2001 From: cjt Date: Fri, 20 Jan 2012 14:18:53 -0500 Subject: [PATCH 01/18] lab support --- courseware/capa/capa_problem.py | 8 +++++--- courseware/capa/inputtypes.py | 14 +++++++++++++- courseware/capa/responsetypes.py | 28 ++++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 6 deletions(-) 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/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 {} From bddfe16e375d9d8e966fe8c6cb5df38624934199 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Fri, 20 Jan 2012 14:58:32 -0500 Subject: [PATCH 02/18] Placeholder pages for TOS, privacy, honor code --- static_template_view/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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'] From 97ebb75046cffcb2fe82837cb1b0296e476770f8 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Fri, 20 Jan 2012 15:04:15 -0500 Subject: [PATCH 03/18] Accidentally switched to wrong settings --- settings_new_askbot.py | 257 +---------------------------------------- 1 file changed, 1 insertion(+), 256 deletions(-) mode change 100644 => 120000 settings_new_askbot.py 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 From 958426020dced8a4efa744683dc6ebfdfdecf545 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Fri, 20 Jan 2012 18:01:15 -0500 Subject: [PATCH 04/18] Two content_parser modules by accident --- courseware/capa/content_parser.py | 117 ------------------------------ courseware/content_parser.py | 21 +++++- 2 files changed, 17 insertions(+), 121 deletions(-) delete mode 100644 courseware/capa/content_parser.py 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/content_parser.py b/courseware/content_parser.py index 5d91eb8d06..5b0e197bc5 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -1,10 +1,13 @@ -from django.conf import settings +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 -from auth.models import UserProfile - ''' This file will eventually form an abstraction layer between the course XML file and the rest of the system. @@ -15,7 +18,9 @@ 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,6 +37,14 @@ 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") From 591f902372067e3d33c542fb41c0c5297bd8fd98 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Fri, 20 Jan 2012 18:54:14 -0500 Subject: [PATCH 05/18] Content parser moved to lxml --- courseware/content_parser.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 5b0e197bc5..bf17b6d7bb 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -4,10 +4,10 @@ try: except: settings = None -from xml.dom.minidom import parse, parseString - from lxml import etree +import json + ''' This file will eventually form an abstraction layer between the course XML file and the rest of the system. @@ -56,7 +56,6 @@ def item(l, default="", process=lambda x:x): 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 @@ -84,26 +83,26 @@ def module_xml(coursefile, module, id_tag, module_id): #return result_set[0].serialize() def toc_from_xml(coursefile, active_chapter, active_section): - dom=parse(coursefile) + dom2 = etree.parse(coursefile) - course = dom.getElementsByTagName('course')[0] - name=course.getAttribute("name") - chapters = course.getElementsByTagName('chapter') + name = dom2.xpath('//course/@name')[0] + + chapters = dom2.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 dom2.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): From 8539b9264f264ecc425dbb8e45a81f257b5cf9e9 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 21 Jan 2012 09:20:24 -0500 Subject: [PATCH 06/18] Profile page uses lxml --- courseware/content_parser.py | 8 ++++---- courseware/views.py | 25 +++++++++++++++---------- settings.py | 2 +- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index bf17b6d7bb..1f6c3c508c 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -83,17 +83,17 @@ def module_xml(coursefile, module, id_tag, module_id): #return result_set[0].serialize() def toc_from_xml(coursefile, active_chapter, active_section): - dom2 = etree.parse(coursefile) + dom = etree.parse(coursefile) - name = dom2.xpath('//course/@name')[0] + name = dom.xpath('//course/@name')[0] - chapters = dom2.xpath('//course[@name=$name]/chapter', name=name) + chapters = dom.xpath('//course[@name=$name]/chapter', name=name) ch=list() for c in chapters: if c.get('name') == 'hidden': continue sections=list() - for s in dom2.xpath('//course[@name=$name]/chapter[@name=$chname]/section', name=name, chname=c.get('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 "", diff --git a/courseware/views.py b/courseware/views.py index 03a7a05965..16f9435cf8 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -31,6 +31,8 @@ import uuid from module_render import * +from lxml import etree + template_imports={'urllib':urllib} def profile(request): @@ -39,20 +41,23 @@ def profile(request): if not request.user.is_authenticated(): return redirect('/') - dom=parse(content_parser.course_file(request.user)) + dom=etree.parse(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 +65,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=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) diff --git a/settings.py b/settings.py index f42725e7f1..57b3227b1f 120000 --- a/settings.py +++ b/settings.py @@ -1 +1 @@ -settings_new_askbot.py \ No newline at end of file +settings_old_askbot.py \ No newline at end of file From 1ec3209d05aaf8a9c629e279defa328e3aba5e88 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 21 Jan 2012 12:12:00 -0500 Subject: [PATCH 07/18] Moved courseware from minidom to lxml2. Appears to work, but big change. may still have bugs. --- courseware/module_render.py | 29 ++++++++++----------- courseware/views.py | 50 ++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/courseware/module_render.py b/courseware/module_render.py index 58574df190..0e83f936f2 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -29,6 +29,9 @@ from django.conf import settings import content_parser +import sys + +from lxml import etree import uuid modx_modules={'problem':capa_module.LoncapaModule, @@ -80,9 +83,8 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): 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] + contents=[(e.get("name"),render_module(request, e)) \ + for e in module] 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]]) @@ -107,9 +109,8 @@ def seq_module(request, module): "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] + contents=[(e.get("name"),j(render_module(request, e))) \ + for e in module] js="" @@ -125,12 +126,12 @@ def seq_module(request, module): # IDs to sequences. destroy_js="".join([e[1]['destroy_js'] for e in contents if 'destroy_js' in e[1]]) - if module.nodeName == 'sequential': + if module.tag == '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': + if module.tag == 'tab': params['id'] = 'tab' return {'init_js':js+render_to_string('tab_module.js',params), "destroy_js":destroy_js, @@ -141,9 +142,9 @@ def seq_module(request, module): 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(module_class.id_attribute) or "" # TODO: remove or "" # Grab state from database s = StudentModule.objects.filter(student=request.user, @@ -157,7 +158,7 @@ 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, @@ -190,9 +191,9 @@ module_types={'video':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) + if str(module.tag) in module_types: + return module_types[module.tag](request, module) print "rm404" raise Http404 diff --git a/courseware/views.py b/courseware/views.py index 16f9435cf8..22d5853fcb 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 @@ -33,6 +32,9 @@ 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): @@ -114,6 +116,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("_"," ") @@ -147,3 +150,48 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti return render_to_response('courseware.html', context) + +def index(request, course="6.002 Spring 2012", chapter="Using the System", section="Hints"): + ''' Displays courseware accordion, and any associated content. + ''' + if not settings.COURSEWARE_ENABLED or not request.user.is_authenticated(): + return redirect('/') + + # 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("_"," ") + + # HACK: Force course to 6.002 for now + # Without this, URLs break + if course!="6.002 Spring 2012": + return redirect('/') + + cf = content_parser.course_file(request.user) + dom=etree.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) + 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 = dom_module[0] + + accordion=render_accordion(request, course, chapter, section) + + module=render_module(request, module) + + if 'init_js' not in module: + module['init_js']='' + + context={'init':accordion['init_js']+module['init_js'], + 'accordion':accordion['content'], + 'content':module['content'], + 'csrf':csrf(request)['csrf_token']} + return render_to_response('courseware.html', context) + + From 829e890a6e746335b208d4d98946fea15ee635c0 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 21 Jan 2012 12:24:17 -0500 Subject: [PATCH 08/18] Very minor cleanups --- courseware/content_parser.py | 9 --------- courseware/views.py | 3 --- 2 files changed, 12 deletions(-) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 1f6c3c508c..dd1cc0b704 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -105,12 +105,3 @@ def toc_from_xml(coursefile, active_chapter, active_section): '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/views.py b/courseware/views.py index 22d5853fcb..8037a05329 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -171,9 +171,6 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti cf = content_parser.course_file(request.user) dom=etree.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) 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: From 714167322e7d4433cb422ed07444de33182ddb81 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 21 Jan 2012 12:36:52 -0500 Subject: [PATCH 09/18] course_file now includes parsing. --- courseware/content_parser.py | 10 +++----- courseware/views.py | 48 ++---------------------------------- 2 files changed, 6 insertions(+), 52 deletions(-) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index dd1cc0b704..0794685a1b 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -57,13 +57,13 @@ def item(l, default="", process=lambda x:x): 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 + # TODO: Cache. + return etree.parse(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) + doc = coursefile # Sanitize input if not module.isalnum(): @@ -82,9 +82,7 @@ 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 = etree.parse(coursefile) - +def toc_from_xml(dom, active_chapter, active_section): name = dom.xpath('//course/@name')[0] chapters = dom.xpath('//course[@name=$name]/chapter', name=name) diff --git a/courseware/views.py b/courseware/views.py index 8037a05329..c5f2325dd2 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -43,7 +43,7 @@ def profile(request): if not request.user.is_authenticated(): return redirect('/') - dom=etree.parse(content_parser.course_file(request.user)) + dom=content_parser.course_file(request.user) hw=[] course = dom.xpath('//course/@name')[0] chapters = dom.xpath('//course[@name=$course]/chapter', course=course) @@ -126,51 +126,7 @@ 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] - else: - module=None - - accordion=render_accordion(request, course, chapter, section) - - module=render_module(request, module) - - if 'init_js' not in module: - module['init_js']='' - - context={'init':accordion['init_js']+module['init_js'], - 'accordion':accordion['content'], - 'content':module['content'], - 'csrf':csrf(request)['csrf_token']} - return render_to_response('courseware.html', context) - - - -def index(request, course="6.002 Spring 2012", chapter="Using the System", section="Hints"): - ''' Displays courseware accordion, and any associated content. - ''' - if not settings.COURSEWARE_ENABLED or not request.user.is_authenticated(): - return redirect('/') - - # 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("_"," ") - - # HACK: Force course to 6.002 for now - # Without this, URLs break - if course!="6.002 Spring 2012": - return redirect('/') - - cf = content_parser.course_file(request.user) - dom=etree.parse(cf) + 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: From d410d8294ab98fc039ee13d50e917a5e12fc39f1 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 21 Jan 2012 12:47:30 -0500 Subject: [PATCH 10/18] All elements in course file include IDs --- courseware/content_parser.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 0794685a1b..8f71739dfd 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -7,6 +7,7 @@ except: from lxml import etree import json +import hashlib ''' This file will eventually form an abstraction layer between the course XML file and the rest of the system. @@ -14,6 +15,11 @@ 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 m.hexdigest() + def xpath(xml, query_string, **args): ''' Safe xpath query into an xml tree: * xml is the tree. @@ -55,10 +61,32 @@ 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]): + elem.set('id', elem.get(default_ids[elem.tag])) + else: + elem.set('id', fasthash(etree.tostring(elem))) + def course_file(user): # TODO: Cache. - return etree.parse(settings.DATA_DIR+UserProfile.objects.get(user=user).courseware) + 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 From 837e87e177e1632721ac5e48a92df5f689b3c666 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 21 Jan 2012 13:21:50 -0500 Subject: [PATCH 11/18] Modules moved into their own directory --HG-- rename : courseware/capa_module.py => courseware/modules/capa_module.py rename : courseware/html_module.py => courseware/modules/html_module.py rename : courseware/schematic_module.py => courseware/modules/schematic_module.py rename : courseware/video_module.py => courseware/modules/video_module.py rename : courseware/x_module.py => courseware/modules/x_module.py --- courseware/content_parser.py | 3 ++- courseware/module_render.py | 17 +++++++++-------- courseware/modules/__init__.py | 0 courseware/{ => modules}/capa_module.py | 4 ++-- courseware/{ => modules}/html_module.py | 0 courseware/{ => modules}/schematic_module.py | 0 courseware/{ => modules}/video_module.py | 0 courseware/{ => modules}/x_module.py | 0 courseware/views.py | 5 ----- 9 files changed, 13 insertions(+), 16 deletions(-) create mode 100644 courseware/modules/__init__.py rename courseware/{ => modules}/capa_module.py (99%) rename courseware/{ => modules}/html_module.py (100%) rename courseware/{ => modules}/schematic_module.py (100%) rename courseware/{ => modules}/video_module.py (100%) rename courseware/{ => modules}/x_module.py (100%) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 8f71739dfd..873e579897 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -52,7 +52,8 @@ def xpath_remove(tree, path): 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: diff --git a/courseware/module_render.py b/courseware/module_render.py index 0e83f936f2..5a69715f02 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -16,10 +16,10 @@ 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.html_module +import courseware.modules.schematic_module from models import StudentModule @@ -34,10 +34,11 @@ 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, + 'schematic':courseware.modules.schematic_module.SchematicModule} def make_track_function(request): def f(event_type, event): 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 99% rename from courseware/capa_module.py rename to courseware/modules/capa_module.py index e13ee453fe..7c6953a8bf 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 diff --git a/courseware/html_module.py b/courseware/modules/html_module.py similarity index 100% rename from courseware/html_module.py rename to courseware/modules/html_module.py diff --git a/courseware/schematic_module.py b/courseware/modules/schematic_module.py similarity index 100% rename from courseware/schematic_module.py rename to courseware/modules/schematic_module.py diff --git a/courseware/video_module.py b/courseware/modules/video_module.py similarity index 100% rename from courseware/video_module.py rename to courseware/modules/video_module.py diff --git a/courseware/x_module.py b/courseware/modules/x_module.py similarity index 100% rename from courseware/x_module.py rename to courseware/modules/x_module.py diff --git a/courseware/views.py b/courseware/views.py index c5f2325dd2..2656f18aa4 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -13,11 +13,6 @@ import StringIO from django.http import Http404 -import urllib - -import capa_module -import video_module - from models import StudentModule import urllib From 16ab1bb564f0e27a5a4f6f94e4f1dff4b7d0887c Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 21 Jan 2012 15:25:25 -0500 Subject: [PATCH 12/18] Vertical Module is now an x_module --- courseware/module_render.py | 21 ++++---------- courseware/modules/capa_module.py | 4 +-- courseware/modules/html_module.py | 4 +-- courseware/modules/schematic_module.py | 4 +-- courseware/modules/vertical_module.py | 39 ++++++++++++++++++++++++++ courseware/modules/video_module.py | 4 +-- courseware/modules/x_module.py | 16 ++++++----- courseware/views.py | 4 +++ 8 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 courseware/modules/vertical_module.py diff --git a/courseware/module_render.py b/courseware/module_render.py index 5a69715f02..e6dd0a5341 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -18,6 +18,7 @@ import urllib 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 @@ -38,6 +39,7 @@ import uuid 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, 'schematic':courseware.modules.schematic_module.SchematicModule} def make_track_function(request): @@ -81,19 +83,6 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): # 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.get("name"),render_module(request, e)) \ - for e in module] - 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 ''' @@ -163,7 +152,9 @@ def render_x_module(request, 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: @@ -184,7 +175,7 @@ def render_x_module(request, xml_module): module_types={'video':render_x_module, 'html':render_x_module, 'tab':seq_module, - 'vertical':vertical_module, + 'vertical':render_x_module, 'sequential':seq_module, 'problem':render_x_module, 'schematic':render_x_module diff --git a/courseware/modules/capa_module.py b/courseware/modules/capa_module.py index 7c6953a8bf..591f9c7516 100644 --- a/courseware/modules/capa_module.py +++ b/courseware/modules/capa_module.py @@ -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/modules/html_module.py b/courseware/modules/html_module.py index 853a322236..ce26ea0045 100644 --- a/courseware/modules/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/modules/schematic_module.py b/courseware/modules/schematic_module.py index 5a7fa390a0..b97e80541b 100644 --- a/courseware/modules/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/vertical_module.py b/courseware/modules/vertical_module.py new file mode 100644 index 0000000000..343cc5e8d5 --- /dev/null +++ b/courseware/modules/vertical_module.py @@ -0,0 +1,39 @@ +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.filename = None + filename_l=xmltree.xpath("/html/@filename") + if len(filename_l)>0: + self.filename=str(filename_l[0]) + + 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/modules/video_module.py b/courseware/modules/video_module.py index 0fdb54f119..4676184b7e 100644 --- a/courseware/modules/video_module.py +++ b/courseware/modules/video_module.py @@ -46,8 +46,8 @@ class VideoModule(XModule): def get_destroy_js(self): return "videoDestroy();" - 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) print state if state!=None and "time" not in json.loads(state): self.video_time = 0 diff --git a/courseware/modules/x_module.py b/courseware/modules/x_module.py index 2b79519d20..75ff038f23 100644 --- a/courseware/modules/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 2656f18aa4..98d575baf5 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -133,9 +133,13 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti module=render_module(request, module) + print "Here",module['init_js'] + if 'init_js' not in module: module['init_js']='' + + context={'init':accordion['init_js']+module['init_js'], 'accordion':accordion['content'], 'content':module['content'], From ba2c03357cdd3759f982318a90f2151cbb4c51b4 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 21 Jan 2012 18:46:34 -0500 Subject: [PATCH 13/18] All modules moved into new OO framework --- courseware/module_render.py | 62 ++--------------------- courseware/modules/seq_module.py | 71 +++++++++++++++++++++++++++ courseware/modules/vertical_module.py | 5 -- 3 files changed, 74 insertions(+), 64 deletions(-) create mode 100644 courseware/modules/seq_module.py diff --git a/courseware/module_render.py b/courseware/module_render.py index e6dd0a5341..36f14508ef 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -21,6 +21,7 @@ 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 @@ -40,6 +41,7 @@ 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): @@ -83,52 +85,6 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): # Return whatever the module wanted to return to the client/caller return HttpResponse(ajax_return) -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.get("name"),j(render_module(request, e))) \ - for e in module] - - 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.tag == '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.tag == '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 @@ -172,20 +128,8 @@ def render_x_module(request, xml_module): return content -module_types={'video':render_x_module, - 'html':render_x_module, - 'tab':seq_module, - 'vertical':render_x_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 : return {"content":""} - if str(module.tag) in module_types: - return module_types[module.tag](request, module) - print "rm404" - raise Http404 + return render_x_module(request, module) diff --git a/courseware/modules/seq_module.py b/courseware/modules/seq_module.py new file mode 100644 index 0000000000..f6d4cc53b1 --- /dev/null +++ b/courseware/modules/seq_module.py @@ -0,0 +1,71 @@ +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 SequentialModule(XModule): + ''' Layout module which lays out content in a temporal sequence + ''' + id_attribute = 'id' + + def get_state(self): + return json.dumps({ }) + + 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 __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) + + 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':"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 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 index 343cc5e8d5..b5ad1be368 100644 --- a/courseware/modules/vertical_module.py +++ b/courseware/modules/vertical_module.py @@ -28,11 +28,6 @@ class VerticalModule(XModule): 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") - if len(filename_l)>0: - self.filename=str(filename_l[0]) - 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]]) From 29d23d1594b8755e21093e2252c917140b4adad4 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 21 Jan 2012 18:50:51 -0500 Subject: [PATCH 14/18] Accidentally left in a print --- courseware/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/courseware/views.py b/courseware/views.py index 98d575baf5..b613f9fb46 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -133,8 +133,6 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti module=render_module(request, module) - print "Here",module['init_js'] - if 'init_js' not in module: module['init_js']='' From d989621bff5e458b14d60d42a77842130c25ebbf Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 21 Jan 2012 23:52:46 -0500 Subject: [PATCH 15/18] Series remembers last spot --- courseware/content_parser.py | 2 +- courseware/module_render.py | 10 +++++++--- courseware/modules/seq_module.py | 20 ++++++++++++++++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 873e579897..13660ed1e9 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -18,7 +18,7 @@ TODO: Shift everything from xml.dom.minidom to XPath (or XQuery) def fasthash(string): m = hashlib.new("md4") m.update(string) - return m.hexdigest() + return "id"+m.hexdigest() def xpath(xml, query_string, **args): ''' Safe xpath query into an xml tree: diff --git a/courseware/module_render.py b/courseware/module_render.py index 36f14508ef..08231fac2d 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -52,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] @@ -75,12 +76,15 @@ 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) diff --git a/courseware/modules/seq_module.py b/courseware/modules/seq_module.py index f6d4cc53b1..5845c23a55 100644 --- a/courseware/modules/seq_module.py +++ b/courseware/modules/seq_module.py @@ -1,5 +1,6 @@ from x_module import XModule from lxml import etree +from django.http import Http404 import json @@ -13,7 +14,7 @@ class SequentialModule(XModule): id_attribute = 'id' def get_state(self): - return json.dumps({ }) + return json.dumps({ 'position':self.position }) def get_xml_tags(): return ["sequential", 'tab'] @@ -27,10 +28,24 @@ class SequentialModule(XModule): 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 @@ -51,7 +66,8 @@ class SequentialModule(XModule): js="" params={'items':contents, - 'id':"seq"} + '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 From ef1c465d5efa26f458c04a4458aad3821c1f7e38 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 23 Jan 2012 09:15:59 -0500 Subject: [PATCH 16/18] Video module set up to store last location. Several bug fixes too. --- courseware/content_parser.py | 4 +++- courseware/module_render.py | 2 +- courseware/modules/video_module.py | 37 +++++++++++++++++++----------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 13660ed1e9..c5c0600f9d 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -79,7 +79,9 @@ def id_tag(course): if elem.get('id'): pass elif elem.get(default_ids[elem.tag]): - elem.set('id', 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))) diff --git a/courseware/module_render.py b/courseware/module_render.py index 08231fac2d..9375facc4c 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -94,7 +94,7 @@ def render_x_module(request, xml_module): # Check if problem has an instance in DB module_type=xml_module.tag module_class=modx_modules[module_type] - module_id=xml_module.get(module_class.id_attribute) or "" # TODO: remove or "" + module_id=xml_module.get('id') #module_class.id_attribute) or "" # Grab state from database s = StudentModule.objects.filter(student=request.user, diff --git a/courseware/modules/video_module.py b/courseware/modules/video_module.py index 4676184b7e..6fadc17941 100644 --- a/courseware/modules/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, render_function = None, meta = None): XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) - print state - if state!=None and "time" not in json.loads(state): - self.video_time = 0 + 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 From 11f30122140ae7796e6af674874ea1884d578162 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 23 Jan 2012 12:20:25 -0500 Subject: [PATCH 17/18] Bug fix in profile page --- courseware/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/courseware/views.py b/courseware/views.py index b613f9fb46..be93b8131e 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -62,7 +62,7 @@ def profile(request): correct=response.grade else: correct=0 - total=capa_module.LoncapaModule(etree.tostring(p), "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, 'section':s.get("name"), From 747f5ac290af3a3d71fe9d5449186ca4d6b546cf Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 23 Jan 2012 12:35:37 -0500 Subject: [PATCH 18/18] Switching to new Askbot --- settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.py b/settings.py index 57b3227b1f..f42725e7f1 120000 --- a/settings.py +++ b/settings.py @@ -1 +1 @@ -settings_old_askbot.py \ No newline at end of file +settings_new_askbot.py \ No newline at end of file