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 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 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): ...@@ -11,23 +11,32 @@ class Migration(SchemaMigration):
# Adding model 'ProctoredExam' # Adding model 'ProctoredExam'
db.create_table('edx_proctoring_proctoredexam', ( db.create_table('edx_proctoring_proctoredexam', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('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)), ('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)), ('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)), ('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')()), ('time_limit_mins', self.gf('django.db.models.fields.IntegerField')()),
('is_proctored', self.gf('django.db.models.fields.BooleanField')(default=False)), ('is_proctored', self.gf('django.db.models.fields.BooleanField')(default=False)),
('is_active', 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']) 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' # Adding model 'ProctoredExamStudentAttempt'
db.create_table('edx_proctoring_proctoredexamstudentattempt', ( db.create_table('edx_proctoring_proctoredexamstudentattempt', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('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)), ('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'])), ('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)), ('started_at', self.gf('django.db.models.fields.DateTimeField')(null=True)),
('completed_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)), ('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']) db.send_create_signal('edx_proctoring', ['ProctoredExamStudentAttempt'])
...@@ -43,11 +52,15 @@ class Migration(SchemaMigration): ...@@ -43,11 +52,15 @@ class Migration(SchemaMigration):
)) ))
db.send_create_signal('edx_proctoring', ['ProctoredExamStudentAllowance']) 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' # Adding model 'ProctoredExamStudentAllowanceHistory'
db.create_table('edx_proctoring_proctoredexamstudentallowancehistory', ( db.create_table('edx_proctoring_proctoredexamstudentallowancehistory', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)), ('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)),
('modified', self.gf('model_utils.fields.AutoLastModifiedField')(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')()), ('user_id', self.gf('django.db.models.fields.IntegerField')()),
('proctored_exam', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['edx_proctoring.ProctoredExam'])), ('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)), ('key', self.gf('django.db.models.fields.CharField')(max_length=255)),
...@@ -57,6 +70,12 @@ class Migration(SchemaMigration): ...@@ -57,6 +70,12 @@ class Migration(SchemaMigration):
def backwards(self, orm): 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' # Deleting model 'ProctoredExam'
db.delete_table('edx_proctoring_proctoredexam') db.delete_table('edx_proctoring_proctoredexam')
...@@ -72,17 +91,20 @@ class Migration(SchemaMigration): ...@@ -72,17 +91,20 @@ class Migration(SchemaMigration):
models = { models = {
'edx_proctoring.proctoredexam': { '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'}), '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'}), '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'}), 'external_id': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_proctored': ('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', [], {}) 'time_limit_mins': ('django.db.models.fields.IntegerField', [], {})
}, },
'edx_proctoring.proctoredexamstudentallowance': { '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'}), 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
...@@ -93,6 +115,7 @@ class Migration(SchemaMigration): ...@@ -93,6 +115,7 @@ class Migration(SchemaMigration):
}, },
'edx_proctoring.proctoredexamstudentallowancehistory': { 'edx_proctoring.proctoredexamstudentallowancehistory': {
'Meta': {'object_name': 'ProctoredExamStudentAllowanceHistory'}, 'Meta': {'object_name': 'ProctoredExamStudentAllowanceHistory'},
'allowance_id': ('django.db.models.fields.IntegerField', [], {}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
...@@ -104,10 +127,13 @@ class Migration(SchemaMigration): ...@@ -104,10 +127,13 @@ class Migration(SchemaMigration):
'edx_proctoring.proctoredexamstudentattempt': { 'edx_proctoring.proctoredexamstudentattempt': {
'Meta': {'object_name': 'ProctoredExamStudentAttempt'}, 'Meta': {'object_name': 'ProctoredExamStudentAttempt'},
'completed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), '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'}), 'external_id': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': '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']"}), 'proctored_exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']"}),
'started_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), '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'}) 'user_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'})
} }
} }
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
Data models for the proctoring subsystem Data models for the proctoring subsystem
""" """
from django.db import models 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 django.dispatch import receiver
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
class ProctoredExam(models.Model): class ProctoredExam(TimeStampedModel):
""" """
Information about the Proctored Exam. Information about the Proctored Exam.
""" """
...@@ -21,6 +21,9 @@ class ProctoredExam(models.Model): ...@@ -21,6 +21,9 @@ class ProctoredExam(models.Model):
# This will be a integration specific ID - say to SoftwareSecure. # This will be a integration specific ID - say to SoftwareSecure.
external_id = models.TextField(null=True, db_index=True) 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 (in minutes) that a student can finish this exam
time_limit_mins = models.IntegerField() time_limit_mins = models.IntegerField()
...@@ -30,8 +33,12 @@ class ProctoredExam(models.Model): ...@@ -30,8 +33,12 @@ class ProctoredExam(models.Model):
# This will be a integration specific ID - say to SoftwareSecure. # This will be a integration specific ID - say to SoftwareSecure.
is_active = models.BooleanField() 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 Information about the Student Attempt on a
Proctored Exam. Proctored Exam.
...@@ -47,6 +54,14 @@ class ProctoredExamStudentAttempt(models.Model): ...@@ -47,6 +54,14 @@ class ProctoredExamStudentAttempt(models.Model):
# This will be a integration specific ID - say to SoftwareSecure. # This will be a integration specific ID - say to SoftwareSecure.
external_id = models.TextField(null=True, db_index=True) 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): class QuerySetWithUpdateOverride(models.query.QuerySet):
""" """
...@@ -82,6 +97,10 @@ class ProctoredExamStudentAllowance(TimeStampedModel): ...@@ -82,6 +97,10 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
value = models.CharField(max_length=255) value = models.CharField(max_length=255)
class Meta:
""" Meta class for this Django model """
unique_together = (('user_id', 'proctored_exam', 'key'),)
class ProctoredExamStudentAllowanceHistory(TimeStampedModel): class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
""" """
...@@ -89,6 +108,9 @@ class ProctoredExamStudentAllowanceHistory(TimeStampedModel): ...@@ -89,6 +108,9 @@ class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
but will record (for audit history) all entries that have been updated. 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() user_id = models.IntegerField()
proctored_exam = models.ForeignKey(ProctoredExam) proctored_exam = models.ForeignKey(ProctoredExam)
...@@ -100,7 +122,7 @@ class ProctoredExamStudentAllowanceHistory(TimeStampedModel): ...@@ -100,7 +122,7 @@ class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
# Hook up the custom POST_UPDATE_SIGNAL signal to record updations in the ProctoredExamStudentAllowanceHistory table. # Hook up the custom POST_UPDATE_SIGNAL signal to record updations in the ProctoredExamStudentAllowanceHistory table.
@receiver(post_save, sender=ProctoredExamStudentAllowance) @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. Archiving all changes made to the Student Allowance.
Will only archive on update, and not on new entries created. Will only archive on update, and not on new entries created.
...@@ -110,12 +132,22 @@ def archive_allowance_updations(sender, instance, created, **kwargs): # pylint: ...@@ -110,12 +132,22 @@ def archive_allowance_updations(sender, instance, created, **kwargs): # pylint:
_make_archive_copy(instance) _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): def _make_archive_copy(item):
""" """
Make a clone and populate in the History table Make a clone and populate in the History table
""" """
archive_object = ProctoredExamStudentAllowanceHistory( archive_object = ProctoredExamStudentAllowanceHistory(
allowance_id=item.id,
user_id=item.user_id, user_id=item.user_id,
proctored_exam=item.proctored_exam, proctored_exam=item.proctored_exam,
key=item.key, key=item.key,
......
...@@ -28,6 +28,7 @@ class ProctoredExamModelTests(LoggedInTestCase): ...@@ -28,6 +28,7 @@ class ProctoredExamModelTests(LoggedInTestCase):
proctored_exam = ProctoredExam.objects.create( proctored_exam = ProctoredExam.objects.create(
course_id='test_course', course_id='test_course',
content_id='test_content', content_id='test_content',
exam_name='Test Exam',
external_id='123aXqe3', external_id='123aXqe3',
time_limit_mins=90 time_limit_mins=90
) )
...@@ -74,3 +75,32 @@ class ProctoredExamModelTests(LoggedInTestCase): ...@@ -74,3 +75,32 @@ class ProctoredExamModelTests(LoggedInTestCase):
proctored_exam_student_history = ProctoredExamStudentAllowanceHistory.objects.filter(user_id=1) proctored_exam_student_history = ProctoredExamStudentAllowanceHistory.objects.filter(user_id=1)
self.assertEqual(len(proctored_exam_student_history), 3) 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= ...@@ -39,7 +39,7 @@ load-plugins=
# no Warning level messages displayed, use"--disable=all --enable=classes # no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W" # --disable=W"
# I0011 locally-disabled (module-level pylint overrides) # 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] [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