switch to using timelimit module for Pearson test
This commit is contained in:
@@ -39,12 +39,15 @@ from certificates.models import CertificateStatuses, certificate_status_for_stud
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from courseware.courses import get_courses, sort_by_announcement
|
||||
from courseware.access import has_access
|
||||
from courseware.models import TimedModule
|
||||
from courseware.models import StudentModuleCache
|
||||
from courseware.views import get_module_for_descriptor
|
||||
from courseware.module_render import get_instance_module
|
||||
|
||||
from statsd import statsd
|
||||
|
||||
@@ -1082,13 +1085,14 @@ def test_center_login(request):
|
||||
# errors are returned by navigating to the error_url, adding a query parameter named "code"
|
||||
# which contains the error code describing the exceptional condition.
|
||||
def makeErrorURL(error_url, error_code):
|
||||
return "{}&code={}".format(error_url, error_code);
|
||||
log.error("generating error URL with error code {}".format(error_code))
|
||||
return "{}?code={}".format(error_url, error_code);
|
||||
|
||||
# get provided error URL, which will be used as a known prefix for returning error messages to the
|
||||
# Pearson shell. It does not have a trailing slash, so we need to add one when creating output URLs.
|
||||
# Pearson shell.
|
||||
error_url = request.POST.get("errorURL")
|
||||
|
||||
# check that the parameters have not been tampered with, by comparing the code provided by Pearson
|
||||
# TODO: check that the parameters have not been tampered with, by comparing the code provided by Pearson
|
||||
# with the code we calculate for the same parameters.
|
||||
if 'code' not in request.POST:
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode"));
|
||||
@@ -1112,65 +1116,81 @@ def test_center_login(request):
|
||||
try:
|
||||
testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
|
||||
except TestCenterUser.DoesNotExist:
|
||||
log.error("not able to find demographics for cand ID {}".format(client_candidate_id))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"));
|
||||
|
||||
|
||||
# find testcenter_registration that matches the provided exam code:
|
||||
# Note that we could rely on either the registrationId or the exam code,
|
||||
# or possibly both.
|
||||
# Note that we could rely in future on either the registrationId or the exam code,
|
||||
# or possibly both. But for now we know what to do with an ExamSeriesCode,
|
||||
# while we currently have no record of RegistrationID values at all.
|
||||
if 'vueExamSeriesCode' not in request.POST:
|
||||
# TODO: confirm this error code (made up, not in documentation)
|
||||
log.error("missing exam series code for cand ID {}".format(client_candidate_id))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "missingExamSeriesCode"));
|
||||
exam_series_code = request.POST.get('vueExamSeriesCode')
|
||||
|
||||
registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code)
|
||||
|
||||
if not registrations:
|
||||
log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned"));
|
||||
|
||||
# TODO: figure out what to do if there are more than one registrations....
|
||||
# for now, just take the first...
|
||||
registration = registrations[0]
|
||||
|
||||
course_id = registration.course_id
|
||||
|
||||
# if we want to look up whether the test has already been taken, or to
|
||||
# communicate that a time accommodation needs to be applied, we need to
|
||||
# know the module_id to use that corresponds to the particular exam_series_code.
|
||||
# For now, we can hardcode that...
|
||||
if exam_series_code == '6002x001':
|
||||
# This should not be hardcoded here, but should be added to the exam definition.
|
||||
# TODO: look the location up in the course, by finding the exam_info with the matching code,
|
||||
# and get the location from that.
|
||||
location = 'i4x://MITx/6.002x/sequential/Final_Exam_Fall_2012'
|
||||
redirect_url = reverse('jump_to', kwargs={'course_id': course_id, 'location': location})
|
||||
else:
|
||||
# TODO: clarify if this is the right error code for this condition.
|
||||
course = course_from_id(course_id) # assume it will be found....
|
||||
if not course:
|
||||
log.error("not able to find course from ID {} for cand ID {}".format(course_id, client_candidate_id))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"));
|
||||
exam = course.get_test_center_exam(exam_series_code)
|
||||
if not exam:
|
||||
log.error("not able to find exam {} for course ID {} and cand ID {}".format(exam_series_code, course_id, client_candidate_id))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"));
|
||||
location = exam.exam_url
|
||||
redirect_url = reverse('jump_to', kwargs={'course_id': course_id, 'location': location})
|
||||
|
||||
log.info("proceeding with test of cand {} on exam {} for course {}: URL = {}".format(client_candidate_id, exam_series_code, course_id, location))
|
||||
|
||||
# check if the test has already been taken
|
||||
timelimit_descriptor = modulestore().get_instance(course_id, Location(location))
|
||||
if not timelimit_descriptor:
|
||||
log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
|
||||
|
||||
timelimit_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
|
||||
timelimit_descriptor, depth=None)
|
||||
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
|
||||
timelimit_module_cache, course_id, position=None)
|
||||
if not timelimit_module.category == 'timelimit':
|
||||
log.error("cand {} on exam {} for course {}: non-timelimit module at location {}".format(client_candidate_id, exam_series_code, course_id, location))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
|
||||
|
||||
if timelimit_module and timelimit_module.has_ended:
|
||||
log.warning("cand {} on exam {} for course {}: test already over at {}".format(client_candidate_id, exam_series_code, course_id, timelimit_module.ending_at))
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"));
|
||||
|
||||
|
||||
# check if we need to provide an accommodation:
|
||||
time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME',
|
||||
'ET30MN' : 'ADD30MIN',
|
||||
'ETDBTM' : 'ADDDOUBLE', }
|
||||
|
||||
# check if the test has already been taken
|
||||
timed_modules = TimedModule.objects.filter(student=testcenteruser.user, course_id=course_id, location=location)
|
||||
if timed_modules:
|
||||
timed_module = timed_modules[0]
|
||||
if timed_module.has_ended:
|
||||
return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"));
|
||||
elif registration.get_accommodation_codes():
|
||||
# we don't have a timed module created yet, so if we have time accommodations
|
||||
# to implement, create an entry now:
|
||||
time_accommodation_code = None
|
||||
|
||||
time_accommodation_code = None
|
||||
if registration.get_accommodation_codes():
|
||||
for code in registration.get_accommodation_codes():
|
||||
if code in time_accommodation_mapping:
|
||||
time_accommodation_code = time_accommodation_mapping[code]
|
||||
if client_candidate_id == "edX003671291147":
|
||||
time_accommodation_code = 'TESTING'
|
||||
if time_accommodation_code:
|
||||
timed_module = TimedModule(student=request.user, course_id=course_id, location=location)
|
||||
timed_module.accommodation_code = time_accommodation_code
|
||||
timed_module.save()
|
||||
# special, hard-coded client ID used by Pearson shell for testing:
|
||||
if client_candidate_id == "edX003671291147":
|
||||
time_accommodation_code = 'TESTING'
|
||||
|
||||
if time_accommodation_code:
|
||||
timelimit_module.accommodation_code = time_accommodation_code
|
||||
instance_module = get_instance_module(course_id, testcenteruser.user, timelimit_module, timelimit_module_cache)
|
||||
instance_module.state = timelimit_module.get_instance_state()
|
||||
instance_module.save()
|
||||
log.info("cand {} on exam {} for course {}: receiving accommodation {}".format(client_candidate_id, exam_series_code, course_id, time_accommodation_code))
|
||||
|
||||
# UGLY HACK!!!
|
||||
# Login assumes that authentication has occurred, and that there is a
|
||||
|
||||
@@ -23,7 +23,6 @@ setup(
|
||||
"course = xmodule.course_module:CourseDescriptor",
|
||||
"customtag = xmodule.template_module:CustomTagDescriptor",
|
||||
"discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"fixedtime = xmodule.fixed_time_module:FixedTimeDescriptor",
|
||||
"html = xmodule.html_module:HtmlDescriptor",
|
||||
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"error = xmodule.error_module:ErrorDescriptor",
|
||||
@@ -32,6 +31,7 @@ setup(
|
||||
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
|
||||
"sequential = xmodule.seq_module:SequenceDescriptor",
|
||||
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"timelimit = xmodule.timelimit_module:TimeLimitDescriptor",
|
||||
"vertical = xmodule.vertical_module:VerticalDescriptor",
|
||||
"video = xmodule.video_module:VideoDescriptor",
|
||||
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
|
||||
@@ -648,7 +648,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
raise ValueError("First appointment date must be before last appointment date")
|
||||
if self.registration_end_date > self.last_eligible_appointment_date:
|
||||
raise ValueError("Registration end date must be before last appointment date")
|
||||
|
||||
self.exam_url = exam_info.get('Exam_URL')
|
||||
|
||||
def _try_parse_time(self, key):
|
||||
"""
|
||||
@@ -704,6 +704,10 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_test_center_exam(self, exam_series_code):
|
||||
exams = [exam for exam in self.test_center_exams if exam.exam_series_code == exam_series_code]
|
||||
return exams[0] if len(exams) == 1 else None
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.display_name
|
||||
|
||||
@@ -4,22 +4,16 @@ import logging
|
||||
from lxml import etree
|
||||
from time import time
|
||||
|
||||
from xmodule.mako_module import MakoModuleDescriptor
|
||||
from xmodule.editing_module import XMLEditingDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.progress import Progress
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from pkg_resources import resource_string
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# HACK: This shouldn't be hard-coded to two types
|
||||
# OBSOLETE: This obsoletes 'type'
|
||||
# class_priority = ['video', 'problem']
|
||||
|
||||
|
||||
class FixedTimeModule(XModule):
|
||||
class TimeLimitModule(XModule):
|
||||
'''
|
||||
Wrapper module which imposes a time constraint for the completion of its child.
|
||||
'''
|
||||
@@ -29,9 +23,7 @@ class FixedTimeModule(XModule):
|
||||
XModule.__init__(self, system, location, definition, descriptor,
|
||||
instance_state, shared_state, **kwargs)
|
||||
|
||||
# NOTE: Position is 1-indexed. This is silly, but there are now student
|
||||
# positions saved on prod, so it's not easy to fix.
|
||||
# self.position = 1
|
||||
self.rendered = False
|
||||
self.beginning_at = None
|
||||
self.ending_at = None
|
||||
self.accommodation_code = None
|
||||
@@ -46,13 +38,6 @@ class FixedTimeModule(XModule):
|
||||
if 'accommodation_code' in state:
|
||||
self.accommodation_code = state['accommodation_code']
|
||||
|
||||
|
||||
# if position is specified in system, then use that instead
|
||||
# if system.get('position'):
|
||||
# self.position = int(system.get('position'))
|
||||
|
||||
self.rendered = False
|
||||
|
||||
# For a timed activity, we are only interested here
|
||||
# in time-related accommodations, and these should be disjoint.
|
||||
# (For proctored exams, it is possible to have multiple accommodations
|
||||
@@ -81,8 +66,6 @@ class FixedTimeModule(XModule):
|
||||
elif self.accommodation_code == 'TESTING':
|
||||
# when testing, set timer to run for a week at a time.
|
||||
return 3600 * 24 * 7
|
||||
|
||||
# store state:
|
||||
|
||||
@property
|
||||
def has_begun(self):
|
||||
@@ -101,8 +84,6 @@ class FixedTimeModule(XModule):
|
||||
'''
|
||||
self.beginning_at = time()
|
||||
modified_duration = self._get_accommodated_duration(duration)
|
||||
# datetime_duration = timedelta(seconds=modified_duration)
|
||||
# self.ending_at = self.beginning_at + datetime_duration
|
||||
self.ending_at = self.beginning_at + modified_duration
|
||||
|
||||
def get_end_time_in_ms(self):
|
||||
@@ -132,31 +113,32 @@ class FixedTimeModule(XModule):
|
||||
progress = reduce(Progress.add_counts, progresses)
|
||||
return progress
|
||||
|
||||
def handle_ajax(self, dispatch, get): # TODO: bounds checking
|
||||
# ''' get = request.POST instance '''
|
||||
# if dispatch == 'goto_position':
|
||||
# self.position = int(get['position'])
|
||||
# return json.dumps({'success': True})
|
||||
def handle_ajax(self, dispatch, get):
|
||||
raise NotFoundError('Unexpected dispatch type')
|
||||
|
||||
def render(self):
|
||||
if self.rendered:
|
||||
return
|
||||
# assumes there is one and only one child, so it only renders the first child
|
||||
child = self.get_display_items()[0]
|
||||
self.content = child.get_html()
|
||||
children = self.get_display_items()
|
||||
if children:
|
||||
child = children[0]
|
||||
self.content = child.get_html()
|
||||
self.rendered = True
|
||||
|
||||
def get_icon_class(self):
|
||||
return self.get_children()[0].get_icon_class()
|
||||
children = self.get_children()
|
||||
if children:
|
||||
return children[0].get_icon_class()
|
||||
else:
|
||||
return "other"
|
||||
|
||||
class TimeLimitDescriptor(XMLEditingDescriptor, XmlDescriptor):
|
||||
|
||||
class FixedTimeDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
# TODO: fix this template?!
|
||||
mako_template = 'widgets/sequence-edit.html'
|
||||
module_class = FixedTimeModule
|
||||
module_class = TimeLimitModule
|
||||
|
||||
stores_state = True # For remembering when a student started, and when they should end
|
||||
# For remembering when a student started, and when they should end
|
||||
stores_state = True
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
@@ -165,14 +147,14 @@ class FixedTimeDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
try:
|
||||
children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url())
|
||||
except Exception as e:
|
||||
log.exception("Unable to load child when parsing FixedTime wrapper. Continuing...")
|
||||
log.exception("Unable to load child when parsing TimeLimit wrapper. Continuing...")
|
||||
if system.error_tracker is not None:
|
||||
system.error_tracker("ERROR: " + str(e))
|
||||
continue
|
||||
return {'children': children}
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
xml_object = etree.Element('fixedtime')
|
||||
xml_object = etree.Element('timelimit')
|
||||
for child in self.get_children():
|
||||
xml_object.append(
|
||||
etree.fromstring(child.export_to_xml(resource_fs)))
|
||||
@@ -1,119 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'TimedModule'
|
||||
db.create_table('courseware_timedmodule', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('location', self.gf('django.db.models.fields.CharField')(max_length=255, db_column='location', db_index=True)),
|
||||
('student', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
|
||||
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
|
||||
('accommodation_code', self.gf('django.db.models.fields.CharField')(default='NONE', max_length=12, db_index=True)),
|
||||
('beginning_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
|
||||
('ending_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
|
||||
('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
|
||||
('modified_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
|
||||
))
|
||||
db.send_create_signal('courseware', ['TimedModule'])
|
||||
|
||||
# Adding unique constraint on 'TimedModule', fields ['student', 'location', 'course_id']
|
||||
db.create_unique('courseware_timedmodule', ['student_id', 'location', 'course_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'TimedModule', fields ['student', 'location', 'course_id']
|
||||
db.delete_unique('courseware_timedmodule', ['student_id', 'location', 'course_id'])
|
||||
|
||||
# Deleting model 'TimedModule'
|
||||
db.delete_table('courseware_timedmodule')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'courseware.offlinecomputedgrade': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'OfflineComputedGrade'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'gradeset': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'courseware.offlinecomputedgradelog': {
|
||||
'Meta': {'ordering': "['-created']", 'object_name': 'OfflineComputedGradeLog'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'nstudents': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'seconds': ('django.db.models.fields.IntegerField', [], {'default': '0'})
|
||||
},
|
||||
'courseware.studentmodule': {
|
||||
'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}),
|
||||
'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}),
|
||||
'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}),
|
||||
'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'courseware.timedmodule': {
|
||||
'Meta': {'unique_together': "(('student', 'location', 'course_id'),)", 'object_name': 'TimedModule'},
|
||||
'accommodation_code': ('django.db.models.fields.CharField', [], {'default': "'NONE'", 'max_length': '12', 'db_index': 'True'}),
|
||||
'beginning_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'ending_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'location'", 'db_index': 'True'}),
|
||||
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['courseware']
|
||||
@@ -212,87 +212,3 @@ class OfflineComputedGradeLog(models.Model):
|
||||
def __unicode__(self):
|
||||
return "[OCGLog] %s: %s" % (self.course_id, self.created)
|
||||
|
||||
class TimedModule(models.Model):
|
||||
"""
|
||||
Keeps student state for a timed activity in a particular course.
|
||||
Includes information about time accommodations granted,
|
||||
time started, and ending time.
|
||||
"""
|
||||
## These three are the key for the object
|
||||
|
||||
# Key used to share state. By default, this is the module_id,
|
||||
# but for abtests and the like, this can be set to a shared value
|
||||
# for many instances of the module.
|
||||
# Filename for homeworks, etc.
|
||||
# module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id')
|
||||
location = models.CharField(max_length=255, db_index=True, db_column='location')
|
||||
student = models.ForeignKey(User, db_index=True)
|
||||
course_id = models.CharField(max_length=255, db_index=True)
|
||||
|
||||
class Meta:
|
||||
# unique_together = (('student', 'module_state_key', 'course_id'),)
|
||||
unique_together = (('student', 'location', 'course_id'),)
|
||||
|
||||
# For a timed activity, we are only interested here
|
||||
# in time-related accommodations, and these should be disjoint.
|
||||
# (For proctored exams, it is possible to have multiple accommodations
|
||||
# apply to an exam, so they require accommodating a multi-choice.)
|
||||
TIME_ACCOMMODATION_CODES = (('NONE', 'No Time Accommodation'),
|
||||
('ADDHALFTIME', 'Extra Time - 1 1/2 Time'),
|
||||
('ADD30MIN', 'Extra Time - 30 Minutes'),
|
||||
('DOUBLE', 'Extra Time - Double Time'),
|
||||
('TESTING', 'Extra Time -- Large amount for testing purposes')
|
||||
)
|
||||
accommodation_code = models.CharField(max_length=12, choices=TIME_ACCOMMODATION_CODES, default='NONE', db_index=True)
|
||||
|
||||
def _get_accommodated_duration(self, duration):
|
||||
'''
|
||||
Get duration for activity, as adjusted for accommodations.
|
||||
Input and output are expressed in seconds.
|
||||
'''
|
||||
if self.accommodation_code == 'NONE':
|
||||
return duration
|
||||
elif self.accommodation_code == 'ADDHALFTIME':
|
||||
# TODO: determine what type to return
|
||||
return int(duration * 1.5)
|
||||
elif self.accommodation_code == 'ADD30MIN':
|
||||
return (duration + (30 * 60))
|
||||
elif self.accommodation_code == 'DOUBLE':
|
||||
return (duration * 2)
|
||||
elif self.accommodation_code == 'TESTING':
|
||||
# when testing, set timer to run for a week at a time.
|
||||
return 3600 * 24 * 7
|
||||
|
||||
# store state:
|
||||
|
||||
beginning_at = models.DateTimeField(null=True, db_index=True)
|
||||
ending_at = models.DateTimeField(null=True, db_index=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
modified_at = models.DateTimeField(auto_now=True, db_index=True)
|
||||
|
||||
@property
|
||||
def has_begun(self):
|
||||
return self.beginning_at is not None
|
||||
|
||||
@property
|
||||
def has_ended(self):
|
||||
if not self.ending_at:
|
||||
return False
|
||||
return self.ending_at < datetime.utcnow()
|
||||
|
||||
def begin(self, duration):
|
||||
'''
|
||||
Sets the starting time and ending time for the activity,
|
||||
based on the duration provided (in seconds).
|
||||
'''
|
||||
self.beginning_at = datetime.utcnow()
|
||||
modified_duration = self._get_accommodated_duration(duration)
|
||||
datetime_duration = timedelta(seconds=modified_duration)
|
||||
self.ending_at = self.beginning_at + datetime_duration
|
||||
|
||||
def get_end_time_in_ms(self):
|
||||
return (timegm(self.ending_at.timetuple()) * 1000)
|
||||
|
||||
def __unicode__(self):
|
||||
return '/'.join([self.course_id, self.student.username, self.module_state_key])
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from courseware.access import has_access
|
||||
from courseware.courses import (get_courses, get_course_with_access,
|
||||
get_courses_by_university, sort_by_announcement)
|
||||
import courseware.tabs as tabs
|
||||
from courseware.models import StudentModuleCache, TimedModule
|
||||
from courseware.models import StudentModule, StudentModuleCache
|
||||
from module_render import toc_for_course, get_module, get_instance_module, get_module_for_descriptor
|
||||
|
||||
from django_comment_client.utils import get_discussion_title
|
||||
@@ -31,6 +31,7 @@ from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
#from xmodule.fixed_time_module import FixedTimeModule
|
||||
|
||||
import comment_client
|
||||
|
||||
@@ -152,6 +153,80 @@ def save_child_position(seq_module, child_name, instance_module):
|
||||
instance_module.state = seq_module.get_instance_state()
|
||||
instance_module.save()
|
||||
|
||||
def check_for_active_timelimit_module(request, course_id, course):
|
||||
'''
|
||||
Looks for a timing module for the given user and course that is currently active.
|
||||
If found, returns a context dict with timer-related values to enable display of time remaining.
|
||||
'''
|
||||
context = {}
|
||||
timelimit_student_modules = StudentModule.objects.filter(student=request.user, course_id=course_id, module_type='timelimit')
|
||||
if timelimit_student_modules:
|
||||
for timelimit_student_module in timelimit_student_modules:
|
||||
# get the corresponding section_descriptor for the given StudentModel entry:
|
||||
module_state_key = timelimit_student_module.module_state_key
|
||||
timelimit_descriptor = modulestore().get_instance(course_id, Location(module_state_key))
|
||||
timelimit_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course.id, request.user,
|
||||
timelimit_descriptor, depth=None)
|
||||
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
|
||||
timelimit_module_cache, course.id, position=None)
|
||||
if timelimit_module is not None and timelimit_module.category == 'timelimit' and \
|
||||
timelimit_module.has_begun and not timelimit_module.has_ended:
|
||||
location = timelimit_module.location
|
||||
# determine where to go when the timer expires:
|
||||
if 'time_expired_redirect_url' not in timelimit_descriptor.metadata:
|
||||
# TODO: provide a better error
|
||||
raise Http404
|
||||
time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url')
|
||||
context['time_expired_redirect_url'] = time_expired_redirect_url
|
||||
# Fetch the end time (in GMT) as stored in the module when it was started.
|
||||
# This value should be UTC time as number of milliseconds since epoch.
|
||||
end_date = timelimit_module.get_end_time_in_ms()
|
||||
context['timer_expiration_datetime'] = end_date
|
||||
if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata:
|
||||
context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation']
|
||||
return_url = reverse('jump_to', kwargs={'course_id':course_id, 'location':location})
|
||||
context['timer_navigation_return_url'] = return_url
|
||||
return context
|
||||
|
||||
def update_timelimit_module(user, course_id, student_module_cache, timelimit_descriptor, timelimit_module):
|
||||
'''
|
||||
Updates the state of the provided timing module, starting it if it hasn't begun.
|
||||
Returns dict with timer-related values to enable display of time remaining.
|
||||
Returns 'timer_expiration_datetime' in dict if timer is still active, and not if timer has expired.
|
||||
'''
|
||||
context = {}
|
||||
# determine where to go when the exam ends:
|
||||
if 'time_expired_redirect_url' not in timelimit_descriptor.metadata:
|
||||
# TODO: provide a better error
|
||||
raise Http404
|
||||
time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url')
|
||||
context['time_expired_redirect_url'] = time_expired_redirect_url
|
||||
|
||||
if not timelimit_module.has_ended:
|
||||
if not timelimit_module.has_begun:
|
||||
# user has not started the exam, so start it now.
|
||||
if 'duration' not in timelimit_descriptor.metadata:
|
||||
# TODO: provide a better error
|
||||
raise Http404
|
||||
# The user may have an accommodation that has been granted to them.
|
||||
# This accommodation information should already be stored in the module's state.
|
||||
duration = int(timelimit_descriptor.metadata.get('duration'))
|
||||
timelimit_module.begin(duration)
|
||||
# we have changed state, so we need to persist the change:
|
||||
instance_module = get_instance_module(course_id, user, timelimit_module, student_module_cache)
|
||||
instance_module.state = timelimit_module.get_instance_state()
|
||||
instance_module.save()
|
||||
|
||||
# the exam has been started, either because the student is returning to the
|
||||
# exam page, or because they have just visited it. Fetch the end time (in GMT) as stored
|
||||
# in the module when it was started.
|
||||
# This value should be UTC time as number of milliseconds since epoch.
|
||||
context['timer_expiration_datetime'] = timelimit_module.get_end_time_in_ms()
|
||||
# also use the timed module to determine whether top-level navigation is visible:
|
||||
if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata:
|
||||
context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation']
|
||||
return context
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@@ -215,43 +290,6 @@ def index(request, course_id, chapter=None, section=None,
|
||||
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa')
|
||||
}
|
||||
|
||||
# check here if this page is within a course that has an active timed module running. If so, then
|
||||
# display the appropriate timer information:
|
||||
timed_modules = TimedModule.objects.filter(student=request.user, course_id=course_id)
|
||||
if timed_modules:
|
||||
for timed_module in timed_modules:
|
||||
if timed_module.has_begun and not timed_module.has_ended:
|
||||
# a timed module has been found that is active, so display
|
||||
# the relevant time:
|
||||
# module_state_key = timed_module.module_state_key
|
||||
location = timed_module.location
|
||||
|
||||
# when we actually make the state be stored in the StudentModule, then
|
||||
# we can fetch what we need from that.
|
||||
# student_module = student_module_cache.lookup(course_id, 'sequential', module_state_key)
|
||||
# But the module doesn't give us anything helpful to find the corresponding descriptor
|
||||
|
||||
# get the corresponding section_descriptor for this timed_module entry:
|
||||
section_descriptor = modulestore().get_instance(course_id, Location(location))
|
||||
|
||||
# determine where to go when the timer expires:
|
||||
# Note that if we could get this from the timed_module, we wouldn't have to
|
||||
# fetch the section_descriptor in the first place.
|
||||
if 'time_expired_redirect_url' not in section_descriptor.metadata:
|
||||
raise Http404
|
||||
time_expired_redirect_url = section_descriptor.metadata.get('time_expired_redirect_url')
|
||||
context['time_expired_redirect_url'] = time_expired_redirect_url
|
||||
|
||||
# Fetch the end time (in GMT) as stored in the module when it was started.
|
||||
# This value should be UTC time as number of milliseconds since epoch.
|
||||
end_date = timed_module.get_end_time_in_ms()
|
||||
context['timer_expiration_datetime'] = end_date
|
||||
if 'suppress_toplevel_navigation' in section_descriptor.metadata:
|
||||
context['suppress_toplevel_navigation'] = section_descriptor.metadata['suppress_toplevel_navigation']
|
||||
return_url = reverse('jump_to', kwargs={'course_id': course_id, 'location': location})
|
||||
context['timer_navigation_return_url'] = return_url
|
||||
|
||||
|
||||
chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
|
||||
if chapter_descriptor is not None:
|
||||
instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
|
||||
@@ -286,7 +324,20 @@ def index(request, course_id, chapter=None, section=None,
|
||||
instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
|
||||
save_child_position(chapter_module, section, instance_module)
|
||||
|
||||
|
||||
# check here if this section *is* a timed module.
|
||||
if section_module.category == 'timelimit':
|
||||
timer_context = update_timelimit_module(request.user, course_id, student_module_cache,
|
||||
section_descriptor, section_module)
|
||||
if 'timer_expiration_datetime' in timer_context:
|
||||
context.update(timer_context)
|
||||
else:
|
||||
# if there is no expiration defined, then we know the timer has expired:
|
||||
return HttpResponseRedirect(timer_context['time_expired_redirect_url'])
|
||||
else:
|
||||
# check here if this page is within a course that has an active timed module running. If so, then
|
||||
# add in the appropriate timer information to the rendering context:
|
||||
context.update(check_for_active_timelimit_module(request, course_id, course))
|
||||
|
||||
context['content'] = section_module.get_html()
|
||||
else:
|
||||
# section is none, so display a message
|
||||
@@ -334,201 +385,6 @@ def index(request, course_id, chapter=None, section=None,
|
||||
|
||||
return result
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def timed_exam(request, course_id, chapter, section):
|
||||
"""
|
||||
Displays only associated content. If course, chapter,
|
||||
and section are all specified, renders the page, or returns an error if they
|
||||
are invalid.
|
||||
|
||||
Returns an error if these are not all specified and correct.
|
||||
|
||||
Arguments:
|
||||
|
||||
- request : HTTP request
|
||||
- course_id : course id (str: ORG/course/URL_NAME)
|
||||
- chapter : chapter url_name (str)
|
||||
- section : section url_name (str)
|
||||
|
||||
Returns:
|
||||
|
||||
- HTTPresponse
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'load', depth=2)
|
||||
staff_access = has_access(request.user, course, 'staff')
|
||||
registered = registered_for_course(course, request.user)
|
||||
if not registered:
|
||||
log.debug('User %s tried to view course %s but is not enrolled' % (request.user,course.location.url()))
|
||||
raise # error
|
||||
try:
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
course.id, request.user, course, depth=2)
|
||||
|
||||
# Has this student been in this course before?
|
||||
# first_time = student_module_cache.lookup(course_id, 'course', course.location.url()) is None
|
||||
|
||||
# Load the module for the course
|
||||
course_module = get_module_for_descriptor(request.user, request, course, student_module_cache, course.id)
|
||||
if course_module is None:
|
||||
log.warning('If you see this, something went wrong: if we got this'
|
||||
' far, should have gotten a course module for this user')
|
||||
# return redirect(reverse('about_course', args=[course.id]))
|
||||
raise # error
|
||||
|
||||
if chapter is None:
|
||||
# return redirect_to_course_position(course_module, first_time)
|
||||
raise # error
|
||||
|
||||
# BW: add this test earlier, and remove later clause
|
||||
if section is None:
|
||||
# return redirect_to_course_position(course_module, first_time)
|
||||
raise # error
|
||||
|
||||
context = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'COURSE_TITLE': course.title,
|
||||
'course': course,
|
||||
'init': '',
|
||||
'content': '',
|
||||
'staff_access': staff_access,
|
||||
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa')
|
||||
}
|
||||
|
||||
# in general, we may want to disable accordion display on timed exams.
|
||||
provide_accordion = True
|
||||
if provide_accordion:
|
||||
context['accordion'] = render_accordion(request, course, chapter, section)
|
||||
|
||||
chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
|
||||
if chapter_descriptor is not None:
|
||||
instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
|
||||
save_child_position(course_module, chapter, instance_module)
|
||||
else:
|
||||
raise Http404
|
||||
|
||||
chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter)
|
||||
if chapter_module is None:
|
||||
# User may be trying to access a chapter that isn't live yet
|
||||
raise Http404
|
||||
|
||||
section_descriptor = chapter_descriptor.get_child_by(lambda m: m.url_name == section)
|
||||
if section_descriptor is None:
|
||||
# Specifically asked-for section doesn't exist
|
||||
raise Http404
|
||||
|
||||
# Load all descendents of the section, because we're going to display its
|
||||
# html, which in general will need all of its children
|
||||
section_module = get_module(request.user, request, section_descriptor.location,
|
||||
student_module_cache, course.id, position=None, depth=None)
|
||||
if section_module is None:
|
||||
# User may be trying to be clever and access something
|
||||
# they don't have access to.
|
||||
raise Http404
|
||||
|
||||
# Save where we are in the chapter:
|
||||
instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
|
||||
save_child_position(chapter_module, section, instance_module)
|
||||
|
||||
|
||||
context['content'] = section_module.get_html()
|
||||
|
||||
# determine where to go when the exam ends:
|
||||
if 'time_expired_redirect_url' not in section_descriptor.metadata:
|
||||
raise Http404
|
||||
time_expired_redirect_url = section_descriptor.metadata.get('time_expired_redirect_url')
|
||||
context['time_expired_redirect_url'] = time_expired_redirect_url
|
||||
|
||||
# figure out when the timed exam should end. Going forward, this is determined by getting a "normal"
|
||||
# duration from the test, then doing some math to modify the duration based on accommodations,
|
||||
# and then use that value as the end. Once we have calculated this, it should be sticky -- we
|
||||
# use the same value for future requests, unless it's a tester.
|
||||
|
||||
# get value for duration from the section's metadata:
|
||||
# for now, assume that the duration is set as an integer value, indicating the number of seconds:
|
||||
if 'duration' not in section_descriptor.metadata:
|
||||
raise Http404
|
||||
duration = int(section_descriptor.metadata.get('duration'))
|
||||
|
||||
# get corresponding time module, if one is present:
|
||||
try:
|
||||
timed_module = TimedModule.objects.get(student=request.user, course_id=course_id, location=section_module.location)
|
||||
|
||||
# if a module exists, check to see if it has already been started,
|
||||
# and if it has already ended.
|
||||
if timed_module.has_ended:
|
||||
# the exam has already ended, and the student has tried to
|
||||
# revisit the exam.
|
||||
# TODO: determine what do we do here.
|
||||
# For a Pearson exam, we want to go to the exit page.
|
||||
# (Not so sure what to do in general.)
|
||||
# Proposal: store URL in the section descriptor,
|
||||
# along with the duration. If no such URL is set,
|
||||
# just put up the error page,
|
||||
if time_expired_redirect_url is None:
|
||||
raise Exception("Time expired on {}".format(timed_module))
|
||||
else:
|
||||
return HttpResponseRedirect(time_expired_redirect_url)
|
||||
|
||||
elif not timed_module.has_begun:
|
||||
# user has not started the exam, but may have an accommodation
|
||||
# that has been granted to them.
|
||||
# modified_duration = timed_module.get_accommodated_duration(duration)
|
||||
# timed_module.started_at = datetime.utcnow() # time() * 1000
|
||||
# timed_module.end_date = timed_module.
|
||||
timed_module.begin(duration)
|
||||
timed_module.save()
|
||||
|
||||
except TimedModule.DoesNotExist:
|
||||
# no entry found. So we're starting this test
|
||||
# without any accommodations being preset.
|
||||
timed_module = TimedModule(student=request.user, course_id=course_id, location=section_module.location)
|
||||
timed_module.begin(duration)
|
||||
timed_module.save()
|
||||
|
||||
|
||||
# the exam has already been started, and the student is returning to the
|
||||
# exam page. Fetch the end time (in GMT) as stored
|
||||
# in the module when it was started.
|
||||
end_date = timed_module.get_end_time_in_ms()
|
||||
|
||||
# This value should be UTC time as number of milliseconds since epoch.
|
||||
# context['end_date'] = end_date
|
||||
context['timer_expiration_datetime'] = end_date
|
||||
if 'suppress_toplevel_navigation' in section_descriptor.metadata:
|
||||
context['suppress_toplevel_navigation'] = section_descriptor.metadata['suppress_toplevel_navigation']
|
||||
|
||||
result = render_to_response('courseware/courseware.html', context)
|
||||
except Exception as e:
|
||||
if isinstance(e, Http404):
|
||||
# let it propagate
|
||||
raise
|
||||
|
||||
# In production, don't want to let a 500 out for any reason
|
||||
if settings.DEBUG:
|
||||
raise
|
||||
else:
|
||||
log.exception("Error in exam view: user={user}, course={course},"
|
||||
" chapter={chapter} section={section}"
|
||||
"position={position}".format(
|
||||
user=request.user,
|
||||
course=course,
|
||||
chapter=chapter,
|
||||
section=section
|
||||
))
|
||||
try:
|
||||
result = render_to_response('courseware/courseware-error.html',
|
||||
{'staff_access': staff_access,
|
||||
'course' : course})
|
||||
except:
|
||||
# Let the exception propagate, relying on global config to at
|
||||
# at least return a nice error message
|
||||
log.exception("Error while rendering courseware-error page")
|
||||
raise
|
||||
|
||||
return result
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def jump_to(request, course_id, location):
|
||||
'''
|
||||
|
||||
10
lms/urls.py
10
lms/urls.py
@@ -217,16 +217,6 @@ if settings.COURSEWARE_ENABLED:
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$',
|
||||
'courseware.views.course_about', name="about_course"),
|
||||
|
||||
# timed exam:
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/timed_exam/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$',
|
||||
'courseware.views.timed_exam', name="timed_exam"),
|
||||
# (handle hard-coded 6.002x exam explicitly as a timed exam, but without changing the URL.
|
||||
# not only because Pearson doesn't want us to change its location, but because we also include it
|
||||
# in the navigation accordion we display with this exam (so students can see what work they have already
|
||||
# done). Those are generated automatically using reverse(courseware_section).
|
||||
url(r'^courses/(?P<course_id>MITx/6.002x/2012_Fall)/courseware/(?P<chapter>Final_Exam)/(?P<section>Final_Exam_Fall_2012)/$',
|
||||
'courseware.views.timed_exam'),
|
||||
|
||||
#Inside the course
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/$',
|
||||
'courseware.views.course_info', name="course_root"),
|
||||
|
||||
Reference in New Issue
Block a user