Skip to content

Commit

Permalink
Merge branch 'master' into mohtamba/adjust_allowance_modal
Browse files Browse the repository at this point in the history
  • Loading branch information
mohtamba committed Jul 16, 2021
2 parents 4941bcf + 053d38c commit e1bf5ef
Show file tree
Hide file tree
Showing 22 changed files with 385 additions and 247 deletions.
16 changes: 14 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,24 @@ Change Log
Unreleased
~~~~~~~~~~

[3.18.0] - 2021-07-16
[3.19.0] - 2021-07-16
~~~~~~~~~~~~~~~~~~~~~
* Updated allowance modal to allow bulk allowances to be added.
* Added waffle flag to enable/disable bulk allowances feature.

[3.17.1] - 2021-07-2
[3.18.0] - 2021-07-15
~~~~~~~~~~~~~~~~~~~~~
* Remove old proctored exam attempt url.
* Fix onboarding link generation in proctored exam attempt view when exam attempt is in
onboarding errors status, don't return the link to exams that are not accessible to user.
* Update onboarding link url in student onboarding status view to link
to the learning mfe page instead of LMS.

[3.17.3] - 2021-07-14
~~~~~~~~~~~~~~~~~~~~~
* Add missing get_proctoring_config method to base backend provider class.

[3.17.2] - 2021-07-2
~~~~~~~~~~~~~~~~~~~~~
* Updated ProctoredExamAttempt view to use the content id from the query.

Expand Down
14 changes: 7 additions & 7 deletions edx_proctoring/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,12 +308,12 @@ def has_delete_permission(self, request, obj=None):
""" Allow deletes """
return True

def save_model(self, request, review, form, change): # pylint: disable=arguments-differ
def save_model(self, request, obj, form, change):
"""
Override callback so that we can inject the user_id that made the change
"""
review.reviewed_by = request.user
review.save()
obj.reviewed_by = request.user
obj.save()

def get_form(self, request, obj=None, change=False, **kwargs):
""" Returns software secure review form """
Expand All @@ -322,11 +322,11 @@ def get_form(self, request, obj=None, change=False, **kwargs):
del form.base_fields['video_url']
return form

def lookup_allowed(self, key, value): # pylint: disable=arguments-differ
def lookup_allowed(self, lookup, value):
""" Checks if lookup allowed or not """
if key == 'exam__course_id':
if lookup == 'exam__course_id':
return True
return super().lookup_allowed(key, value)
return super().lookup_allowed(lookup, value)


class ProctoredExamSoftwareSecureReviewHistoryAdmin(ProctoredExamSoftwareSecureReviewAdmin):
Expand All @@ -344,7 +344,7 @@ class ProctoredExamSoftwareSecureReviewHistoryAdmin(ProctoredExamSoftwareSecureR
'modified',
]

def save_model(self, request, review, form, change):
def save_model(self, request, obj, form, change): # pylint: disable=unused-argument
"""
History can't be updated
"""
Expand Down
41 changes: 36 additions & 5 deletions edx_proctoring/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,17 @@
)
from edx_proctoring.statuses import InstructorDashboardOnboardingAttemptStatus, ProctoredExamStudentAttemptStatus
from edx_proctoring.utils import (
categorize_inaccessible_exams_by_date,
emit_event,
get_exam_due_date,
get_exam_type,
get_exam_url,
get_time_remaining_for_attempt,
get_user_course_outline_details,
has_due_date_passed,
humanized_time,
is_reattempting_exam,
obscured_user_id,
resolve_exam_url_for_learning_mfe,
verify_and_add_wait_deadline
)

Expand All @@ -81,6 +83,38 @@
USER_MODEL = get_user_model()


def get_onboarding_exam_link(course_id, user, is_learning_mfe):
"""
Get link to the onboarding exam available to the user for given course.
Parameters:
* course_id: id of a course where search for the onboarding exam is performed
* user: user for whom the link is built
* is_learning_mfe: Boolean, indicates if the url should be built for the learning mfe application.
Returns: url pointing to the available exam, if there is no onboarding exam available
to the user returns empty string.
"""
onboarding_exams = list(ProctoredExam.get_practice_proctored_exams_for_course(course_id).order_by('-created'))
if not onboarding_exams or not get_backend_provider(name=onboarding_exams[0].backend).supports_onboarding:
onboarding_link = ''
else:
details = get_user_course_outline_details(user, course_id)
categorized_exams = categorize_inaccessible_exams_by_date(onboarding_exams, details)
non_date_inaccessible_exams = categorized_exams[0]

