Skip to content

Commit

Permalink
PHX-149 added new screen
Browse files Browse the repository at this point in the history
  • Loading branch information
hasnain-naveed authored and Chris Dodge committed Oct 9, 2015
1 parent d30a3a9 commit 45b572a
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 23 deletions.
15 changes: 12 additions & 3 deletions edx_proctoring/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@
ProctoredExamStudentAttemptSerializer,
ProctoredExamStudentAllowanceSerializer,
)
from edx_proctoring.utils import humanized_time
from edx_proctoring.utils import (
humanized_time,
has_client_app_shutdown
)

from edx_proctoring.backends import get_backend_provider
from edx_proctoring.runtime import get_runtime_service
Expand Down Expand Up @@ -1312,7 +1315,10 @@ def _get_practice_exam_view(exam, context, exam_id, user_id, course_id):
elif attempt_status == ProctoredExamStudentAttemptStatus.error:
student_view_template = 'practice_exam/error.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.submitted:
student_view_template = 'practice_exam/submitted.html'
if has_client_app_shutdown(attempt):
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:
student_view_template = 'proctored_exam/ready_to_submit.html'

Expand Down Expand Up @@ -1414,7 +1420,10 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id):
elif attempt_status == ProctoredExamStudentAttemptStatus.timed_out:
raise NotImplementedError('There is no defined rendering for ProctoredExamStudentAttemptStatus.timed_out!')
elif attempt_status == ProctoredExamStudentAttemptStatus.submitted:
student_view_template = 'proctored_exam/submitted.html'
if has_client_app_shutdown(attempt):
student_view_template = 'proctored_exam/submitted.html'
else:
student_view_template = 'proctored_exam/waiting_for_app_shutdown.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.verified:
student_view_template = 'proctored_exam/verified.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.rejected:
Expand Down
12 changes: 12 additions & 0 deletions edx_proctoring/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,15 @@
'REQUIRE_FAILURE_SECOND_REVIEWS' in settings.PROCTORING_SETTINGS
else getattr(settings, 'REQUIRE_FAILURE_SECOND_REVIEWS', True)
)

SOFTWARE_SECURE_CLIENT_TIMEOUT = (
settings.PROCTORING_SETTINGS['SOFTWARE_SECURE_CLIENT_TIMEOUT'] if
'SOFTWARE_SECURE_CLIENT_TIMEOUT' in settings.PROCTORING_SETTINGS
else getattr(settings, 'SOFTWARE_SECURE_CLIENT_TIMEOUT', 30)
)

SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD = (
settings.PROCTORING_SETTINGS['SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD'] if
'SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD' in settings.PROCTORING_SETTINGS
else getattr(settings, 'SOFTWARE_SECURE_SHUT_DOWN_GRACEPERIOD', 10)
)
2 changes: 1 addition & 1 deletion edx_proctoring/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class ProctoredExamStudentAttemptStatus(object):
might change over time.
"""

# the student is eligible to decide if he/she wants to persue credit
# the student is eligible to decide if he/she wants to pursue credit
eligible = 'eligible'

# the attempt record has been created, but the exam has not yet
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{% 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 }}';
$.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>
56 changes: 53 additions & 3 deletions edx_proctoring/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def setUp(self):
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_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_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'
Expand Down Expand Up @@ -243,6 +244,7 @@ def _create_started_practice_exam_attempt(self, started_at=None):
"""
return ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=self.practice_exam_id,
taking_as_proctored=True,
user_id=self.user_id,
external_id=self.external_id,
started_at=started_at if started_at else datetime.now(pytz.UTC),
Expand Down Expand Up @@ -1067,6 +1069,7 @@ def test_get_studentview_submitted_status(self):
"""
exam_attempt = self._create_started_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.submitted
exam_attempt.last_poll_timestamp = datetime.now(pytz.UTC)
exam_attempt.save()

rendered_response = get_student_view(
Expand All @@ -1079,14 +1082,29 @@ def test_get_studentview_submitted_status(self):
'default_time_limit_mins': 90
}
)
self.assertIn(self.proctored_exam_submitted_msg, rendered_response)
self.assertIn(self.proctored_exam_waiting_for_app_shutdown_msg, rendered_response)

reset_time = datetime.now(pytz.UTC) + timedelta(minutes=2)
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,
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_practiceexam(self):
"""
Test for get_student_view practice exam which has been submitted.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.submitted
exam_attempt.last_poll_timestamp = datetime.now(pytz.UTC)
exam_attempt.save()

