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
7acd9b56
Commit
7acd9b56
authored
Oct 26, 2016
by
Diana Huang
Committed by
GitHub
Oct 26, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #314 from edx/diana/remove-client-polling
Delete in-client polling.
parents
14d6164c
38a129bf
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
35 additions
and
433 deletions
+35
-433
edx_proctoring/api.py
+5
-12
edx_proctoring/callbacks.py
+0
-48
edx_proctoring/models.py
+4
-0
edx_proctoring/templates/proctored_exam/proctoring_launch_callback.html
+0
-31
edx_proctoring/templates/proctored_exam/submitted.html
+5
-0
edx_proctoring/templates/proctored_exam/waiting_for_app_shutdown.html
+0
-51
edx_proctoring/tests/test_student_view.py
+20
-53
edx_proctoring/tests/test_views.py
+0
-184
edx_proctoring/urls.py
+0
-5
edx_proctoring/utils.py
+0
-14
edx_proctoring/views.py
+1
-35
No files found.
edx_proctoring/api.py
View file @
7acd9b56
...
@@ -44,7 +44,6 @@ from edx_proctoring.serializers import (
...
@@ -44,7 +44,6 @@ from edx_proctoring.serializers import (
)
)
from
edx_proctoring.utils
import
(
from
edx_proctoring.utils
import
(
humanized_time
,
humanized_time
,
has_client_app_shutdown
,
emit_event
emit_event
)
)
...
@@ -1620,10 +1619,7 @@ def _get_practice_exam_view(exam, context, exam_id, user_id, course_id):
...
@@ -1620,10 +1619,7 @@ def _get_practice_exam_view(exam, context, exam_id, user_id, course_id):
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
error
:
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
error
:
student_view_template
=
'practice_exam/error.html'
student_view_template
=
'practice_exam/error.html'
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
submitted
:
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
submitted
:
if
has_client_app_shutdown
(
attempt
):
student_view_template
=
'practice_exam/submitted.html'
student_view_template
=
'practice_exam/submitted.html'
else
:
student_view_template
=
'proctored_exam/waiting_for_app_shutdown.html'
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
ready_to_submit
:
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
ready_to_submit
:
student_view_template
=
'proctored_exam/ready_to_submit.html'
student_view_template
=
'proctored_exam/ready_to_submit.html'
...
@@ -1748,13 +1744,10 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id):
...
@@ -1748,13 +1744,10 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id):
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
timed_out
:
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
timed_out
:
raise
NotImplementedError
(
'There is no defined rendering for ProctoredExamStudentAttemptStatus.timed_out!'
)
raise
NotImplementedError
(
'There is no defined rendering for ProctoredExamStudentAttemptStatus.timed_out!'
)
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
submitted
:
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
submitted
:
if
has_client_app_shutdown
(
attempt
):
student_view_template
=
None
if
_was_review_status_acknowledged
(
student_view_template
=
None
if
_was_review_status_acknowledged
(
attempt
[
'is_status_acknowledged'
],
attempt
[
'is_status_acknowledged'
],
exam
[
'due_date'
]
exam
[
'due_date'
]
)
else
'proctored_exam/submitted.html'
)
else
'proctored_exam/submitted.html'
else
:
student_view_template
=
'proctored_exam/waiting_for_app_shutdown.html'
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
second_review_required
:
elif
attempt_status
==
ProctoredExamStudentAttemptStatus
.
second_review_required
:
# the student should still see a 'submitted'
# the student should still see a 'submitted'
# rendering even if the review needs a 2nd review
# rendering even if the review needs a 2nd review
...
...
edx_proctoring/callbacks.py
View file @
7acd9b56
...
@@ -6,10 +6,6 @@ import logging
...
@@ -6,10 +6,6 @@ import logging
from
django.template
import
Context
,
loader
from
django.template
import
Context
,
loader
from
django.conf
import
settings
from
django.conf
import
settings
from
django.http
import
HttpResponse
from
django.http
import
HttpResponse
import
pytz
from
datetime
import
datetime
from
ipware.ip
import
get_ip
from
django.core.urlresolvers
import
reverse
from
rest_framework.views
import
APIView
from
rest_framework.views
import
APIView
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
...
@@ -18,7 +14,6 @@ from rest_framework.negotiation import BaseContentNegotiation
...
@@ -18,7 +14,6 @@ from rest_framework.negotiation import BaseContentNegotiation
from
edx_proctoring.api
import
(
from
edx_proctoring.api
import
(
get_exam_attempt_by_code
,
get_exam_attempt_by_code
,
mark_exam_attempt_as_ready
,
mark_exam_attempt_as_ready
,
update_exam_attempt
)
)
from
edx_proctoring.backends
import
get_backend_provider
from
edx_proctoring.backends
import
get_backend_provider
from
edx_proctoring.exceptions
import
ProctoredBaseException
from
edx_proctoring.exceptions
import
ProctoredBaseException
...
@@ -56,15 +51,9 @@ def start_exam_callback(request, attempt_code): # pylint: disable=unused-argume
...
@@ -56,15 +51,9 @@ def start_exam_callback(request, attempt_code): # pylint: disable=unused-argume
log
.
info
(
"Exam
%
r has been marked as ready"
,
attempt
[
'proctored_exam'
][
'id'
])
log
.
info
(
"Exam
%
r has been marked as ready"
,
attempt
[
'proctored_exam'
][
'id'
])
template
=
loader
.
get_template
(
'proctored_exam/proctoring_launch_callback.html'
)
template
=
loader
.
get_template
(
'proctored_exam/proctoring_launch_callback.html'
)
poll_url
=
reverse
(
'edx_proctoring.anonymous.proctoring_poll_status'
,
args
=
[
attempt_code
]
)
return
HttpResponse
(
return
HttpResponse
(
template
.
render
(
template
.
render
(
Context
({
Context
({
'exam_attempt_status_url'
:
poll_url
,
'platform_name'
:
settings
.
PLATFORM_NAME
,
'platform_name'
:
settings
.
PLATFORM_NAME
,
'link_urls'
:
settings
.
PROCTORING_SETTINGS
.
get
(
'LINK_URLS'
,
{})
'link_urls'
:
settings
.
PROCTORING_SETTINGS
.
get
(
'LINK_URLS'
,
{})
})
})
...
@@ -126,40 +115,3 @@ class ExamReviewCallback(APIView):
...
@@ -126,40 +115,3 @@ class ExamReviewCallback(APIView):
data
=
'OK'
,
data
=
'OK'
,
status
=
200
status
=
200
)
)
class
AttemptStatus
(
APIView
):
"""
This endpoint is called by a 3rd party proctoring review service to determine
status of an exam attempt.
IMPORTANT: This is an unauthenticated endpoint, so be VERY CAREFUL about extending
this endpoint
"""
def
get
(
self
,
request
,
attempt_code
):
# pylint: disable=unused-argument
"""
Returns the status of an exam attempt. Given that this is an unauthenticated
caller, we will only return the status string, no additional information
about the exam
"""
attempt
=
get_exam_attempt_by_code
(
attempt_code
)
ip_address
=
get_ip
(
request
)
timestamp
=
datetime
.
now
(
pytz
.
UTC
)
if
not
attempt
:
return
HttpResponse
(
content
=
'You have entered an exam code that is not valid.'
,
status
=
404
)
update_exam_attempt
(
attempt
[
'id'
],
last_poll_timestamp
=
timestamp
,
last_poll_ipaddr
=
ip_address
)
return
Response
(
data
=
{
# IMPORTANT: Don't add more information to this as it is an
# unauthenticated endpoint
'status'
:
attempt
[
'status'
],
},
status
=
200
)
edx_proctoring/models.py
View file @
7acd9b56
...
@@ -443,6 +443,8 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
...
@@ -443,6 +443,8 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
# completed_at means when the attempt was 'submitted'
# completed_at means when the attempt was 'submitted'
completed_at
=
models
.
DateTimeField
(
null
=
True
)
completed_at
=
models
.
DateTimeField
(
null
=
True
)
# These two fields have been deprecated.
# They were used in client polling that no longer exists.
last_poll_timestamp
=
models
.
DateTimeField
(
null
=
True
)
last_poll_timestamp
=
models
.
DateTimeField
(
null
=
True
)
last_poll_ipaddr
=
models
.
CharField
(
max_length
=
32
,
null
=
True
)
last_poll_ipaddr
=
models
.
CharField
(
max_length
=
32
,
null
=
True
)
...
@@ -558,6 +560,8 @@ class ProctoredExamStudentAttemptHistory(TimeStampedModel):
...
@@ -558,6 +560,8 @@ class ProctoredExamStudentAttemptHistory(TimeStampedModel):
# this ID might point to a record that is in the History table
# this ID might point to a record that is in the History table
review_policy_id
=
models
.
IntegerField
(
null
=
True
)
review_policy_id
=
models
.
IntegerField
(
null
=
True
)
# These two fields have been deprecated.
# They were used in client polling that no longer exists.
last_poll_timestamp
=
models
.
DateTimeField
(
null
=
True
)
last_poll_timestamp
=
models
.
DateTimeField
(
null
=
True
)
last_poll_ipaddr
=
models
.
CharField
(
max_length
=
32
,
null
=
True
)
last_poll_ipaddr
=
models
.
CharField
(
max_length
=
32
,
null
=
True
)
...
...
edx_proctoring/templates/proctored_exam/proctoring_launch_callback.html
View file @
7acd9b56
...
@@ -177,38 +177,7 @@
...
@@ -177,38 +177,7 @@
{% endblocktrans %}
{% endblocktrans %}
</h5>
</h5>
</div>
</div>
<!--
<div class="footer">
<button>
{% blocktrans %} Go to my exam {% endblocktrans %}
</button>
</div>
</div>
-->
</div>
<script
type=
"text/javascript"
>
var
_poll_interval
=
null
;
$
(
document
).
ready
(
function
()
{
_poll_interval
=
setInterval
(
poll_exam_status
,
5000
);
});
function
poll_exam_status
()
{
var
url
=
'{{exam_attempt_status_url}}'
;
$
.
ajax
(
url
).
success
(
function
(
data
){
// has the end user completed and submitted the exam in the LMS?!?
if
(
data
.
status
===
'submitted'
||
data
.
status
===
'error'
||
data
.
status
===
'verified'
||
data
.
status
===
'rejected'
)
{
// Signal that the desktop software should terminate
// NOTE: This is per the API documentation from SoftwareSecure
window
.
external
.
quitApplication
();
}
});
}
</script>
</body>
</body>
</html>
</html>
edx_proctoring/templates/proctored_exam/submitted.html
View file @
7acd9b56
...
@@ -7,6 +7,11 @@
...
@@ -7,6 +7,11 @@
</h3>
</h3>
<p>
<p>
{% blocktrans %}
{% blocktrans %}
If the proctoring software window is still open, you can close it now. Confirm that you want to quit the application when you are prompted.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
•
After you quit the proctoring session, the recorded data is uploaded for review.
</br>
•
After you quit the proctoring session, the recorded data is uploaded for review.
</br>
•
Proctoring results are usually available within 5 business days after you submit your exam.
•
Proctoring results are usually available within 5 business days after you submit your exam.
{% endblocktrans %}
{% endblocktrans %}
...
...
edx_proctoring/templates/proctored_exam/waiting_for_app_shutdown.html
deleted
100644 → 0
View file @
14d6164c
{% load i18n %}
<div
class=
"sequence proctored-exam completed"
data-exam-id=
"{{exam_id}}"
>
<h3>
{% blocktrans %}
You are about to complete your proctored exam
{% endblocktrans %}
</h3>
<p>
{% blocktrans %}
Make sure you return to the proctoring software and select
<strong>
Quit
</strong>
to end proctoring
and submit your exam.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
If you have questions about the status of your proctored exam results, contact {{ platform_name }} Support.
{% endblocktrans %}
</p>
</div>
<script
type=
"text/javascript"
>
var
_waiting_for_app_timeout
=
null
;
$
(
document
).
ready
(
function
(){
_waiting_for_app_timeout
=
setInterval
(
poll_exam_started
,
1000
);
});
function
poll_exam_started
()
{
var
url
=
'{{ exam_started_poll_url }}'
+
'?sourceid=app_shutdown'
;
$
.
ajax
(
url
).
success
(
function
(
data
){
if
(
data
.
status
===
'submitted'
)
{
// Do we believe the client proctoring app has shut down
// if so, then refresh the page which will expose
// other content
if
(
data
.
client_has_shutdown
!==
undefined
&&
data
.
client_has_shutdown
)
{
if
(
_waiting_for_app_timeout
!=
null
)
{
clearInterval
(
_waiting_for_app_timeout
)
}
// we've state transitioned, so refresh the page
// to reflect the new state (which will expose the test)
location
.
reload
();
}
}
});
}
</script>
edx_proctoring/tests/test_student_view.py
View file @
7acd9b56
...
@@ -55,7 +55,6 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
...
@@ -55,7 +55,6 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
self
.
chose_proctored_exam_msg
=
'Follow these steps to set up and start your proctored exam'
self
.
chose_proctored_exam_msg
=
'Follow these steps to set up and start your proctored exam'
self
.
proctored_exam_optout_msg
=
'Take this exam as an open exam instead'
self
.
proctored_exam_optout_msg
=
'Take this exam as an open exam instead'
self
.
proctored_exam_completed_msg
=
'Are you sure you want to end your proctored exam'
self
.
proctored_exam_completed_msg
=
'Are you sure you want to end your proctored exam'
self
.
proctored_exam_waiting_for_app_shutdown_msg
=
'You are about to complete your proctored exam'
self
.
proctored_exam_submitted_msg
=
'You have submitted this proctored exam for review'
self
.
proctored_exam_submitted_msg
=
'You have submitted this proctored exam for review'
self
.
proctored_exam_verified_msg
=
'Your proctoring session was reviewed and passed all requirements'
self
.
proctored_exam_verified_msg
=
'Your proctoring session was reviewed and passed all requirements'
self
.
proctored_exam_rejected_msg
=
'Your proctoring session was reviewed and did not pass requirements'
self
.
proctored_exam_rejected_msg
=
'Your proctoring session was reviewed and did not pass requirements'
...
@@ -704,7 +703,6 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
...
@@ -704,7 +703,6 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
"""
"""
exam_attempt
=
self
.
_create_started_exam_attempt
()
exam_attempt
=
self
.
_create_started_exam_attempt
()
exam_attempt
.
status
=
ProctoredExamStudentAttemptStatus
.
submitted
exam_attempt
.
status
=
ProctoredExamStudentAttemptStatus
.
submitted
exam_attempt
.
last_poll_timestamp
=
datetime
.
now
(
pytz
.
UTC
)
exam_attempt
.
save
()
exam_attempt
.
save
()
rendered_response
=
get_student_view
(
rendered_response
=
get_student_view
(
...
@@ -717,40 +715,26 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
...
@@ -717,40 +715,26 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
'default_time_limit_mins'
:
90
'default_time_limit_mins'
:
90
}
}
)
)
self
.
assertIn
(
self
.
proctored_exam_
waiting_for_app_shutdown
_msg
,
rendered_response
)
self
.
assertIn
(
self
.
proctored_exam_
submitted
_msg
,
rendered_response
)
reset_time
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
2
)
# now make sure if this status transitions to 'second_review_required'
with
freeze_time
(
reset_time
):
# the student will still see a 'submitted' message
rendered_response
=
get_student_view
(
update_attempt_status
(
user_id
=
self
.
user_id
,
exam_attempt
.
proctored_exam_id
,
course_id
=
self
.
course_id
,
exam_attempt
.
user_id
,
content_id
=
self
.
content_id
,
ProctoredExamStudentAttemptStatus
.
second_review_required
context
=
{
)
'is_proctored'
:
True
,
rendered_response
=
get_student_view
(
'display_name'
:
self
.
exam_name
,
user_id
=
self
.
user_id
,
'default_time_limit_mins'
:
90
course_id
=
self
.
course_id
,
}
content_id
=
self
.
content_id
,
)
context
=
{
self
.
assertIn
(
self
.
proctored_exam_submitted_msg
,
rendered_response
)
'is_proctored'
:
True
,
'display_name'
:
self
.
exam_name
,
# now make sure if this status transitions to 'second_review_required'
'default_time_limit_mins'
:
90
# the student will still see a 'submitted' message
}
update_attempt_status
(
)
exam_attempt
.
proctored_exam_id
,
self
.
assertIn
(
self
.
proctored_exam_submitted_msg
,
rendered_response
)
exam_attempt
.
user_id
,
ProctoredExamStudentAttemptStatus
.
second_review_required
)
rendered_response
=
get_student_view
(
user_id
=
self
.
user_id
,
course_id
=
self
.
course_id
,
content_id
=
self
.
content_id
,
context
=
{
'is_proctored'
:
True
,
'display_name'
:
self
.
exam_name
,
'default_time_limit_mins'
:
90
}
)
self
.
assertIn
(
self
.
proctored_exam_submitted_msg
,
rendered_response
)
def
test_get_studentview_submitted_status_with_duedate
(
self
):
def
test_get_studentview_submitted_status_with_duedate
(
self
):
"""
"""
...
@@ -775,7 +759,6 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
...
@@ -775,7 +759,6 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
taking_as_proctored
=
True
,
taking_as_proctored
=
True
,
external_id
=
proctored_exam
.
external_id
,
external_id
=
proctored_exam
.
external_id
,
status
=
ProctoredExamStudentAttemptStatus
.
submitted
,
status
=
ProctoredExamStudentAttemptStatus
.
submitted
,
last_poll_timestamp
=
datetime
.
now
(
pytz
.
UTC
)
)
)
# due date is after 10 minutes
# due date is after 10 minutes
...
@@ -813,7 +796,6 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
...
@@ -813,7 +796,6 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
"""
"""
exam_attempt
=
self
.
_create_started_practice_exam_attempt
()
exam_attempt
=
self
.
_create_started_practice_exam_attempt
()
exam_attempt
.
status
=
ProctoredExamStudentAttemptStatus
.
submitted
exam_attempt
.
status
=
ProctoredExamStudentAttemptStatus
.
submitted
exam_attempt
.
last_poll_timestamp
=
datetime
.
now
(
pytz
.
UTC
)
exam_attempt
.
save
()
exam_attempt
.
save
()
rendered_response
=
get_student_view
(
rendered_response
=
get_student_view
(
...
@@ -826,21 +808,8 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
...
@@ -826,21 +808,8 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
'default_time_limit_mins'
:
90
'default_time_limit_mins'
:
90
}
}
)
)
self
.
assertIn
(
self
.
proctored_exam_waiting_for_app_shutdown_msg
,
rendered_response
)
reset_time
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
2
)
self
.
assertIn
(
self
.
practice_exam_submitted_msg
,
rendered_response
)
with
freeze_time
(
reset_time
):
rendered_response
=
get_student_view
(
user_id
=
self
.
user_id
,
course_id
=
self
.
course_id
,
content_id
=
self
.
content_id_practice
,
context
=
{
'is_proctored'
:
True
,
'display_name'
:
self
.
exam_name
,
'default_time_limit_mins'
:
90
}
)
self
.
assertIn
(
self
.
practice_exam_submitted_msg
,
rendered_response
)
@ddt.data
(
@ddt.data
(
ProctoredExamStudentAttemptStatus
.
created
,
ProctoredExamStudentAttemptStatus
.
created
,
...
@@ -1185,8 +1154,6 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
...
@@ -1185,8 +1154,6 @@ class ProctoredExamStudentViewTests(ProctoredExamTestCase):
)
)
self
.
assertIsNotNone
(
rendered_response
)
self
.
assertIsNotNone
(
rendered_response
)
if
status
==
ProctoredExamStudentAttemptStatus
.
submitted
:
if
status
==
ProctoredExamStudentAttemptStatus
.
submitted
:
exam_attempt
.
last_poll_timestamp
=
datetime
.
now
(
pytz
.
UTC
)
exam_attempt
.
save
()
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
):
...
...
edx_proctoring/tests/test_views.py
View file @
7acd9b56
...
@@ -736,94 +736,11 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -736,94 +736,11 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self
.
assertIsNone
(
response_data
[
'completed_at'
])
self
.
assertIsNone
(
response_data
[
'completed_at'
])
self
.
assertEqual
(
response_data
[
'time_remaining_seconds'
],
0
)
self
.
assertEqual
(
response_data
[
'time_remaining_seconds'
],
0
)
def
test_attempt_status_error
(
self
):
"""
Test to confirm that attempt status is marked as error, because client
has stopped sending it's polling timestamp
"""
# 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'
]
# 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_waiting_for_app_shutdown
(
self
):
"""
Test to confirm that attempt status is submitted when proctored client is shutdown
"""
exam_attempt
=
self
.
_create_exam_attempt
()
exam_attempt
.
last_poll_timestamp
=
datetime
.
now
(
pytz
.
UTC
)
exam_attempt
.
status
=
ProctoredExamStudentAttemptStatus
.
submitted
exam_attempt
.
save
()
response
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.proctored_exam.attempt'
,
args
=
[
exam_attempt
.
id
])
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertFalse
(
response_data
[
'client_has_shutdown'
])
# 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
=
[
exam_attempt
.
id
])
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertTrue
(
response_data
[
'client_has_shutdown'
])
def
test_attempt_status_for_exception
(
self
):
def
test_attempt_status_for_exception
(
self
):
"""
"""
Test to confirm that exception will not effect the API call
Test to confirm that exception will not effect the API call
"""
"""
exam_attempt
=
self
.
_create_exam_attempt
()
exam_attempt
=
self
.
_create_exam_attempt
()
exam_attempt
.
last_poll_timestamp
=
datetime
.
now
(
pytz
.
UTC
)
exam_attempt
.
status
=
ProctoredExamStudentAttemptStatus
.
verified
exam_attempt
.
status
=
ProctoredExamStudentAttemptStatus
.
verified
exam_attempt
.
save
()
exam_attempt
.
save
()
...
@@ -869,16 +786,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -869,16 +786,6 @@ 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'
],
ProctoredExamStudentAttemptStatus
.
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 switched to a submitted state
# now switched to a submitted state
update_attempt_status
(
update_attempt_status
(
...
@@ -901,77 +808,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -901,77 +808,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
ProctoredExamStudentAttemptStatus
.
submitted
ProctoredExamStudentAttemptStatus
.
submitted
)
)
@ddt.data
(
ProctoredExamStudentAttemptStatus
.
created
,
ProctoredExamStudentAttemptStatus
.
ready_to_start
,
ProctoredExamStudentAttemptStatus
.
started
,
ProctoredExamStudentAttemptStatus
.
ready_to_submit
)
def
test_attempt_callback_timeout
(
self
,
running_status
):
"""
Ensures that the polling from the client will cause the
server to transition to timed_out if the user runs out of time
"""
# 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'
],
'started'
)
attempt_code
=
response_data
[
'attempt_code'
]
# now set status to what we want per DDT
update_attempt_status
(
proctored_exam
.
id
,
self
.
user
.
id
,
running_status
)
# 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
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'status'
],
running_status
)
# set time to be in future
reset_time
=
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
minutes
=
180
)
with
freeze_time
(
reset_time
):
# Now the callback should transition us away from started
response
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.anonymous.proctoring_poll_status'
,
args
=
[
attempt_code
]
)
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'status'
],
'submitted'
)
def
test_attempt_with_duedate_expired
(
self
):
def
test_attempt_with_duedate_expired
(
self
):
"""
"""
Tests that an exam with duedate passed cannot be accessed
Tests that an exam with duedate passed cannot be accessed
...
@@ -1852,17 +1688,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -1852,17 +1688,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
attempt
=
get_exam_attempt_by_id
(
attempt_id
)
attempt
=
get_exam_attempt_by_id
(
attempt_id
)
self
.
assertEqual
(
attempt
[
'status'
],
'ready_to_start'
)
self
.
assertEqual
(
attempt
[
'status'
],
'ready_to_start'
)
# 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
)
response_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response_data
[
'status'
],
'ready_to_start'
)
def
test_bad_exam_code_callback
(
self
):
def
test_bad_exam_code_callback
(
self
):
"""
"""
Assert that we get a 404 when doing a callback on an exam code that does not exist
Assert that we get a 404 when doing a callback on an exam code that does not exist
...
@@ -1875,15 +1700,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
...
@@ -1875,15 +1700,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
)
self
.
assertEqual
(
response
.
status_code
,
404
)
self
.
assertEqual
(
response
.
status_code
,
404
)
# test the polling callback point as well
response
=
self
.
client
.
get
(
reverse
(
'edx_proctoring.anonymous.proctoring_poll_status'
,
args
=
[
'foo'
]
)
)
self
.
assertEqual
(
response
.
status_code
,
404
)
def
test_review_callback
(
self
):
def
test_review_callback
(
self
):
"""
"""
Simulates a callback from the proctoring service with the
Simulates a callback from the proctoring service with the
...
...
edx_proctoring/urls.py
View file @
7acd9b56
...
@@ -85,10 +85,5 @@ urlpatterns = patterns( # pylint: disable=invalid-name
...
@@ -85,10 +85,5 @@ urlpatterns = patterns( # pylint: disable=invalid-name
callbacks
.
ExamReviewCallback
.
as_view
(),
callbacks
.
ExamReviewCallback
.
as_view
(),
name
=
'edx_proctoring.anonymous.proctoring_review_callback'
name
=
'edx_proctoring.anonymous.proctoring_review_callback'
),
),
url
(
r'edx_proctoring/proctoring_poll_status/(?P<attempt_code>[-\w]+)$'
,
callbacks
.
AttemptStatus
.
as_view
(),
name
=
'edx_proctoring.anonymous.proctoring_poll_status'
),
url
(
r'^'
,
include
(
'rest_framework.urls'
,
namespace
=
'rest_framework'
))
url
(
r'^'
,
include
(
'rest_framework.urls'
,
namespace
=
'rest_framework'
))
)
)
edx_proctoring/utils.py
View file @
7acd9b56
...
@@ -15,7 +15,6 @@ from edx_proctoring.models import (
...
@@ -15,7 +15,6 @@ from edx_proctoring.models import (
ProctoredExamStudentAttempt
,
ProctoredExamStudentAttempt
,
ProctoredExamStudentAttemptHistory
,
ProctoredExamStudentAttemptHistory
,
)
)
from
edx_proctoring
import
constants
# import dependent libraries (in local_requirements.txt otherwise pick up from running Open edX LMS runtime)
# import dependent libraries (in local_requirements.txt otherwise pick up from running Open edX LMS runtime)
from
eventtracking
import
tracker
from
eventtracking
import
tracker
...
@@ -122,19 +121,6 @@ def locate_attempt_by_attempt_code(attempt_code):
...
@@ -122,19 +121,6 @@ def locate_attempt_by_attempt_code(attempt_code):
return
(
attempt_obj
,
is_archived_attempt
)
return
(
attempt_obj
,
is_archived_attempt
)
def
has_client_app_shutdown
(
attempt
):
"""
Returns True if the client app has shut down, False otherwise
"""
# we never heard from the client, so it must not have started
if
not
attempt
[
'last_poll_timestamp'
]:
return
True
elapsed_time
=
(
datetime
.
now
(
pytz
.
UTC
)
-
attempt
[
'last_poll_timestamp'
])
.
total_seconds
()
return
elapsed_time
>
constants
.
SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD
def
emit_event
(
exam
,
event_short_name
,
attempt
=
None
,
override_data
=
None
):
def
emit_event
(
exam
,
event_short_name
,
attempt
=
None
,
override_data
=
None
):
"""
"""
Helper method to emit an analytics event
Helper method to emit an analytics event
...
...
edx_proctoring/views.py
View file @
7acd9b56
...
@@ -3,8 +3,6 @@ Proctored Exams HTTP-based API endpoints
...
@@ -3,8 +3,6 @@ Proctored Exams HTTP-based API endpoints
"""
"""
import
logging
import
logging
import
pytz
from
datetime
import
datetime
from
django.utils.translation
import
ugettext
as
_
from
django.utils.translation
import
ugettext
as
_
from
django.utils.decorators
import
method_decorator
from
django.utils.decorators
import
method_decorator
...
@@ -39,11 +37,9 @@ from edx_proctoring.exceptions import (
...
@@ -39,11 +37,9 @@ from edx_proctoring.exceptions import (
UserNotFoundException
,
UserNotFoundException
,
ProctoredExamPermissionDenied
,
ProctoredExamPermissionDenied
,
StudentExamAttemptDoesNotExistsException
,
StudentExamAttemptDoesNotExistsException
,
ProctoredExamIllegalStatusTransition
,
ProctoredExamNotActiveException
,
ProctoredExamNotActiveException
,
AllowanceValueNotAllowedException
AllowanceValueNotAllowedException
)
)
from
edx_proctoring
import
constants
from
edx_proctoring.runtime
import
get_runtime_service
from
edx_proctoring.runtime
import
get_runtime_service
from
edx_proctoring.serializers
import
ProctoredExamSerializer
,
ProctoredExamStudentAttemptSerializer
from
edx_proctoring.serializers
import
ProctoredExamSerializer
,
ProctoredExamStudentAttemptSerializer
from
edx_proctoring.models
import
ProctoredExamStudentAttemptStatus
,
ProctoredExamStudentAttempt
,
ProctoredExam
from
edx_proctoring.models
import
ProctoredExamStudentAttemptStatus
,
ProctoredExamStudentAttempt
,
ProctoredExam
...
@@ -52,7 +48,6 @@ from edx_proctoring.utils import (
...
@@ -52,7 +48,6 @@ from edx_proctoring.utils import (
AuthenticatedAPIView
,
AuthenticatedAPIView
,
get_time_remaining_for_attempt
,
get_time_remaining_for_attempt
,
humanized_time
,
humanized_time
,
has_client_app_shutdown
,
)
)
ATTEMPTS_PER_PAGE
=
25
ATTEMPTS_PER_PAGE
=
25
...
@@ -328,36 +323,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
...
@@ -328,36 +323,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
)
)
raise
ProctoredExamPermissionDenied
(
err_msg
)
raise
ProctoredExamPermissionDenied
(
err_msg
)
# check if the last_poll_timestamp is not None
# add in the computed time remaining as a helper
# and if it is older than SOFTWARE_SECURE_CLIENT_TIMEOUT
# then attempt status should be marked as error.
last_poll_timestamp
=
attempt
[
'last_poll_timestamp'
]
# if we never heard from the client, then we assume it is shut down
attempt
[
'client_has_shutdown'
]
=
last_poll_timestamp
is
None
if
last_poll_timestamp
is
not
None
:
# Let's pass along information if we think the SoftwareSecure has completed
# a healthy shutdown which is when our attempt is in a 'submitted' status
if
attempt
[
'status'
]
==
ProctoredExamStudentAttemptStatus
.
submitted
:
attempt
[
'client_has_shutdown'
]
=
has_client_app_shutdown
(
attempt
)
else
:
# otherwise, let's see if the shutdown happened in error
# e.g. a crash
time_passed_since_last_poll
=
(
datetime
.
now
(
pytz
.
UTC
)
-
last_poll_timestamp
)
.
total_seconds
()
if
time_passed_since_last_poll
>
constants
.
SOFTWARE_SECURE_CLIENT_TIMEOUT
:
try
:
update_attempt_status
(
attempt
[
'proctored_exam'
][
'id'
],
attempt
[
'user'
][
'id'
],
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
time_remaining_seconds
=
get_time_remaining_for_attempt
(
attempt
)
time_remaining_seconds
=
get_time_remaining_for_attempt
(
attempt
)
attempt
[
'time_remaining_seconds'
]
=
time_remaining_seconds
attempt
[
'time_remaining_seconds'
]
=
time_remaining_seconds
...
...
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