Commit 34e11630 by Hasnain Committed by Chris Dodge

Added new field "Due date"

parent 0ab7dfc5
......@@ -57,7 +57,7 @@ def is_feature_enabled():
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):
"""
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,
external_id=external_id,
exam_name=exam_name,
time_limit_mins=time_limit_mins,
due_date=due_date,
is_proctored=is_proctored,
is_practice_exam=is_practice_exam,
is_active=is_active
......@@ -97,7 +98,7 @@ def create_exam(course_id, content_id, exam_name, time_limit_mins,
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):
"""
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,
log_msg = (
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'external_id={external_id}, is_active={is_active}'.format(
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
)
)
......@@ -126,6 +127,8 @@ def update_exam(exam_id, exam_name=None, time_limit_mins=None,
proctored_exam.exam_name = exam_name
if time_limit_mins is not None:
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:
proctored_exam.is_proctored = is_proctored
if is_practice_exam is not None:
......@@ -319,6 +322,13 @@ def update_exam_attempt(attempt_id, **kwargs):
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):
"""
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):
raise StudentExamAttemptAlreadyExistsException(err_msg)
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
allowance_extra_mins = ProctoredExamStudentAllowance.get_additional_time_granted(exam_id, user_id)
if 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()
external_id = None
review_policy = ProctoredExamReviewPolicy.get_review_policy_for_exam(exam_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'
callback_url = '{scheme}://{hostname}{path}'.format(
scheme=scheme,
......@@ -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,
)
if is_exam_past_due_date:
update_attempt_status(
exam_id,
user_id,
ProctoredExamStudentAttemptStatus.declined
)
log_msg = (
'Created exam attempt ({attempt_id}) for exam_id {exam_id} for '
'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,
exam_name=context['display_name'],
time_limit_mins=context['default_time_limit_mins'],
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)
......
......@@ -3,6 +3,7 @@ Lists of constants that can be used in the edX proctoring
"""
from django.conf import settings
import datetime
SITE_NAME = (
settings.PROCTORING_SETTINGS['SITE_NAME'] if
......@@ -53,3 +54,5 @@ SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD = (
'SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD' in settings.PROCTORING_SETTINGS
else getattr(settings, 'SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD', 10)
)
MINIMUM_TIME = datetime.datetime.fromtimestamp(0)
......@@ -36,6 +36,9 @@ class ProctoredExam(TimeStampedModel):
# Time limit (in minutes) that a student can finish this exam.
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.
is_proctored = models.BooleanField()
......
......@@ -19,6 +19,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
is_active = serializers.BooleanField(required=True)
is_practice_exam = serializers.BooleanField(required=True)
is_proctored = serializers.BooleanField(required=True)
due_date = serializers.DateTimeField(required=False, format=None)
class Meta:
"""
......@@ -28,7 +29,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
fields = (
"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):
super(ProctoredExamApiTests, self).setUp()
self.default_time_limit = 21
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_timed = 'test_content_id_timed'
self.content_id_practice = 'test_content_id_practice'
......@@ -166,6 +167,18 @@ class ProctoredExamApiTests(LoggedInTestCase):
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):
"""
Calls the api's create_exam to create an exam object.
......@@ -305,7 +318,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
updated_proctored_exam_id = update_exam(
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
......@@ -433,6 +447,49 @@ class ProctoredExamApiTests(LoggedInTestCase):
remove_allowance_for_user(student_allowance.proctored_exam.id, self.user_id, self.key)
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):
"""
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