Commit b3d9949c by Chris Dodge

update models

parent 92d47c45
# pylint: disable=unused-argument
# remove pylint rule after we implement each method
"""
In-Proc API (aka Library) for the edx_proctoring subsystem. This is not to be confused with a HTTP REST
API which is in the views.py file, per edX coding standards
"""
def create_exam(course_id, content_id, exam_name, time_limit_mins,
is_proctored=True, external_id=None, is_active=True):
"""
Creates a new ProctoredExam entity, if the course_id/content_id pair do not already exist.
If that pair already exists, then raise exception.
Returns: id (PK)
"""
def update_exam(exam_id, exam_name=None, time_limit_mins=None,
is_proctored=None, external_id=None, is_active=None):
"""
Given a Django ORM id, update the existing record, otherwise raise exception if not found.
If an argument is not passed in, then do not change it's current value.
Returns: id
"""
def get_exam_by_id(exam_id):
"""
Looks up exam by the Primary Key. Raises exception if not found.
Returns dictionary version of the Django ORM object
"""
def get_exam_by_content_id(course_id, content_id):
"""
Looks up exam by the course_id/content_id pair. Raises exception if not found.
Returns dictionary version of the Django ORM object
"""
def add_allowance_for_user(exam_id, user_id, key, value):
"""
Adds (or updates) an allowance for a user within a given exam
"""
def remove_allowance_for_user(exam_id, user_id, key):
"""
Deletes an allowance for a user within a given exam.
"""
def start_exam_attempt(exam_id, user_id, external_id):
"""
Signals the beginning of an exam attempt for a given
exam_id. If one already exists, then an exception should be thrown.
Returns: exam_attempt_id (PK)
"""
def stop_exam_attempt(exam_id, user):
"""
Marks the exam attempt as completed (sets the completed_at field and updates the record)
"""
def get_active_exams_for_user(user_id, course_id=None):
"""
This method will return a list of active exams for the user,
i.e. started_at != None and completed_at == None. Theoretically there
could be more than one, but in practice it will be one active exam.
If course_id is set, then attempts only for an exam in that course_id
should be returned.
The return set should be a list of dictionary objects which are nested
[{
'exam': <exam fields as dict>,
'attempt': <student attempt fields as dict>,
'allowances': <student allowances as dict of key/value pairs
}, {}, ...]
"""
......@@ -11,23 +11,32 @@ class Migration(SchemaMigration):
# Adding model 'ProctoredExam'
db.create_table('edx_proctoring_proctoredexam', (
('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)),
('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)),
('exam_name', self.gf('django.db.models.fields.TextField')()),
('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 unique constraint on 'ProctoredExam', fields ['course_id', 'content_id']
db.create_unique('edx_proctoring_proctoredexam', ['course_id', 'content_id'])
# Adding model 'ProctoredExamStudentAttempt'
db.create_table('edx_proctoring_proctoredexamstudentattempt', (
('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')(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)),
('status', self.gf('django.db.models.fields.CharField')(max_length=64)),
))
db.send_create_signal('edx_proctoring', ['ProctoredExamStudentAttempt'])
......@@ -43,11 +52,15 @@ class Migration(SchemaMigration):
))
db.send_create_signal('edx_proctoring', ['ProctoredExamStudentAllowance'])
# Adding unique constraint on 'ProctoredExamStudentAllowance', fields ['user_id', 'proctored_exam', 'key']
db.create_unique('edx_proctoring_proctoredexamstudentallowance', ['user_id', 'proctored_exam_id', 'key'])
# 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)),
('allowance_id', self.gf('django.db.models.fields.IntegerField')()),
('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)),
......@@ -57,6 +70,12 @@ class Migration(SchemaMigration):
def backwards(self, orm):
# Removing unique constraint on 'ProctoredExamStudentAllowance', fields ['user_id', 'proctored_exam', 'key']
db.delete_unique('edx_proctoring_proctoredexamstudentallowance', ['user_id', 'proctored_exam_id', 'key'])
# Removing unique constraint on 'ProctoredExam', fields ['course_id', 'content_id']
db.delete_unique('edx_proctoring_proctoredexam', ['course_id', 'content_id'])
# Deleting model 'ProctoredExam'
db.delete_table('edx_proctoring_proctoredexam')
......@@ -72,17 +91,20 @@ class Migration(SchemaMigration):
models = {
'edx_proctoring.proctoredexam': {
'Meta': {'object_name': 'ProctoredExam'},
'Meta': {'unique_together': "(('course_id', 'content_id'),)", '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'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'exam_name': ('django.db.models.fields.TextField', [], {}),
'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'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'time_limit_mins': ('django.db.models.fields.IntegerField', [], {})
},
'edx_proctoring.proctoredexamstudentallowance': {
'Meta': {'object_name': 'ProctoredExamStudentAllowance'},
'Meta': {'unique_together': "(('user_id', 'proctored_exam', 'key'),)", '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'}),
......@@ -93,6 +115,7 @@ class Migration(SchemaMigration):
},
'edx_proctoring.proctoredexamstudentallowancehistory': {
'Meta': {'object_name': 'ProctoredExamStudentAllowanceHistory'},
'allowance_id': ('django.db.models.fields.IntegerField', [], {}),
'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'}),
......@@ -104,10 +127,13 @@ class Migration(SchemaMigration):
'edx_proctoring.proctoredexamstudentattempt': {
'Meta': {'object_name': 'ProctoredExamStudentAttempt'},
'completed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'external_id': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proctored_exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']"}),
'started_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'user_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'})
}
}
......
......@@ -2,12 +2,12 @@
Data models for the proctoring subsystem
"""
from django.db import models
from django.db.models.signals import post_save
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from model_utils.models import TimeStampedModel
class ProctoredExam(models.Model):
class ProctoredExam(TimeStampedModel):
"""
Information about the Proctored Exam.
"""
......@@ -21,6 +21,9 @@ class ProctoredExam(models.Model):
# This will be a integration specific ID - say to SoftwareSecure.
external_id = models.TextField(null=True, db_index=True)
# This is the display name of the course
exam_name = models.TextField()
# Time limit (in minutes) that a student can finish this exam
time_limit_mins = models.IntegerField()
......@@ -30,8 +33,12 @@ class ProctoredExam(models.Model):
# This will be a integration specific ID - say to SoftwareSecure.
is_active = models.BooleanField()
class Meta:
""" Meta class for this Django model """
unique_together = (('course_id', 'content_id'),)
class ProctoredExamStudentAttempt(models.Model):
class ProctoredExamStudentAttempt(TimeStampedModel):
"""
Information about the Student Attempt on a
Proctored Exam.
......@@ -47,6 +54,14 @@ class ProctoredExamStudentAttempt(models.Model):
# This will be a integration specific ID - say to SoftwareSecure.
external_id = models.TextField(null=True, db_index=True)
# what is the status of this attempt
status = models.CharField(max_length=64)
@property
def is_active(self):
""" returns boolean if this attempt is considered active """
return self.started_at and not self.completed_at
class QuerySetWithUpdateOverride(models.query.QuerySet):
"""
......@@ -82,6 +97,10 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
value = models.CharField(max_length=255)
class Meta:
""" Meta class for this Django model """
unique_together = (('user_id', 'proctored_exam', 'key'),)
class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
"""
......@@ -89,6 +108,9 @@ class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
but will record (for audit history) all entries that have been updated.
"""
# what was the original id of the allowance
allowance_id = models.IntegerField()
user_id = models.IntegerField()
proctored_exam = models.ForeignKey(ProctoredExam)
......@@ -100,7 +122,7 @@ class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
# 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
def on_allowance_saved(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.
......@@ -110,12 +132,22 @@ def archive_allowance_updations(sender, instance, created, **kwargs): # pylint:
_make_archive_copy(instance)
@receiver(pre_delete, sender=ProctoredExamStudentAllowance)
def on_allowance_deleted(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Archive the allowance when the item is about to be deleted
"""
_make_archive_copy(instance)
def _make_archive_copy(item):
"""
Make a clone and populate in the History table
"""
archive_object = ProctoredExamStudentAllowanceHistory(
allowance_id=item.id,
user_id=item.user_id,
proctored_exam=item.proctored_exam,
key=item.key,
......
......@@ -28,6 +28,7 @@ class ProctoredExamModelTests(LoggedInTestCase):
proctored_exam = ProctoredExam.objects.create(
course_id='test_course',
content_id='test_content',
exam_name='Test Exam',
external_id='123aXqe3',
time_limit_mins=90
)
......@@ -74,3 +75,32 @@ class ProctoredExamModelTests(LoggedInTestCase):
proctored_exam_student_history = ProctoredExamStudentAllowanceHistory.objects.filter(user_id=1)
self.assertEqual(len(proctored_exam_student_history), 3)
def test_delete_proctored_exam_student_allowance_history(self): # pylint: disable=invalid-name
"""
Test to delete 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',
exam_name='Test Exam',
external_id='123aXqe3',
time_limit_mins=90
)
allowance = 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)
allowance.delete()
proctored_exam_student_history = ProctoredExamStudentAllowanceHistory.objects.filter(user_id=1)
self.assertEqual(len(proctored_exam_student_history), 1)
......@@ -39,7 +39,7 @@ load-plugins=
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
# I0011 locally-disabled (module-level pylint overrides)
disable=I0011,W0232,too-few-public-methods,abstract-class-little-used,abstract-class-not-used,too-many-public-methods,no-self-use,too-many-instance-attributes,duplicate-code,too-many-arguments,too-many-locals
disable=I0011,W0232,too-few-public-methods,abstract-class-little-used,abstract-class-not-used,too-many-public-methods,no-self-use,too-many-instance-attributes,duplicate-code,too-many-arguments,too-many-locals,old-style-class
[REPORTS]
......
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