diff --git a/common/djangoapps/course_modes/helpers.py b/common/djangoapps/course_modes/helpers.py index b56f1f71df..c9a7e0926e 100644 --- a/common/djangoapps/course_modes/helpers.py +++ b/common/djangoapps/course_modes/helpers.py @@ -10,7 +10,6 @@ from requests.exceptions import ConnectionError, Timeout # pylint: disable=rede from slumber.exceptions import SlumberBaseException from common.djangoapps.course_modes.models import CourseMode -from common.djangoapps.student.helpers import VERIFY_STATUS_APPROVED, VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED # lint-amnesty, pylint: disable=line-too-long from openedx.core.djangoapps.commerce.utils import ecommerce_api_client DISPLAY_VERIFIED = "verified" @@ -21,7 +20,7 @@ DISPLAY_PROFESSIONAL = "professional" LOGGER = logging.getLogger(__name__) -def enrollment_mode_display(mode, verification_status, course_id): +def enrollment_mode_display(mode, course_id): """ Select appropriate display strings and CSS classes. Uses mode and verification status to select appropriate display strings and CSS classes @@ -38,19 +37,13 @@ def enrollment_mode_display(mode, verification_status, course_id): image_alt = '' enrollment_title = '' enrollment_value = '' - display_mode = _enrollment_mode_display(mode, verification_status, course_id) + display_mode = _enrollment_mode_display(mode, course_id) if display_mode == DISPLAY_VERIFIED: - if settings.FEATURES.get('ENABLE_INTEGRITY_SIGNATURE') or verification_status == VERIFY_STATUS_APPROVED: - enrollment_title = _("You're enrolled as a verified student") - enrollment_value = _("Verified") - show_image = True - image_alt = _("ID Verified Ribbon/Badge") - elif verification_status in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED]: - enrollment_title = _("Your verification is pending") - enrollment_value = _("Verified: Pending Verification") - show_image = True - image_alt = _("ID verification pending") + enrollment_title = _("You're enrolled as a verified student") + enrollment_value = _("Verified") + show_image = True + image_alt = _("ID Verified Ribbon/Badge") elif display_mode == DISPLAY_HONOR: enrollment_title = _("You're enrolled as an honor code student") enrollment_value = _("Honor Code") @@ -67,7 +60,7 @@ def enrollment_mode_display(mode, verification_status, course_id): } -def _enrollment_mode_display(enrollment_mode, verification_status, course_id): +def _enrollment_mode_display(enrollment_mode, course_id): """Checking enrollment mode and status and returns the display mode Args: enrollment_mode (str): enrollment mode. @@ -79,15 +72,9 @@ def _enrollment_mode_display(enrollment_mode, verification_status, course_id): course_mode_slugs = [mode.slug for mode in CourseMode.modes_for_course(course_id)] if enrollment_mode == CourseMode.VERIFIED: - if ( - settings.FEATURES.get('ENABLE_INTEGRITY_SIGNATURE') - or verification_status in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED] - ): - display_mode = DISPLAY_VERIFIED - elif DISPLAY_HONOR in course_mode_slugs: + display_mode = DISPLAY_VERIFIED + if DISPLAY_HONOR in course_mode_slugs: display_mode = DISPLAY_HONOR - else: - display_mode = DISPLAY_AUDIT elif enrollment_mode in [CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE]: display_mode = DISPLAY_PROFESSIONAL else: diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index 14faaf907d..267b0259e1 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -11,7 +11,6 @@ from datetime import timedelta from unittest.mock import patch import ddt -from django.conf import settings from django.core.exceptions import ValidationError from django.test import TestCase, override_settings from django.utils.timezone import now @@ -342,49 +341,15 @@ class CourseModeModelTest(TestCase): assert exc.messages == ['Professional education modes are not allowed to have expiration_datetime set.'] @ddt.data( - ("verified", "verify_need_to_verify", True), - ("verified", "verify_submitted", True), - ("verified", "verify_approved", True), - ("verified", 'dummy', True), - ("verified", None, True), - ('honor', None, True), - ('honor', 'dummy', True), - ('audit', None, True), - ('professional', None, True), - ('no-id-professional', None, True), - ('no-id-professional', 'dummy', True), - ("verified", "verify_need_to_verify", False), - ("verified", "verify_submitted", False), - ("verified", "verify_approved", False), - ("verified", 'dummy', False), - ("verified", None, False), - ('honor', None, False), - ('honor', 'dummy', False), - ('audit', None, False), - ('professional', None, False), - ('no-id-professional', None, False), - ('no-id-professional', 'dummy', False) + "verified", + "honor", + "audit", + "professional", + "no-id-professional", ) - @ddt.unpack - def test_enrollment_mode_display(self, mode, verification_status, enable_integrity_signature): - - with patch.dict(settings.FEATURES, ENABLE_INTEGRITY_SIGNATURE=enable_integrity_signature): - if mode == "verified": - assert enrollment_mode_display(mode, verification_status, self.course_key) ==\ - self._enrollment_display_modes_dicts(mode, verification_status, enable_integrity_signature) - assert enrollment_mode_display(mode, verification_status, self.course_key) ==\ - self._enrollment_display_modes_dicts(mode, verification_status, enable_integrity_signature) - assert enrollment_mode_display(mode, verification_status, self.course_key) ==\ - self._enrollment_display_modes_dicts(mode, verification_status, enable_integrity_signature) - elif mode == "honor": - assert enrollment_mode_display(mode, verification_status, self.course_key) ==\ - self._enrollment_display_modes_dicts(mode, mode, enable_integrity_signature) - elif mode == "audit": - assert enrollment_mode_display(mode, verification_status, self.course_key) ==\ - self._enrollment_display_modes_dicts(mode, mode, enable_integrity_signature) - elif mode == "professional": - assert enrollment_mode_display(mode, verification_status, self.course_key) ==\ - self._enrollment_display_modes_dicts(mode, mode, enable_integrity_signature) + def test_enrollment_mode_display(self, mode): + assert enrollment_mode_display(mode, self.course_key) == \ + self._enrollment_display_modes_dicts(mode) @ddt.data( (['honor', 'verified', 'credit'], ['honor', 'verified']), @@ -408,30 +373,23 @@ class CourseModeModelTest(TestCase): all_modes = CourseMode.modes_for_course_dict(self.course_key, only_selectable=False) self.assertCountEqual(list(all_modes.keys()), available_modes) - def _enrollment_display_modes_dicts(self, mode, dict_type, enable_flag): + def _enrollment_display_modes_dicts(self, mode): """ Helper function to generate the enrollment display mode dict. """ dict_keys = ['enrollment_title', 'enrollment_value', 'show_image', 'image_alt', 'display_mode'] display_values = { - "verify_need_to_verify": ["Your verification is pending", "Verified: Pending Verification", True, - 'ID verification pending', 'verified'], - "verify_approved": ["You're enrolled as a verified student", "Verified", True, 'ID Verified Ribbon/Badge', - 'verified'], - "verify_none": ["", "", False, '', 'audit'], + "verified": ["You're enrolled as a verified student", "Verified", True, 'ID Verified Ribbon/Badge', + 'verified'], "honor": ["You're enrolled as an honor code student", "Honor Code", False, '', 'honor'], "audit": ["", "", False, '', 'audit'], "professional": ["You're enrolled as a professional education student", "Professional Ed", False, '', - 'professional'] + 'professional'], + "no-id-professional": ["You're enrolled as a professional education student", "Professional Ed", False, '', + 'professional'], } - if mode == 'verified' and enable_flag: - return dict(list(zip(dict_keys, display_values.get('verify_approved')))) - elif dict_type in ['verify_need_to_verify', 'verify_submitted']: - return dict(list(zip(dict_keys, display_values.get('verify_need_to_verify')))) - elif dict_type is None or dict_type == 'dummy': - return dict(list(zip(dict_keys, display_values.get('verify_none')))) - else: - return dict(list(zip(dict_keys, display_values.get(dict_type)))) + + return dict(list(zip(dict_keys, display_values.get(mode)))) def test_expiration_datetime_explicitly_set(self): """ Verify that setting the expiration_date property sets the explicit flag. """ diff --git a/common/djangoapps/student/tests/test_email.py b/common/djangoapps/student/tests/test_email.py index dde8a0daad..4c1774251e 100644 --- a/common/djangoapps/student/tests/test_email.py +++ b/common/djangoapps/student/tests/test_email.py @@ -29,7 +29,6 @@ from common.djangoapps.student.views import ( ) from common.djangoapps.third_party_auth.views import inactive_user_view from common.djangoapps.util.testing import EventTestMixin -from lms.djangoapps.verify_student.services import IDVerificationService from openedx.core.djangoapps.ace_common.tests.mixins import EmailTemplateTagMixin from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme @@ -237,7 +236,6 @@ class ProctoringRequirementsEmailTests(EmailTemplateTagMixin, ModuleStoreTestCas send_proctoring_requirements_email(context) self._assert_email() - @patch.dict('django.conf.settings.FEATURES', {'ENABLE_INTEGRITY_SIGNATURE': True}) def test_send_proctoring_requirements_email_honor(self): self.course = CourseFactory( display_name='honor code on course', @@ -259,13 +257,10 @@ class ProctoringRequirementsEmailTests(EmailTemplateTagMixin, ModuleStoreTestCas assert message.subject == f"Proctoring requirements for {self.course.display_name}" - appears, does_not_appear = self._get_fragments() + appears = self._get_fragments() for fragment in appears: self.assertIn(fragment, text) self.assertIn(fragment, html) - for fragment in does_not_appear: - self.assertNotIn(fragment, text) - self.assertNotIn(fragment, html) def _get_fragments(self): """ @@ -273,7 +268,6 @@ class ProctoringRequirementsEmailTests(EmailTemplateTagMixin, ModuleStoreTestCas """ course_module = modulestore().get_course(self.course.id) proctoring_provider = capwords(course_module.proctoring_provider.replace('_', ' ')) - id_verification_url = IDVerificationService.get_verify_location() fragments = [ ( "You are enrolled in {} at {}. This course contains proctored exams.".format( @@ -292,14 +286,7 @@ class ProctoringRequirementsEmailTests(EmailTemplateTagMixin, ModuleStoreTestCas ), settings.PROCTORING_SETTINGS.get('LINK_URLS', {}).get('faq', ''), ] - idv_fragments = [ - escape("Before taking a graded proctored exam, you must have approved ID verification photos."), - id_verification_url, - ] - if not settings.FEATURES.get('ENABLE_INTEGRITY_SIGNATURE'): - fragments.extend(idv_fragments) - return (fragments, []) - return (fragments, idv_fragments) + return fragments @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") diff --git a/common/djangoapps/student/tests/test_verification_status.py b/common/djangoapps/student/tests/test_verification_status.py deleted file mode 100644 index 372e4724be..0000000000 --- a/common/djangoapps/student/tests/test_verification_status.py +++ /dev/null @@ -1,469 +0,0 @@ -"""Tests for per-course verification status on the dashboard. """ - - -import unittest -from datetime import datetime, timedelta -from unittest.mock import patch - -import ddt -import six -from django.conf import settings -from django.test import override_settings -from django.urls import reverse -from django.utils.timezone import now -from pytz import UTC - -from common.djangoapps.course_modes.tests.factories import CourseModeFactory -from common.djangoapps.student.helpers import ( - VERIFY_STATUS_APPROVED, - VERIFY_STATUS_MISSED_DEADLINE, - VERIFY_STATUS_NEED_TO_REVERIFY, - VERIFY_STATUS_NEED_TO_VERIFY, - VERIFY_STATUS_RESUBMITTED, - VERIFY_STATUS_SUBMITTED -) -from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory -from common.djangoapps.util.testing import UrlResetMixin -from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order - - -@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) -@override_settings(PLATFORM_NAME='edX') -@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -@ddt.ddt -class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): - """Tests for per-course verification status on the dashboard. """ - - PAST = 'past' - FUTURE = 'future' - DATES = { - PAST: datetime.now(UTC) - timedelta(days=5), - FUTURE: datetime.now(UTC) + timedelta(days=5), - None: None, - } - - URLCONF_MODULES = ['lms.djangoapps.verify_student.urls'] - - def setUp(self): - # Invoke UrlResetMixin - super().setUp() - - self.user = UserFactory(password="edx") - self.course = CourseFactory.create() - success = self.client.login(username=self.user.username, password="edx") - assert success, 'Did not log in successfully' - self.dashboard_url = reverse('dashboard') - - def test_enrolled_as_non_verified(self): - self._setup_mode_and_enrollment(None, "audit") - - # Expect that the course appears on the dashboard - # without any verification messaging - self._assert_course_verification_status(None) - - def test_no_verified_mode_available(self): - # Enroll the student in a verified mode, but don't - # create any verified course mode. - # This won't happen unless someone deletes a course mode, - # but if so, make sure we handle it gracefully. - CourseEnrollmentFactory( - course_id=self.course.id, - user=self.user, - mode="verified" - ) - - # Continue to show the student as needing to verify. - # The student is enrolled as verified, so we might as well let them - # complete verification. We'd need to change their enrollment mode - # anyway to ensure that the student is issued the correct kind of certificate. - self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY) - - def test_need_to_verify_no_expiration(self): - self._setup_mode_and_enrollment(None, "verified") - - # Since the student has not submitted a photo verification, - # the student should see a "need to verify" message - self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY) - - # Start the photo verification process, but do not submit - # Since we haven't submitted the verification, we should still - # see the "need to verify" message - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY) - - # Upload images, but don't submit to the verification service - # We should still need to verify - attempt.mark_ready() - self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY) - - def test_need_to_verify_expiration(self): - self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified") - response = self.client.get(self.dashboard_url) - self.assertContains(response, self.BANNER_ALT_MESSAGES[VERIFY_STATUS_NEED_TO_VERIFY]) - self.assertContains(response, "You only have 4 days left to verify for this course.") - - @ddt.data(None, FUTURE) - def test_waiting_approval(self, expiration): - self._setup_mode_and_enrollment(self.DATES[expiration], "verified") - - # The student has submitted a photo verification - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.mark_ready() - attempt.submit() - - # Now the student should see a "verification submitted" message - self._assert_course_verification_status(VERIFY_STATUS_SUBMITTED) - - @ddt.data(None, FUTURE) - def test_fully_verified(self, expiration): - self._setup_mode_and_enrollment(self.DATES[expiration], "verified") - - # The student has an approved verification - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.mark_ready() - attempt.submit() - attempt.approve() - - # Expect that the successfully verified message is shown - self._assert_course_verification_status(VERIFY_STATUS_APPROVED) - - # Check that the "verification good until" date is displayed - response = self.client.get(self.dashboard_url) - self.assertContains(response, attempt.expiration_datetime.strftime("%m/%d/%Y")) - - @patch("lms.djangoapps.verify_student.services.is_verification_expiring_soon") - def test_verify_resubmit_button_on_dashboard(self, mock_expiry): - mock_expiry.return_value = True - SoftwareSecurePhotoVerification.objects.create( - user=self.user, - status='approved', - expiration_date=now() + timedelta(days=1) - ) - response = self.client.get(self.dashboard_url) - self.assertContains(response, "Resubmit Verification") - - mock_expiry.return_value = False - response = self.client.get(self.dashboard_url) - self.assertNotContains(response, "Resubmit Verification") - - def test_missed_verification_deadline(self): - # Expiration date in the past - self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified") - - # The student does NOT have an approved verification - # so the status should show that the student missed the deadline. - self._assert_course_verification_status(VERIFY_STATUS_MISSED_DEADLINE) - - def test_missed_verification_deadline_verification_was_expired(self): - # Expiration date in the past - self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified") - - # Create a verification, but the expiration date of the verification - # occurred before the deadline. - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.mark_ready() - attempt.submit() - attempt.approve() - attempt.expiration_date = self.DATES[self.PAST] - timedelta(days=900) - attempt.save() - - # The student didn't have an approved verification at the deadline, - # so we should show that the student missed the deadline. - self._assert_course_verification_status(VERIFY_STATUS_MISSED_DEADLINE) - - def test_missed_verification_deadline_but_later_verified(self): - # Expiration date in the past - self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified") - - # Successfully verify, but after the deadline has already passed - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.mark_ready() - attempt.submit() - attempt.approve() - attempt.expiration_date = self.DATES[self.PAST] - timedelta(days=900) - attempt.save() - - # The student didn't have an approved verification at the deadline, - # so we should show that the student missed the deadline. - self._assert_course_verification_status(VERIFY_STATUS_MISSED_DEADLINE) - - def test_verification_denied(self): - # Expiration date in the future - self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified") - - # Create a verification with the specified status - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.mark_ready() - attempt.submit() - attempt.deny("Not valid!") - - # Since this is not a status we handle, don't display any - # messaging relating to verification - self._assert_course_verification_status(None) - - def test_verification_error(self): - # Expiration date in the future - self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified") - - # Create a verification with the specified status - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.status = "must_retry" - attempt.system_error("Error!") - - # Since this is not a status we handle, don't display any - # messaging relating to verification - self._assert_course_verification_status(None) - - @override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10}) - def test_verification_will_expire_by_deadline(self): - # Expiration date in the future - self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified") - - # Create a verification attempt that: - # 1) Is current (submitted in the last year) - # 2) Will expire by the deadline for the course - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.mark_ready() - attempt.submit() - attempt.approve() - attempt.save() - - # Verify that learner can submit photos if verification is set to expire soon. - self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_REVERIFY) - - @override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10}) - def test_reverification_submitted_with_current_approved_verificaiton(self): - # Expiration date in the future - self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified") - - # Create a verification attempt that is approved but expiring soon - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.mark_ready() - attempt.submit() - attempt.approve() - attempt.save() - - # Verify that learner can submit photos if verification is set to expire soon. - self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_REVERIFY) - - # Submit photos for reverification - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.mark_ready() - attempt.submit() - - # Expect that learner has submitted photos for reverfication and their - # previous verification is set to expired soon. - self._assert_course_verification_status(VERIFY_STATUS_RESUBMITTED) - - def test_verification_occurred_after_deadline(self): - # Expiration date in the past - self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified") - - # The deadline has passed, and we've asked the student - # to reverify (through the support team). - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.mark_ready() - attempt.submit() - - # Expect that the user's displayed enrollment mode is verified. - self._assert_course_verification_status(VERIFY_STATUS_APPROVED) - - def test_with_two_verifications(self): - # checking if a user has two verification and but most recent verification course deadline is expired - - self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified") - - # The student has an approved verification - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.mark_ready() - attempt.submit() - attempt.approve() - # Making created at to previous date to differentiate with 2nd attempt. - attempt.created_at = datetime.now(UTC) - timedelta(days=1) - attempt.save() - - # Expect that the successfully verified message is shown - self._assert_course_verification_status(VERIFY_STATUS_APPROVED) - - # Check that the "verification good until" date is displayed - response = self.client.get(self.dashboard_url) - self.assertContains(response, attempt.expiration_datetime.strftime("%m/%d/%Y")) - - # Adding another verification with different course. - # Its created_at is greater than course deadline. - course2 = CourseFactory.create() - CourseModeFactory.create( - course_id=course2.id, - mode_slug="verified", - expiration_datetime=self.DATES[self.PAST] - ) - CourseEnrollmentFactory( - course_id=course2.id, - user=self.user, - mode="verified" - ) - - # The student has an approved verification - attempt2 = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt2.mark_ready() - attempt2.submit() - attempt2.approve() - attempt2.save() - - # Mark the attemp2 as approved so its date will appear on dasboard. - self._assert_course_verification_status(VERIFY_STATUS_APPROVED) - response2 = self.client.get(self.dashboard_url) - self.assertContains(response2, attempt2.expiration_datetime.strftime("%m/%d/%Y")) - self.assertContains(response2, attempt2.expiration_datetime.strftime("%m/%d/%Y"), count=2) - - @patch.dict(settings.FEATURES, {'ENABLE_INTEGRITY_SIGNATURE': True}) - @ddt.data( - None, - 'past', - 'future' - ) - def test_verify_message_idv_disabled(self, deadline_key): - if deadline_key: - self._setup_mode_and_enrollment(self.DATES[deadline_key], "verified") - else: - self._setup_mode_and_enrollment(None, "verified") - - self._assert_course_verification_status(None, "verified") - - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - self._assert_course_verification_status(None, "verified") - attempt.mark_ready() - self._assert_course_verification_status(None, "verified") - attempt.submit() - self._assert_course_verification_status(None, "verified") - attempt.approve() - self._assert_course_verification_status(None, "verified") - attempt.expiration_date = self.DATES[self.PAST] - timedelta(days=900) - attempt.save() - self._assert_course_verification_status(None, "verified") - - @ddt.data(True, False) - def test_integrity_disables_sidebar(self, enable_integrity_signature): - self._setup_mode_and_enrollment(None, "verified") - - #no sidebar when no IDV yet - response = self.client.get(self.dashboard_url) - self.assertNotContains(response, "profile-sidebar") - - # The student has an approved verification - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.mark_ready() - attempt.submit() - attempt.approve() - - # sidebar only appears after IDV if integrity signature setting is not on - with patch.dict(settings.FEATURES, {'ENABLE_INTEGRITY_SIGNATURE': enable_integrity_signature}): - response = self.client.get(self.dashboard_url) - if enable_integrity_signature: - self.assertNotContains(response, "profile-sidebar") - else: - self.assertContains(response, "profile-sidebar") - - def _setup_mode_and_enrollment(self, deadline, enrollment_mode): - """Create a course mode and enrollment. - - Arguments: - deadline (datetime): The deadline for submitting your verification. - enrollment_mode (str): The mode of the enrollment. - - """ - CourseModeFactory.create( - course_id=self.course.id, - mode_slug="verified", - expiration_datetime=deadline - ) - CourseEnrollmentFactory( - course_id=self.course.id, - user=self.user, - mode=enrollment_mode - ) - VerificationDeadline.set_deadline(self.course.id, deadline) - - BANNER_ALT_MESSAGES = { - VERIFY_STATUS_NEED_TO_VERIFY: "ID verification pending", - VERIFY_STATUS_SUBMITTED: "ID verification pending", - VERIFY_STATUS_APPROVED: "ID Verified Ribbon/Badge", - } - - NOTIFICATION_MESSAGES = { - VERIFY_STATUS_NEED_TO_VERIFY: [ - "You still need to verify for this course.", - "Verification not yet complete" - ], - VERIFY_STATUS_SUBMITTED: ["You have submitted your verification information."], - VERIFY_STATUS_RESUBMITTED: ["You have submitted your reverification information."], - VERIFY_STATUS_APPROVED: ["You have successfully verified your ID with edX"], - VERIFY_STATUS_NEED_TO_REVERIFY: ["Your current verification will expire soon."] - } - - MODE_CLASSES = { - None: "audit", - VERIFY_STATUS_NEED_TO_VERIFY: "verified", - VERIFY_STATUS_SUBMITTED: "verified", - VERIFY_STATUS_APPROVED: "verified", - VERIFY_STATUS_MISSED_DEADLINE: "audit", - VERIFY_STATUS_NEED_TO_REVERIFY: "audit", - VERIFY_STATUS_RESUBMITTED: "audit" - } - - def _assert_course_verification_status(self, status, enrollment_mode=None): - """Check whether the specified verification status is shown on the dashboard. - - Arguments: - status (str): One of the verification status constants. - If None, check that *none* of the statuses are displayed. - - Raises: - AssertionError - - """ - response = self.client.get(self.dashboard_url) - - # Sanity check: verify that the course is on the page - self.assertContains(response, str(self.course.id)) - - # Verify that the correct banner is rendered on the dashboard - alt_text = self.BANNER_ALT_MESSAGES.get(status) - if alt_text: - self.assertContains(response, alt_text) - - mode = enrollment_mode if enrollment_mode else self.MODE_CLASSES[status] - - # Verify that the correct banner color is rendered - self.assertContains( - response, - f"
- {% filter force_escape %} - {% blocktrans %} - Before taking a graded proctored exam, you must have approved ID verification photos. - {% endblocktrans %} - {% endfilter %} - - {% filter force_escape %} - {% blocktrans %} - You can submit ID verification photos here. - {% endblocktrans %} - {% endfilter %} - -

