Commit b4585ffe by Matthew Mongeau

Course navigation work

parent 79b9ba8f
......@@ -67,7 +67,7 @@ def info(request, course_id=None):
# We're bypassing the templating system for this part. We should cache
# this.
sections = ["updates", "handouts", "guest_updates", "guest_handouts"]
sections_to_content = {}
sections_to_content = { 'course': course }
for section in sections:
filename = section + ".html"
with open(course.path / "info" / filename) as f:
......
'''
courseware/content_parser.py
This file interfaces between all courseware modules and the top-level course.xml file for a course.
Does some caching (to be explained).
'''
import logging
import os
import sys
import urllib
from lxml import etree
from util.memcache import fasthash
from django.conf import settings
from student.models import UserProfile
from student.models import UserTestGroup
from mitxmako.shortcuts import render_to_string
from util.cache import cache
from multicourse import multicourse_settings
import xmodule
''' 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)
'''
class ContentException(Exception):
pass
log = logging.getLogger("mitx.courseware")
def format_url_params(params):
return [ urllib.quote(string.replace(' ','_')) for string in params ]
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('<html><problem name="Bob"></problem></html>', '/{search}/problem[@name="{name}"]',
search='html', name="Bob")
def id_tag(course):
''' Tag all course elements with unique IDs '''
default_ids = xmodule.get_default_ids()
# 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])
new_id = "".join([a for a in new_id if a.isalnum()]) # Convert to alphanumeric
# Without this, a conflict may occur between an hmtl or youtube id
new_id = default_ids[elem.tag] + new_id
elem.set('id', new_id)
else:
elem.set('id', "id"+fasthash(etree.tostring(elem)))
def propogate_downward_tag(element, attribute_name, parent_attribute = None):
''' This call is to pass down an attribute to all children. If an element
has this attribute, it will be "inherited" by all of its children. If a
child (A) already has that attribute, A will keep the same attribute and
all of A's children will inherit A's attribute. This is a recursive call.'''
if (parent_attribute is None): #This is the entry call. Select all elements with this attribute
all_attributed_elements = element.xpath("//*[@" + attribute_name +"]")
for attributed_element in all_attributed_elements:
attribute_value = attributed_element.get(attribute_name)
for child_element in attributed_element:
propogate_downward_tag(child_element, attribute_name, attribute_value)
else:
'''The hack below is because we would get _ContentOnlyELements from the
iterator that can't have attributes set. We can't find API for it. If we
ever have an element which subclasses BaseElement, we will not tag it'''
if not element.get(attribute_name) and type(element) == etree._Element:
element.set(attribute_name, parent_attribute)
for child_element in element:
propogate_downward_tag(child_element, attribute_name, parent_attribute)
else:
#This element would have already been found by Xpath, so we return
#for now and trust that this element will get its turn to propogate
#to its children later.
return
def user_groups(user):
if not user.is_authenticated():
return []
# TODO: Rewrite in Django
key = 'user_group_names_{user.id}'.format(user=user)
cache_expiration = 60 * 60 # one hour
# Kill caching on dev machines -- we switch groups a lot
group_names = cache.get(key)
if group_names is None:
group_names = [u.name for u in UserTestGroup.objects.filter(users=user)]
cache.set(key, group_names, cache_expiration)
return group_names
# return [u.name for u in UserTestGroup.objects.raw("select * from auth_user, student_usertestgroup, student_usertestgroup_users where auth_user.id = student_usertestgroup_users.user_id and student_usertestgroup_users.usertestgroup_id = student_usertestgroup.id and auth_user.id = %s", [user.id])]
def replace_custom_tags(course, tree):
tags = os.listdir(course.path+'/custom_tags')
for tag in tags:
for element in tree.iter(tag):
element.tag = 'customtag'
impl = etree.SubElement(element, 'impl')
impl.text = tag
def course_xml_process(course, tree):
''' Do basic pre-processing of an XML tree. Assign IDs to all
items without. Propagate due dates, grace periods, etc. to child
items.
'''
replace_custom_tags(course, tree)
id_tag(tree)
propogate_downward_tag(tree, "due")
propogate_downward_tag(tree, "graded")
propogate_downward_tag(tree, "graceperiod")
propogate_downward_tag(tree, "showanswer")
propogate_downward_tag(tree, "rerandomize")
return tree
def course_file(user,course=None):
''' Given a user, return course.xml'''
if user.is_authenticated():
filename = os.path.basename(course.path)+"/"+UserProfile.objects.get(user=user).courseware # user.profile_cache.courseware
else:
filename = 'guest_course.xml'
# if a specific course is specified, then use multicourse to get the right path to the course XML directory
# if coursename and settings.ENABLE_MULTICOURSE:
# xp = multicourse_settings.get_course_xmlpath(coursename)
# filename = xp + filename # prefix the filename with the path
groups = user_groups(user)
options = {'dev_content':settings.DEV_CONTENT,
'groups' : groups}
cache_key = filename + "_processed?dev_content:" + str(options['dev_content']) + "&groups:" + str(sorted(groups))
if "dev" not in settings.DEFAULT_GROUPS:
tree_string = cache.get(cache_key)
else:
tree_string = None
if settings.DEBUG:
log.info('[courseware.content_parser.course_file] filename=%s, cache_key=%s' % (filename,cache_key))
# print '[courseware.content_parser.course_file] tree_string = ',tree_string
if not tree_string:
tree = course_xml_process(course, etree.XML(render_to_string(filename, options, namespace = 'course')))
tree_string = etree.tostring(tree)
cache.set(cache_key, tree_string, 60)
else:
tree = etree.XML(tree_string)
return tree
def section_file(user, section, course=None, dironly=False):
'''
Given a user and the name of a section, return that section.
This is done specific to each course.
If dironly=True then return the sections directory.
TODO: This is a bit weird; dironly should be scrapped.
'''
filename = section+".xml"
# if a specific course is specified, then use multicourse to get the right path to the course XML directory
xp = ''
if coursename and settings.ENABLE_MULTICOURSE: xp = multicourse_settings.get_course_xmlpath(coursename)
dirname = settings.DATA_DIR + xp + '/sections/'
if dironly: return dirname
if filename not in os.listdir(dirname):
log.error(filename+" not in "+str(os.listdir(dirname)))
return None
options = {'dev_content':settings.DEV_CONTENT,
'groups' : user_groups(user)}
tree = course_xml_process(course, etree.XML(render_to_string(filename, options, namespace = 'sections')))
return tree
def module_xml(user, module, id_tag, module_id, coursename=None):
''' Get XML for a module based on module and module_id. Assumes
module occurs once in courseware XML file or hidden section. '''
# Sanitize input
if not module.isalnum():
raise Exception("Module is not alphanumeric")
if not module_id.isalnum():
raise Exception("Module ID is not alphanumeric")
# Generate search
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)
doc = course_file(user,coursename)
sdirname = section_file(user,'',coursename,True) # get directory where sections information is stored
section_list = (s[:-4] for s in os.listdir(sdirname) if s[-4:]=='.xml')
result_set=doc.xpath(xpath_search)
if len(result_set)<1:
for section in section_list:
try:
s = section_file(user, section, coursename)
except etree.XMLSyntaxError:
ex= sys.exc_info()
raise ContentException("Malformed XML in " + section+ "("+str(ex[1].msg)+")")
result_set = s.xpath(xpath_search)
if len(result_set) != 0:
break
if len(result_set)>1:
log.error("WARNING: Potentially malformed course file", module, module_id)
if len(result_set)==0:
if settings.DEBUG:
log.error('[courseware.content_parser.module_xml] cannot find %s in course.xml tree' % xpath_search)
log.error('tree = %s' % etree.tostring(doc,pretty_print=True))
return None
if settings.DEBUG:
log.info('[courseware.content_parser.module_xml] found %s' % result_set)
return etree.tostring(result_set[0])
#return result_set[0].serialize()
def toc_from_xml(dom, active_chapter, active_section):
name = dom.xpath('//course/@name')[0]
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 dom.xpath('//course[@name=$name]/chapter[@name=$chname]/section', name=name, chname=c.get('name')):
sections.append({'name':s.get("name") or "",
'format':s.get("subtitle") if s.get("subtitle") else 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.get("name")==active_chapter)})
return ch
......@@ -44,6 +44,7 @@ class I4xSystem(object):
filestore - A filestore ojbect. Defaults to an instance of OSFS based at
settings.DATA_DIR.
'''
self.course = course
self.ajax_url = ajax_url
self.track_function = track_function
self.filestore = filestore
......
......@@ -70,7 +70,7 @@ def gradebook(request):
if 'course_admin' not in user_groups(request.user):
raise Http404
coursename = multicourse_settings.get_coursename_from_request(request)
course = settings.COURSES_BY_ID[course_id]
student_objects = User.objects.all()[:100]
student_info = []
......@@ -89,15 +89,15 @@ def gradebook(request):
'realname': UserProfile.objects.get(user=student).name
})
return render_to_response('gradebook.html', {'students': student_info})
return render_to_response('gradebook.html', {'students': student_info, 'course': course})
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def profile(request, student_id=None):
def profile(request, course_id=None, student_id=None):
''' User profile. Show username, location, etc, as well as grades .
We need to allow the user to change some of these settings .'''
course = settings.COURSES_BY_ID[course_id]
if student_id is None:
student = request.user
else:
......@@ -197,12 +197,16 @@ def index(request, course=None, chapter=None, section=None,
if not settings.COURSEWARE_ENABLED:
return redirect('/')
course = clean(get_course(request, course))
if not multicourse_settings.is_valid_course(course):
return redirect('/')
# course = clean(get_course(request, course))
# if not multicourse_settings.is_valid_course(course):
# return redirect('/')
try:
course = settings.COURSES_BY_ID[course_id]
except KeyError:
raise Http404("Course not found")
# keep track of current course being viewed in django's request.session
request.session['coursename'] = course
request.session['coursename'] = course.title
chapter = clean(chapter)
section = clean(section)
......@@ -210,7 +214,8 @@ def index(request, course=None, chapter=None, section=None,
context = {
'csrf': csrf(request)['csrf_token'],
'accordion': render_accordion(request, course, chapter, section),
'COURSE_TITLE': multicourse_settings.get_course_title(course),
'COURSE_TITLE': course.title,
'course': course,
'init': '',
'content': ''
}
......
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response
from django.conf import settings
@login_required
def index(request, page=0):
return render_to_response('staticbook.html',{'page':int(page)})
def index(request, course_id=None, page=0):
course = settings.COURSES_BY_ID[course_id]
return render_to_response('staticbook.html',{'page':int(page), 'course': course})
def index_shifted(request, page):
return index(request, int(page)+24)
......@@ -6,19 +6,22 @@ def url_class(url):
return "active"
return ""
%>
<%!
from django.core.urlresolvers import reverse
%>
<nav class="${active_page} course-material">
<div class="inner-wrapper">
<ol class="course-tabs">
<li class="courseware"><a href="${ MITX_ROOT_URL }/courseware/" class="${url_class('courseware')}">Courseware</a></li>
<li class="info"><a href="${ MITX_ROOT_URL }/info" class="${url_class('info')}">Course Info</a></li>
<li class="courseware"><a href="${reverse('courseware', args=[course.id])}" class="${url_class('courseware')}">Courseware</a></li>
<li class="info"><a href="${reverse('info', args=[course.id])}" class="${url_class('info')}">Course Info</a></li>
% if user.is_authenticated():
<li class="book"><a href="${ MITX_ROOT_URL }/book" class="${url_class('book')}">Textbook</a></li>
<li class="book"><a href="${reverse('book', args=[course.id])}" class="${url_class('book')}">Textbook</a></li>
<li class="discussion"><a href="${ MITX_ROOT_URL }/discussion/questions/">Discussion</a></li>
% endif
<li class="wiki"><a href="${ MITX_ROOT_URL }/wiki/view/" class="${url_class('wiki')}">Wiki</a></li>
<li class="wiki"><a href="${reverse('wiki_root')}" class="${url_class('wiki')}">Wiki</a></li>
% if user.is_authenticated():
<li class="profile"><a href="${ MITX_ROOT_URL }/profile" class="${url_class('profile')}">Profile</a></li>
<li class="profile"><a href="${reverse('profile', args=[course.id])}" class="${url_class('profile')}">Profile</a></li>
% endif
</ol>
</div>
......
......@@ -49,11 +49,10 @@ if settings.PERFSTATS:
if settings.COURSEWARE_ENABLED:
urlpatterns += (
url(r'^courseware/$', 'courseware.views.index', name="courseware"),
url(r'^wiki/', include('simplewiki.urls')),
url(r'^masquerade/', include('masquerade.urls')),
url(r'^courseware/(?P<course>[^/]*)/(?P<chapter>[^/]*)/(?P<section>[^/]*)/(?P<position>[^/]*)$', 'courseware.views.index'),
url(r'^courseware/(?P<course>[^/]*)/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$', 'courseware.views.index', name="courseware_section"),
# url(r'^courseware/(?P<course>[^/]*)/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$', 'courseware.views.index', name="courseware_section"),
url(r'^courseware/(?P<course>[^/]*)/(?P<chapter>[^/]*)/$', 'courseware.views.index', name="courseware_chapter"),
url(r'^courseware/(?P<course>[^/]*)/$', 'courseware.views.index', name="courseware_course"),
url(r'^jumpto/(?P<probname>[^/]+)/$', 'courseware.views.jump_to'),
......@@ -74,8 +73,12 @@ if settings.COURSEWARE_ENABLED:
# Multicourse related:
url(r'^courses$', 'courseware.views.courses'),
url(r'^courses/(?P<course_id>[^/]*)/info$', 'util.views.info'),
url(r'^courses/(?P<course_id>[^/]*)/courseware$', 'courseware.views.index'),
url(r'^courses/(?P<course_id>[^/]*)/info$', 'util.views.info', name="info"),
url(r'^courses/(?P<course_id>[^/]*)/book$', 'staticbook.views.index', name="book"),
url(r'^courses/(?P<course_id>[^/]*)/courseware/?$', 'courseware.views.index', name="courseware"),
url(r'^courses/(?P<course_id>[^/]*)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$', 'courseware.views.index', name="courseware_section"),
url(r'^courses/(?P<course_id>[^/]*)/profile$', 'courseware.views.profile', name="profile"),
url(r'^courses/(?P<course_id>[^/]*)/profile/(?P<student_id>[^/]*)/$', 'courseware.views.profile'),
)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment