Adds a feature to the edX platform which allows instructors to set individual due dates for students on particular coursework. This code is meant primarily for on-campus use--it is not intended that this feature would be used for MOOCs. It adds a new tab, "Extensions", to the beta instructor dashboard which allows changing due dates per student. This feature is enabled by setting FEATURES['INDIVIDUAL_DUE_DATES'] = True.
225 lines
6.6 KiB
Python
225 lines
6.6 KiB
Python
"""
|
|
Tools for the instructor dashboard
|
|
"""
|
|
import dateutil
|
|
import json
|
|
|
|
from django.contrib.auth.models import User
|
|
from django.http import HttpResponseBadRequest
|
|
from django.utils.timezone import utc
|
|
from django.utils.translation import ugettext as _
|
|
|
|
from courseware.models import StudentModule
|
|
from xmodule.fields import Date
|
|
|
|
DATE_FIELD = Date()
|
|
|
|
|
|
class DashboardError(Exception):
|
|
"""
|
|
Errors arising from use of the instructor dashboard.
|
|
"""
|
|
def response(self):
|
|
"""
|
|
Generate an instance of HttpResponseBadRequest for this error.
|
|
"""
|
|
error = unicode(self)
|
|
return HttpResponseBadRequest(json.dumps({'error': error}))
|
|
|
|
|
|
def handle_dashboard_error(view):
|
|
"""
|
|
Decorator which adds seamless DashboardError handling to a view. If a
|
|
DashboardError is raised during view processing, an HttpResponseBadRequest
|
|
is sent back to the client with JSON data about the error.
|
|
"""
|
|
def wrapper(request, course_id):
|
|
"""
|
|
Wrap the view.
|
|
"""
|
|
try:
|
|
return view(request, course_id=course_id)
|
|
except DashboardError, error:
|
|
return error.response()
|
|
|
|
return wrapper
|
|
|
|
|
|
def strip_if_string(value):
|
|
if isinstance(value, basestring):
|
|
return value.strip()
|
|
return value
|
|
|
|
|
|
def get_student_from_identifier(unique_student_identifier):
|
|
"""
|
|
Gets a student object using either an email address or username.
|
|
|
|
Returns the student object associated with `unique_student_identifier`
|
|
|
|
Raises User.DoesNotExist if no user object can be found.
|
|
"""
|
|
unique_student_identifier = strip_if_string(unique_student_identifier)
|
|
if "@" in unique_student_identifier:
|
|
student = User.objects.get(email=unique_student_identifier)
|
|
else:
|
|
student = User.objects.get(username=unique_student_identifier)
|
|
return student
|
|
|
|
|
|
def parse_datetime(datestr):
|
|
"""
|
|
Convert user input date string into an instance of `datetime.datetime` in
|
|
UTC.
|
|
"""
|
|
try:
|
|
return dateutil.parser.parse(datestr).replace(tzinfo=utc)
|
|
except ValueError:
|
|
raise DashboardError(_("Unable to parse date: ") + datestr)
|
|
|
|
|
|
def find_unit(course, url):
|
|
"""
|
|
Finds the unit (block, module, whatever the terminology is) with the given
|
|
url in the course tree and returns the unit. Raises DashboardError if no
|
|
unit is found.
|
|
"""
|
|
def find(node, url):
|
|
"""
|
|
Find node in course tree for url.
|
|
"""
|
|
if node.location.url() == url:
|
|
return node
|
|
for child in node.get_children():
|
|
found = find(child, url)
|
|
if found:
|
|
return found
|
|
return None
|
|
|
|
unit = find(course, url)
|
|
if unit is None:
|
|
raise DashboardError(_("Couldn't find module for url: {0}").format(url))
|
|
return unit
|
|
|
|
|
|
def get_units_with_due_date(course):
|
|
"""
|
|
Returns all top level units which have due dates. Does not return
|
|
descendents of those nodes.
|
|
"""
|
|
units = []
|
|
|
|
def visit(node):
|
|
"""
|
|
Visit a node. Checks to see if node has a due date and appends to
|
|
`units` if it does. Otherwise recurses into children to search for
|
|
nodes with due dates.
|
|
"""
|
|
if getattr(node, 'due', None):
|
|
units.append(node)
|
|
else:
|
|
for child in node.get_children():
|
|
visit(child)
|
|
visit(course)
|
|
#units.sort(key=_title_or_url)
|
|
return units
|
|
|
|
|
|
def title_or_url(node):
|
|
"""
|
|
Returns the `display_name` attribute of the passed in node of the course
|
|
tree, if it has one. Otherwise returns the node's url.
|
|
"""
|
|
title = getattr(node, 'display_name', None)
|
|
if not title:
|
|
title = node.location.url()
|
|
return title
|
|
|
|
|
|
def set_due_date_extension(course, unit, student, due_date):
|
|
"""
|
|
Sets a due date extension.
|
|
"""
|
|
def set_due_date(node):
|
|
"""
|
|
Recursively set the due date on a node and all of its children.
|
|
"""
|
|
try:
|
|
student_module = StudentModule.objects.get(
|
|
student_id=student.id,
|
|
course_id=course.id,
|
|
module_state_key=node.location.url()
|
|
)
|
|
|
|
state = json.loads(student_module.state)
|
|
state['extended_due'] = DATE_FIELD.to_json(due_date)
|
|
student_module.state = json.dumps(state)
|
|
student_module.save()
|
|
except StudentModule.DoesNotExist:
|
|
pass
|
|
|
|
for child in node.get_children():
|
|
set_due_date(child)
|
|
|
|
set_due_date(unit)
|
|
|
|
|
|
def dump_module_extensions(course, unit):
|
|
"""
|
|
Dumps data about students with due date extensions for a particular module,
|
|
specified by 'url', in a particular course.
|
|
"""
|
|
data = []
|
|
header = [_("Username"), _("Full Name"), _("Extended Due Date")]
|
|
query = StudentModule.objects.filter(
|
|
course_id=course.id,
|
|
module_state_key=unit.location.url())
|
|
for module in query:
|
|
state = json.loads(module.state)
|
|
extended_due = state.get("extended_due")
|
|
if not extended_due:
|
|
continue
|
|
extended_due = DATE_FIELD.from_json(extended_due)
|
|
extended_due = extended_due.strftime("%Y-%m-%d %H:%M")
|
|
fullname = module.student.profile.name
|
|
data.append(dict(zip(
|
|
header,
|
|
(module.student.username, fullname, extended_due))))
|
|
data.sort(key=lambda x: x[header[0]])
|
|
return {
|
|
"header": header,
|
|
"title": _("Users with due date extensions for {0}").format(
|
|
title_or_url(unit)),
|
|
"data": data
|
|
}
|
|
|
|
|
|
def dump_student_extensions(course, student):
|
|
"""
|
|
Dumps data about the due date extensions granted for a particular student
|
|
in a particular course.
|
|
"""
|
|
data = []
|
|
header = [_("Unit"), _("Extended Due Date")]
|
|
units = get_units_with_due_date(course)
|
|
units = dict([(u.location.url(), u) for u in units])
|
|
query = StudentModule.objects.filter(
|
|
course_id=course.id,
|
|
student_id=student.id)
|
|
for module in query:
|
|
state = json.loads(module.state)
|
|
if module.module_state_key not in units:
|
|
continue
|
|
extended_due = state.get("extended_due")
|
|
if not extended_due:
|
|
continue
|
|
extended_due = DATE_FIELD.from_json(extended_due)
|
|
extended_due = extended_due.strftime("%Y-%m-%d %H:%M")
|
|
title = title_or_url(units[module.module_state_key])
|
|
data.append(dict(zip(header, (title, extended_due))))
|
|
return {
|
|
"header": header,
|
|
"title": _("Due date extensions for {0} {1} ({2})").format(
|
|
student.first_name, student.last_name, student.username),
|
|
"data": data}
|