Skip to content

Commit

Permalink
Merge pull request #974 from edx/alangsto/use_video_url_in_redirect
Browse files Browse the repository at this point in the history
feat: use video review url provided by PSI in redirect
  • Loading branch information
alangsto committed Oct 13, 2021
2 parents 5a251fd + aeac231 commit 010d9e8
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 7 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Change Log
Unreleased
~~~~~~~~~~

[4.1.2] - 2021-10-07
~~~~~~~~~~~~~~~~~~~~
* Instructor dashboard view should redirect to review url for PSI exam attempts

[4.1.1] - 2021-10-05
~~~~~~~~~~~~~~~~~~~~
* Bug fix to redact video url from raw data in exam review
Expand Down
2 changes: 1 addition & 1 deletion edx_proctoring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"""

# Be sure to update the version number in edx_proctoring/package.json
__version__ = '4.1.1'
__version__ = '4.1.2'

default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
5 changes: 4 additions & 1 deletion edx_proctoring/backends/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ def get_attempt(self, attempt):
return attempt

# pylint: disable=unused-argument
def get_instructor_url(self, course_id, user, exam_id=None, attempt_id=None, show_configuration_dashboard=False):
def get_instructor_url(
self, course_id, user, exam_id=None, attempt_id=None,
show_configuration_dashboard=False, encrypted_video_review_url=None
):
"""
Returns the instructor dashboard url for reviews
"""
Expand Down
5 changes: 4 additions & 1 deletion edx_proctoring/backends/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,10 @@ def on_exam_saved(self, exam):
data = {}
return data.get('id')

def get_instructor_url(self, course_id, user, exam_id=None, attempt_id=None, show_configuration_dashboard=False):
def get_instructor_url(
self, course_id, user, exam_id=None, attempt_id=None,
show_configuration_dashboard=False, encrypted_video_review_url=None
):
"""
Return a URL to the instructor dashboard
course_id: str
Expand Down
28 changes: 28 additions & 0 deletions edx_proctoring/backends/software_secure.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import base64
import binascii
import codecs
import datetime
import hmac
import json
Expand All @@ -22,6 +23,7 @@
from edx_proctoring.backends.backend import ProctoringBackendProvider
from edx_proctoring.exceptions import BackendProviderCannotRegisterAttempt, ProctoredExamSuspiciousLookup
from edx_proctoring.statuses import SoftwareSecureReviewStatus
from edx_proctoring.utils import decode_and_decrypt

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -390,3 +392,29 @@ def get_video_review_aes_key(self):
Returns the aes key used to encrypt the video review url
"""
return self.video_review_aes_key

def get_instructor_url(
self, course_id, user, exam_id=None, attempt_id=None,
show_configuration_dashboard=False, encrypted_video_review_url=None
):
"""
Returns the url for video reviews
"""
# video_review_url is required for PSI backend
if not encrypted_video_review_url:
return None

try:
aes_key = codecs.decode(self.video_review_aes_key, "hex")
decrypted_video_url = decode_and_decrypt(encrypted_video_review_url, aes_key).decode("utf-8")

# reformat video url as per MST-871 findings
reformatted_url = decrypted_video_url.replace('DirectLink-Generic', 'DirectLink-HTML5')
return reformatted_url
except Exception as err: # pylint: disable=broad-except
log.exception(
'Could not decrypt video url for attempt_id=%(attempt_id)s '
'due to the following error: %(error_string)s',
{'attempt_id': attempt_id, 'error_string': str(err)}
)
return None
5 changes: 4 additions & 1 deletion edx_proctoring/backends/tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ def on_exam_saved(self, exam):
return exam.get('external_id', None) or 'externalid'

# pylint: disable=unused-argument
def get_instructor_url(self, course_id, user, exam_id=None, attempt_id=None, show_configuration_dashboard=False):
def get_instructor_url(
self, course_id, user, exam_id=None, attempt_id=None,
show_configuration_dashboard=False, encrypted_video_review_url=None
):
"Return a fake instructor url"
url = f'/instructor/{course_id}/'
if exam_id:
Expand Down
2 changes: 2 additions & 0 deletions edx_proctoring/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@

ONBOARDING_PROFILE_INSTRUCTOR_DASHBOARD_API = 'edx_proctoring.onboarding_profile_instructor_dashboard_api'

REDS_API_REDIRECT = 'edx_proctoring.reds_api_redirect'

CONTENT_VIEWABLE_PAST_DUE_DATE = getattr(settings, 'PROCTORED_EXAM_VIEWABLE_PAST_DUE', False)

TIME_MULTIPLIER = 'time_multiplier'
177 changes: 177 additions & 0 deletions edx_proctoring/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5479,6 +5479,7 @@ def setUp(self):
self.client.login_user(self.user)
self.course_id = 'a/b/c'

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

def test_launch_for_course(self):
Expand Down Expand Up @@ -5654,6 +5655,182 @@ def test_multiple_exams_returns_correct_dashboard(self, exam_1_is_proctored, exa
else:
self.assertRedirects(response, expected_url, fetch_redirect_response=False)

@patch('edx_proctoring.views.waffle.switch_is_active')
@ddt.data(
(True, False),
(True, True),
(False, False),
(False, True),
)
@ddt.unpack
def test_psi_instructor_dashboard_url(self, include_attempt_id, is_switch_active, mock_switch):
"""
Test that instructor dashboard redirects correctly for psi software secure backend
"""
mock_switch.return_value = is_switch_active
# set up exam
exam = ProctoredExam.objects.create(
course_id=self.course_id,
content_id='test_content',
exam_name='Test Exam',
external_id='123aXqe3',
time_limit_mins=90,
is_active=True,
is_proctored=True,
backend='software_secure',
)

# create exam attempt
with HTTMock(mock_response_content):
attempt_id = create_exam_attempt(exam.id, self.second_user.id, True)

attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['external_id'])

# create review for attempt
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id=attempt['external_id'].upper(),
review_status='Clean'
)
response = self.client.post(
reverse('edx_proctoring:anonymous.proctoring_review_callback'),
data=test_payload,
content_type='foo'
)
self.assertEqual(response.status_code, 200)

# call dashboard exam url with attempt id
dashboard_url = reverse('edx_proctoring:instructor_dashboard_exam',
kwargs={'course_id': self.course_id, 'exam_id': exam.id})
query_params = {'attempt': attempt['external_id']} if include_attempt_id else {}
response = self.client.get(dashboard_url, query_params)
if not (include_attempt_id and is_switch_active):
self.assertEqual(response.status_code, 404)
else:
video_url = json.loads(test_payload)['videoReviewLink']
expected_url = video_url.replace('DirectLink-Generic', 'DirectLink-HTML5')
self.assertRedirects(response, expected_url, fetch_redirect_response=False)

@patch('edx_proctoring.views.waffle.switch_is_active')
def test_psi_instructor_dashboard_url_deleted_attempt(self, mock_switch):
"""
Test that instructor dashboard redirects correctly for psi software secure backend when an attempt is deleted
"""
mock_switch.return_value = True
# set up exam
exam = ProctoredExam.objects.create(
course_id=self.course_id,
content_id='test_content',
exam_name='Test Exam',
external_id='123aXqe3',
time_limit_mins=90,
is_active=True,
is_proctored=True,
backend='software_secure',
)

# create exam attempt
with HTTMock(mock_response_content):
attempt_id = create_exam_attempt(exam.id, self.second_user.id, True)

attempt = get_exam_attempt_by_id(attempt_id)
external_id = attempt['external_id']

# remove the attempt
response = self.client.delete(
reverse('edx_proctoring:proctored_exam.attempt', args=[attempt_id])
)
self.assertEqual(response.status_code, 200)

# call dashboard exam url with attempt id
dashboard_url = reverse('edx_proctoring:instructor_dashboard_exam',
kwargs={'course_id': self.course_id, 'exam_id': exam.id})
response = self.client.get(dashboard_url, {'attempt': external_id})

self.assertEqual(response.status_code, 404)

@patch('edx_proctoring.views.waffle.switch_is_active')
def test_psi_instructor_dashboard_url_no_review(self, mock_switch):
"""
Test that instructor dashboard redirects correctly for psi software secure backend when there
is no review for an attempt
"""
mock_switch.return_value = True
# set up exam
exam = ProctoredExam.objects.create(
course_id=self.course_id,
content_id='test_content',
exam_name='Test Exam',
external_id='123aXqe3',
time_limit_mins=90,
is_active=True,
is_proctored=True,
backend='software_secure',
)

# create exam attempt
with HTTMock(mock_response_content):
attempt_id = create_exam_attempt(exam.id, self.second_user.id, True)

attempt = get_exam_attempt_by_id(attempt_id)
external_id = attempt['external_id']

# call dashboard exam url with attempt id
dashboard_url = reverse('edx_proctoring:instructor_dashboard_exam',
kwargs={'course_id': self.course_id, 'exam_id': exam.id})
response = self.client.get(dashboard_url, {'attempt': external_id})

self.assertEqual(response.status_code, 404)

@patch('edx_proctoring.backends.software_secure.decode_and_decrypt')
@patch('edx_proctoring.views.waffle.switch_is_active')
def test_psi_instructor_dashboard_url_decoding_error(self, mock_switch, mock_decode):
"""
Test that the instructor dashboard returns a 404 if there was an error decoding the video url
"""
mock_switch.return_value = True
mock_decode.side_effect = Exception()
# set up exam
exam = ProctoredExam.objects.create(
course_id=self.course_id,
content_id='test_content',
exam_name='Test Exam',
external_id='123aXqe3',
time_limit_mins=90,
is_active=True,
is_proctored=True,
backend='software_secure',
)

# create exam attempt
with HTTMock(mock_response_content):
attempt_id = create_exam_attempt(exam.id, self.second_user.id, True)

attempt = get_exam_attempt_by_id(attempt_id)
self.assertIsNotNone(attempt['external_id'])

# create review for attempt
test_payload = create_test_review_payload(
attempt_code=attempt['attempt_code'],
external_id=attempt['external_id'].upper(),
review_status='Clean'
)
response = self.client.post(
reverse('edx_proctoring:anonymous.proctoring_review_callback'),
data=test_payload,
content_type='foo'
)
self.assertEqual(response.status_code, 200)

# call dashboard exam url with attempt id
dashboard_url = reverse('edx_proctoring:instructor_dashboard_exam',
kwargs={'course_id': self.course_id, 'exam_id': exam.id})
query_params = {'attempt': attempt['external_id']}
response = self.client.get(dashboard_url, query_params)

self.assertEqual(response.status_code, 404)


class TestBackendUserDeletion(LoggedInTestCase):
"""
Expand Down
13 changes: 11 additions & 2 deletions edx_proctoring/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
from edx_proctoring.constants import (
ONBOARDING_PROFILE_API,
ONBOARDING_PROFILE_INSTRUCTOR_DASHBOARD_API,
PING_FAILURE_PASSTHROUGH_TEMPLATE
PING_FAILURE_PASSTHROUGH_TEMPLATE,
REDS_API_REDIRECT
)
from edx_proctoring.exceptions import (
AllowanceValueNotAllowedException,
Expand Down Expand Up @@ -1998,12 +1999,20 @@ def get(self, request, course_id, exam_id=None):
'email': request.user.email
}

encrypted_video_review_url = None
if waffle.switch_is_active(REDS_API_REDIRECT) and attempt_id:
attempt = get_exam_attempt_by_external_id(attempt_id)
if attempt:
reviews = ProctoredExamSoftwareSecureReview.objects.filter(attempt_code=attempt['attempt_code'])
encrypted_video_review_url = reviews[0].encrypted_video_url if reviews else None

url = backend.get_instructor_url(
exam['course_id'],
user,
exam_id=ext_exam_id,
attempt_id=attempt_id,
show_configuration_dashboard=show_configuration_dashboard
show_configuration_dashboard=show_configuration_dashboard,
encrypted_video_review_url=encrypted_video_review_url
)
if not url:
return Response(
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@edx/edx-proctoring",
"//": "Note that the version format is slightly different than that of the Python version when using prereleases.",
"version": "4.1.1",
"version": "4.1.2",
"main": "edx_proctoring/static/index.js",
"scripts": {
"test": "gulp test"
Expand Down

0 comments on commit 010d9e8

Please sign in to comment.