chore: remove IDV references for proctoring and student/instructor display (#30146)

Places where IDV is referenced for blocking proctoring or displaying the IDV status to students and instructors should be removed. This includes the use of the ENABLE_INTEGRITY_SIGNATURE setting, which was used in part to deprecate IDV in those specified places. Other areas where ENABLE_INTEGRITY_SIGNATURE is used (such as for showing/hiding the integrity signature, for the verification deadline, or for certs) shall be left as is due to deprecation concerns.
This commit is contained in:
alangsto
2022-04-04 08:11:40 -07:00
committed by GitHub
parent 3abd1c5d9f
commit 681c2f1d6c
18 changed files with 51 additions and 780 deletions

View File

@@ -10,7 +10,6 @@ from requests.exceptions import ConnectionError, Timeout # pylint: disable=rede
from slumber.exceptions import SlumberBaseException from slumber.exceptions import SlumberBaseException
from common.djangoapps.course_modes.models import CourseMode 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 from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
DISPLAY_VERIFIED = "verified" DISPLAY_VERIFIED = "verified"
@@ -21,7 +20,7 @@ DISPLAY_PROFESSIONAL = "professional"
LOGGER = logging.getLogger(__name__) 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. """ Select appropriate display strings and CSS classes.
Uses mode and verification status to 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 = '' image_alt = ''
enrollment_title = '' enrollment_title = ''
enrollment_value = '' 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 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_title = _("You're enrolled as a verified student") enrollment_value = _("Verified")
enrollment_value = _("Verified") show_image = True
show_image = True image_alt = _("ID Verified Ribbon/Badge")
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")
elif display_mode == DISPLAY_HONOR: elif display_mode == DISPLAY_HONOR:
enrollment_title = _("You're enrolled as an honor code student") enrollment_title = _("You're enrolled as an honor code student")
enrollment_value = _("Honor Code") 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 """Checking enrollment mode and status and returns the display mode
Args: Args:
enrollment_mode (str): enrollment mode. 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)] course_mode_slugs = [mode.slug for mode in CourseMode.modes_for_course(course_id)]
if enrollment_mode == CourseMode.VERIFIED: if enrollment_mode == CourseMode.VERIFIED:
if ( display_mode = DISPLAY_VERIFIED
settings.FEATURES.get('ENABLE_INTEGRITY_SIGNATURE') if DISPLAY_HONOR in course_mode_slugs:
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_HONOR display_mode = DISPLAY_HONOR
else:
display_mode = DISPLAY_AUDIT
elif enrollment_mode in [CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE]: elif enrollment_mode in [CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE]:
display_mode = DISPLAY_PROFESSIONAL display_mode = DISPLAY_PROFESSIONAL
else: else:

View File

@@ -11,7 +11,6 @@ from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
import ddt import ddt
from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.utils.timezone import now 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.'] assert exc.messages == ['Professional education modes are not allowed to have expiration_datetime set.']
@ddt.data( @ddt.data(
("verified", "verify_need_to_verify", True), "verified",
("verified", "verify_submitted", True), "honor",
("verified", "verify_approved", True), "audit",
("verified", 'dummy', True), "professional",
("verified", None, True), "no-id-professional",
('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)
) )
@ddt.unpack def test_enrollment_mode_display(self, mode):
def test_enrollment_mode_display(self, mode, verification_status, enable_integrity_signature): assert enrollment_mode_display(mode, self.course_key) == \
self._enrollment_display_modes_dicts(mode)
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)
@ddt.data( @ddt.data(
(['honor', 'verified', 'credit'], ['honor', 'verified']), (['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) all_modes = CourseMode.modes_for_course_dict(self.course_key, only_selectable=False)
self.assertCountEqual(list(all_modes.keys()), available_modes) 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. Helper function to generate the enrollment display mode dict.
""" """
dict_keys = ['enrollment_title', 'enrollment_value', 'show_image', 'image_alt', 'display_mode'] dict_keys = ['enrollment_title', 'enrollment_value', 'show_image', 'image_alt', 'display_mode']
display_values = { display_values = {
"verify_need_to_verify": ["Your verification is pending", "Verified: Pending Verification", True, "verified": ["You're enrolled as a verified student", "Verified", True, 'ID Verified Ribbon/Badge',
'ID verification pending', 'verified'], 'verified'],
"verify_approved": ["You're enrolled as a verified student", "Verified", True, 'ID Verified Ribbon/Badge',
'verified'],
"verify_none": ["", "", False, '', 'audit'],
"honor": ["You're enrolled as an honor code student", "Honor Code", False, '', 'honor'], "honor": ["You're enrolled as an honor code student", "Honor Code", False, '', 'honor'],
"audit": ["", "", False, '', 'audit'], "audit": ["", "", False, '', 'audit'],
"professional": ["You're enrolled as a professional education student", "Professional Ed", False, '', "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')))) return dict(list(zip(dict_keys, display_values.get(mode))))
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))))
def test_expiration_datetime_explicitly_set(self): def test_expiration_datetime_explicitly_set(self):
""" Verify that setting the expiration_date property sets the explicit flag. """ """ Verify that setting the expiration_date property sets the explicit flag. """

View File

@@ -29,7 +29,6 @@ from common.djangoapps.student.views import (
) )
from common.djangoapps.third_party_auth.views import inactive_user_view from common.djangoapps.third_party_auth.views import inactive_user_view
from common.djangoapps.util.testing import EventTestMixin 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.ace_common.tests.mixins import EmailTemplateTagMixin
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme 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) send_proctoring_requirements_email(context)
self._assert_email() self._assert_email()
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_INTEGRITY_SIGNATURE': True})
def test_send_proctoring_requirements_email_honor(self): def test_send_proctoring_requirements_email_honor(self):
self.course = CourseFactory( self.course = CourseFactory(
display_name='honor code on course', 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}" 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: for fragment in appears:
self.assertIn(fragment, text) self.assertIn(fragment, text)
self.assertIn(fragment, html) self.assertIn(fragment, html)
for fragment in does_not_appear:
self.assertNotIn(fragment, text)
self.assertNotIn(fragment, html)
def _get_fragments(self): def _get_fragments(self):
""" """
@@ -273,7 +268,6 @@ class ProctoringRequirementsEmailTests(EmailTemplateTagMixin, ModuleStoreTestCas
""" """
course_module = modulestore().get_course(self.course.id) course_module = modulestore().get_course(self.course.id)
proctoring_provider = capwords(course_module.proctoring_provider.replace('_', ' ')) proctoring_provider = capwords(course_module.proctoring_provider.replace('_', ' '))
id_verification_url = IDVerificationService.get_verify_location()
fragments = [ fragments = [
( (
"You are enrolled in {} at {}. This course contains proctored exams.".format( "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', ''), settings.PROCTORING_SETTINGS.get('LINK_URLS', {}).get('faq', ''),
] ]
idv_fragments = [ return 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)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS")

View File

@@ -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"<article class=\"course {mode}\""
)
# Verify that the correct copy is rendered on the dashboard
if status is not None:
if status in self.NOTIFICATION_MESSAGES:
# Different states might have different messaging
# so in some cases we check several possibilities
# and fail if none of these are found.
found_msg = False
for message in self.NOTIFICATION_MESSAGES[status]:
if six.b(message) in response.content:
found_msg = True
break
fail_msg = "Could not find any of these messages: {expected}".format(
expected=self.NOTIFICATION_MESSAGES[status]
)
assert found_msg, fail_msg
else:
# Combine all possible messages into a single list
all_messages = []
for msg_group in self.NOTIFICATION_MESSAGES.values():
all_messages.extend(msg_group)
# Verify that none of the messages are displayed
for msg in all_messages:
self.assertNotContains(response, msg)

View File

@@ -748,11 +748,6 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
else: else:
redirect_message = '' redirect_message = ''
valid_verification_statuses = ['approved', 'must_reverify', 'pending', 'expired']
display_sidebar_on_dashboard = not settings.FEATURES.get('ENABLE_INTEGRITY_SIGNATURE') and \
verification_status['status'] in valid_verification_statuses and \
verification_status['should_display']
# Filter out any course enrollment course cards that are associated with fulfilled entitlements # Filter out any course enrollment course cards that are associated with fulfilled entitlements
for entitlement in [e for e in course_entitlements if e.enrollment_course_run is not None]: for entitlement in [e for e in course_entitlements if e.enrollment_course_run is not None]:
course_enrollments = [ course_enrollments = [
@@ -804,7 +799,6 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
'show_dashboard_tabs': True, 'show_dashboard_tabs': True,
'disable_courseware_js': True, 'disable_courseware_js': True,
'display_course_modes_on_dashboard': enable_verified_certificates and display_course_modes_on_dashboard, 'display_course_modes_on_dashboard': enable_verified_certificates and display_course_modes_on_dashboard,
'display_sidebar_on_dashboard': display_sidebar_on_dashboard,
'display_sidebar_account_activation_message': not(user.is_active or hide_dashboard_courses_until_activated), 'display_sidebar_account_activation_message': not(user.is_active or hide_dashboard_courses_until_activated),
'display_dashboard_courses': (user.is_active or not hide_dashboard_courses_until_activated), 'display_dashboard_courses': (user.is_active or not hide_dashboard_courses_until_activated),
'empty_dashboard_message': empty_dashboard_message, 'empty_dashboard_message': empty_dashboard_message,

View File

@@ -246,7 +246,6 @@ class ProctoringFields:
@XBlock.wants('proctoring') @XBlock.wants('proctoring')
@XBlock.wants('verification')
@XBlock.wants('gating') @XBlock.wants('gating')
@XBlock.wants('credit') @XBlock.wants('credit')
@XBlock.wants('completion') @XBlock.wants('completion')
@@ -922,7 +921,6 @@ class SequenceBlock(
proctoring_service = self.runtime.service(self, 'proctoring') proctoring_service = self.runtime.service(self, 'proctoring')
credit_service = self.runtime.service(self, 'credit') credit_service = self.runtime.service(self, 'credit')
verification_service = self.runtime.service(self, 'verification')
# Is this sequence designated as a Timed Examination, which includes # Is this sequence designated as a Timed Examination, which includes
# Proctored Exams # Proctored Exams
@@ -949,7 +947,6 @@ class SequenceBlock(
'allow_proctoring_opt_out': self.allow_proctoring_opt_out, 'allow_proctoring_opt_out': self.allow_proctoring_opt_out,
'due_date': self.due, 'due_date': self.due,
'grace_period': self.graceperiod, # lint-amnesty, pylint: disable=no-member 'grace_period': self.graceperiod, # lint-amnesty, pylint: disable=no-member
'is_integrity_signature_enabled': settings.FEATURES.get('ENABLE_INTEGRITY_SIGNATURE'),
} }
# inject the user's credit requirements and fulfillments # inject the user's credit requirements and fulfillments
@@ -960,14 +957,6 @@ class SequenceBlock(
'credit_state': credit_state 'credit_state': credit_state
}) })
# inject verification status
if verification_service:
verification_status = verification_service.get_status(user_id)
context.update({
'verification_status': verification_status['status'],
'reverify_url': verification_service.reverify_url(),
})
# See if the edx-proctoring subsystem wants to present # See if the edx-proctoring subsystem wants to present
# a special view to the student rather # a special view to the student rather
# than the actual sequence content # than the actual sequence content

View File

@@ -56,23 +56,6 @@
{% endfilter %} {% endfilter %}
{% include "ace_common/edx_ace/common/return_to_course_cta.html" with course_cta_text=course_cta_text course_cta_url=proctoring_requirements_url %} {% include "ace_common/edx_ace/common/return_to_course_cta.html" with course_cta_text=course_cta_text course_cta_url=proctoring_requirements_url %}
{% if idv_required %}
<p style="color: rgba(0,0,0,.75);">
{% filter force_escape %}
{% blocktrans %}
Before taking a graded proctored exam, you must have approved ID verification photos.
{% endblocktrans %}
{% endfilter %}
<a href="{{ id_verification_url }}">
{% filter force_escape %}
{% blocktrans %}
You can submit ID verification photos here.
{% endblocktrans %}
{% endfilter %}
</a>
</p>
{% endif %}
<p> <p>
{% trans "Thank you," as tmsg %}{{ tmsg | force_escape }} {% trans "Thank you," as tmsg %}{{ tmsg | force_escape }}
<br /> <br />

View File

@@ -8,10 +8,6 @@
{% blocktrans %}To view proctoring instructions and requirements, please visit: {{ proctoring_requirements_url }}{% endblocktrans %} {% 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," %} {% trans "Thank you," %}
{% blocktrans %}The {{ platform_name }} Team {% endblocktrans %} {% blocktrans %}The {{ platform_name }} Team {% endblocktrans %}

View File

@@ -2808,32 +2808,24 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
response = self.client.post(url, {}) response = self.client.post(url, {})
self.assertContains(response, success_status) self.assertContains(response, success_status)
@ddt.data(
True,
False
)
@valid_problem_location @valid_problem_location
def test_idv_retirement_student_features_report(self, enable_integrity_signature): def test_idv_retirement_student_features_report(self):
with patch.dict(settings.FEATURES, {'ENABLE_INTEGRITY_SIGNATURE': enable_integrity_signature}): kwargs = {'course_id': str(self.course.id)}
kwargs = {'course_id': str(self.course.id)} kwargs.update({'csv': '/csv'})
kwargs.update({'csv': '/csv'}) url = reverse('get_students_features', kwargs=kwargs)
url = reverse('get_students_features', kwargs=kwargs) success_status = 'The enrolled learner profile report is being created.'
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:
with patch('lms.djangoapps.instructor_task.api.submit_calculate_students_features_csv') \ CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
as mock_task_endpoint: response = self.client.post(url, {})
CourseFinanceAdminRole(self.course.id).add_users(self.instructor) self.assertContains(response, success_status)
response = self.client.post(url, {})
self.assertContains(response, success_status)
# assert that if the integrity signature is enabled, the verification # assert that if the integrity signature is enabled, the verification
# status is not included as a query feature # status is not included as a query feature
args = mock_task_endpoint.call_args.args args = mock_task_endpoint.call_args.args
self.assertEqual(len(args), 3) self.assertEqual(len(args), 3)
query_features = args[2] query_features = args[2]
if not enable_integrity_signature:
self.assertIn('verification_status', query_features) self.assertNotIn('verification_status', query_features)
else:
self.assertNotIn('verification_status', query_features)
def test_get_ora2_responses_success(self): def test_get_ora2_responses_success(self):
url = reverse('export_ora2_data', kwargs={'course_id': str(self.course.id)}) url = reverse('export_ora2_data', kwargs={'course_id': str(self.course.id)})

View File

@@ -1425,8 +1425,7 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red
query_features = [ query_features = [
'id', 'username', 'name', 'email', 'language', 'location', 'id', 'username', 'name', 'email', 'language', 'location',
'year_of_birth', 'gender', 'level_of_education', 'mailing_address', 'year_of_birth', 'gender', 'level_of_education', 'mailing_address',
'goals', 'enrollment_mode', 'verification_status', 'goals', 'enrollment_mode', 'last_login', 'date_joined', 'external_user_key'
'last_login', 'date_joined', 'external_user_key'
] ]
# Provide human-friendly and translatable names for these features. These names # 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'), 'mailing_address': _('Mailing Address'),
'goals': _('Goals'), 'goals': _('Goals'),
'enrollment_mode': _('Enrollment Mode'), 'enrollment_mode': _('Enrollment Mode'),
'verification_status': _('Verification Status'),
'last_login': _('Last Login'), 'last_login': _('Last Login'),
'date_joined': _('Date Joined'), 'date_joined': _('Date Joined'),
'external_user_key': _('External User Key'), '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.append('team')
query_features_names['team'] = _('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. # For compatibility reasons, city and country should always appear last.
query_features.append('city') query_features.append('city')
query_features_names['city'] = _('City') query_features_names['city'] = _('City')

View File

@@ -214,13 +214,12 @@ from common.djangoapps.student.models import CourseEnrollment
course_mode_info = all_course_modes.get(session_id) course_mode_info = all_course_modes.get(session_id)
is_paid_course = True if entitlement else (session_id in enrolled_courses_either_paid) 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) 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) 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)) 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) show_consent_link = (session_id in consent_required_courses)
resume_button_url = resume_button_urls[dashboard_index] 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 % endfor
% if show_load_all_courses_link: % if show_load_all_courses_link:
<br/> <br/>
@@ -301,20 +300,6 @@ from common.djangoapps.student.models import CourseEnrollment
<%include file='dashboard/_dashboard_announcements.html' /> <%include file='dashboard/_dashboard_announcements.html' />
% endif % endif
% if display_sidebar_on_dashboard:
<div class="profile-sidebar" id="profile-sidebar" role="region" aria-label="Account Status Info">
<header class="profile">
<h2 class="account-status-title sr">${_("Account Status Info")}: </h2>
</header>
<div class="user-info">
<ul>
<%include file="${static.get_template_path('dashboard/_dashboard_status_verification.html')}" />
</ul>
</div>
</div>
% endif
</div> </div>
</div> </div>
</main> </main>

View File

@@ -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 from django.urls import reverse
@@ -157,12 +157,6 @@ else:
% endif % endif
% endif % endif
% if cert_status['status'] == 'downloadable' and enrollment.mode == 'verified' and cert_status['mode'] == 'honor':
<div class="certificate-explanation">
${_('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)}
</div>
% endif
</div> </div>
% endif % endif
% elif course_ended_not_passing: % elif course_ended_not_passing:

View File

@@ -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 import datetime
@@ -65,7 +65,6 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG
<% <%
course_verified_certs = enrollment_mode_display( course_verified_certs = enrollment_mode_display(
enrollment.mode, enrollment.mode,
verification_status.get('status'),
course_overview.id 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"/> <%include file="_dashboard_show_consent.html" args="course_overview=course_overview, course_target=course_target, enrollment=enrollment, enterprise_customer_name=enterprise_customer_name"/>
%endif %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]:
<div class="message message-status wrapper-message-primary is-shown">
% if verification_status['status'] == VERIFY_STATUS_NEED_TO_VERIFY:
<div class="verification-reminder">
% if verification_status['days_until_deadline'] is not None:
<h4 class="message-title">${_('To earn a certificate, you must verify your ID')}</h4>
<p class="message-copy">${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'])}</p>
% else:
<h4 class="message-title">${_('Almost there!')}</h4>
<p class="message-copy">${_('You still need to verify for this course.')}</p>
% endif
</div>
<div class="verification-cta">
<a href="${IDVerificationService.get_verify_location(course_overview.id)}" class="btn" data-course-id="${course_overview.id}">${_('Verify Now')}</a>
</div>
% elif verification_status['status'] == VERIFY_STATUS_SUBMITTED:
<h4 class="message-title">${_('You have submitted your verification information.')}</h4>
<p class="message-copy">${_('You will see a message on your dashboard when the verification process is complete (usually within 5-7 days).')}</p>
% elif verification_status['status'] == VERIFY_STATUS_RESUBMITTED:
<h4 class="message-title">${_('Your current verification will expire soon!')}</h4>
<p class="message-copy">${_('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).')}</p>
% elif verification_status['status'] == VERIFY_STATUS_APPROVED:
<h4 class="message-title">${_('You have successfully verified your ID with {platform_name}').format(platform_name=settings.PLATFORM_NAME)}</h4>
% if verification_status.get('verification_good_until') is not None:
<p class="message-copy">${_('Your current verification is effective until {date}.').format(date=verification_status['verification_good_until'])}
% endif
% elif verification_status['status'] == VERIFY_STATUS_NEED_TO_REVERIFY:
<h4 class="message-title">${_('Your current verification will expire soon.')}</h4>
## Translators: start_link and end_link will be replaced with HTML tags;
## please do not translate these.
<p class="message-copy">${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('<a href="{href}">').format(href=IDVerificationService.get_verify_location()),
end_link=HTML('</a>'),
days=settings.VERIFY_STUDENT.get("EXPIRING_SOON_WINDOW")
)}
</p>
% endif
</div>
% endif
% if display_course_upgrade: % if display_course_upgrade:
<div class="message message-upsell has-actions is-shown"> <div class="message message-upsell has-actions is-shown">
<div class="wrapper-extended"> <div class="wrapper-extended">

View File

@@ -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':
<li class="status status-verification is-accepted">
<span class="title status-title">${_("Current ID Verification Status: Approved")}</span>
<p class="status-note">${_("Your {platform_name} ID verification has been approved. Your ID verification is effective for two years after submission.").format(platform_name=settings.PLATFORM_NAME)}</p>
%if verification_expiry:
<p class="status-note"><span><b>${_("Warning")}: </b></span><i>${_("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)}</i></p>
<div class="btn-reverify">
<a href="${IDVerificationService.get_verify_location()}" class="action action-reverify">${_("Resubmit Verification")}</a>
</div>
%endif
</li>
%elif verification_status == 'pending':
<li class="status status-verification is-pending">
<span class="title status-title">${_("Current ID Verification Status: Pending")}</span>
<p class="status-note">${_("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)}</p>
</li>
%elif verification_status in ['denied','must_reverify', 'must_retry']:
<li class="status status-verification is-denied">
<span class="title status-title">${_("Current ID Verification Status: Denied")}</span>
<p class="status-note">
${_("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:
<br><br>
${_("Your ID verification was denied for the following reasons:")}<br>
<ul>
%for error in verification_errors:
<li>${error}</li>
%endfor
</ul>
%endif
</p>
<div class="btn-reverify">
<a href="${IDVerificationService.get_verify_location()}" class="action action-reverify">${_("Resubmit Verification")}</a>
</div>
</li>
%elif verification_status == 'expired':
<li class="status status-verification is-denied">
<span class="title status-title">${_("Current ID Verification Status: Expired")}</span>
<p class="status-note">${_("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.")}</p>
<p class="status-note"><span><b>${_("Warning")}: </b></span>${_(" 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.")}</p>
<div class="btn-reverify">
<a href="${IDVerificationService.get_verify_location()}" class="action action-reverify">${_("Resubmit Verification")}</a>
</div>
</li>
%endif
%endif

View File

@@ -481,7 +481,7 @@ edx-opaque-keys[django]==2.3.0
# xmodule # xmodule
edx-organizations==6.11.1 edx-organizations==6.11.1
# via -r requirements/edx/base.in # via -r requirements/edx/base.in
edx-proctoring==4.9.0 edx-proctoring==4.10.0
# via # via
# -r requirements/edx/base.in # -r requirements/edx/base.in
# edx-proctoring-proctortrack # edx-proctoring-proctortrack

View File

@@ -590,7 +590,7 @@ edx-opaque-keys[django]==2.3.0
# xmodule # xmodule
edx-organizations==6.11.1 edx-organizations==6.11.1
# via -r requirements/edx/testing.txt # via -r requirements/edx/testing.txt
edx-proctoring==4.9.0 edx-proctoring==4.10.0
# via # via
# -r requirements/edx/testing.txt # -r requirements/edx/testing.txt
# edx-proctoring-proctortrack # edx-proctoring-proctortrack

View File

@@ -575,7 +575,7 @@ edx-opaque-keys[django]==2.3.0
# xmodule # xmodule
edx-organizations==6.11.1 edx-organizations==6.11.1
# via -r requirements/edx/base.txt # via -r requirements/edx/base.txt
edx-proctoring==4.9.0 edx-proctoring==4.10.0
# via # via
# -r requirements/edx/base.txt # -r requirements/edx/base.txt
# edx-proctoring-proctortrack # edx-proctoring-proctortrack

View File

@@ -244,14 +244,13 @@ from common.djangoapps.student.models import CourseEnrollment
course_mode_info = all_course_modes.get(session_id) course_mode_info = all_course_modes.get(session_id)
is_paid_course = True if entitlement else (session_id in enrolled_courses_either_paid) 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) 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) 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)) 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) show_consent_link = (session_id in consent_required_courses)
course_overview = enrollment.course_overview course_overview = enrollment.course_overview
resume_button_url = resume_button_urls[dashboard_index] 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 % endfor
% if show_load_all_courses_link: % if show_load_all_courses_link:
<br/> <br/>
@@ -350,21 +349,6 @@ from common.djangoapps.student.models import CourseEnrollment
</div> </div>
</div> </div>
% endif % endif
% if display_sidebar_on_dashboard:
<section class="profile-sidebar" id="profile-sidebar" role="region" aria-label="Account Status Info">
<header class="profile">
<h2 class="account-status-title sr">${_("Account Status Info")}: </h2>
</header>
<section class="user-info">
<ul>
<%include file="${theming_helpers.get_template_path('dashboard/_dashboard_status_verification.html')}" />
</ul>
</section>
</section>
% endif
</div> </div>
</section> </section>