- {% endif %} -

{% trans "Thank you," as tmsg %}{{ tmsg | force_escape }}
diff --git a/common/templates/student/edx_ace/proctoringrequirements/email/body.txt b/common/templates/student/edx_ace/proctoringrequirements/email/body.txt index 0c45632fea..74b0d00d25 100644 --- a/common/templates/student/edx_ace/proctoringrequirements/email/body.txt +++ b/common/templates/student/edx_ace/proctoringrequirements/email/body.txt @@ -8,10 +8,6 @@ {% blocktrans %}To view proctoring instructions and requirements, please visit: {{ proctoring_requirements_url }}{% endblocktrans %} -{% if idv_required %} -{% blocktrans %}Before taking a graded proctored exam, you must have approved ID verification photos. You can submit ID verification photos here: {{ id_verification_url }}{% endblocktrans %} -{% endif %} - {% trans "Thank you," %} {% blocktrans %}The {{ platform_name }} Team {% endblocktrans %} diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index 77bef6407a..e4955524d6 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -2808,32 +2808,24 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment response = self.client.post(url, {}) self.assertContains(response, success_status) - @ddt.data( - True, - False - ) @valid_problem_location - def test_idv_retirement_student_features_report(self, enable_integrity_signature): - with patch.dict(settings.FEATURES, {'ENABLE_INTEGRITY_SIGNATURE': enable_integrity_signature}): - kwargs = {'course_id': str(self.course.id)} - kwargs.update({'csv': '/csv'}) - url = reverse('get_students_features', kwargs=kwargs) - success_status = 'The enrolled learner profile report is being created.' - with patch('lms.djangoapps.instructor_task.api.submit_calculate_students_features_csv') \ - as mock_task_endpoint: - CourseFinanceAdminRole(self.course.id).add_users(self.instructor) - response = self.client.post(url, {}) - self.assertContains(response, success_status) + def test_idv_retirement_student_features_report(self): + kwargs = {'course_id': str(self.course.id)} + kwargs.update({'csv': '/csv'}) + url = reverse('get_students_features', kwargs=kwargs) + success_status = 'The enrolled learner profile report is being created.' + with patch('lms.djangoapps.instructor_task.api.submit_calculate_students_features_csv') as mock_task_endpoint: + CourseFinanceAdminRole(self.course.id).add_users(self.instructor) + response = self.client.post(url, {}) + self.assertContains(response, success_status) - # assert that if the integrity signature is enabled, the verification - # status is not included as a query feature - args = mock_task_endpoint.call_args.args - self.assertEqual(len(args), 3) - query_features = args[2] - if not enable_integrity_signature: - self.assertIn('verification_status', query_features) - else: - self.assertNotIn('verification_status', query_features) + # assert that if the integrity signature is enabled, the verification + # status is not included as a query feature + args = mock_task_endpoint.call_args.args + self.assertEqual(len(args), 3) + query_features = args[2] + + self.assertNotIn('verification_status', query_features) def test_get_ora2_responses_success(self): url = reverse('export_ora2_data', kwargs={'course_id': str(self.course.id)}) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 81b9b6b487..b59b425ceb 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -1425,8 +1425,7 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red query_features = [ 'id', 'username', 'name', 'email', 'language', 'location', 'year_of_birth', 'gender', 'level_of_education', 'mailing_address', - 'goals', 'enrollment_mode', 'verification_status', - 'last_login', 'date_joined', 'external_user_key' + 'goals', 'enrollment_mode', 'last_login', 'date_joined', 'external_user_key' ] # Provide human-friendly and translatable names for these features. These names @@ -1445,7 +1444,6 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red 'mailing_address': _('Mailing Address'), 'goals': _('Goals'), 'enrollment_mode': _('Enrollment Mode'), - 'verification_status': _('Verification Status'), 'last_login': _('Last Login'), 'date_joined': _('Date Joined'), 'external_user_key': _('External User Key'), @@ -1460,11 +1458,6 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red query_features.append('team') query_features_names['team'] = _('Team') - if settings.FEATURES.get('ENABLE_INTEGRITY_SIGNATURE'): - if 'verification_status' in query_features: - query_features.remove('verification_status') - query_features_names.pop('verification_status') - # For compatibility reasons, city and country should always appear last. query_features.append('city') query_features_names['city'] = _('City') diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 5c4443c6bb..59c5a77e58 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -214,13 +214,12 @@ from common.djangoapps.student.models import CourseEnrollment course_mode_info = all_course_modes.get(session_id) is_paid_course = True if entitlement else (session_id in enrolled_courses_either_paid) is_course_voucher_refundable = (session_id in enrolled_courses_voucher_refundable) - course_verification_status = verification_status_by_course.get(session_id, {}) course_requirements = courses_requirements_not_met.get(session_id) related_programs = inverted_programs.get(six.text_type(entitlement.course_uuid if is_unfulfilled_entitlement else session_id)) show_consent_link = (session_id in consent_required_courses) resume_button_url = resume_button_urls[dashboard_index] %> - <%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, entitlement_expiration_date=entitlement_expiration_date, entitlement_expired_at=entitlement_expired_at, show_courseware_link=show_courseware_link, cert_status=cert_status, can_refund_entitlement=can_refund_entitlement, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_voucher_refundable=is_course_voucher_refundable, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name, resume_button_url=resume_button_url, partner_managed_enrollment=partner_managed_enrollment' /> + <%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, entitlement_expiration_date=entitlement_expiration_date, entitlement_expired_at=entitlement_expired_at, show_courseware_link=show_courseware_link, cert_status=cert_status, can_refund_entitlement=can_refund_entitlement, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_voucher_refundable=is_course_voucher_refundable, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name, resume_button_url=resume_button_url, partner_managed_enrollment=partner_managed_enrollment' /> % endfor % if show_load_all_courses_link:
@@ -301,20 +300,6 @@ from common.djangoapps.student.models import CourseEnrollment <%include file='dashboard/_dashboard_announcements.html' /> % endif - % if display_sidebar_on_dashboard: -

-
- -
- -
- % endif diff --git a/lms/templates/dashboard/_dashboard_certificate_information.html b/lms/templates/dashboard/_dashboard_certificate_information.html index 861c724c40..51e2c58dc6 100644 --- a/lms/templates/dashboard/_dashboard_certificate_information.html +++ b/lms/templates/dashboard/_dashboard_certificate_information.html @@ -1,4 +1,4 @@ -<%page expression_filter="h" args="cert_status, course_ended_not_passing, course_overview, enrollment, reverify_link" /> +<%page expression_filter="h" args="cert_status, course_ended_not_passing, course_overview, enrollment" /> <%! from django.urls import reverse @@ -157,12 +157,6 @@ else: % endif % endif - % if cert_status['status'] == 'downloadable' and enrollment.mode == 'verified' and cert_status['mode'] == 'honor': -
- ${_('Since we did not have a valid set of verification photos from you when your {cert_name_long} was generated, we could not grant you a verified {cert_name_short}. An honor code {cert_name_short} has been granted instead.').format(cert_name_short=cert_name_short, cert_name_long=cert_name_long)} -
- % endif - % endif % elif course_ended_not_passing: diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index b291eeb297..9d0cd188d2 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -1,4 +1,4 @@ -<%page args="course_overview, enrollment, entitlement, entitlement_session, course_card_index, is_unfulfilled_entitlement, is_fulfilled_entitlement, entitlement_available_sessions, entitlement_expiration_date, entitlement_expired_at, show_courseware_link, cert_status, can_refund_entitlement, can_unenroll, credit_status, show_email_settings, course_mode_info, is_paid_course, is_course_voucher_refundable, verification_status, course_requirements, dashboard_index, share_settings, related_programs, display_course_modes_on_dashboard, show_consent_link, enterprise_customer_name, resume_button_url, partner_managed_enrollment" expression_filter="h"/> +<%page args="course_overview, enrollment, entitlement, entitlement_session, course_card_index, is_unfulfilled_entitlement, is_fulfilled_entitlement, entitlement_available_sessions, entitlement_expiration_date, entitlement_expired_at, show_courseware_link, cert_status, can_refund_entitlement, can_unenroll, credit_status, show_email_settings, course_mode_info, is_paid_course, is_course_voucher_refundable, course_requirements, dashboard_index, share_settings, related_programs, display_course_modes_on_dashboard, show_consent_link, enterprise_customer_name, resume_button_url, partner_managed_enrollment" expression_filter="h"/> <%! import datetime @@ -65,7 +65,6 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG <% course_verified_certs = enrollment_mode_display( enrollment.mode, - verification_status.get('status'), course_overview.id ) %> @@ -377,50 +376,6 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG <%include file="_dashboard_show_consent.html" args="course_overview=course_overview, course_target=course_target, enrollment=enrollment, enterprise_customer_name=enterprise_customer_name"/> %endif - % if verification_status.get('should_display') and verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_RESUBMITTED, VERIFY_STATUS_APPROVED, VERIFY_STATUS_NEED_TO_REVERIFY]: -
- % if verification_status['status'] == VERIFY_STATUS_NEED_TO_VERIFY: -
- % if verification_status['days_until_deadline'] is not None: -

