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
edx
edx-proctoring
Commits
34e11630
Commit
34e11630
authored
Oct 12, 2015
by
Hasnain
Committed by
Chris Dodge
Oct 14, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added new field "Due date"
parent
0ab7dfc5
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 @
34e11630
...
@@ -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
)
...
...
edx_proctoring/constants.py
View file @
34e11630
...
@@ -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 @
34e11630
This diff is collapsed.
Click to expand it.
edx_proctoring/models.py
View file @
34e11630
...
@@ -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 @
34e11630
...
@@ -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 @
34e11630
...
@@ -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.
...
...
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