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:
@@ -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:
|
||||
|
||||
@@ -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. """
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
@@ -748,11 +748,6 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
|
||||
else:
|
||||
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
|
||||
for entitlement in [e for e in course_entitlements if e.enrollment_course_run is not None]:
|
||||
course_enrollments = [
|
||||
@@ -804,7 +799,6 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
|
||||
'show_dashboard_tabs': True,
|
||||
'disable_courseware_js': True,
|
||||
'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_dashboard_courses': (user.is_active or not hide_dashboard_courses_until_activated),
|
||||
'empty_dashboard_message': empty_dashboard_message,
|
||||
|
||||
@@ -246,7 +246,6 @@ class ProctoringFields:
|
||||
|
||||
|
||||
@XBlock.wants('proctoring')
|
||||
@XBlock.wants('verification')
|
||||
@XBlock.wants('gating')
|
||||
@XBlock.wants('credit')
|
||||
@XBlock.wants('completion')
|
||||
@@ -922,7 +921,6 @@ class SequenceBlock(
|
||||
|
||||
proctoring_service = self.runtime.service(self, 'proctoring')
|
||||
credit_service = self.runtime.service(self, 'credit')
|
||||
verification_service = self.runtime.service(self, 'verification')
|
||||
|
||||
# Is this sequence designated as a Timed Examination, which includes
|
||||
# Proctored Exams
|
||||
@@ -949,7 +947,6 @@ class SequenceBlock(
|
||||
'allow_proctoring_opt_out': self.allow_proctoring_opt_out,
|
||||
'due_date': self.due,
|
||||
'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
|
||||
@@ -960,14 +957,6 @@ class SequenceBlock(
|
||||
'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
|
||||
# a special view to the student rather
|
||||
# than the actual sequence content
|
||||
|
||||
@@ -56,23 +56,6 @@
|
||||
{% 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 %}
|
||||
|
||||
{% 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>
|
||||
{% trans "Thank you," as tmsg %}{{ tmsg | force_escape }}
|
||||
<br />
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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)})
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
<br/>
|
||||
@@ -301,20 +300,6 @@ from common.djangoapps.student.models import CourseEnrollment
|
||||
<%include file='dashboard/_dashboard_announcements.html' />
|
||||
% 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>
|
||||
</main>
|
||||
|
||||
@@ -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':
|
||||
<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>
|
||||
% endif
|
||||
% elif course_ended_not_passing:
|
||||
|
||||
@@ -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]:
|
||||
<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:
|
||||
<div class="message message-upsell has-actions is-shown">
|
||||
<div class="wrapper-extended">
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
<br/>
|
||||
@@ -350,21 +349,6 @@ from common.djangoapps.student.models import CourseEnrollment
|
||||
</div>
|
||||
</div>
|
||||
% 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>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user