243 lines
9.3 KiB
Python
243 lines
9.3 KiB
Python
"""
|
|
Implementation of abstraction layer for other parts of the system to make queries related to ID Verification.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import timedelta
|
|
from itertools import chain
|
|
from urllib.parse import quote
|
|
|
|
from django.conf import settings
|
|
from django.utils.timezone import now
|
|
from django.utils.translation import ugettext as _
|
|
|
|
from common.djangoapps.course_modes.models import CourseMode
|
|
from common.djangoapps.student.models import User
|
|
from lms.djangoapps.verify_student.utils import is_verification_expiring_soon
|
|
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
|
|
|
from .models import ManualVerification, SoftwareSecurePhotoVerification, SSOVerification
|
|
from .utils import most_recent_verification
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class XBlockVerificationService:
|
|
"""
|
|
Learner verification XBlock service.
|
|
"""
|
|
|
|
def get_status(self, user_id):
|
|
"""
|
|
Returns the user's current photo verification status.
|
|
|
|
Args:
|
|
user_id: the user's id
|
|
|
|
Returns: one of the following strings
|
|
'none' - no such verification exists
|
|
'expired' - verification has expired
|
|
'approved' - verification has been approved
|
|
'pending' - verification process is still ongoing
|
|
'must_reverify' - verification has been denied and user must resubmit photos
|
|
"""
|
|
user = User.objects.get(id=user_id)
|
|
return IDVerificationService.user_status(user)
|
|
|
|
def reverify_url(self):
|
|
"""
|
|
Returns the URL for a user to verify themselves.
|
|
"""
|
|
return IDVerificationService.get_verify_location()
|
|
|
|
|
|
class IDVerificationService:
|
|
"""
|
|
Learner verification service interface for callers within edx-platform.
|
|
"""
|
|
|
|
@classmethod
|
|
def user_is_verified(cls, user):
|
|
"""
|
|
Return whether or not a user has satisfactorily proved their identity.
|
|
Depending on the policy, this can expire after some period of time, so
|
|
a user might have to renew periodically.
|
|
"""
|
|
expiration_datetime = cls.get_expiration_datetime(user, ['approved'])
|
|
if expiration_datetime:
|
|
return expiration_datetime >= now()
|
|
return False
|
|
|
|
@classmethod
|
|
def verifications_for_user(cls, user):
|
|
"""
|
|
Return a list of all verifications associated with the given user.
|
|
"""
|
|
verifications = []
|
|
for verification in chain(SoftwareSecurePhotoVerification.objects.filter(user=user).order_by('-created_at'),
|
|
SSOVerification.objects.filter(user=user).order_by('-created_at'),
|
|
ManualVerification.objects.filter(user=user).order_by('-created_at')):
|
|
verifications.append(verification)
|
|
return verifications
|
|
|
|
@classmethod
|
|
def get_verified_user_ids(cls, users):
|
|
"""
|
|
Given a list of users, returns an iterator of user ids that have non-expired verifications of any type.
|
|
"""
|
|
filter_kwargs = {
|
|
'user__in': users,
|
|
'status': 'approved',
|
|
'created_at__gt': now() - timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
|
|
}
|
|
return chain(
|
|
SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs).values_list('user_id', flat=True),
|
|
SSOVerification.objects.filter(**filter_kwargs).values_list('user_id', flat=True),
|
|
ManualVerification.objects.filter(**filter_kwargs).values_list('user_id', flat=True)
|
|
)
|
|
|
|
@classmethod
|
|
def get_expiration_datetime(cls, user, statuses):
|
|
"""
|
|
Check whether the user has a verification with one of the given
|
|
statuses and return the "expiration_datetime" of most recent verification that
|
|
matches one of the given statuses.
|
|
|
|
Arguments:
|
|
user (Object): User
|
|
statuses: List of verification statuses (e.g., ['approved'])
|
|
|
|
Returns:
|
|
expiration_datetime: expiration_datetime of most recent verification that
|
|
matches one of the given statuses.
|
|
"""
|
|
filter_kwargs = {
|
|
'user': user,
|
|
'status__in': statuses,
|
|
}
|
|
|
|
photo_id_verifications = SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs)
|
|
sso_id_verifications = SSOVerification.objects.filter(**filter_kwargs)
|
|
manual_id_verifications = ManualVerification.objects.filter(**filter_kwargs)
|
|
|
|
attempt = most_recent_verification(
|
|
photo_id_verifications,
|
|
sso_id_verifications,
|
|
manual_id_verifications,
|
|
'updated_at'
|
|
)
|
|
return attempt and attempt.expiration_datetime
|
|
|
|
@classmethod
|
|
def user_has_valid_or_pending(cls, user):
|
|
"""
|
|
Check whether the user has an active or pending verification attempt
|
|
|
|
Returns:
|
|
bool: True or False according to existence of valid verifications
|
|
"""
|
|
expiration_datetime = cls.get_expiration_datetime(user, ['submitted', 'approved', 'must_retry'])
|
|
if expiration_datetime:
|
|
return expiration_datetime >= now()
|
|
return False
|
|
|
|
@classmethod
|
|
def user_status(cls, user):
|
|
"""
|
|
Returns the status of the user based on their past verification attempts, and any corresponding error messages.
|
|
|
|
If no such verification exists, returns 'none'
|
|
If verification has expired, returns 'expired'
|
|
If the verification has been approved, returns 'approved'
|
|
If the verification process is still ongoing, returns 'pending'
|
|
If the verification has been denied and the user must resubmit photos, returns 'must_reverify'
|
|
|
|
This checks most recent verification
|
|
"""
|
|
# should_display only refers to displaying the verification attempt status to a user
|
|
# once a verification attempt has been made, otherwise we will display a prompt to complete ID verification.
|
|
user_status = {
|
|
'status': 'none',
|
|
'error': '',
|
|
'should_display': True,
|
|
'status_date': '',
|
|
'verification_expiry': '',
|
|
}
|
|
|
|
attempt = None
|
|
|
|
verifications = cls.verifications_for_user(user)
|
|
|
|
if verifications:
|
|
attempt = verifications[0]
|
|
for verification in verifications:
|
|
if verification.expiration_datetime > now() and verification.status == 'approved':
|
|
# Always select the LATEST non-expired approved verification if there is such
|
|
if attempt.status != 'approved' or (
|
|
attempt.expiration_datetime < verification.expiration_datetime
|
|
):
|
|
attempt = verification
|
|
|
|
if not attempt:
|
|
return user_status
|
|
|
|
user_status['should_display'] = attempt.should_display_status_to_user()
|
|
|
|
if attempt.expiration_datetime < now() and attempt.status == 'approved':
|
|
if user_status['should_display']:
|
|
user_status['status'] = 'expired'
|
|
user_status['error'] = _("Your {platform_name} verification has expired.").format(
|
|
platform_name=configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
|
|
)
|
|
else:
|
|
# If we have a verification attempt that never would have displayed to the user,
|
|
# and that attempt is expired, then we should treat it as if the user had never verified.
|
|
return user_status
|
|
|
|
# If someone is denied their original verification attempt, they can try to reverify.
|
|
elif attempt.status == 'denied':
|
|
user_status['status'] = 'must_reverify'
|
|
if hasattr(attempt, 'error_msg') and attempt.error_msg:
|
|
user_status['error'] = attempt.parsed_error_msg()
|
|
|
|
elif attempt.status == 'approved':
|
|
user_status['status'] = 'approved'
|
|
expiration_datetime = cls.get_expiration_datetime(user, ['approved'])
|
|
if is_verification_expiring_soon(expiration_datetime):
|
|
user_status['verification_expiry'] = attempt.expiration_datetime.date().strftime("%m/%d/%Y")
|
|
user_status['status_date'] = attempt.status_changed
|
|
|
|
elif attempt.status in ['submitted', 'approved', 'must_retry']:
|
|
# user_has_valid_or_pending does include 'approved', but if we are
|
|
# here, we know that the attempt is still pending
|
|
user_status['status'] = 'pending'
|
|
|
|
return user_status
|
|
|
|
@classmethod
|
|
def verification_status_for_user(cls, user, user_enrollment_mode, user_is_verified=None):
|
|
"""
|
|
Returns the verification status for use in grade report.
|
|
"""
|
|
if user_enrollment_mode not in CourseMode.VERIFIED_MODES:
|
|
return 'N/A'
|
|
|
|
if user_is_verified is None:
|
|
user_is_verified = cls.user_is_verified(user)
|
|
|
|
if not user_is_verified:
|
|
return 'Not ID Verified'
|
|
else:
|
|
return 'ID Verified'
|
|
|
|
@classmethod
|
|
def get_verify_location(cls, course_id=None):
|
|
"""
|
|
Returns a string:
|
|
Returns URL for IDV on Account Microfrontend
|
|
"""
|
|
location = f'{settings.ACCOUNT_MICROFRONTEND_URL}/id-verification'
|
|
if course_id:
|
|
location += '?course_id={}'.format(quote(str(course_id)))
|
|
return location
|