${_('To earn a certificate, you must verify your ID')}

-

${ungettext( - 'You only have {days} day left to verify for this course.', - 'You only have {days} days left to verify for this course.', - verification_status['days_until_deadline'] - ).format(days=verification_status['days_until_deadline'])}

- % else: -

${_('Almost there!')}

-

${_('You still need to verify for this course.')}

- % endif -
- - % elif verification_status['status'] == VERIFY_STATUS_SUBMITTED: -

${_('You have submitted your verification information.')}

-

${_('You will see a message on your dashboard when the verification process is complete (usually within 5-7 days).')}

- % elif verification_status['status'] == VERIFY_STATUS_RESUBMITTED: -

${_('Your current verification will expire soon!')}

-

${_('You have submitted your reverification information. You will see a message on your dashboard when the verification process is complete (usually within 5-7 days).')}

- % elif verification_status['status'] == VERIFY_STATUS_APPROVED: -

${_('You have successfully verified your ID with {platform_name}').format(platform_name=settings.PLATFORM_NAME)}

- % if verification_status.get('verification_good_until') is not None: -

${_('Your current verification is effective until {date}.').format(date=verification_status['verification_good_until'])} - % endif - % elif verification_status['status'] == VERIFY_STATUS_NEED_TO_REVERIFY: -

