Commit 92d47c45 by chrisndodge

Merge pull request #4 from edx/muhhshoaib/PHX-37-create-data-models-for-proctored-exams

PHX-37 data models for the Proctored Exams.
parents 9277b5a7 a5947563
Chris Dodge <cdodge@edx.org>
Muhammad Shoaib <mshoaib@edx.org>
Afzal Wali <afzal@edx.org>
\ No newline at end of file
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as 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 'ProctoredExam'
db.create_table('edx_proctoring_proctoredexam', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('content_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('external_id', self.gf('django.db.models.fields.TextField')(null=True, db_index=True)),
('time_limit_mins', self.gf('django.db.models.fields.IntegerField')()),
('is_proctored', self.gf('django.db.models.fields.BooleanField')(default=False)),
('is_active', self.gf('django.db.models.fields.BooleanField')(default=False)),
))
db.send_create_signal('edx_proctoring', ['ProctoredExam'])
# Adding model 'ProctoredExamStudentAttempt'
db.create_table('edx_proctoring_proctoredexamstudentattempt', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user_id', self.gf('django.db.models.fields.IntegerField')(db_index=True)),
('proctored_exam', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['edx_proctoring.ProctoredExam'])),
('started_at', self.gf('django.db.models.fields.DateTimeField')(null=True)),
('completed_at', self.gf('django.db.models.fields.DateTimeField')(null=True)),
('external_id', self.gf('django.db.models.fields.TextField')(null=True, db_index=True)),
))
db.send_create_signal('edx_proctoring', ['ProctoredExamStudentAttempt'])
# Adding model 'ProctoredExamStudentAllowance'
db.create_table('edx_proctoring_proctoredexamstudentallowance', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)),
('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)),
('user_id', self.gf('django.db.models.fields.IntegerField')()),
('proctored_exam', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['edx_proctoring.ProctoredExam'])),
('key', self.gf('django.db.models.fields.CharField')(max_length=255)),
('value', self.gf('django.db.models.fields.CharField')(max_length=255)),
))
db.send_create_signal('edx_proctoring', ['ProctoredExamStudentAllowance'])
# Adding model 'ProctoredExamStudentAllowanceHistory'
db.create_table('edx_proctoring_proctoredexamstudentallowancehistory', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)),
('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)),
('user_id', self.gf('django.db.models.fields.IntegerField')()),
('proctored_exam', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['edx_proctoring.ProctoredExam'])),
('key', self.gf('django.db.models.fields.CharField')(max_length=255)),
('value', self.gf('django.db.models.fields.CharField')(max_length=255)),
))
db.send_create_signal('edx_proctoring', ['ProctoredExamStudentAllowanceHistory'])
def backwards(self, orm):
# Deleting model 'ProctoredExam'
db.delete_table('edx_proctoring_proctoredexam')
# Deleting model 'ProctoredExamStudentAttempt'
db.delete_table('edx_proctoring_proctoredexamstudentattempt')
# Deleting model 'ProctoredExamStudentAllowance'
db.delete_table('edx_proctoring_proctoredexamstudentallowance')
# Deleting model 'ProctoredExamStudentAllowanceHistory'
db.delete_table('edx_proctoring_proctoredexamstudentallowancehistory')
models = {
'edx_proctoring.proctoredexam': {
'Meta': {'object_name': 'ProctoredExam'},
'content_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'external_id': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_proctored': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'time_limit_mins': ('django.db.models.fields.IntegerField', [], {})
},
'edx_proctoring.proctoredexamstudentallowance': {
'Meta': {'object_name': 'ProctoredExamStudentAllowance'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proctored_exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']"}),
'user_id': ('django.db.models.fields.IntegerField', [], {}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'edx_proctoring.proctoredexamstudentallowancehistory': {
'Meta': {'object_name': 'ProctoredExamStudentAllowanceHistory'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proctored_exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']"}),
'user_id': ('django.db.models.fields.IntegerField', [], {}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'edx_proctoring.proctoredexamstudentattempt': {
'Meta': {'object_name': 'ProctoredExamStudentAttempt'},
'completed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'external_id': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'proctored_exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']"}),
'started_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'user_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'})
}
}
complete_apps = ['edx_proctoring']
\ No newline at end of file
"""
Data models for the proctoring subsystem
"""
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from model_utils.models import TimeStampedModel
class ProctoredExam(models.Model):
"""
Information about the Proctored Exam.
"""
course_id = models.CharField(max_length=255, db_index=True)
# This will be the pointer to the id of the piece
# of course_ware which is the proctored exam.
content_id = models.CharField(max_length=255, db_index=True)
# This will be a integration specific ID - say to SoftwareSecure.
external_id = models.TextField(null=True, db_index=True)
# Time limit (in minutes) that a student can finish this exam
time_limit_mins = models.IntegerField()
# Whether this exam actually is proctored or not
is_proctored = models.BooleanField()
# This will be a integration specific ID - say to SoftwareSecure.
is_active = models.BooleanField()
class ProctoredExamStudentAttempt(models.Model):
"""
Information about the Student Attempt on a
Proctored Exam.
"""
user_id = models.IntegerField(db_index=True)
proctored_exam = models.ForeignKey(ProctoredExam, db_index=True)
# started/completed date times
started_at = models.DateTimeField(null=True)
completed_at = models.DateTimeField(null=True)
# This will be a integration specific ID - say to SoftwareSecure.
external_id = models.TextField(null=True, db_index=True)
class QuerySetWithUpdateOverride(models.query.QuerySet):
"""
Custom QuerySet class to send the POST_UPDATE_SIGNAL
every time the object is updated.
"""
def update(self, **kwargs):
super(QuerySetWithUpdateOverride, self).update(**kwargs)
_make_archive_copy(self.get())
class ProctoredExamStudentAllowanceManager(models.Manager):
"""
Custom manager to override with the custom queryset
to enable the POST_UPDATE_SIGNAL
"""
def get_query_set(self):
return QuerySetWithUpdateOverride(self.model, using=self._db)
class ProctoredExamStudentAllowance(TimeStampedModel):
"""
Information about allowing a student additional time on exam.
"""
objects = ProctoredExamStudentAllowanceManager()
user_id = models.IntegerField()
proctored_exam = models.ForeignKey(ProctoredExam)
key = models.CharField(max_length=255)
value = models.CharField(max_length=255)
class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
"""
This should be the same schema as ProctoredExamStudentAllowance
but will record (for audit history) all entries that have been updated.
"""
user_id = models.IntegerField()
proctored_exam = models.ForeignKey(ProctoredExam)
key = models.CharField(max_length=255)
value = models.CharField(max_length=255)
# Hook up the custom POST_UPDATE_SIGNAL signal to record updations in the ProctoredExamStudentAllowanceHistory table.
@receiver(post_save, sender=ProctoredExamStudentAllowance)
def archive_allowance_updations(sender, instance, created, **kwargs): # pylint: disable=unused-argument
"""
Archiving all changes made to the Student Allowance.
Will only archive on update, and not on new entries created.
"""
if not created:
_make_archive_copy(instance)
def _make_archive_copy(item):
"""
Make a clone and populate in the History table
"""
archive_object = ProctoredExamStudentAllowanceHistory(
user_id=item.user_id,
proctored_exam=item.proctored_exam,
key=item.key,
value=item.value
)
archive_object.save()
"""
All tests for the models.py
"""
from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAllowanceHistory
from .utils import (
LoggedInTestCase
)
class ProctoredExamModelTests(LoggedInTestCase):
"""
All tests for the models.py
"""
def setUp(self):
"""
Build out test harnessing
"""
super(ProctoredExamModelTests, self).setUp()
def test_save_proctored_exam_student_allowance_history(self): # pylint: disable=invalid-name
"""
Test to Save and update the proctored Exam Student Allowance object.
Upon first save, a new entry is _not_ created in the History table
However, a new entry in the History table is created every time the Student Allowance entry is updated.
"""
proctored_exam = ProctoredExam.objects.create(
course_id='test_course',
content_id='test_content',
external_id='123aXqe3',
time_limit_mins=90
)
ProctoredExamStudentAllowance.objects.create(
user_id=1,
proctored_exam=proctored_exam,
key='allowance_key',
value='20 minutes'
)
# No entry in the History table on creation of the Allowance entry.
proctored_exam_student_history = ProctoredExamStudentAllowanceHistory.objects.filter(user_id=1)
self.assertEqual(len(proctored_exam_student_history), 0)
# Update the allowance object twice
ProctoredExamStudentAllowance.objects.filter(
user_id=1,
proctored_exam=proctored_exam,
).update(
user_id=1,
proctored_exam=proctored_exam,
key='allowance_key update 1',
value='10 minutes'
)
ProctoredExamStudentAllowance.objects.filter(
user_id=1,
proctored_exam=proctored_exam,
).update(
user_id=1,
proctored_exam=proctored_exam,
key='allowance_key update 2',
value='5 minutes'
)
# 2 new entries are created in the History table.
proctored_exam_student_history = ProctoredExamStudentAllowanceHistory.objects.filter(user_id=1)
self.assertEqual(len(proctored_exam_student_history), 2)
# also check with save() method
allowance = ProctoredExamStudentAllowance.objects.get(user_id=1, proctored_exam=proctored_exam)
allowance.value = '15 minutes'
allowance.save()
proctored_exam_student_history = ProctoredExamStudentAllowanceHistory.objects.filter(user_id=1)
self.assertEqual(len(proctored_exam_student_history), 3)
......@@ -6,7 +6,7 @@ import sys
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
BASE_DIR = os.path.dirname(__file__)
DEBUG=True
TEST_MODE=True
......
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