# remove onboarding exams not accessible to learners
for onboarding_exam in non_date_inaccessible_exams:
onboarding_exams.remove(onboarding_exam)

if onboarding_exams:
onboarding_exam = onboarding_exams[0]
onboarding_link = get_exam_url(course_id, onboarding_exam.content_id, is_learning_mfe)
else:
onboarding_link = ''
return onboarding_link


def get_total_allowed_time_for_exam(exam, user_id):
"""
Returns total allowed time in humanized form for exam including allowance time.
Expand Down Expand Up @@ -752,10 +786,7 @@ def get_exam_attempt_data(exam_id, attempt_id, is_learning_mfe=False):

# resolve the LMS url, note we can't assume we're running in
# a same process as the LMS
if is_learning_mfe:
exam_url_path = resolve_exam_url_for_learning_mfe(exam['course_id'], exam['content_id'])
else:
exam_url_path = reverse('jump_to', args=[exam['course_id'], exam['content_id']])
exam_url_path = get_exam_url(exam['course_id'], exam['content_id'], is_learning_mfe)

attempt_data = {
'in_timed_exam': True,
Expand Down
6 changes: 6 additions & 0 deletions edx_proctoring/backends/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,9 @@ def get_onboarding_profile_info(self, course_id, **kwargs):
Returns onboarding profile information for a given course and optional user
"""
return None

def get_proctoring_config(self):
"""
Returns the metadata and configuration options for the proctoring service
"""
return None
2 changes: 1 addition & 1 deletion edx_proctoring/backends/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def get_javascript(self):
Returns the url of the javascript bundle into which the provider's JS will be loaded
"""
# use the defined npm_module name, or else the python package name
package = getattr(self, 'npm_module', self.__class__.__module__.split('.')[0])
package = getattr(self, 'npm_module', self.__class__.__module__.split('.', maxsplit=1)[0])
js_url = ''
try:
bundle_chunks = get_files(package, config="WORKERS")
Expand Down
12 changes: 12 additions & 0 deletions edx_proctoring/backends/software_secure.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,15 @@ def should_block_access_to_exam_material(self):
req = get_current_request()
# pylint: disable=illegal-waffle-usage
return not req.get_signed_cookie('exam', default=False)

def get_proctoring_config(self):
"""
Returns the metadata and configuration options for the proctoring service
"""
proctoring_config = {
'download_url': self.get_software_download_url(),
'name': self.verbose_name,
'rules': {},
'instructions': []
}
return proctoring_config
2 changes: 2 additions & 0 deletions edx_proctoring/backends/tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ def test_raises_exception(self):

self.assertIsNone(provider.get_onboarding_profile_info(course_id='test'))

self.assertIsNone(provider.get_proctoring_config())

def test_null_provider(self):
"""
Assert that the Null provider does nothing
Expand Down
11 changes: 11 additions & 0 deletions edx_proctoring/backends/tests/test_software_secure.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ def test_get_software_download_url(self):
provider = get_backend_provider()
self.assertEqual(provider.get_software_download_url(), 'http://example.com')

def test_get_proctoring_config(self):
"""
Makes sure proctoring config is returned
"""

provider = get_backend_provider()
config = provider.get_proctoring_config()
self.assertIsNotNone(config)
self.assertEqual(config['name'], provider.verbose_name)
self.assertEqual(config['download_url'], 'http://example.com')

def test_register_attempt(self):
"""
Makes sure we can register an attempt
Expand Down
50 changes: 46 additions & 4 deletions edx_proctoring/tests/test_mfe_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@

import ddt
from mock import patch
from opaque_keys.edx.locator import BlockUsageLocator

from django.conf import settings
from django.test.utils import override_settings
from django.urls import reverse
from django.utils import timezone

from edx_proctoring.api import get_exam_by_id, get_review_policy_by_exam_id
from edx_proctoring.exceptions import BackendProviderNotConfigured, ProctoredExamNotFoundException
from edx_proctoring.models import ProctoredExam, ProctoredExamStudentAllowance
from edx_proctoring.runtime import set_runtime_service
from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus
from edx_proctoring.utils import humanized_time

from .test_services import MockLearningSequencesService, MockScheduleItemData
from .utils import ProctoredExamTestCase


Expand Down Expand Up @@ -48,6 +52,18 @@ def setUp(self):
self.expected_exam_url = '{}/course/{}/{}'.format(
settings.LEARNING_MICROFRONTEND_URL, self.course_id, self.content_id
)
yesterday = timezone.now() - timezone.timedelta(days=1)
self.course_scheduled_sections = {
BlockUsageLocator.from_string(self.content_id_onboarding): MockScheduleItemData(yesterday),
BlockUsageLocator.from_string(
'block-v1:test+course+1+type@sequential+block@assignment'
): MockScheduleItemData(yesterday),
}

set_runtime_service('learning_sequences', MockLearningSequencesService(
list(self.course_scheduled_sections.keys()),
self.course_scheduled_sections,
))

def assertHasExamData(self, response_data, has_attempt,
has_verification_url=False, has_download_url=False, content_id=None):
Expand Down Expand Up @@ -286,9 +302,7 @@ def test_exam_data_contains_link_to_onboarding_exam_if_attempt_in_onboarding_err
"""
self._create_exam_attempt(self.proctored_exam_id, status=status)

