Commit 34e11630 by Hasnain Committed by Chris Dodge

Added new field "Due date"

parent 0ab7dfc5
...@@ -57,7 +57,7 @@ def is_feature_enabled(): ...@@ -57,7 +57,7 @@ def is_feature_enabled():
return hasattr(settings, 'FEATURES') and settings.FEATURES.get('ENABLE_PROCTORED_EXAMS', False) return hasattr(settings, 'FEATURES') and settings.FEATURES.get('ENABLE_PROCTORED_EXAMS', False)
def create_exam(course_id, content_id, exam_name, time_limit_mins, def create_exam(course_id, content_id, exam_name, time_limit_mins, due_date=None,
is_proctored=True, is_practice_exam=False, external_id=None, is_active=True): is_proctored=True, is_practice_exam=False, external_id=None, is_active=True):
""" """
Creates a new ProctoredExam entity, if the course_id/content_id pair do not already exist. Creates a new ProctoredExam entity, if the course_id/content_id pair do not already exist.
...@@ -75,6 +75,7 @@ def create_exam(course_id, content_id, exam_name, time_limit_mins, ...@@ -75,6 +75,7 @@ def create_exam(course_id, content_id, exam_name, time_limit_mins,
external_id=external_id, external_id=external_id,
exam_name=exam_name, exam_name=exam_name,
time_limit_mins=time_limit_mins, time_limit_mins=time_limit_mins,
due_date=due_date,
is_proctored=is_proctored, is_proctored=is_proctored,
is_practice_exam=is_practice_exam, is_practice_exam=is_practice_exam,
is_active=is_active is_active=is_active
...@@ -97,7 +98,7 @@ def create_exam(course_id, content_id, exam_name, time_limit_mins, ...@@ -97,7 +98,7 @@ def create_exam(course_id, content_id, exam_name, time_limit_mins,
return proctored_exam.id return proctored_exam.id
def update_exam(exam_id, exam_name=None, time_limit_mins=None, def update_exam(exam_id, exam_name=None, time_limit_mins=None, due_date=constants.MINIMUM_TIME,
is_proctored=None, is_practice_exam=None, external_id=None, is_active=None): is_proctored=None, is_practice_exam=None, external_id=None, is_active=None):
""" """
Given a Django ORM id, update the existing record, otherwise raise exception if not found. Given a Django ORM id, update the existing record, otherwise raise exception if not found.
...@@ -108,11 +109,11 @@ def update_exam(exam_id, exam_name=None, time_limit_mins=None, ...@@ -108,11 +109,11 @@ def update_exam(exam_id, exam_name=None, time_limit_mins=None,
log_msg = ( log_msg = (
u'Updating exam_id {exam_id} with parameters ' u'Updating exam_id {exam_id} with parameters '
u'exam_name={exam_name}, time_limit_mins={time_limit_mins}, ' u'exam_name={exam_name}, time_limit_mins={time_limit_mins}, due_date={due_date}'
u'is_proctored={is_proctored}, is_practice_exam={is_practice_exam}, ' u'is_proctored={is_proctored}, is_practice_exam={is_practice_exam}, '
u'external_id={external_id}, is_active={is_active}'.format( u'external_id={external_id}, is_active={is_active}'.format(
exam_id=exam_id, exam_name=exam_name, time_limit_mins=time_limit_mins, exam_id=exam_id, exam_name=exam_name, time_limit_mins=time_limit_mins,
is_proctored=is_proctored, is_practice_exam=is_practice_exam, due_date=due_date, is_proctored=is_proctored, is_practice_exam=is_practice_exam,
external_id=external_id, is_active=is_active external_id=external_id, is_active=is_active
) )
) )
...@@ -126,6 +127,8 @@ def update_exam(exam_id, exam_name=None, time_limit_mins=None, ...@@ -126,6 +127,8 @@ def update_exam(exam_id, exam_name=None, time_limit_mins=None,
proctored_exam.exam_name = exam_name proctored_exam.exam_name = exam_name
if time_limit_mins is not None: if time_limit_mins is not None:
proctored_exam.time_limit_mins = time_limit_mins proctored_exam.time_limit_mins = time_limit_mins
if due_date is not constants.MINIMUM_TIME:
proctored_exam.due_date = due_date
if is_proctored is not None: if is_proctored is not None:
proctored_exam.is_proctored = is_proctored proctored_exam.is_proctored = is_proctored
if is_practice_exam is not None: if is_practice_exam is not None:
...@@ -319,6 +322,13 @@ def update_exam_attempt(attempt_id, **kwargs): ...@@ -319,6 +322,13 @@ def update_exam_attempt(attempt_id, **kwargs):
exam_attempt_obj.save() exam_attempt_obj.save()
def _has_due_date_passed(due_datetime):
"""
return True if due date is lesser than current datetime, otherwise False
"""
return due_datetime <= datetime.now(pytz.UTC)
def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
""" """
Creates an exam attempt for user_id against exam_id. There should only be Creates an exam attempt for user_id against exam_id. There should only be
...@@ -350,19 +360,32 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): ...@@ -350,19 +360,32 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
raise StudentExamAttemptAlreadyExistsException(err_msg) raise StudentExamAttemptAlreadyExistsException(err_msg)
allowed_time_limit_mins = exam['time_limit_mins'] allowed_time_limit_mins = exam['time_limit_mins']
due_datetime = exam['due_date']
current_datetime = datetime.now(pytz.UTC)
is_exam_past_due_date = False
# add in the allowed additional time # add in the allowed additional time
allowance_extra_mins = ProctoredExamStudentAllowance.get_additional_time_granted(exam_id, user_id) allowance_extra_mins = ProctoredExamStudentAllowance.get_additional_time_granted(exam_id, user_id)
if allowance_extra_mins: if allowance_extra_mins:
allowed_time_limit_mins += allowance_extra_mins allowed_time_limit_mins += allowance_extra_mins
if due_datetime:
if _has_due_date_passed(due_datetime):
is_exam_past_due_date = True
elif current_datetime + timedelta(minutes=allowed_time_limit_mins) > due_datetime:
# e.g current_datetime=09:00, due_datetime=10:00 and allowed_time_limit_mins=120(2hours)
# then allowed_time_limit_mins should be 60(1hour)
allowed_time_limit_mins = int((due_datetime - current_datetime).seconds / 60)
attempt_code = unicode(uuid.uuid4()).upper() attempt_code = unicode(uuid.uuid4()).upper()
external_id = None external_id = None
review_policy = ProctoredExamReviewPolicy.get_review_policy_for_exam(exam_id) review_policy = ProctoredExamReviewPolicy.get_review_policy_for_exam(exam_id)
review_policy_exception = ProctoredExamStudentAllowance.get_review_policy_exception(exam_id, user_id) review_policy_exception = ProctoredExamStudentAllowance.get_review_policy_exception(exam_id, user_id)
if taking_as_proctored: if not is_exam_past_due_date and taking_as_proctored:
scheme = 'https' if getattr(settings, 'HTTPS', 'on') == 'on' else 'http' scheme = 'https' if getattr(settings, 'HTTPS', 'on') == 'on' else 'http'
callback_url = '{scheme}://{hostname}{path}'.format( callback_url = '{scheme}://{hostname}{path}'.format(
scheme=scheme, scheme=scheme,
...@@ -422,6 +445,13 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): ...@@ -422,6 +445,13 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
review_policy_id=review_policy.id if review_policy else None, review_policy_id=review_policy.id if review_policy else None,
) )
if is_exam_past_due_date:
update_attempt_status(
exam_id,
user_id,
ProctoredExamStudentAttemptStatus.declined
)
log_msg = ( log_msg = (
'Created exam attempt ({attempt_id}) for exam_id {exam_id} for ' 'Created exam attempt ({attempt_id}) for exam_id {exam_id} for '
'user_id {user_id} with taking as proctored = {taking_as_proctored} ' 'user_id {user_id} with taking as proctored = {taking_as_proctored} '
...@@ -1471,7 +1501,8 @@ def get_student_view(user_id, course_id, content_id, ...@@ -1471,7 +1501,8 @@ def get_student_view(user_id, course_id, content_id,
exam_name=context['display_name'], exam_name=context['display_name'],
time_limit_mins=context['default_time_limit_mins'], time_limit_mins=context['default_time_limit_mins'],
is_proctored=context.get('is_proctored', False), is_proctored=context.get('is_proctored', False),
is_practice_exam=context.get('is_practice_exam', False) is_practice_exam=context.get('is_practice_exam', False),
due_date=context.get('due_date', None)
) )
exam = get_exam_by_content_id(course_id, content_id) exam = get_exam_by_content_id(course_id, content_id)
......
...@@ -3,6 +3,7 @@ Lists of constants that can be used in the edX proctoring ...@@ -3,6 +3,7 @@ Lists of constants that can be used in the edX proctoring
""" """
from django.conf import settings from django.conf import settings
import datetime
SITE_NAME = ( SITE_NAME = (
settings.PROCTORING_SETTINGS['SITE_NAME'] if settings.PROCTORING_SETTINGS['SITE_NAME'] if
...@@ -53,3 +54,5 @@ SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD = ( ...@@ -53,3 +54,5 @@ SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD = (
'SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD' in settings.PROCTORING_SETTINGS 'SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD' in settings.PROCTORING_SETTINGS
else getattr(settings, 'SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD', 10) else getattr(settings, 'SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD', 10)
) )
MINIMUM_TIME = datetime.datetime.fromtimestamp(0)
# -*- 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 field 'ProctoredExam.due_date'
db.add_column('proctoring_proctoredexam', 'due_date',
self.gf('django.db.models.fields.DateTimeField')(null=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'ProctoredExam.due_date'
db.delete_column('proctoring_proctoredexam', 'due_date')
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'})
},
'edx_proctoring.proctoredexam': {
'Meta': {'unique_together': "(('course_id', 'content_id'),)", 'object_name': 'ProctoredExam', 'db_table': "'proctoring_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'}),
'due_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'exam_name': ('django.db.models.fields.TextField', [], {}),
'external_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_practice_exam': ('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.proctoredexamreviewpolicy': {
'Meta': {'object_name': 'ProctoredExamReviewPolicy', 'db_table': "'proctoring_proctoredexamreviewpolicy'"},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'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']"}),
'review_policy': ('django.db.models.fields.TextField', [], {}),
'set_by_user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'edx_proctoring.proctoredexamreviewpolicyhistory': {
'Meta': {'object_name': 'ProctoredExamReviewPolicyHistory', 'db_table': "'proctoring_proctoredexamreviewpolicyhistory'"},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'original_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
'proctored_exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']"}),
'review_policy': ('django.db.models.fields.TextField', [], {}),
'set_by_user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'edx_proctoring.proctoredexamsoftwaresecurecomment': {
'Meta': {'object_name': 'ProctoredExamSoftwareSecureComment', 'db_table': "'proctoring_proctoredexamstudentattemptcomment'"},
'comment': ('django.db.models.fields.TextField', [], {}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'duration': ('django.db.models.fields.IntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'review': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExamSoftwareSecureReview']"}),
'start_time': ('django.db.models.fields.IntegerField', [], {}),
'status': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'stop_time': ('django.db.models.fields.IntegerField', [], {})
},
'edx_proctoring.proctoredexamsoftwaresecurereview': {
'Meta': {'object_name': 'ProctoredExamSoftwareSecureReview', 'db_table': "'proctoring_proctoredexamsoftwaresecurereview'"},
'attempt_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']", 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'raw_data': ('django.db.models.fields.TextField', [], {}),
'review_status': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'reviewed_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}),
'student': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}),
'video_url': ('django.db.models.fields.TextField', [], {})
},
'edx_proctoring.proctoredexamsoftwaresecurereviewhistory': {
'Meta': {'object_name': 'ProctoredExamSoftwareSecureReviewHistory', 'db_table': "'proctoring_proctoredexamsoftwaresecurereviewhistory'"},
'attempt_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']", 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'raw_data': ('django.db.models.fields.TextField', [], {}),
'review_status': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'reviewed_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}),
'student': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"}),
'video_url': ('django.db.models.fields.TextField', [], {})
},
'edx_proctoring.proctoredexamstudentallowance': {
'Meta': {'unique_together': "(('user', 'proctored_exam', 'key'),)", 'object_name': 'ProctoredExamStudentAllowance', 'db_table': "'proctoring_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': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'edx_proctoring.proctoredexamstudentallowancehistory': {
'Meta': {'object_name': 'ProctoredExamStudentAllowanceHistory', 'db_table': "'proctoring_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'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proctored_exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'edx_proctoring.proctoredexamstudentattempt': {
'Meta': {'unique_together': "(('user', 'proctored_exam'),)", 'object_name': 'ProctoredExamStudentAttempt', 'db_table': "'proctoring_proctoredexamstudentattempt'"},
'allowed_time_limit_mins': ('django.db.models.fields.IntegerField', [], {}),
'attempt_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': '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.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_sample_attempt': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_poll_ipaddr': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'last_poll_timestamp': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proctored_exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']"}),
'review_policy_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
'started_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'student_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'taking_as_proctored': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'edx_proctoring.proctoredexamstudentattempthistory': {
'Meta': {'object_name': 'ProctoredExamStudentAttemptHistory', 'db_table': "'proctoring_proctoredexamstudentattempthistory'"},
'allowed_time_limit_mins': ('django.db.models.fields.IntegerField', [], {}),
'attempt_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}),
'attempt_id': ('django.db.models.fields.IntegerField', [], {'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.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_sample_attempt': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_poll_ipaddr': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'last_poll_timestamp': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proctored_exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']"}),
'review_policy_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
'started_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'student_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'taking_as_proctored': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
}
}
complete_apps = ['edx_proctoring']
\ No newline at end of file
...@@ -36,6 +36,9 @@ class ProctoredExam(TimeStampedModel): ...@@ -36,6 +36,9 @@ class ProctoredExam(TimeStampedModel):
# 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()
# Due date is a deadline to finish the exam
due_date = models.DateTimeField(null=True)
# Whether this exam actually is proctored or not. # Whether this exam actually is proctored or not.
is_proctored = models.BooleanField() is_proctored = models.BooleanField()
......
...@@ -19,6 +19,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer): ...@@ -19,6 +19,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
is_active = serializers.BooleanField(required=True) is_active = serializers.BooleanField(required=True)
is_practice_exam = serializers.BooleanField(required=True) is_practice_exam = serializers.BooleanField(required=True)
is_proctored = serializers.BooleanField(required=True) is_proctored = serializers.BooleanField(required=True)
due_date = serializers.DateTimeField(required=False, format=None)
class Meta: class Meta:
""" """
...@@ -28,7 +29,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer): ...@@ -28,7 +29,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
fields = ( fields = (
"id", "course_id", "content_id", "external_id", "exam_name", "id", "course_id", "content_id", "external_id", "exam_name",
"time_limit_mins", "is_proctored", "is_practice_exam", "is_active" "time_limit_mins", "is_proctored", "is_practice_exam", "is_active", "due_date"
) )
......
...@@ -83,6 +83,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -83,6 +83,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
super(ProctoredExamApiTests, self).setUp() super(ProctoredExamApiTests, self).setUp()
self.default_time_limit = 21 self.default_time_limit = 21
self.course_id = 'test_course' self.course_id = 'test_course'
self.content_id_for_exam_with_due_date = 'test_content_due_date_id'
self.content_id = 'test_content_id' self.content_id = 'test_content_id'
self.content_id_timed = 'test_content_id_timed' self.content_id_timed = 'test_content_id_timed'
self.content_id_practice = 'test_content_id_practice' self.content_id_practice = 'test_content_id_practice'
...@@ -166,6 +167,18 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -166,6 +167,18 @@ class ProctoredExamApiTests(LoggedInTestCase):
time_limit_mins=self.default_time_limit time_limit_mins=self.default_time_limit
) )
def _create_proctored_exam_with_due_time(self, due_date=None):
"""
Calls the api's create_exam to create an exam object.
"""
return create_exam(
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit,
due_date=due_date
)
def _create_timed_exam(self): def _create_timed_exam(self):
""" """
Calls the api's create_exam to create an exam object. Calls the api's create_exam to create an exam object.
...@@ -305,7 +318,8 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -305,7 +318,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
updated_proctored_exam_id = update_exam( updated_proctored_exam_id = update_exam(
self.proctored_exam_id, exam_name='Updated Exam Name', time_limit_mins=30, self.proctored_exam_id, exam_name='Updated Exam Name', time_limit_mins=30,
is_proctored=True, external_id='external_id', is_active=True is_proctored=True, external_id='external_id', is_active=True,
due_date=datetime.now(pytz.UTC)
) )
# only those fields were updated, whose # only those fields were updated, whose
...@@ -433,6 +447,49 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -433,6 +447,49 @@ class ProctoredExamApiTests(LoggedInTestCase):
remove_allowance_for_user(student_allowance.proctored_exam.id, self.user_id, self.key) remove_allowance_for_user(student_allowance.proctored_exam.id, self.user_id, self.key)
self.assertEqual(len(ProctoredExamStudentAllowance.objects.filter()), 0) self.assertEqual(len(ProctoredExamStudentAllowance.objects.filter()), 0)
def test_create_an_exam_attempt_with_due_datetime(self):
"""
Create the exam attempt with due date
"""
due_date = datetime.now(pytz.UTC) + timedelta(days=1)
# exam is created with due datetime > current_datetime and due_datetime < current_datetime + allowed_mins
exam_id = self._create_proctored_exam_with_due_time(due_date=due_date)
# due_date is exactly after 24 hours, our exam's allowed minutes are 21
# student will get full allowed minutes if student will start exam within next 23 hours and 39 minutes
# otherwise allowed minutes = due_datetime - exam_attempt_datetime
# so if students arrives after 23 hours and 45 minutes later then he will get only 15 minutes
minutes_before_past_due_date = 15
reset_time = due_date - timedelta(minutes=minutes_before_past_due_date)
with freeze_time(reset_time):
attempt_id = create_exam_attempt(exam_id, self.user_id)
attempt = get_exam_attempt_by_id(attempt_id)
self.assertTrue(
minutes_before_past_due_date - 1 <= attempt['allowed_time_limit_mins'] <= minutes_before_past_due_date
)
def test_create_an_exam_attempt_with_past_due_datetime(self):
"""
Create the exam attempt with past due date
"""
due_date = datetime.now(pytz.UTC) + timedelta(days=1)
# exam is created with due datetime which has already passed
exam_id = self._create_proctored_exam_with_due_time(due_date=due_date)
# due_date is exactly after 24 hours, if student arrives after 2 days
# then he can not attempt the proctored exam
reset_time = due_date + timedelta(days=2)
with freeze_time(reset_time):
attempt_id = create_exam_attempt(exam_id, self.user_id)
attempt = get_exam_attempt_by_id(attempt_id)
self.assertEqual(attempt['status'], ProctoredExamStudentAttemptStatus.declined)
def test_create_an_exam_attempt(self): def test_create_an_exam_attempt(self):
""" """
Create an unstarted exam attempt. Create an unstarted exam attempt.
......
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