rendered_response = get_student_view(
Expand All @@ -1099,7 +1117,21 @@ def test_get_studentview_submitted_status_practiceexam(self):
'default_time_limit_mins': 90
}
)
self.assertIn(self.practice_exam_submitted_msg, rendered_response)
self.assertIn(self.proctored_exam_waiting_for_app_shutdown_msg, rendered_response)

reset_time = datetime.now(pytz.UTC) + timedelta(minutes=2)
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)

def test_get_studentview_created_status_practiceexam(self):
"""
Expand Down Expand Up @@ -2046,7 +2078,25 @@ def test_footer_present(self, status):
}
)
self.assertIsNotNone(rendered_response)
self.assertIn(self.footer_msg, rendered_response)
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)
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,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.footer_msg, rendered_response)
else:
self.assertIn(self.footer_msg, rendered_response)

def test_requirement_status_order(self):
"""
Expand Down
78 changes: 77 additions & 1 deletion edx_proctoring/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import json
import pytz
import ddt
from mock import Mock
from mock import Mock, patch
from freezegun import freeze_time
from httmock import HTTMock
from string import Template # pylint: disable=deprecated-module
Expand All @@ -20,6 +20,9 @@
ProctoredExamStudentAllowance,
ProctoredExamStudentAttemptStatus,
)
from edx_proctoring.exceptions import (
ProctoredExamIllegalStatusTransition,
)
from edx_proctoring.views import require_staff, require_course_or_global_staff
from edx_proctoring.api import (
create_exam,
Expand Down Expand Up @@ -408,6 +411,34 @@ def setUp(self):

set_runtime_service('instructor', MockInstructorService(is_user_course_staff=True))

def _create_exam_attempt(self):
"""
Create and start the exam attempt, and return the exam attempt object
"""

# 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,
}

# Starting exam attempt
response = self.client.post(
reverse('edx_proctoring.proctored_exam.attempt.collection'),
attempt_data
)
self.assertEqual(response.status_code, 200)

return ProctoredExamStudentAttempt.objects.get_exam_attempt(proctored_exam.id, self.user.id)

def test_start_exam_create(self):
"""
Start an exam (create an exam attempt)
Expand Down Expand Up @@ -617,6 +648,51 @@ def test_attempt_status_error(self):
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):
"""
Test to confirm that exception will not effect the API call
"""
exam_attempt = self._create_exam_attempt()
exam_attempt.last_poll_timestamp = datetime.now(pytz.UTC)
exam_attempt.status = ProctoredExamStudentAttemptStatus.verified
exam_attempt.save()

# now reset the time to 2 minutes in the future.
reset_time = datetime.now(pytz.UTC) + timedelta(minutes=2)
with patch('edx_proctoring.api.update_attempt_status', Mock(side_effect=ProctoredExamIllegalStatusTransition)):
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)

def test_attempt_status_stickiness(self):
"""
Test to confirm that a status timeout error will not alter a completed state
Expand Down
14 changes: 14 additions & 0 deletions edx_proctoring/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ProctoredExamStudentAttempt,
ProctoredExamStudentAttemptHistory,
)
from edx_proctoring import constants

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -114,3 +115,16 @@ def locate_attempt_by_attempt_code(attempt_code):
log.error(err_msg)

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

0 comments on commit 45b572a

Please sign in to comment.