onboarding_exam = ProctoredExam.objects.filter(
course_id=self.course_id, is_active=True, is_practice_exam=True
).first()
onboarding_exam = ProctoredExam.objects.get(id=self.onboarding_exam_id)

if is_learning_mfe:
expected_exam_url = '{}/course/{}/{}'.format(
Expand Down Expand Up @@ -332,7 +346,35 @@ def test_exam_data_does_not_fail_if_onboarding_errors_and_no_onboarding_exam(sel
response = self.client.get(url)
response_data = json.loads(response.content.decode('utf-8'))
exam = response_data['exam']
assert 'onboarding_link' not in exam
assert 'onboarding_link' in exam
self.assertEqual(exam['onboarding_link'], '')

def test_onboarding_errors_and_onboarding_exam_not_available(self):
"""
Tests the GET exam attempts data not contain link to onboarding exam if
when user tries to take proctored exam and has not yet completed required
onboarding exam and onboarding exams are not available for the user.
"""
self._create_exam_attempt(self.proctored_exam_id, status=ProctoredExamStudentAttemptStatus.onboarding_missing)
set_runtime_service('learning_sequences', MockLearningSequencesService(
[], # sections user can see (none)
self.course_scheduled_sections, # all scheduled sections
))
url = reverse(
'edx_proctoring:proctored_exam.exam_attempts',
kwargs={
'course_id': self.course_id,
}
) + '?' + urlencode({
'content_id': self.content_id,
'is_learning_mfe': True,
})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content.decode('utf-8'))
exam = response_data['exam']
assert 'onboarding_link' in exam
self.assertEqual(exam['onboarding_link'], '')


class ProctoredSettingsViewTests(ProctoredExamTestCase):
Expand Down
18 changes: 17 additions & 1 deletion edx_proctoring/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
)
from edx_proctoring.tests import mock_perm
from edx_proctoring.urls import urlpatterns
from edx_proctoring.utils import obscured_user_id
from edx_proctoring.utils import obscured_user_id, resolve_exam_url_for_learning_mfe
from edx_proctoring.views import require_course_or_global_staff, require_staff
from mock_apps.models import Profile

Expand Down Expand Up @@ -551,6 +551,22 @@ def test_no_onboarding_exam(self):
message = 'There is no onboarding exam related to this course id.'
self.assertEqual(response_data['detail'], message)

@override_settings(LEARNING_MICROFRONTEND_URL='https://learningmfe')
def test_onboarding_mfe_link(self):
"""
Test that the request returns correct link to onboarding exam for learning mfe application.
"""
response = self.client.get(
reverse('edx_proctoring:user_onboarding.status')
+ '?course_id={}&is_learning_mfe=True'.format(self.course_id)
)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content.decode('utf-8'))
self.assertEqual(
response_data['onboarding_link'],
resolve_exam_url_for_learning_mfe(self.course_id, self.onboarding_exam.content_id)
)

def test_no_exam_attempts(self):
"""
Test that the onboarding status is None if there are no exam attempts
Expand Down
7 changes: 0 additions & 7 deletions edx_proctoring/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,6 @@
views.ProctoredExamAttemptView.as_view(),
name='proctored_exam.exam_attempts'
),
# TODO: remove url after updating frontend-lib-special-exams
url(
r'edx_proctoring/v1/proctored_exam/attempt/course_id/{}/content_id/{}$'.format(
settings.COURSE_ID_PATTERN, CONTENT_ID_PATTERN),
views.ProctoredExamAttemptView.as_view(),
name='proctored_exam.exam_attempts_old'
),
url(
r'edx_proctoring/v1/proctored_exam/settings/exam_id/(?P<exam_id>\d+)/$',
views.ProctoredSettingsView.as_view(),
Expand Down
Loading

0 comments on commit e1bf5ef

Please sign in to comment.