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
5f19a077
Commit
5f19a077
authored
Sep 22, 2015
by
Chris Dodge
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
allow for optional (default True) ability to allow SoftwareSecure to retransmit proctored results
parent
19a9e1ce
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
170 additions
and
20 deletions
+170
-20
edx_proctoring/backends/software_secure.py
+28
-14
edx_proctoring/backends/tests/test_software_secure.py
+71
-1
edx_proctoring/constants.py
+5
-0
edx_proctoring/migrations/0009_auto__add_proctoredexamsoftwaresecurereviewhistory.py
+0
-0
edx_proctoring/models.py
+66
-5
No files found.
edx_proctoring/backends/software_secure.py
View file @
5f19a077
...
@@ -15,6 +15,7 @@ import logging
...
@@ -15,6 +15,7 @@ import logging
from
django.conf
import
settings
from
django.conf
import
settings
from
edx_proctoring.backends.backend
import
ProctoringBackendProvider
from
edx_proctoring.backends.backend
import
ProctoringBackendProvider
from
edx_proctoring
import
constants
from
edx_proctoring.exceptions
import
(
from
edx_proctoring.exceptions
import
(
BackendProvideCannotRegisterAttempt
,
BackendProvideCannotRegisterAttempt
,
StudentExamAttemptDoesNotExistsException
,
StudentExamAttemptDoesNotExistsException
,
...
@@ -188,29 +189,42 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider):
...
@@ -188,29 +189,42 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider):
)
)
raise
ProctoredExamSuspiciousLookup
(
err_msg
)
raise
ProctoredExamSuspiciousLookup
(
err_msg
)
# do we already have a review for this attempt?!? It should not be updated!
# do some limited parsing of the JSON payload
review_status
=
payload
[
'reviewStatus'
]
video_review_link
=
payload
[
'videoReviewLink'
]
# do we already have a review for this attempt?!? We may not allow updates
review
=
ProctoredExamSoftwareSecureReview
.
get_review_by_attempt_code
(
attempt_code
)
review
=
ProctoredExamSoftwareSecureReview
.
get_review_by_attempt_code
(
attempt_code
)
if
review
:
if
review
:
err_msg
=
(
if
not
constants
.
ALLOW_REVIEW_UPDATES
:
err_msg
=
(
'We already have a review submitted from SoftwareSecure regarding '
'attempt_code {attempt_code}. We do not allow for updates!'
.
format
(
attempt_code
=
attempt_code
)
)
raise
ProctoredExamReviewAlreadyExists
(
err_msg
)
# we allow updates
warn_msg
=
(
'We already have a review submitted from SoftwareSecure regarding '
'We already have a review submitted from SoftwareSecure regarding '
'attempt_code {attempt_code}. We do not allow for updates!'
.
format
(
'attempt_code {attempt_code}. We have been configured to allow for '
'updates and will continue...'
.
format
(
attempt_code
=
attempt_code
attempt_code
=
attempt_code
)
)
)
)
raise
ProctoredExamReviewAlreadyExists
(
err_msg
)
log
.
warn
(
warn_msg
)
else
:
# this is first time we've received this attempt_code, so
# make a new record in the review table
review
=
ProctoredExamSoftwareSecureReview
()
# do some limited parsing of the JSON payload
review
.
attempt_code
=
attempt_code
review_status
=
payload
[
'reviewStatus'
]
review
.
raw_data
=
json
.
dumps
(
payload
)
video_review_link
=
payload
[
'videoReviewLink'
]
review
.
review_status
=
review_status
review
.
video_url
=
video_review_link
# make a new record in the review table
review
=
ProctoredExamSoftwareSecureReview
(
attempt_code
=
attempt_code
,
raw_data
=
json
.
dumps
(
payload
),
review_status
=
review_status
,
video_url
=
video_review_link
,
)
review
.
save
()
review
.
save
()
# go through and populate all of the specific comments
# go through and populate all of the specific comments
...
...
edx_proctoring/backends/tests/test_software_secure.py
View file @
5f19a077
# coding=utf-8
# coding=utf-8
# pylint: disable=too-many-lines, invalid-name
"""
"""
Tests for the software_secure module
Tests for the software_secure module
"""
"""
...
@@ -32,6 +33,7 @@ from edx_proctoring. models import (
...
@@ -32,6 +33,7 @@ from edx_proctoring. models import (
ProctoredExamSoftwareSecureReview
,
ProctoredExamSoftwareSecureReview
,
ProctoredExamSoftwareSecureComment
,
ProctoredExamSoftwareSecureComment
,
ProctoredExamStudentAttemptStatus
,
ProctoredExamStudentAttemptStatus
,
ProctoredExamSoftwareSecureReviewHistory
,
)
)
from
edx_proctoring.backends.tests.test_review_payload
import
TEST_REVIEW_PAYLOAD
from
edx_proctoring.backends.tests.test_review_payload
import
TEST_REVIEW_PAYLOAD
...
@@ -471,7 +473,8 @@ class SoftwareSecureTests(TestCase):
...
@@ -471,7 +473,8 @@ class SoftwareSecureTests(TestCase):
self
.
assertEqual
(
len
(
comments
),
6
)
self
.
assertEqual
(
len
(
comments
),
6
)
def
test_review_resubmission
(
self
):
@patch
(
'edx_proctoring.constants.ALLOW_REVIEW_UPDATES'
,
False
)
def
test_disallow_review_resubmission
(
self
):
"""
"""
Tests that an exception is raised if a review report is resubmitted for the same
Tests that an exception is raised if a review report is resubmitted for the same
attempt
attempt
...
@@ -508,3 +511,70 @@ class SoftwareSecureTests(TestCase):
...
@@ -508,3 +511,70 @@ class SoftwareSecureTests(TestCase):
# now call again
# now call again
with
self
.
assertRaises
(
ProctoredExamReviewAlreadyExists
):
with
self
.
assertRaises
(
ProctoredExamReviewAlreadyExists
):
provider
.
on_review_callback
(
json
.
loads
(
test_payload
))
provider
.
on_review_callback
(
json
.
loads
(
test_payload
))
@patch
(
'edx_proctoring.constants.ALLOW_REVIEW_UPDATES'
,
True
)
def
test_allow_review_resubmission
(
self
):
"""
Tests that an resubmission is allowed
"""
provider
=
get_backend_provider
()
exam_id
=
create_exam
(
course_id
=
'foo/bar/baz'
,
content_id
=
'content'
,
exam_name
=
'Sample Exam'
,
time_limit_mins
=
10
,
is_proctored
=
True
)
# be sure to use the mocked out SoftwareSecure handlers
with
HTTMock
(
mock_response_content
):
attempt_id
=
create_exam_attempt
(
exam_id
,
self
.
user
.
id
,
taking_as_proctored
=
True
)
attempt
=
get_exam_attempt_by_id
(
attempt_id
)
self
.
assertIsNotNone
(
attempt
[
'external_id'
])
test_payload
=
Template
(
TEST_REVIEW_PAYLOAD
)
.
substitute
(
attempt_code
=
attempt
[
'attempt_code'
],
external_id
=
attempt
[
'external_id'
]
)
provider
.
on_review_callback
(
json
.
loads
(
test_payload
))
# make sure history table is empty
records
=
ProctoredExamSoftwareSecureReviewHistory
.
objects
.
filter
(
attempt_code
=
attempt
[
'attempt_code'
])
self
.
assertEqual
(
len
(
records
),
0
)
# now call again, this will not throw exception
test_payload
=
test_payload
.
replace
(
'Clean'
,
'Suspicious'
)
provider
.
on_review_callback
(
json
.
loads
(
test_payload
))
# make sure that what we have in the Database matches what we expect
review
=
ProctoredExamSoftwareSecureReview
.
get_review_by_attempt_code
(
attempt
[
'attempt_code'
])
self
.
assertIsNotNone
(
review
)
self
.
assertEqual
(
review
.
review_status
,
'Suspicious'
)
self
.
assertEqual
(
review
.
video_url
,
'http://www.remoteproctor.com/AdminSite/Account/Reviewer/DirectLink-Generic.aspx?ID=foo'
)
self
.
assertIsNotNone
(
review
.
raw_data
)
# make sure history table is no longer empty
records
=
ProctoredExamSoftwareSecureReviewHistory
.
objects
.
filter
(
attempt_code
=
attempt
[
'attempt_code'
])
self
.
assertEqual
(
len
(
records
),
1
)
self
.
assertEqual
(
records
[
0
]
.
review_status
,
'Clean'
)
# now try to delete the record and make sure it was archived
review
.
delete
()
records
=
ProctoredExamSoftwareSecureReviewHistory
.
objects
.
filter
(
attempt_code
=
attempt
[
'attempt_code'
])
self
.
assertEqual
(
len
(
records
),
2
)
self
.
assertEqual
(
records
[
0
]
.
review_status
,
'Clean'
)
self
.
assertEqual
(
records
[
1
]
.
review_status
,
'Suspicious'
)
edx_proctoring/constants.py
View file @
5f19a077
...
@@ -24,3 +24,8 @@ CONTACT_EMAIL = (
...
@@ -24,3 +24,8 @@ CONTACT_EMAIL = (
settings
.
PROCTORING_SETTINGS
[
'CONTACT_EMAIL'
]
if
settings
.
PROCTORING_SETTINGS
[
'CONTACT_EMAIL'
]
if
'CONTACT_EMAIL'
in
settings
.
PROCTORING_SETTINGS
else
getattr
(
settings
,
'CONTACT_EMAIL'
,
FROM_EMAIL
)
'CONTACT_EMAIL'
in
settings
.
PROCTORING_SETTINGS
else
getattr
(
settings
,
'CONTACT_EMAIL'
,
FROM_EMAIL
)
)
)
ALLOW_REVIEW_UPDATES
=
(
settings
.
PROCTORING_SETTINGS
[
'ALLOW_REVIEW_UPDATES'
]
if
'ALLOW_REVIEW_UPDATES'
in
settings
.
PROCTORING_SETTINGS
else
getattr
(
settings
,
'ALLOW_REVIEW_UPDATES'
,
True
)
)
edx_proctoring/migrations/0009_auto__add_proctoredexamsoftwaresecurereviewhistory.py
0 → 100644
View file @
5f19a077
This diff is collapsed.
Click to expand it.
edx_proctoring/models.py
View file @
5f19a077
...
@@ -3,7 +3,7 @@ Data models for the proctoring subsystem
...
@@ -3,7 +3,7 @@ Data models for the proctoring subsystem
"""
"""
from
django.db
import
models
from
django.db
import
models
from
django.db.models
import
Q
from
django.db.models
import
Q
from
django.db.models.signals
import
p
ost
_save
,
pre_delete
from
django.db.models.signals
import
p
re
_save
,
pre_delete
from
django.dispatch
import
receiver
from
django.dispatch
import
receiver
from
model_utils.models
import
TimeStampedModel
from
model_utils.models
import
TimeStampedModel
from
django.utils.translation
import
ugettext
as
_
from
django.utils.translation
import
ugettext
as
_
...
@@ -543,15 +543,16 @@ class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
...
@@ -543,15 +543,16 @@ class ProctoredExamStudentAllowanceHistory(TimeStampedModel):
# Hook up the post_save signal to record creations in the ProctoredExamStudentAllowanceHistory table.
# Hook up the post_save signal to record creations in the ProctoredExamStudentAllowanceHistory table.
@receiver
(
p
ost
_save
,
sender
=
ProctoredExamStudentAllowance
)
@receiver
(
p
re
_save
,
sender
=
ProctoredExamStudentAllowance
)
def
on_allowance_saved
(
sender
,
instance
,
created
,
**
kwargs
):
# pylint: disable=unused-argument
def
on_allowance_saved
(
sender
,
instance
,
**
kwargs
):
# pylint: disable=unused-argument
"""
"""
Archiving all changes made to the Student Allowance.
Archiving all changes made to the Student Allowance.
Will only archive on update, and not on new entries created.
Will only archive on update, and not on new entries created.
"""
"""
if
not
created
:
if
instance
.
id
:
_make_archive_copy
(
instance
)
original
=
ProctoredExamStudentAllowance
.
objects
.
get
(
id
=
instance
.
id
)
_make_archive_copy
(
original
)
@receiver
(
pre_delete
,
sender
=
ProctoredExamStudentAllowance
)
@receiver
(
pre_delete
,
sender
=
ProctoredExamStudentAllowance
)
...
@@ -614,6 +615,66 @@ class ProctoredExamSoftwareSecureReview(TimeStampedModel):
...
@@ -614,6 +615,66 @@ class ProctoredExamSoftwareSecureReview(TimeStampedModel):
return
None
return
None
class
ProctoredExamSoftwareSecureReviewHistory
(
TimeStampedModel
):
"""
When records get updated, we will archive them here
"""
# which student attempt is this feedback for?
attempt_code
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
)
# overall status of the review
review_status
=
models
.
CharField
(
max_length
=
255
)
# The raw payload that was received back from the
# reviewing service
raw_data
=
models
.
TextField
()
# URL for the exam video that had been reviewed
video_url
=
models
.
TextField
()
class
Meta
:
""" Meta class for this Django model """
db_table
=
'proctoring_proctoredexamsoftwaresecurereviewhistory'
verbose_name
=
'proctored exam review history'
# Hook up the post_save signal to record creations in the ProctoredExamStudentAllowanceHistory table.
@receiver
(
pre_save
,
sender
=
ProctoredExamSoftwareSecureReview
)
def
on_review_saved
(
sender
,
instance
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Archiving all changes made to the Student Allowance.
Will only archive on update, and not on new entries created.
"""
if
instance
.
id
:
original
=
ProctoredExamSoftwareSecureReview
.
objects
.
get
(
id
=
instance
.
id
)
_make_review_archive_copy
(
original
)
@receiver
(
pre_delete
,
sender
=
ProctoredExamSoftwareSecureReview
)
def
on_review_deleted
(
sender
,
instance
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Archive the allowance when the item is about to be deleted
"""
_make_review_archive_copy
(
instance
)
def
_make_review_archive_copy
(
instance
):
"""
Do the copying into the history table
"""
archive_object
=
ProctoredExamSoftwareSecureReviewHistory
(
attempt_code
=
instance
.
attempt_code
,
review_status
=
instance
.
review_status
,
raw_data
=
instance
.
raw_data
,
video_url
=
instance
.
video_url
,
)
archive_object
.
save
()
class
ProctoredExamSoftwareSecureComment
(
TimeStampedModel
):
class
ProctoredExamSoftwareSecureComment
(
TimeStampedModel
):
"""
"""
This is where we store the proctored exam review comments
This is where we store the proctored exam review comments
...
...
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