Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-proctoring
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
edx-proctoring
Commits
bd382902
Commit
bd382902
authored
Oct 14, 2015
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #198 from edx/hasnain-naveed/PHX-171
PHX-171/Added due date
parents
f3e1da2d
34e11630
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
103 additions
and
8 deletions
+103
-8
edx_proctoring/api.py
+37
-6
edx_proctoring/constants.py
+3
-0
edx_proctoring/migrations/0014_auto__add_field_proctoredexam_due_date.py
+0
-0
edx_proctoring/models.py
+3
-0
edx_proctoring/serializers.py
+2
-1
edx_proctoring/tests/test_api.py
+58
-1
No files found.
edx_proctoring/api.py
View file @
bd382902
...
@@ -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} '
...
@@ -1495,7 +1525,8 @@ def get_student_view(user_id, course_id, content_id,
...
@@ -1495,7 +1525,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
)
...
...
edx_proctoring/constants.py
View file @
bd382902
...
@@ -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
)
edx_proctoring/migrations/0014_auto__add_field_proctoredexam_due_date.py
0 → 100644
View file @
bd382902
This diff is collapsed.
Click to expand it.
edx_proctoring/models.py
View file @
bd382902
...
@@ -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
()
...
...
edx_proctoring/serializers.py
View file @
bd382902
...
@@ -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"
)
)
...
...
edx_proctoring/tests/test_api.py
View file @
bd382902
...
@@ -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'
...
@@ -199,6 +200,18 @@ class ProctoredExamApiTests(LoggedInTestCase):
...
@@ -199,6 +200,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.
...
@@ -338,7 +351,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
...
@@ -338,7 +351,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
...
@@ -466,6 +480,49 @@ class ProctoredExamApiTests(LoggedInTestCase):
...
@@ -466,6 +480,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.
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment