Commit 4134033a by Victor Shnayder

Clean and refactor courseware/views.py and module_render.py

* Refactor index() so that it makes sense to me and hopefully others :)
* Rename preloaded cache of student modules to student_module_cache
* Fix line length and whitespace throughout
* add docstrings and other comments
* a few behavior-preserving tweaks to the code to make it clearer.
* Separate codepaths for with-module and without-module in index view
* Remove default chapter + section, since they don't exist anyway in course.xml
parent 33d150db
......@@ -26,8 +26,29 @@ class I4xSystem(object):
This is an abstraction such that x_modules can function independent
of the courseware (e.g. import into other types of courseware, LMS,
or if we want to have a sandbox server for user-contributed content)
I4xSystem objects are passed to x_modules to provide access to system
functionality.
'''
def __init__(self, ajax_url, track_function, render_function, render_template, filestore=None):
def __init__(self, ajax_url, track_function, render_function,
render_template, filestore=None):
'''
Create a closure around the system environment.
ajax_url - the url where ajax calls to the encapsulating module go.
track_function - function of (event_type, event), intended for logging
or otherwise tracking the event.
TODO: Not used, and has inconsistent args in different
files. Update or remove.
render_function - function that takes (module_xml) and renders it,
returning a dictionary with a context for rendering the
module to html. Dictionary will contain keys 'content'
and 'type'.
render_template - a function that takes (template_file, context), and returns
rendered html.
filestore - A filestore ojbect. Defaults to an instance of OSFS based at
settings.DATA_DIR.
'''
self.ajax_url = ajax_url
self.track_function = track_function
if not filestore:
......@@ -35,37 +56,47 @@ class I4xSystem(object):
else:
self.filestore = filestore
if settings.DEBUG:
log.info("[courseware.module_render.I4xSystem] filestore path = %s" % filestore)
log.info("[courseware.module_render.I4xSystem] filestore path = %s",
filestore)
self.render_function = render_function
self.render_template = render_template
self.exception404 = Http404
self.DEBUG = settings.DEBUG
def get(self,attr): # uniform access to attributes (like etree)
def get(self, attr):
''' provide uniform access to attributes (like etree).'''
return self.__dict__.get(attr)
def set(self,attr,val): # uniform access to attributes (like etree)
def set(self,attr,val):
'''provide uniform access to attributes (like etree)'''
self.__dict__[attr] = val
def __repr__(self):
return repr(self.__dict__)
def __str__(self):
return str(self.__dict__)
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
# is broken.
def smod_cache_lookup(cache, module_type, module_id):
'''
Look for a student module with the given type and id in the cache.
cache -- list of student modules
returns first found object, or None
'''
for o in cache:
if o.module_type == module_type and \
o.module_id == module_id:
if o.module_type == module_type and o.module_id == module_id:
return o
return None
def make_track_function(request):
''' We want the capa problem (and other modules) to be able to
track/log what happens inside them without adding dependencies on
Django or the rest of the codebase. We do this by passing a
tracking function to them. This generates a closure for each request
that gives a clean interface on both sides.
Django or the rest of the codebase.
To do this in a clean way, we pass a tracking function to the module,
which calls it to log events.
'''
import track.views
......@@ -75,85 +106,91 @@ def make_track_function(request):
def grade_histogram(module_id):
''' Print out a histogram of grades on a given problem.
Part of staff member debug info.
Part of staff member debug info.
'''
from django.db import connection
cursor = connection.cursor()
cursor.execute("select courseware_studentmodule.grade,COUNT(courseware_studentmodule.student_id) from courseware_studentmodule where courseware_studentmodule.module_id=%s group by courseware_studentmodule.grade", [module_id])
q = """SELECT courseware_studentmodule.grade,
COUNT(courseware_studentmodule.student_id)
FROM courseware_studentmodule
WHERE courseware_studentmodule.module_id=%s
GROUP BY courseware_studentmodule.grade"""
# Passing module_id this way prevents sql-injection.
cursor.execute(q, [module_id])
grades = list(cursor.fetchall())
grades.sort(key=lambda x:x[0]) # Probably not necessary
if (len(grades) == 1 and grades[0][0] is None):
grades.sort(key=lambda x: x[0]) # Add ORDER BY to sql query?
if len(grades) == 1 and grades[0][0] is None:
return []
return grades
def get_module(user, request, xml_module, module_object_preload, position=None):
''' Get the appropriate xmodule and StudentModule.
def get_module(user, request, module_xml, student_module_cache, position=None):
''' Get an instance of the xmodule class corresponding to module_xml,
setting the state based on an existing StudentModule, or creating one if none
exists.
Arguments:
- user : current django User
- request : current django HTTPrequest
- xml_module : lxml etree of xml subtree for the current module
- module_object_preload : list of StudentModule objects, one of which may match this module type and id
- position : extra information from URL for user-specified position within module
- module_xml : lxml etree of xml subtree for the requested module
- student_module_cache : list of StudentModule objects, one of which may
match this module type and id
- position : extra information from URL for user-specified
position within module
Returns:
- a tuple (xmodule instance, student module, module type).
'''
module_type=xml_module.tag
module_class=xmodule.get_module_class(module_type)
module_id=xml_module.get('id') #module_class.id_attribute) or ""
# Grab state from database
smod = object_cache(module_object_preload,
user,
module_type,
module_id)
if not smod: # If nothing in the database...
state=None
else:
state = smod.state
module_type = module_xml.tag
module_class = xmodule.get_module_class(module_type)
module_id = module_xml.get('id')
# Grab xmodule state from StudentModule cache
smod = smod_cache_lookup(student_module_cache, module_type, module_id)
state = smod.state if smod else None
# get coursename if stored
# get coursename if present in request
coursename = multicourse_settings.get_coursename_from_request(request)
if coursename and settings.ENABLE_MULTICOURSE:
xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
# path to XML for the course
xp = multicourse_settings.get_course_xmlpath(coursename)
data_root = settings.DATA_DIR + xp
else:
data_root = settings.DATA_DIR
# Create a new instance
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module_type+'/'+module_id+'/'
# Setup system context for module instance
ajax_url = settings.MITX_ROOT_URL + '/modx/' + module_type + '/' + module_id + '/'
def render_function(module_xml):
return render_x_module(user, request, module_xml, student_module_cache, position)
system = I4xSystem(track_function = make_track_function(request),
render_function = lambda x: render_x_module(user, request, x, module_object_preload, position),
render_function = render_function,
render_template = render_to_string,
ajax_url = ajax_url,
filestore = OSFS(data_root),
)
system.set('position',position) # pass URL specified position along to module, through I4xSystem
instance=module_class(system,
etree.tostring(xml_module),
module_id,
state=state)
# pass position specified in URL to module through I4xSystem
system.set('position', position)
instance = module_class(system,
etree.tostring(module_xml),
module_id,
state=state)
# If instance wasn't already in the database, and this
# isn't a guest user, create it
# If StudentModule for this instance wasn't already in the database,
# and this isn't a guest user, create it.
if not smod and user.is_authenticated():
smod=StudentModule(student=user,
module_type = module_type,
module_id=module_id,
state=instance.get_state())
smod = StudentModule(student=user, module_type = module_type,
module_id=module_id, state=instance.get_state())
smod.save()
module_object_preload.append(smod)
# Add to cache. The caller and the system context have references
# to it, so the change persists past the return
student_module_cache.append(smod)
return (instance, smod, module_type)
def render_x_module(user, request, xml_module, module_object_preload, position=None):
def render_x_module(user, request, module_xml, student_module_cache, position=None):
''' Generic module for extensions. This renders to HTML.
modules include sequential, vertical, problem, video, html
......@@ -164,37 +201,36 @@ def render_x_module(user, request, xml_module, module_object_preload, position=N
- user : current django User
- request : current django HTTPrequest
- xml_module : lxml etree of xml subtree for the current module
- module_object_preload : list of StudentModule objects, one of which may match this module type and id
- module_xml : lxml etree of xml subtree for the current module
- student_module_cache : list of StudentModule objects, one of which may match this module type and id
- position : extra information from URL for user-specified position within module
Returns:
- dict which is context for HTML rendering of the specified module
- dict which is context for HTML rendering of the specified module. Will have
key 'content', and will have 'type' key if passed a valid module.
'''
if xml_module==None :
return {"content":""}
if module_xml is None :
return {"content": ""}
(instance, smod, module_type) = get_module(user, request, xml_module, module_object_preload, position)
(instance, smod, module_type) = get_module(
user, request, module_xml, student_module_cache, position)
# Grab content
content = instance.get_html()
# special extra information about each problem, only for users who are staff
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff:
module_id = xml_module.get('id')
module_id = module_xml.get('id')
histogram = grade_histogram(module_id)
render_histogram = len(histogram) > 0
content=content+render_to_string("staff_problem_info.html", {'xml':etree.tostring(xml_module),
'module_id' : module_id,
'histogram': json.dumps(histogram),
'render_histogram' : render_histogram})
staff_context = {'xml': etree.tostring(module_xml),
'module_id': module_id,
'histogram': json.dumps(histogram),
'render_histogram': render_histogram}
content += render_to_string("staff_problem_info.html", staff_context)
content = {'content':content,
'type':module_type}
return content
context = {'content': content, 'type': module_type}
return context
def modx_dispatch(request, module=None, dispatch=None, id=None):
''' Generic view for extensions. This is where AJAX calls go.
......@@ -210,32 +246,38 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
- id -- the module id. Used to look up the student module.
e.g. filenamexformularesponse
'''
# ''' (fix emacs broken parsing)
if not request.user.is_authenticated():
return redirect('/')
# python concats adjacent strings
error_msg = ("We're sorry, this module is temporarily unavailable."
"Our staff is working to fix it as soon as possible")
# Grab the student information for the module from the database
s = StudentModule.objects.filter(student=request.user,
module_id=id)
#s = StudentModule.get_with_caching(request.user, id)
if len(s) == 0 or s is None:
log.debug("Couldnt find module for user and id " + str(module) + " " + str(request.user) + " "+ str(id))
# s = StudentModule.get_with_caching(request.user, id)
if s is None or len(s) == 0:
log.debug("Couldn't find module '%s' for user '%s' and id '%s'",
module, request.user, id)
raise Http404
s = s[0]
oldgrade = s.grade
oldstate = s.state
# TODO: if dispatch is left at default value None, this will go boom. What's the correct
# behavior?
dispatch=dispatch.split('?')[0]
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module+'/'+id+'/'
# If there are arguments, get rid of them
if '?' in dispatch:
dispatch = dispatch.split('?')[0]
# get coursename if stored
ajax_url = '{root}/modx/{module}/{id}'.format(root = settings.MITX_ROOT_URL,
module=module, id=id)
coursename = multicourse_settings.get_coursename_from_request(request)
if coursename and settings.ENABLE_MULTICOURSE:
xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
xp = multicourse_settings.get_course_xmlpath(coursename)
data_root = settings.DATA_DIR + xp
else:
data_root = settings.DATA_DIR
......@@ -244,11 +286,13 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
try:
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
except:
log.exception("Unable to load module during ajax call. module=%s, dispatch=%s, id=%s" % (module, dispatch, id))
log.exception(
"Unable to load module during ajax call. module=%s, dispatch=%s, id=%s",
module, dispatch, id)
if accepts(request, 'text/html'):
return render_to_response("module-error.html", {})
else:
response = HttpResponse(json.dumps({'success': "We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible"}))
response = HttpResponse(json.dumps({'success': error_msg}))
return response
# Create the module
......@@ -260,24 +304,23 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
)
try:
instance=xmodule.get_module_class(module)(system,
xml,
id,
state=oldstate)
module_class = xmodule.get_module_class(module)
instance = module_class(system, xml, id, state=oldstate)
except:
log.exception("Unable to load module instance during ajax call")
if accepts(request, 'text/html'):
return render_to_response("module-error.html", {})
else:
response = HttpResponse(json.dumps({'success': "We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible"}))
response = HttpResponse(json.dumps({'success': error_msg}))
return response
# Let the module handle the AJAX
ajax_return=instance.handle_ajax(dispatch, request.POST)
ajax_return = instance.handle_ajax(dispatch, request.POST)
# Save the state back to the database
s.state=instance.get_state()
s.state = instance.get_state()
if instance.get_score():
s.grade=instance.get_score()['score']
s.grade = instance.get_score()['score']
if s.grade != oldgrade or s.state != oldstate:
s.save()
# Return whatever the module wanted to return to the client/caller
......
......@@ -41,66 +41,71 @@ def gradebook(request):
coursename = multicourse_settings.get_coursename_from_request(request)
student_objects = User.objects.all()[:100]
student_info = [{'username' :s.username,
'id' : s.id,
student_info = [{'username': s.username,
'id': s.id,
'email': s.email,
'grade_info' : grades.grade_sheet(s,coursename),
'realname' : UserProfile.objects.get(user = s).name
'grade_info': grades.grade_sheet(s, coursename),
'realname': UserProfile.objects.get(user = s).name
} for s in student_objects]
return render_to_response('gradebook.html',{'students':student_info})
return render_to_response('gradebook.html', {'students': student_info})
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def profile(request, student_id = None):
def profile(request, 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 .'''
if student_id is None:
student = request.user
else:
else:
if 'course_admin' not in content_parser.user_groups(request.user):
raise Http404
student = User.objects.get( id = int(student_id))
user_info = UserProfile.objects.get(user=student) # request.user.profile_cache #
user_info = UserProfile.objects.get(user=student) # request.user.profile_cache #
coursename = multicourse_settings.get_coursename_from_request(request)
context={'name':user_info.name,
'username':student.username,
'location':user_info.location,
'language':user_info.language,
'email':student.email,
'format_url_params' : content_parser.format_url_params,
'csrf':csrf(request)['csrf_token']
}
context.update(grades.grade_sheet(student,coursename))
context = {'name': user_info.name,
'username': student.username,
'location': user_info.location,
'language': user_info.language,
'email': student.email,
'format_url_params': content_parser.format_url_params,
'csrf': csrf(request)['csrf_token']
}
context.update(grades.grade_sheet(student, coursename))
return render_to_response('profile.html', context)
def render_accordion(request,course,chapter,section):
def render_accordion(request, course, chapter, section):
''' Draws navigation bar. Takes current position in accordion as
parameter. Returns (initialization_javascript, content)'''
if not course:
course = "6.002 Spring 2012"
toc=content_parser.toc_from_xml(content_parser.course_file(request.user,course), chapter, section)
active_chapter=1
toc = content_parser.toc_from_xml(
content_parser.course_file(request.user, course), chapter, section)
active_chapter = 1
for i in range(len(toc)):
if toc[i]['active']:
active_chapter=i
context=dict([['active_chapter',active_chapter],
['toc',toc],
['course_name',course],
['format_url_params',content_parser.format_url_params],
['csrf',csrf(request)['csrf_token']]] + \
active_chapter = i
context=dict([('active_chapter', active_chapter),
('toc', toc),
('course_name', course),
('format_url_params', content_parser.format_url_params),
('csrf', csrf(request)['csrf_token'])] +
template_imports.items())
return render_to_string('accordion.html',context)
return render_to_string('accordion.html', context)
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def render_section(request, section):
''' TODO: Consolidate with index
''' TODO: Consolidate with index
'''
user = request.user
if not settings.COURSEWARE_ENABLED:
......@@ -120,15 +125,15 @@ def render_section(request, section):
}
module_ids = dom.xpath("//@id")
if user.is_authenticated():
module_object_preload = list(StudentModule.objects.filter(student=user,
student_module_cache = list(StudentModule.objects.filter(student=user,
module_id__in=module_ids))
else:
module_object_preload = []
student_module_cache = []
try:
module = render_x_module(user, request, dom, module_object_preload)
module = render_x_module(user, request, dom, student_module_cache)
except:
log.exception("Unable to load module")
context.update({
......@@ -138,35 +143,19 @@ def render_section(request, section):
return render_to_response('courseware.html', context)
context.update({
'init':module.get('init_js', ''),
'content':module['content'],
'init': module.get('init_js', ''),
'content': module['content'],
})
result = render_to_response('courseware.html', context)
return result
def get_course(request, course):
''' Figure out what the correct course is.
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def index(request, course=None, chapter="Using the System", section="Hints",position=None):
''' Displays courseware accordion, and any associated content.
Arguments:
- request : HTTP request
- course : coursename (str)
- chapter : chapter name (str)
- section : section name (str)
- position : position in module, eg of <sequential> module (str)
Returns:
- HTTPresponse
'''
user = request.user
if not settings.COURSEWARE_ENABLED:
return redirect('/')
Needed to preserve backwards compatibility with non-multi-course version.
TODO: Can this go away once multicourse becomes standard?
'''
if course==None:
if not settings.ENABLE_MULTICOURSE:
......@@ -175,97 +164,165 @@ def index(request, course=None, chapter="Using the System", section="Hints",posi
course = request.session['coursename']
else:
course = settings.COURSE_DEFAULT
return course
# 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("_"," ")
# use multicourse module to determine if "course" is valid
#if course!=settings.COURSE_NAME.replace('_',' '):
if not multicourse_settings.is_valid_course(course):
return redirect('/')
def get_module_xml(user, course, chapter, section):
''' Look up the module xml for the given course/chapter/section path.
request.session['coursename'] = course # keep track of current course being viewed in django's request.session
Takes the user to look up the course file.
Returns None if there was a problem, or the lxml etree for the module.
'''
try:
# this is the course.xml etree
dom = content_parser.course_file(user,course) # also pass course to it, for course-specific XML path
# this is the course.xml etree
dom = content_parser.course_file(user, course)
except:
log.exception("Unable to parse courseware xml")
return render_to_response('courseware-error.html', {})
return None
# this is the module's parent's etree
dom_module = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]",
course=course, chapter=chapter, section=section)
#print "DM", dom_module
if len(dom_module) == 0:
module_wrapper = None
else:
module_wrapper = dom_module[0]
path = "//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]"
dom_module = dom.xpath(path, course=course, chapter=chapter, section=section)
module_wrapper = dom_module[0] if len(dom_module) > 0 else None
if module_wrapper is None:
module = None
elif module_wrapper.get("src"):
module = content_parser.section_file(user=user, section=module_wrapper.get("src"), coursename=course)
module = content_parser.section_file(
user=user, section=module_wrapper.get("src"), coursename=course)
else:
# this is the module's etree
module = etree.XML(etree.tostring(module_wrapper[0])) # Copy the element out of the tree
# Copy the element out of the module's etree
module = etree.XML(etree.tostring(module_wrapper[0]))
return module
module_ids = []
if module is not None:
module_ids = module.xpath("//@id",
course=course, chapter=chapter, section=section)
if user.is_authenticated():
module_object_preload = list(StudentModule.objects.filter(student=user,
module_id__in=module_ids))
else:
module_object_preload = []
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def index(request, course=None, chapter=None, section=None,
position=None):
''' Displays courseware accordion, and any associated content.
If course, chapter, and section aren't all specified, just returns
the accordion. If they are specified, returns an error if they don't
point to a valid module.
Arguments:
- request : HTTP request
- course : coursename (str)
- chapter : chapter name (str)
- section : section name (str)
- position : position in module, eg of <sequential> module (str)
Returns:
- HTTPresponse
'''
def clean(s):
''' Fixes URLs -- we convert spaces to _ in URLs to prevent
funny encoding characters and keep the URLs readable. This undoes
that transformation.
TODO: Properly replace underscores. (Q: what is properly?)
'''
return s.replace('_', ' ')
def get_submodule_ids(module_xml):
'''
Get a list with ids of the modules within this module.
'''
return module_xml.xpath("//@id")
def preload_student_modules(module_xml):
'''
Find any StudentModule objects for this user that match
one of the given module_ids. Used as a cache to avoid having
each rendered module hit the db separately.
Returns the list, or None on error.
'''
if request.user.is_authenticated():
module_ids = get_submodule_ids(module_xml)
return list(StudentModule.objects.filter(student=request.user,
module_id__in=module_ids))
else:
return []
def get_module_context():
'''
Look up the module object and render it. If all goes well, returns
{'init': module-init-js, 'content': module-rendered-content}
If there's an error, returns
{'content': module-error message}
'''
# Can't modify variables of outer scope, so need new ones
chapter_ = clean(chapter)
section_ = clean(section)
user = request.user
module_xml = get_module_xml(user, course, chapter_, section_)
if module_xml is None:
log.exception("couldn't get module_xml: course/chapter/section: '%s/%s/%s'",
course, chapter_, section_)
return {'content' : render_to_string("module-error.html", {})}
student_module_cache = preload_student_modules(module_xml)
try:
module_context = render_x_module(user, request, module_xml,
student_module_cache, position)
except:
log.exception("Unable to load module")
return {'content' : render_to_string("module-error.html", {})}
return {'init': module_context.get('init_js', ''),
'content': module_context['content']}
if not settings.COURSEWARE_ENABLED:
return redirect('/')
course = clean(get_course(request, course))
if not multicourse_settings.is_valid_course(course):
return redirect('/')
# keep track of current course being viewed in django's request.session
request.session['coursename'] = course
context = {
'csrf': csrf(request)['csrf_token'],
'accordion': render_accordion(request, course, chapter, section),
'COURSE_TITLE':multicourse_settings.get_course_title(course),
'COURSE_TITLE': multicourse_settings.get_course_title(course),
'init': '',
'content': ''
}
try:
module_context = render_x_module(user, request, module, module_object_preload, position)
except:
log.exception("Unable to load module")
context.update({
'init': '',
'content': render_to_string("module-error.html", {}),
})
return render_to_response('courseware.html', context)
context.update({
'init': module_context.get('init_js', ''),
'content': module_context['content'],
})
look_for_module = chapter is not None and section is not None
if look_for_module:
context.update(get_module_context())
result = render_to_response('courseware.html', context)
return result
def jump_to(request, probname=None):
'''
Jump to viewing a specific problem. The problem is specified by a problem name - currently the filename (minus .xml)
of the problem. Maybe this should change to a more generic tag, eg "name" given as an attribute in <problem>.
Jump to viewing a specific problem. The problem is specified by a
problem name - currently the filename (minus .xml) of the problem.
Maybe this should change to a more generic tag, eg "name" given as
an attribute in <problem>.
We do the jump by (1) reading course.xml to find the first instance of <problem> with the given filename, then
(2) finding the parent element of the problem, then (3) rendering that parent element with a specific computed position
value (if it is <sequential>).
We do the jump by (1) reading course.xml to find the first
instance of <problem> with the given filename, then (2) finding
the parent element of the problem, then (3) rendering that parent
element with a specific computed position value (if it is
<sequential>).
'''
# get coursename if stored
coursename = multicourse_settings.get_coursename_from_request(request)
# begin by getting course.xml tree
xml = content_parser.course_file(request.user,coursename)
xml = content_parser.course_file(request.user, coursename)
# look for problem of given name
pxml = xml.xpath('//problem[@filename="%s"]' % probname)
......@@ -279,12 +336,16 @@ def jump_to(request, probname=None):
section = None
branch = parent
for k in range(4): # max depth of recursion
if branch.tag=='section': section = branch.get('name')
if branch.tag=='chapter': chapter = branch.get('name')
if branch.tag == 'section':
section = branch.get('name')
if branch.tag == 'chapter':
chapter = branch.get('name')
branch = branch.getparent()
position = None
if parent.tag=='sequential':
position = parent.index(pxml)+1 # position in sequence
return index(request,course=coursename,chapter=chapter,section=section,position=position)
if parent.tag == 'sequential':
position = parent.index(pxml) + 1 # position in sequence
return index(request,
course=coursename, chapter=chapter,
section=section, position=position)
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