${_('Your current verification will expire soon.')}

- ## Translators: start_link and end_link will be replaced with HTML tags; - ## please do not translate these. -

${Text(_('Your current verification will expire in {days} days. {start_link}Re-verify your identity now{end_link} using a webcam and a government-issued photo ID.')).format( - start_link=HTML('').format(href=IDVerificationService.get_verify_location()), - end_link=HTML(''), - days=settings.VERIFY_STUDENT.get("EXPIRING_SOON_WINDOW") - )} -

- % endif -
- % endif - % if display_course_upgrade:
diff --git a/lms/templates/dashboard/_dashboard_status_verification.html b/lms/templates/dashboard/_dashboard_status_verification.html deleted file mode 100644 index 0c22e169ce..0000000000 --- a/lms/templates/dashboard/_dashboard_status_verification.html +++ /dev/null @@ -1,57 +0,0 @@ -<%page expression_filter="h"/> -<%namespace name='static' file='../static_content.html'/> -<%! -from django.conf import settings -from django.urls import reverse -from django.utils.translation import gettext as _ -from lms.djangoapps.verify_student.services import IDVerificationService -%> - -%if verification_display: - %if verification_status == 'approved': -
  • - ${_("Current ID Verification Status: Approved")} -

    ${_("Your {platform_name} ID verification has been approved. Your ID verification is effective for two years after submission.").format(platform_name=settings.PLATFORM_NAME)}

    - %if verification_expiry: -

    ${_("Warning")}: ${_("Your photo ID verification expires on {verification_expiry}. Please be aware photo verification can take up to three days once initiated and you will not be able to earn a certificate or take a proctored exam until approved.").format(verification_expiry=verification_expiry)}

    - - %endif -
  • - %elif verification_status == 'pending': -
  • - ${_("Current ID Verification Status: Pending")} -

    ${_("Your {platform_name} ID verification is pending. Your ID verification information has been submitted and will be reviewed shortly.").format(platform_name=settings.PLATFORM_NAME)}

    -
  • - %elif verification_status in ['denied','must_reverify', 'must_retry']: -
  • - ${_("Current ID Verification Status: Denied")} -

    - ${_("Your ID verification submission was not accepted. To receive a verified certificate, you must submit a new photo of yourself and your government-issued photo ID before the verification deadline for your course.")} - - %if verification_errors: -

    - ${_("Your ID verification was denied for the following reasons:")}
    -

      - %for error in verification_errors: -
    • ${error}
    • - %endfor -
    - %endif -

    - -
  • - %elif verification_status == 'expired': -
  • - ${_("Current ID Verification Status: Expired")} -

    ${_("Your ID verification has expired. To receive a verified certificate, you must submit a new photo of yourself and your government-issued photo ID before the verification deadline for your course.")}

    -

    ${_("Warning")}: ${_(" Please be aware photo verification can take up to three days once initiated and you will not be able to earn a certificate or take a proctored exam until approved.")}

    - -
  • - %endif -%endif diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 856ccb48ee..676315c03f 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -481,7 +481,7 @@ edx-opaque-keys[django]==2.3.0 # xmodule edx-organizations==6.11.1 # via -r requirements/edx/base.in -edx-proctoring==4.9.0 +edx-proctoring==4.10.0 # via # -r requirements/edx/base.in # edx-proctoring-proctortrack diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index cb7f353118..ebce84fd53 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -590,7 +590,7 @@ edx-opaque-keys[django]==2.3.0 # xmodule edx-organizations==6.11.1 # via -r requirements/edx/testing.txt -edx-proctoring==4.9.0 +edx-proctoring==4.10.0 # via # -r requirements/edx/testing.txt # edx-proctoring-proctortrack diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 4183088a8c..9298e491e7 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -575,7 +575,7 @@ edx-opaque-keys[django]==2.3.0 # xmodule edx-organizations==6.11.1 # via -r requirements/edx/base.txt -edx-proctoring==4.9.0 +edx-proctoring==4.10.0 # via # -r requirements/edx/base.txt # edx-proctoring-proctortrack diff --git a/themes/edx.org/lms/templates/dashboard.html b/themes/edx.org/lms/templates/dashboard.html index 1594e4ad4c..aec777c87a 100644 --- a/themes/edx.org/lms/templates/dashboard.html +++ b/themes/edx.org/lms/templates/dashboard.html @@ -244,14 +244,13 @@ from common.djangoapps.student.models import CourseEnrollment course_mode_info = all_course_modes.get(session_id) is_paid_course = True if entitlement else (session_id in enrolled_courses_either_paid) is_course_voucher_refundable = (session_id in enrolled_courses_voucher_refundable) - course_verification_status = verification_status_by_course.get(session_id, {}) course_requirements = courses_requirements_not_met.get(session_id) related_programs = inverted_programs.get(six.text_type(entitlement.course_uuid if is_unfulfilled_entitlement else session_id)) show_consent_link = (session_id in consent_required_courses) course_overview = enrollment.course_overview resume_button_url = resume_button_urls[dashboard_index] %> - <%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, entitlement_expiration_date=entitlement_expiration_date, entitlement_expired_at=entitlement_expired_at, show_courseware_link=show_courseware_link, cert_status=cert_status, can_refund_entitlement=can_refund_entitlement, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_voucher_refundable=is_course_voucher_refundable, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name, resume_button_url=resume_button_url, partner_managed_enrollment=partner_managed_enrollment' /> + <%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, entitlement_expiration_date=entitlement_expiration_date, entitlement_expired_at=entitlement_expired_at, show_courseware_link=show_courseware_link, cert_status=cert_status, can_refund_entitlement=can_refund_entitlement, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_voucher_refundable=is_course_voucher_refundable, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name, resume_button_url=resume_button_url, partner_managed_enrollment=partner_managed_enrollment' /> % endfor % if show_load_all_courses_link:
    @@ -350,21 +349,6 @@ from common.djangoapps.student.models import CourseEnrollment
    % endif - - % if display_sidebar_on_dashboard: -
    -
    - -
    - -
    - % endif