Files
edx-platform/lms/djangoapps/courseware/module_render.py
2012-07-13 13:45:38 -04:00

423 lines
16 KiB
Python

import json
import logging
from django.conf import settings
from django.http import Http404
from django.http import HttpResponse
from functools import wraps
from xmodule.modulestore.django import modulestore
from mitxmako.shortcuts import render_to_string
from models import StudentModule, StudentModuleCache
from static_replace import replace_urls
log = logging.getLogger("mitx.courseware")
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.
Note that these functions can be closures over e.g. a django request
and user, or other environment-specific info.
'''
def __init__(self, ajax_url, track_function,
get_module, render_template, replace_urls,
user=None, filestore=None, xqueue_callback_url=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.
get_module - function that takes (location) and returns a corresponding
module instance object.
render_template - a function that takes (template_file, context), and returns
rendered html.
user - The user to base the random number generator seed off of for this request
filestore - A filestore ojbect. Defaults to an instance of OSFS based at
settings.DATA_DIR.
replace_urls - TEMPORARY - A function like static_replace.replace_urls
that capa_module can use to fix up the static urls in ajax results.
'''
self.ajax_url = ajax_url
self.xqueue_callback_url = xqueue_callback_url
self.track_function = track_function
self.filestore = filestore
self.get_module = get_module
self.render_template = render_template
self.exception404 = Http404
self.DEBUG = settings.DEBUG
self.seed = user.id if user is not None else 0
self.replace_urls = replace_urls
def get(self, attr):
''' provide uniform access to attributes (like etree).'''
return self.__dict__.get(attr)
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 make_track_function(request):
'''
Make a tracking function that logs what happened.
For use in I4xSystem.
'''
import track.views
def f(event_type, event):
return track.views.server_track(request, event_type, event, page='x_module')
return f
def grade_histogram(module_id):
''' Print out a histogram of grades on a given problem.
Part of staff member debug info.
'''
from django.db import connection
cursor = connection.cursor()
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]) # Add ORDER BY to sql query?
if len(grades) == 1 and grades[0][0] is None:
return []
return grades
def toc_for_course(user, request, course, active_chapter, active_section):
'''
Create a table of contents from the module store
Return format:
[ {'name': name, 'sections': SECTIONS, 'active': bool}, ... ]
where SECTIONS is a list
[ {'name': name, 'format': format, 'due': due, 'active' : bool}, ...]
active is set for the section and chapter corresponding to the passed
parameters. Everything else comes from the xml, or defaults to "".
chapters with name 'hidden' are skipped.
'''
student_module_cache = StudentModuleCache(user, course, depth=2)
(course, _, _, _) = get_module(user, request, course.location, student_module_cache)
chapters = list()
for chapter in course.get_display_items():
sections = list()
for section in chapter.get_display_items():
active = (chapter.metadata.get('display_name') == active_chapter and
section.metadata.get('display_name') == active_section)
sections.append({'name': section.metadata.get('display_name'),
'format': section.metadata.get('format', ''),
'due': section.metadata.get('due', ''),
'active': active})
chapters.append({'name': chapter.metadata.get('display_name'),
'sections': sections,
'active': chapter.metadata.get('display_name') == active_chapter})
return chapters
def get_section(course_module, chapter, section):
"""
Returns the xmodule descriptor for the name course > chapter > section,
or None if this doesn't specify a valid section
course: Course url
chapter: Chapter name
section: Section name
"""
if course_module is None:
return
chapter_module = None
for _chapter in course_module.get_children():
if _chapter.metadata.get('display_name') == chapter:
chapter_module = _chapter
break
if chapter_module is None:
return
section_module = None
for _section in chapter_module.get_children():
if _section.metadata.get('display_name') == section:
section_module = _section
break
return section_module
def get_module(user, request, location, student_module_cache, position=None):
''' Get an instance of the xmodule class identified by location,
setting the state based on an existing StudentModule, or creating one if none
exists.
Arguments:
- user : current django User
- request : current django HTTPrequest
- location : A Location-like object identifying the module to load
- student_module_cache : a StudentModuleCache
- position : extra information from URL for user-specified
position within module
Returns:
- a tuple (xmodule instance, instance_module, shared_module, module category).
instance_module is a StudentModule specific to this module for this student
shared_module is a StudentModule specific to all modules with the same 'shared_state_key' attribute, or None if the module doesn't elect to share state
'''
descriptor = modulestore().get_item(location)
instance_module = student_module_cache.lookup(descriptor.category, descriptor.location.url())
shared_state_key = getattr(descriptor, 'shared_state_key', None)
if shared_state_key is not None:
shared_module = student_module_cache.lookup(descriptor.category, shared_state_key)
else:
shared_module = None
instance_state = instance_module.state if instance_module is not None else None
shared_state = shared_module.state if shared_module is not None else None
# Setup system context for module instance
ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
xqueue_callback_url = settings.MITX_ROOT_URL + '/xqueue/' + user.username + '/' + descriptor.location.url() + '/'
def _get_module(location):
(module, _, _, _) = get_module(user, request, location, student_module_cache, position)
return module
# TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory
# that the xml was loaded from
system = I4xSystem(track_function=make_track_function(request),
render_template=render_to_string,
ajax_url=ajax_url,
xqueue_callback_url=xqueue_callback_url,
# TODO (cpennington): Figure out how to share info between systems
filestore=descriptor.system.resources_fs,
get_module=_get_module,
user=user,
# TODO (cpennington): This should be removed when all html from
# a module is coming through get_html and is therefore covered
# by the replace_static_urls code below
replace_urls=replace_urls,
)
# pass position specified in URL to module through I4xSystem
system.set('position', position)
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
replace_prefix = module.metadata['data_dir']
module = replace_static_urls(module, replace_prefix)
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff:
module = add_histogram(module)
# If StudentModule for this instance wasn't already in the database,
# and this isn't a guest user, create it.
if user.is_authenticated():
if not instance_module:
instance_module = StudentModule(
student=user,
module_type=descriptor.category,
module_state_key=module.id,
state=module.get_instance_state(),
max_grade=module.max_score())
instance_module.save()
# Add to cache. The caller and the system context have references
# to it, so the change persists past the return
student_module_cache.append(instance_module)
if not shared_module and shared_state_key is not None:
shared_module = StudentModule(
student=user,
module_type=descriptor.category,
module_state_key=shared_state_key,
state=module.get_shared_state())
shared_module.save()
student_module_cache.append(shared_module)
return (module, instance_module, shared_module, descriptor.category)
def replace_static_urls(module, prefix):
"""
Updates the supplied module with a new get_html function that wraps
the old get_html function and substitutes urls of the form /static/...
with urls that are /static/<prefix>/...
"""
original_get_html = module.get_html
@wraps(original_get_html)
def get_html():
return replace_urls(original_get_html(), staticfiles_prefix=prefix)
module.get_html = get_html
return module
def add_histogram(module):
"""
Updates the supplied module with a new get_html function that wraps
the output of the old get_html function with additional information
for admin users only, including a histogram of student answers and the
definition of the xmodule
"""
original_get_html = module.get_html
@wraps(original_get_html)
def get_html():
module_id = module.id
histogram = grade_histogram(module_id)
render_histogram = len(histogram) > 0
# TODO: fixme - no filename in module.xml in general (this code block for edx4edx)
# the following if block is for summer 2012 edX course development; it will change when the CMS comes online
if settings.MITX_FEATURES.get('DISPLAY_EDIT_LINK') and settings.DEBUG and module_xml.get('filename') is not None:
coursename = multicourse_settings.get_coursename_from_request(request)
github_url = multicourse_settings.get_course_github_url(coursename)
fn = module_xml.get('filename')
if module_xml.tag=='problem': fn = 'problems/' + fn # grrr
edit_link = (github_url + '/tree/master/' + fn) if github_url is not None else None
if module_xml.tag=='problem': edit_link += '.xml' # grrr
else:
edit_link = False
# Cast module.definition and module.metadata to dicts so that json can dump them
# even though they are lazily loaded
staff_context = {'definition': json.dumps(dict(module.definition), indent=4),
'metadata': json.dumps(dict(module.metadata), indent=4),
'element_id': module.location.html_id(),
'edit_link': edit_link,
'histogram': json.dumps(histogram),
'render_histogram': render_histogram,
'module_content': original_get_html()}
return render_to_string("staff_problem_info.html", staff_context)
module.get_html = get_html
return module
# THK: TEMPORARY BYPASS OF AUTH!
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.models import User
@csrf_exempt
def xqueue_callback(request, username, id, dispatch):
# Parse xqueue response
get = request.POST.copy()
try:
header = json.loads(get.pop('xqueue_header')[0]) # 'dict'
except Exception as err:
msg = "Error in xqueue_callback %s: Invalid return format" % err
raise Exception(msg)
# Should proceed only when the request timestamp is more recent than problem timestamp
timestamp = header['timestamp']
# Retrieve target StudentModule
user = User.objects.get(username=username)
student_module_cache = StudentModuleCache(user, modulestore().get_item(id))
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache)
if instance_module is None:
log.debug("Couldn't find module '%s' for user '%s'",
id, request.user)
raise Http404
oldgrade = instance_module.grade
old_instance_state = instance_module.state
# We go through the "AJAX" path
# So far, the only dispatch from xqueue will be 'score_update'
try:
ajax_return = instance.handle_ajax(dispatch, get) # Can ignore the "ajax" return in 'xqueue_callback'
except:
log.exception("error processing ajax call")
raise
# Save state back to database
instance_module.state = instance.get_instance_state()
if instance.get_score():
instance_module.grade = instance.get_score()['score']
if instance_module.grade != oldgrade or instance_module.state != old_instance_state:
instance_module.save()
return HttpResponse("")
def modx_dispatch(request, dispatch=None, id=None):
''' Generic view for extensions. This is where AJAX calls go.
Arguments:
- request -- the django request.
- dispatch -- the command string to pass through to the module's handle_ajax call
(e.g. 'problem_reset'). If this string contains '?', only pass
through the part before the first '?'.
- id -- the module id. Used to look up the XModule instance
'''
# ''' (fix emacs broken parsing)
# If there are arguments, get rid of them
dispatch, _, _ = dispatch.partition('?')
student_module_cache = StudentModuleCache(request.user, modulestore().get_item(id))
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache)
if instance_module is None:
log.debug("Couldn't find module '%s' for user '%s'",
id, request.user)
raise Http404
oldgrade = instance_module.grade
old_instance_state = instance_module.state
old_shared_state = shared_module.state if shared_module is not None else None
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
except:
log.exception("error processing ajax call")
raise
# Save the state back to the database
instance_module.state = instance.get_instance_state()
if instance.get_score():
instance_module.grade = instance.get_score()['score']
if instance_module.grade != oldgrade or instance_module.state != old_instance_state:
instance_module.save()
if shared_module is not None:
shared_module.state = instance.get_shared_state()
if shared_module.state != old_shared_state:
shared_module.save()
# Return whatever the module wanted to return to the client/caller
return HttpResponse(ajax_return)