Commit 7acd9b56 by Diana Huang Committed by GitHub

Merge pull request #314 from edx/diana/remove-client-polling

Delete in-client polling.
parents 14d6164c 38a129bf
...@@ -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
......
...@@ -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
)
...@@ -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)
......
...@@ -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>
...@@ -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 %}
&#8226; After you quit the proctoring session, the recorded data is uploaded for review. </br> &#8226; After you quit the proctoring session, the recorded data is uploaded for review. </br>
&#8226; Proctoring results are usually available within 5 business days after you submit your exam. &#8226; Proctoring results are usually available within 5 business days after you submit your exam.
{% endblocktrans %} {% endblocktrans %}
......
{% 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>
...@@ -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):
......
...@@ -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
......
...@@ -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'))
) )
...@@ -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
......
...@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment