add TimerModule to courseware
This commit is contained in:
119
lms/djangoapps/courseware/migrations/0006_add_timed_module.py
Normal file
119
lms/djangoapps/courseware/migrations/0006_add_timed_module.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# -*- 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)),
|
||||
('module_state_key', self.gf('django.db.models.fields.CharField')(max_length=255, db_column='module_id', 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', 'module_state_key', 'course_id']
|
||||
db.create_unique('courseware_timedmodule', ['student_id', 'module_id', 'course_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'TimedModule', fields ['student', 'module_state_key', 'course_id']
|
||||
db.delete_unique('courseware_timedmodule', ['student_id', 'module_id', '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', 'module_state_key', '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'}),
|
||||
'modified_at': ('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'}),
|
||||
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['courseware']
|
||||
@@ -12,15 +12,12 @@ file and check it in at the same time as your model changes. To do that,
|
||||
ASSUMPTIONS: modules have unique IDs, even across different module_types
|
||||
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from calendar import timegm
|
||||
|
||||
from django.db import models
|
||||
#from django.core.cache import cache
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
#from cache_toolbox import cache_model, cache_relation
|
||||
|
||||
#CACHE_TIMEOUT = 60 * 60 * 4 # Set the cache timeout to be four hours
|
||||
|
||||
|
||||
class StudentModule(models.Model):
|
||||
"""
|
||||
Keeps student state for a particular module in a particular course.
|
||||
@@ -214,3 +211,82 @@ 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')
|
||||
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'),)
|
||||
|
||||
# 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'),
|
||||
)
|
||||
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)
|
||||
|
||||
# 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])
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import logging
|
||||
import urllib
|
||||
|
||||
from functools import partial
|
||||
from time import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.context_processors import csrf
|
||||
@@ -21,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
|
||||
from courseware.models import StudentModuleCache, TimedModule
|
||||
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
|
||||
@@ -402,16 +401,55 @@ def timed_exam(request, course_id, chapter, section):
|
||||
# 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
|
||||
|
||||
# for now, assume that the duration is set as an integer value, indicating the number of seconds:
|
||||
duration = int(section_descriptor.metadata.get('duration'))
|
||||
|
||||
# get corresponding time module, if one is present:
|
||||
# TODO: determine what to use for module_key...
|
||||
try:
|
||||
timed_module = TimedModule.objects.get(student=request.user, course_id=course_id)
|
||||
|
||||
# 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,
|
||||
raise Exception("Time expired on {}".format(timed_module))
|
||||
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.
|
||||
# TODO: determine what to use for module_key...
|
||||
timed_module = TimedModule(student=request.user, course_id=course_id)
|
||||
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'] = (time() + duration) * 1000
|
||||
context['end_date'] = end_date
|
||||
|
||||
result = render_to_response('courseware/testcenter_exam.html', context)
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user