Commit cc11dc2a by Brian Wilson

switch to using timelimit module for Pearson test

parent 33a5a5fd
...@@ -39,12 +39,15 @@ from certificates.models import CertificateStatuses, certificate_status_for_stud ...@@ -39,12 +39,15 @@ from certificates.models import CertificateStatuses, certificate_status_for_stud
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from collections import namedtuple from collections import namedtuple
from courseware.courses import get_courses, sort_by_announcement from courseware.courses import get_courses, sort_by_announcement
from courseware.access import has_access 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 from statsd import statsd
...@@ -1082,13 +1085,14 @@ def test_center_login(request): ...@@ -1082,13 +1085,14 @@ def test_center_login(request):
# errors are returned by navigating to the error_url, adding a query parameter named "code" # errors are returned by navigating to the error_url, adding a query parameter named "code"
# which contains the error code describing the exceptional condition. # which contains the error code describing the exceptional condition.
def makeErrorURL(error_url, error_code): 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 # 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") 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. # with the code we calculate for the same parameters.
if 'code' not in request.POST: if 'code' not in request.POST:
return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode")); return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode"));
...@@ -1112,65 +1116,81 @@ def test_center_login(request): ...@@ -1112,65 +1116,81 @@ def test_center_login(request):
try: try:
testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id) testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
except TestCenterUser.DoesNotExist: except TestCenterUser.DoesNotExist:
log.error("not able to find demographics for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID")); return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"));
# find testcenter_registration that matches the provided exam code: # find testcenter_registration that matches the provided exam code:
# Note that we could rely on either the registrationId or the exam code, # Note that we could rely in future on either the registrationId or the exam code,
# or possibly both. # 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: if 'vueExamSeriesCode' not in request.POST:
# TODO: confirm this error code (made up, not in documentation) # 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")); return HttpResponseRedirect(makeErrorURL(error_url, "missingExamSeriesCode"));
exam_series_code = request.POST.get('vueExamSeriesCode') exam_series_code = request.POST.get('vueExamSeriesCode')
registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code) registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code)
if not registrations: 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")); return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned"));
# TODO: figure out what to do if there are more than one registrations.... # TODO: figure out what to do if there are more than one registrations....
# for now, just take the first... # for now, just take the first...
registration = registrations[0] registration = registrations[0]
course_id = registration.course_id
# if we want to look up whether the test has already been taken, or to course_id = registration.course_id
# communicate that a time accommodation needs to be applied, we need to course = course_from_id(course_id) # assume it will be found....
# know the module_id to use that corresponds to the particular exam_series_code. if not course:
# For now, we can hardcode that... log.error("not able to find course from ID {} for cand ID {}".format(course_id, client_candidate_id))
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.
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests")); 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', time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME',
'ET30MN' : 'ADD30MIN', 'ET30MN' : 'ADD30MIN',
'ETDBTM' : 'ADDDOUBLE', } 'ETDBTM' : 'ADDDOUBLE', }
# check if the test has already been taken time_accommodation_code = None
timed_modules = TimedModule.objects.filter(student=testcenteruser.user, course_id=course_id, location=location) if registration.get_accommodation_codes():
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
for code in registration.get_accommodation_codes(): for code in registration.get_accommodation_codes():
if code in time_accommodation_mapping: if code in time_accommodation_mapping:
time_accommodation_code = time_accommodation_mapping[code] time_accommodation_code = time_accommodation_mapping[code]
if client_candidate_id == "edX003671291147": # special, hard-coded client ID used by Pearson shell for testing:
time_accommodation_code = 'TESTING' if client_candidate_id == "edX003671291147":
if time_accommodation_code: time_accommodation_code = 'TESTING'
timed_module = TimedModule(student=request.user, course_id=course_id, location=location)
timed_module.accommodation_code = time_accommodation_code if time_accommodation_code:
timed_module.save() 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!!! # UGLY HACK!!!
# Login assumes that authentication has occurred, and that there is a # Login assumes that authentication has occurred, and that there is a
......
...@@ -23,7 +23,6 @@ setup( ...@@ -23,7 +23,6 @@ setup(
"course = xmodule.course_module:CourseDescriptor", "course = xmodule.course_module:CourseDescriptor",
"customtag = xmodule.template_module:CustomTagDescriptor", "customtag = xmodule.template_module:CustomTagDescriptor",
"discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor", "discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"fixedtime = xmodule.fixed_time_module:FixedTimeDescriptor",
"html = xmodule.html_module:HtmlDescriptor", "html = xmodule.html_module:HtmlDescriptor",
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor", "image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"error = xmodule.error_module:ErrorDescriptor", "error = xmodule.error_module:ErrorDescriptor",
...@@ -32,6 +31,7 @@ setup( ...@@ -32,6 +31,7 @@ setup(
"section = xmodule.backcompat_module:SemanticSectionDescriptor", "section = xmodule.backcompat_module:SemanticSectionDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor", "sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor", "slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"timelimit = xmodule.timelimit_module:TimeLimitDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor", "vertical = xmodule.vertical_module:VerticalDescriptor",
"video = xmodule.video_module:VideoDescriptor", "video = xmodule.video_module:VideoDescriptor",
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
......
...@@ -648,7 +648,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -648,7 +648,7 @@ class CourseDescriptor(SequenceDescriptor):
raise ValueError("First appointment date must be before last appointment date") raise ValueError("First appointment date must be before last appointment date")
if self.registration_end_date > self.last_eligible_appointment_date: if self.registration_end_date > self.last_eligible_appointment_date:
raise ValueError("Registration end date must be before last 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): def _try_parse_time(self, key):
""" """
...@@ -704,6 +704,10 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -704,6 +704,10 @@ class CourseDescriptor(SequenceDescriptor):
else: else:
return None 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 @property
def title(self): def title(self):
return self.display_name return self.display_name
......
...@@ -4,22 +4,16 @@ import logging ...@@ -4,22 +4,16 @@ import logging
from lxml import etree from lxml import etree
from time import time from time import time
from xmodule.mako_module import MakoModuleDescriptor from xmodule.editing_module import XMLEditingDescriptor
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.progress import Progress from xmodule.progress import Progress
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from pkg_resources import resource_string
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# HACK: This shouldn't be hard-coded to two types class TimeLimitModule(XModule):
# OBSOLETE: This obsoletes 'type'
# class_priority = ['video', 'problem']
class FixedTimeModule(XModule):
''' '''
Wrapper module which imposes a time constraint for the completion of its child. Wrapper module which imposes a time constraint for the completion of its child.
''' '''
...@@ -29,9 +23,7 @@ class FixedTimeModule(XModule): ...@@ -29,9 +23,7 @@ class FixedTimeModule(XModule):
XModule.__init__(self, system, location, definition, descriptor, XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs) instance_state, shared_state, **kwargs)
# NOTE: Position is 1-indexed. This is silly, but there are now student self.rendered = False
# positions saved on prod, so it's not easy to fix.
# self.position = 1
self.beginning_at = None self.beginning_at = None
self.ending_at = None self.ending_at = None
self.accommodation_code = None self.accommodation_code = None
...@@ -46,13 +38,6 @@ class FixedTimeModule(XModule): ...@@ -46,13 +38,6 @@ class FixedTimeModule(XModule):
if 'accommodation_code' in state: if 'accommodation_code' in state:
self.accommodation_code = state['accommodation_code'] 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 # For a timed activity, we are only interested here
# in time-related accommodations, and these should be disjoint. # in time-related accommodations, and these should be disjoint.
# (For proctored exams, it is possible to have multiple accommodations # (For proctored exams, it is possible to have multiple accommodations
...@@ -81,8 +66,6 @@ class FixedTimeModule(XModule): ...@@ -81,8 +66,6 @@ class FixedTimeModule(XModule):
elif self.accommodation_code == 'TESTING': elif self.accommodation_code == 'TESTING':
# when testing, set timer to run for a week at a time. # when testing, set timer to run for a week at a time.
return 3600 * 24 * 7 return 3600 * 24 * 7
# store state:
@property @property
def has_begun(self): def has_begun(self):
...@@ -101,8 +84,6 @@ class FixedTimeModule(XModule): ...@@ -101,8 +84,6 @@ class FixedTimeModule(XModule):
''' '''
self.beginning_at = time() self.beginning_at = time()
modified_duration = self._get_accommodated_duration(duration) 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 self.ending_at = self.beginning_at + modified_duration
def get_end_time_in_ms(self): def get_end_time_in_ms(self):
...@@ -132,31 +113,32 @@ class FixedTimeModule(XModule): ...@@ -132,31 +113,32 @@ class FixedTimeModule(XModule):
progress = reduce(Progress.add_counts, progresses) progress = reduce(Progress.add_counts, progresses)
return progress return progress
def handle_ajax(self, dispatch, get): # TODO: bounds checking def handle_ajax(self, dispatch, get):
# ''' get = request.POST instance '''
# if dispatch == 'goto_position':
# self.position = int(get['position'])
# return json.dumps({'success': True})
raise NotFoundError('Unexpected dispatch type') raise NotFoundError('Unexpected dispatch type')
def render(self): def render(self):
if self.rendered: if self.rendered:
return return
# assumes there is one and only one child, so it only renders the first child # assumes there is one and only one child, so it only renders the first child
child = self.get_display_items()[0] children = self.get_display_items()
self.content = child.get_html() if children:
child = children[0]
self.content = child.get_html()
self.rendered = True self.rendered = True
def get_icon_class(self): 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): module_class = TimeLimitModule
# TODO: fix this template?!
mako_template = 'widgets/sequence-edit.html'
module_class = FixedTimeModule
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 @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
...@@ -165,14 +147,14 @@ class FixedTimeDescriptor(MakoModuleDescriptor, XmlDescriptor): ...@@ -165,14 +147,14 @@ class FixedTimeDescriptor(MakoModuleDescriptor, XmlDescriptor):
try: try:
children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url()) children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url())
except Exception as e: 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: if system.error_tracker is not None:
system.error_tracker("ERROR: " + str(e)) system.error_tracker("ERROR: " + str(e))
continue continue
return {'children': children} return {'children': children}
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
xml_object = etree.Element('fixedtime') xml_object = etree.Element('timelimit')
for child in self.get_children(): for child in self.get_children():
xml_object.append( xml_object.append(
etree.fromstring(child.export_to_xml(resource_fs))) etree.fromstring(child.export_to_xml(resource_fs)))
......
# -*- 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']
\ No newline at end of file
...@@ -212,87 +212,3 @@ class OfflineComputedGradeLog(models.Model): ...@@ -212,87 +212,3 @@ class OfflineComputedGradeLog(models.Model):
def __unicode__(self): def __unicode__(self):
return "[OCGLog] %s: %s" % (self.course_id, self.created) 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])
...@@ -217,16 +217,6 @@ if settings.COURSEWARE_ENABLED: ...@@ -217,16 +217,6 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$',
'courseware.views.course_about', name="about_course"), '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 #Inside the course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/$',
'courseware.views.course_info', name="course_root"), 'courseware.views.course_info', name="course_root"),
......
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