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
19a9e1ce
Commit
19a9e1ce
authored
Sep 22, 2015
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #167 from edx/hotfix/2015-09-21
Hotfix/2015 09 21
parents
da5746f5
f100c234
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
269 additions
and
10 deletions
+269
-10
edx_proctoring/api.py
+22
-0
edx_proctoring/backends/software_secure.py
+6
-1
edx_proctoring/management/__init__.py
+3
-0
edx_proctoring/management/commands/__init__.py
+3
-0
edx_proctoring/management/commands/set_attempt_status.py
+64
-0
edx_proctoring/management/commands/tests/__init__.py
+3
-0
edx_proctoring/management/commands/tests/test_set_attempt_status.py
+81
-0
edx_proctoring/models.py
+7
-0
edx_proctoring/tests/test_api.py
+1
-0
edx_proctoring/tests/test_views.py
+67
-2
edx_proctoring/views.py
+11
-6
setup.py
+1
-1
No files found.
edx_proctoring/api.py
View file @
19a9e1ce
...
@@ -552,6 +552,21 @@ def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True,
...
@@ -552,6 +552,21 @@ def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True,
)
)
raise
ProctoredExamIllegalStatusTransition
(
err_msg
)
raise
ProctoredExamIllegalStatusTransition
(
err_msg
)
# special case logic, if we are in a completed status we shouldn't allow
# for a transition to 'Error' state
if
in_completed_status
and
to_status
==
ProctoredExamStudentAttemptStatus
.
error
:
err_msg
=
(
'A status transition from {from_status} to {to_status} was attempted '
'on exam_id {exam_id} for user_id {user_id}. This is not '
'allowed!'
.
format
(
from_status
=
exam_attempt_obj
.
status
,
to_status
=
to_status
,
exam_id
=
exam_id
,
user_id
=
user_id
)
)
raise
ProctoredExamIllegalStatusTransition
(
err_msg
)
# OK, state transition is fine, we can proceed
# OK, state transition is fine, we can proceed
exam_attempt_obj
.
status
=
to_status
exam_attempt_obj
.
status
=
to_status
exam_attempt_obj
.
save
()
exam_attempt_obj
.
save
()
...
@@ -591,6 +606,13 @@ def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True,
...
@@ -591,6 +606,13 @@ def update_attempt_status(exam_id, user_id, to_status, raise_if_not_found=True,
)
)
if
cascade_effects
and
ProctoredExamStudentAttemptStatus
.
is_a_cascadable_failure
(
to_status
):
if
cascade_effects
and
ProctoredExamStudentAttemptStatus
.
is_a_cascadable_failure
(
to_status
):
if
to_status
==
ProctoredExamStudentAttemptStatus
.
declined
:
# if user declines attempt, make sure we clear out the external_id and
# taking_as_proctored fields
exam_attempt_obj
.
taking_as_proctored
=
False
exam_attempt_obj
.
external_id
=
None
exam_attempt_obj
.
save
()
# some state transitions (namely to a rejected or declined status)
# some state transitions (namely to a rejected or declined status)
# will mark other exams as declined because once we fail or decline
# will mark other exams as declined because once we fail or decline
# one exam all other (un-completed) proctored exams will be likewise
# one exam all other (un-completed) proctored exams will be likewise
...
...
edx_proctoring/backends/software_secure.py
View file @
19a9e1ce
...
@@ -298,7 +298,12 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider):
...
@@ -298,7 +298,12 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider):
"organization"
:
self
.
organization
,
"organization"
:
self
.
organization
,
"duration"
:
time_limit_mins
,
"duration"
:
time_limit_mins
,
"reviewedExam"
:
not
is_sample_attempt
,
"reviewedExam"
:
not
is_sample_attempt
,
"reviewerNotes"
:
'Closed Book'
,
# NOTE: we will have to allow these notes to be authorable in Studio
# and then we will pull this from the exam database model
"reviewerNotes"
:
(
'Closed Book; Allow users to take notes on paper during the exam; '
'Allow users to use a hand-held calculator during the exam'
),
"examPassword"
:
self
.
_encrypt_password
(
self
.
crypto_key
,
attempt_code
),
"examPassword"
:
self
.
_encrypt_password
(
self
.
crypto_key
,
attempt_code
),
"examSponsor"
:
self
.
exam_sponsor
,
"examSponsor"
:
self
.
exam_sponsor
,
"examName"
:
exam
[
'exam_name'
],
"examName"
:
exam
[
'exam_name'
],
...
...
edx_proctoring/management/__init__.py
0 → 100644
View file @
19a9e1ce
"""
This is a python module
"""
edx_proctoring/management/commands/__init__.py
0 → 100644
View file @
19a9e1ce
"""
This is a python module
"""
edx_proctoring/management/commands/set_attempt_status.py
0 → 100644
View file @
19a9e1ce
"""
Django management command to manually set the attempt status for a user in a proctored exam
"""
from
optparse
import
make_option
from
django.core.management.base
import
BaseCommand
from
edx_proctoring.models
import
ProctoredExamStudentAttemptStatus
class
Command
(
BaseCommand
):
"""
Django Management command to force a background check of all possible notifications
"""
option_list
=
BaseCommand
.
option_list
+
(
make_option
(
'-e'
,
'--exam'
,
metavar
=
'EXAM_ID'
,
dest
=
'exam_id'
,
help
=
'exam_id to change'
),
make_option
(
'-u'
,
'--user'
,
metavar
=
'USER'
,
dest
=
'user_id'
,
help
=
"user_id of user to affect"
),
make_option
(
'-t'
,
'--to'
,
metavar
=
'TO_STATUS'
,
dest
=
'to_status'
,
help
=
'the status to set'
),
)
def
handle
(
self
,
*
args
,
**
options
):
"""
Management command entry point, simply call into the signal firiing
"""
from
edx_proctoring.api
import
(
update_attempt_status
,
get_exam_by_id
)
exam_id
=
options
[
'exam_id'
]
user_id
=
options
[
'user_id'
]
to_status
=
options
[
'to_status'
]
msg
=
(
'Running management command to update user {user_id} '
'attempt status on exam_id {exam_id} to {to_status}'
.
format
(
user_id
=
user_id
,
exam_id
=
exam_id
,
to_status
=
to_status
)
)
print
msg
if
not
ProctoredExamStudentAttemptStatus
.
is_valid_status
(
to_status
):
raise
Exception
(
'{to_status} is not a valid attempt status!'
.
format
(
to_status
=
to_status
))
# get exam, this will throw exception if does not exist, so let it bomb out
get_exam_by_id
(
exam_id
)
update_attempt_status
(
exam_id
,
user_id
,
to_status
)
print
'Completed!'
edx_proctoring/management/commands/tests/__init__.py
0 → 100644
View file @
19a9e1ce
"""
This is a python module
"""
edx_proctoring/management/commands/tests/test_set_attempt_status.py
0 → 100644
View file @
19a9e1ce
"""
Tests for the set_attempt_status management command
"""
from
datetime
import
datetime
import
pytz
from
edx_proctoring.tests.utils
import
LoggedInTestCase
from
edx_proctoring.api
import
create_exam
,
get_exam_attempt
from
edx_proctoring.management.commands
import
set_attempt_status
from
edx_proctoring.models
import
ProctoredExamStudentAttemptStatus
,
ProctoredExamStudentAttempt
from
edx_proctoring.tests.test_services
import
(
MockCreditService
,
)
from
edx_proctoring.runtime
import
set_runtime_service
class
SetAttemptStatusTests
(
LoggedInTestCase
):
"""
Coverage of the set_attempt_status.py file
"""
def
setUp
(
self
):
"""
Build up test data
"""
super
(
SetAttemptStatusTests
,
self
)
.
setUp
()
set_runtime_service
(
'credit'
,
MockCreditService
())
self
.
exam_id
=
create_exam
(
course_id
=
'foo'
,
content_id
=
'bar'
,
exam_name
=
'Test Exam'
,
time_limit_mins
=
90
)
ProctoredExamStudentAttempt
.
objects
.
create
(
proctored_exam_id
=
self
.
exam_id
,
user_id
=
self
.
user
.
id
,
external_id
=
'foo'
,
started_at
=
datetime
.
now
(
pytz
.
UTC
),
status
=
ProctoredExamStudentAttemptStatus
.
started
,
allowed_time_limit_mins
=
10
,
taking_as_proctored
=
True
,
is_sample_attempt
=
False
)
def
test_run_comand
(
self
):
"""
Run the management command
"""
set_attempt_status
.
Command
()
.
handle
(
exam_id
=
self
.
exam_id
,
user_id
=
self
.
user
.
id
,
to_status
=
ProctoredExamStudentAttemptStatus
.
rejected
)
attempt
=
get_exam_attempt
(
self
.
exam_id
,
self
.
user
.
id
)
self
.
assertEqual
(
attempt
[
'status'
],
ProctoredExamStudentAttemptStatus
.
rejected
)
set_attempt_status
.
Command
()
.
handle
(
exam_id
=
self
.
exam_id
,
user_id
=
self
.
user
.
id
,
to_status
=
ProctoredExamStudentAttemptStatus
.
verified
)
attempt
=
get_exam_attempt
(
self
.
exam_id
,
self
.
user
.
id
)
self
.
assertEqual
(
attempt
[
'status'
],
ProctoredExamStudentAttemptStatus
.
verified
)
def
test_bad_status
(
self
):
"""
Try passing a bad status
"""
with
self
.
assertRaises
(
Exception
):
set_attempt_status
.
Command
()
.
handle
(
exam_id
=
self
.
exam_id
,
user_id
=
self
.
user
.
id
,
to_status
=
'bad'
)
edx_proctoring/models.py
View file @
19a9e1ce
...
@@ -200,6 +200,13 @@ class ProctoredExamStudentAttemptStatus(object):
...
@@ -200,6 +200,13 @@ class ProctoredExamStudentAttemptStatus(object):
return
cls
.
status_alias_mapping
.
get
(
status
,
''
)
return
cls
.
status_alias_mapping
.
get
(
status
,
''
)
@classmethod
def
is_valid_status
(
cls
,
status
):
"""
Makes sure that passed in status string is valid
"""
return
cls
.
is_completed_status
(
status
)
or
cls
.
is_incomplete_status
(
status
)
class
ProctoredExamStudentAttemptManager
(
models
.
Manager
):
class
ProctoredExamStudentAttemptManager
(
models
.
Manager
):
"""
"""
...
...
edx_proctoring/tests/test_api.py
View file @
19a9e1ce
...
@@ -1230,6 +1230,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
...
@@ -1230,6 +1230,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
(
ProctoredExamStudentAttemptStatus
.
rejected
,
ProctoredExamStudentAttemptStatus
.
started
),
(
ProctoredExamStudentAttemptStatus
.
rejected
,
ProctoredExamStudentAttemptStatus
.
started
),
(
ProctoredExamStudentAttemptStatus
.
not_reviewed
,
ProctoredExamStudentAttemptStatus
.
started
),
(
ProctoredExamStudentAttemptStatus
.
not_reviewed
,
ProctoredExamStudentAttemptStatus
.
started
),
(
ProctoredExamStudentAttemptStatus
.
error
,
ProctoredExamStudentAttemptStatus
.
started
),
(
ProctoredExamStudentAttemptStatus
.
error
,
ProctoredExamStudentAttemptStatus
.
started
),
(
ProctoredExamStudentAttemptStatus
.
submitted
,
ProctoredExamStudentAttemptStatus
.
error
),
)
)
@ddt.unpack
@ddt.unpack
@patch.dict
(
'django.conf.settings.PROCTORING_SETTINGS'
,
{
'ALLOW_TIMED_OUT_STATE'
:
True
})
@patch.dict
(
'django.conf.settings.PROCTORING_SETTINGS'
,
{
'ALLOW_TIMED_OUT_STATE'
:
True
})
...
...
edx_proctoring/tests/test_views.py
View file @
19a9e1ce
...
@@ -580,7 +580,61 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -580,7 +580,61 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'status'
],
'started'
)
self
.
assertEqual
(
response_data
[
'status'
],
ProctoredExamStudentAttemptStatus
.
started
)
attempt_code
=
response_data
[
'attempt_code'
]
# test the polling callback point
response
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.anonymous.proctoring_poll_status'
,
args
=
[
attempt_code
]
)
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# now reset the time to 2 minutes in the future.
reset_time
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
2
)
with
freeze_time
(
reset_time
):
response
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
attempt_id
])
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'status'
],
ProctoredExamStudentAttemptStatus
.
error
)
def
test_attempt_status_stickiness
(
self
):
"""
Test to confirm that a status timeout error will not alter a completed state
"""
# Create an exam.
proctored_exam
=
ProctoredExam
.
objects
.
create
(
course_id
=
'a/b/c'
,
content_id
=
'test_content'
,
exam_name
=
'Test Exam'
,
external_id
=
'123aXqe3'
,
time_limit_mins
=
90
)
attempt_data
=
{
'exam_id'
:
proctored_exam
.
id
,
'external_id'
:
proctored_exam
.
external_id
,
'start_clock'
:
True
,
}
response
=
self
.
client
.
post
(
reverse
(
'edx_proctoring.proctored_exam.attempt.collection'
),
attempt_data
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
attempt_id
=
response_data
[
'exam_attempt_id'
]
self
.
assertEqual
(
attempt_id
,
1
)
response
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
attempt_id
])
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'status'
],
ProctoredExamStudentAttemptStatus
.
started
)
attempt_code
=
response_data
[
'attempt_code'
]
attempt_code
=
response_data
[
'attempt_code'
]
# test the polling callback point
# test the polling callback point
...
@@ -592,6 +646,13 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -592,6 +646,13 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# now switched to a submitted state
update_attempt_status
(
proctored_exam
.
id
,
self
.
user
.
id
,
ProctoredExamStudentAttemptStatus
.
submitted
)
# now reset the time to 2 minutes in the future.
# now reset the time to 2 minutes in the future.
reset_time
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
2
)
reset_time
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
2
)
with
freeze_time
(
reset_time
):
with
freeze_time
(
reset_time
):
...
@@ -600,7 +661,11 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -600,7 +661,11 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'status'
],
'error'
)
# make sure the submitted status is sticky
self
.
assertEqual
(
response_data
[
'status'
],
ProctoredExamStudentAttemptStatus
.
submitted
)
@ddt.data
(
@ddt.data
(
ProctoredExamStudentAttemptStatus
.
created
,
ProctoredExamStudentAttemptStatus
.
created
,
...
...
edx_proctoring/views.py
View file @
19a9e1ce
...
@@ -37,6 +37,7 @@ from edx_proctoring.exceptions import (
...
@@ -37,6 +37,7 @@ from edx_proctoring.exceptions import (
UserNotFoundException
,
UserNotFoundException
,
ProctoredExamPermissionDenied
,
ProctoredExamPermissionDenied
,
StudentExamAttemptDoesNotExistsException
,
StudentExamAttemptDoesNotExistsException
,
ProctoredExamIllegalStatusTransition
,
)
)
from
edx_proctoring.serializers
import
ProctoredExamSerializer
,
ProctoredExamStudentAttemptSerializer
from
edx_proctoring.serializers
import
ProctoredExamSerializer
,
ProctoredExamStudentAttemptSerializer
from
edx_proctoring.models
import
ProctoredExamStudentAttemptStatus
,
ProctoredExamStudentAttempt
from
edx_proctoring.models
import
ProctoredExamStudentAttemptStatus
,
ProctoredExamStudentAttempt
...
@@ -285,12 +286,16 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
...
@@ -285,12 +286,16 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
last_poll_timestamp
=
attempt
[
'last_poll_timestamp'
]
last_poll_timestamp
=
attempt
[
'last_poll_timestamp'
]
if
last_poll_timestamp
is
not
None
\
if
last_poll_timestamp
is
not
None
\
and
(
datetime
.
now
(
pytz
.
UTC
)
-
last_poll_timestamp
)
.
total_seconds
()
>
SOFTWARE_SECURE_CLIENT_TIMEOUT
:
and
(
datetime
.
now
(
pytz
.
UTC
)
-
last_poll_timestamp
)
.
total_seconds
()
>
SOFTWARE_SECURE_CLIENT_TIMEOUT
:
attempt
[
'status'
]
=
'error'
try
:
update_attempt_status
(
update_attempt_status
(
attempt
[
'proctored_exam'
][
'id'
],
attempt
[
'proctored_exam'
][
'id'
],
attempt
[
'user'
][
'id'
],
attempt
[
'user'
][
'id'
],
ProctoredExamStudentAttemptStatus
.
error
ProctoredExamStudentAttemptStatus
.
error
)
)
attempt
[
'status'
]
=
ProctoredExamStudentAttemptStatus
.
error
except
ProctoredExamIllegalStatusTransition
:
# don't transition a completed state to an error state
pass
# add in the computed time remaining as a helper to a client app
# add in the computed time remaining as a helper to a client app
time_remaining_seconds
=
get_time_remaining_for_attempt
(
attempt
)
time_remaining_seconds
=
get_time_remaining_for_attempt
(
attempt
)
...
...
setup.py
View file @
19a9e1ce
...
@@ -34,7 +34,7 @@ def load_requirements(*requirements_paths):
...
@@ -34,7 +34,7 @@ def load_requirements(*requirements_paths):
setup
(
setup
(
name
=
'edx-proctoring'
,
name
=
'edx-proctoring'
,
version
=
'0.9.6
b
'
,
version
=
'0.9.6
e
'
,
description
=
'Proctoring subsystem for Open edX'
,
description
=
'Proctoring subsystem for Open edX'
,
long_description
=
open
(
'README.md'
)
.
read
(),
long_description
=
open
(
'README.md'
)
.
read
(),
author
=
'edX'
,
author
=
'edX'
,
...
...
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