def test_verification_for_datetime(self): user = UserFactory.create() now = datetime.now(pytz.UTC) # No attempts in the query set, so should return None query = SoftwareSecurePhotoVerification.objects.filter(user=user) result = SoftwareSecurePhotoVerification.verification_for_datetime(now, query) self.assertIs(result, None) # Should also return None if no deadline specified query = SoftwareSecurePhotoVerification.objects.filter(user=user) result = SoftwareSecurePhotoVerification.verification_for_datetime(None, query) self.assertIs(result, None) # Make an attempt attempt = SoftwareSecurePhotoVerification.objects.create(user=user) # Before the created date, should get no results before = attempt.created_at - timedelta(seconds=1) query = SoftwareSecurePhotoVerification.objects.filter(user=user) result = SoftwareSecurePhotoVerification.verification_for_datetime(before, query) self.assertIs(result, None) # Immediately after the created date, should get the attempt after_created = attempt.created_at + timedelta(seconds=1) query = SoftwareSecurePhotoVerification.objects.filter(user=user) result = SoftwareSecurePhotoVerification.verification_for_datetime(after_created, query) self.assertEqual(result, attempt) # If no deadline specified, should return first available query = SoftwareSecurePhotoVerification.objects.filter(user=user) result = SoftwareSecurePhotoVerification.verification_for_datetime(None, query) self.assertEqual(result, attempt) # Immediately before the expiration date, should get the attempt expiration = attempt.created_at + timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]) before_expiration = expiration - timedelta(seconds=1) query = SoftwareSecurePhotoVerification.objects.filter(user=user) result = SoftwareSecurePhotoVerification.verification_for_datetime(before_expiration, query) self.assertEqual(result, attempt) # Immediately after the expiration date, should not get the attempt attempt.created_at = attempt.created_at - timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]) attempt.save() after = datetime.now(pytz.UTC) + timedelta(days=1) query = SoftwareSecurePhotoVerification.objects.filter(user=user) result = SoftwareSecurePhotoVerification.verification_for_datetime(after, query) self.assertIs(result, None) # Create a second attempt in the same window second_attempt = SoftwareSecurePhotoVerification.objects.create(user=user) # Now we should get the newer attempt deadline = second_attempt.created_at + timedelta(days=1) query = SoftwareSecurePhotoVerification.objects.filter(user=user) result = SoftwareSecurePhotoVerification.verification_for_datetime(deadline, query) self.assertEqual(result, second_attempt)
def check_verify_status_by_course(user, course_enrollments): """ Determine the per-course verification statuses for a given user. The possible statuses are: * VERIFY_STATUS_NEED_TO_VERIFY: The student has not yet submitted photos for verification. * VERIFY_STATUS_SUBMITTED: The student has submitted photos for verification, but has have not yet been approved. * VERIFY_STATUS_RESUBMITTED: The student has re-submitted photos for re-verification while they still have an active but expiring ID verification * VERIFY_STATUS_APPROVED: The student has been successfully verified. * VERIFY_STATUS_MISSED_DEADLINE: The student did not submit photos within the course's deadline. * VERIFY_STATUS_NEED_TO_REVERIFY: The student has an active verification, but it is set to expire before the verification deadline for the course. It is is also possible that a course does NOT have a verification status if: * The user is not enrolled in a verified mode, meaning that the user didn't pay. * The course does not offer a verified mode. * The user submitted photos but an error occurred while verifying them. * The user submitted photos but the verification was denied. In the last two cases, we rely on messages in the sidebar rather than displaying messages for each course. Arguments: user (User): The currently logged-in user. course_enrollments (list[CourseEnrollment]): The courses the user is enrolled in. Returns: dict: Mapping of course keys verification status dictionaries. If no verification status is applicable to a course, it will not be included in the dictionary. The dictionaries have these keys: * status (str): One of the enumerated status codes. * days_until_deadline (int): Number of days until the verification deadline. * verification_good_until (str): Date string for the verification expiration date. """ status_by_course = {} # Retrieve all verifications for the user, sorted in descending # order by submission datetime verifications = SoftwareSecurePhotoVerification.objects.filter(user=user) # Check whether the user has an active or pending verification attempt # To avoid another database hit, we re-use the queryset we have already retrieved. has_active_or_pending = SoftwareSecurePhotoVerification.user_has_valid_or_pending( user, queryset=verifications ) # Retrieve expiration_datetime of most recent approved verification # To avoid another database hit, we re-use the queryset we have already retrieved. expiration_datetime = SoftwareSecurePhotoVerification.get_expiration_datetime(user, verifications) verification_expiring_soon = SoftwareSecurePhotoVerification.is_verification_expiring_soon(expiration_datetime) # Retrieve verification deadlines for the enrolled courses enrolled_course_keys = [enrollment.course_id for enrollment in course_enrollments] course_deadlines = VerificationDeadline.deadlines_for_courses(enrolled_course_keys) recent_verification_datetime = None for enrollment in course_enrollments: # If the user hasn't enrolled as verified, then the course # won't display state related to its verification status. if enrollment.mode in CourseMode.VERIFIED_MODES: # Retrieve the verification deadline associated with the course. # This could be None if the course doesn't have a deadline. deadline = course_deadlines.get(enrollment.course_id) relevant_verification = SoftwareSecurePhotoVerification.verification_for_datetime(deadline, verifications) # Picking the max verification datetime on each iteration only with approved status if relevant_verification is not None and relevant_verification.status == "approved": recent_verification_datetime = max( recent_verification_datetime if recent_verification_datetime is not None else relevant_verification.expiration_datetime, relevant_verification.expiration_datetime ) # By default, don't show any status related to verification status = None # Check whether the user was approved or is awaiting approval if relevant_verification is not None: if relevant_verification.status == "approved": if verification_expiring_soon: status = VERIFY_STATUS_NEED_TO_REVERIFY else: status = VERIFY_STATUS_APPROVED elif relevant_verification.status == "submitted": if verification_expiring_soon: status = VERIFY_STATUS_RESUBMITTED else: status = VERIFY_STATUS_SUBMITTED # If the user didn't submit at all, then tell them they need to verify # If the deadline has already passed, then tell them they missed it. # If they submitted but something went wrong (error or denied), # then don't show any messaging next to the course, since we already # show messages related to this on the left sidebar. submitted = ( relevant_verification is not None and relevant_verification.status not in ["created", "ready"] ) if status is None and not submitted: if deadline is None or deadline > datetime.now(UTC): if SoftwareSecurePhotoVerification.user_is_verified(user): if verification_expiring_soon: # The user has an active verification, but the verification # is set to expire within "EXPIRING_SOON_WINDOW" days (default is 4 weeks). # Tell the student to reverify. status = VERIFY_STATUS_NEED_TO_REVERIFY else: status = VERIFY_STATUS_NEED_TO_VERIFY else: # If a user currently has an active or pending verification, # then they may have submitted an additional attempt after # the verification deadline passed. This can occur, # for example, when the support team asks a student # to reverify after the deadline so they can receive # a verified certificate. # In this case, we still want to show them as "verified" # on the dashboard. if has_active_or_pending: status = VERIFY_STATUS_APPROVED # Otherwise, the student missed the deadline, so show # them as "honor" (the kind of certificate they will receive). else: status = VERIFY_STATUS_MISSED_DEADLINE # Set the status for the course only if we're displaying some kind of message # Otherwise, leave the course out of the dictionary. if status is not None: days_until_deadline = None now = datetime.now(UTC) if deadline is not None and deadline > now: days_until_deadline = (deadline - now).days status_by_course[enrollment.course_id] = { 'status': status, 'days_until_deadline': days_until_deadline } if recent_verification_datetime: for key, value in status_by_course.iteritems(): # pylint: disable=unused-variable status_by_course[key]['verification_good_until'] = recent_verification_datetime.strftime("%m/%d/%Y") return status_by_course
def check_verify_status_by_course(user, course_enrollments): """ Determine the per-course verification statuses for a given user. The possible statuses are: * VERIFY_STATUS_NEED_TO_VERIFY: The student has not yet submitted photos for verification. * VERIFY_STATUS_SUBMITTED: The student has submitted photos for verification, but has have not yet been approved. * VERIFY_STATUS_RESUBMITTED: The student has re-submitted photos for re-verification while they still have an active but expiring ID verification * VERIFY_STATUS_APPROVED: The student has been successfully verified. * VERIFY_STATUS_MISSED_DEADLINE: The student did not submit photos within the course's deadline. * VERIFY_STATUS_NEED_TO_REVERIFY: The student has an active verification, but it is set to expire before the verification deadline for the course. It is is also possible that a course does NOT have a verification status if: * The user is not enrolled in a verified mode, meaning that the user didn't pay. * The course does not offer a verified mode. * The user submitted photos but an error occurred while verifying them. * The user submitted photos but the verification was denied. In the last two cases, we rely on messages in the sidebar rather than displaying messages for each course. Arguments: user (User): The currently logged-in user. course_enrollments (list[CourseEnrollment]): The courses the user is enrolled in. Returns: dict: Mapping of course keys verification status dictionaries. If no verification status is applicable to a course, it will not be included in the dictionary. The dictionaries have these keys: * status (str): One of the enumerated status codes. * days_until_deadline (int): Number of days until the verification deadline. * verification_good_until (str): Date string for the verification expiration date. """ status_by_course = {} # Retrieve all verifications for the user, sorted in descending # order by submission datetime verifications = SoftwareSecurePhotoVerification.objects.filter(user=user) # Check whether the user has an active or pending verification attempt # To avoid another database hit, we re-use the queryset we have already retrieved. has_active_or_pending = SoftwareSecurePhotoVerification.user_has_valid_or_pending( user, queryset=verifications) # Retrieve expiration_datetime of most recent approved verification # To avoid another database hit, we re-use the queryset we have already retrieved. expiration_datetime = SoftwareSecurePhotoVerification.get_expiration_datetime( user, verifications) verification_expiring_soon = SoftwareSecurePhotoVerification.is_verification_expiring_soon( expiration_datetime) # Retrieve verification deadlines for the enrolled courses enrolled_course_keys = [ enrollment.course_id for enrollment in course_enrollments ] course_deadlines = VerificationDeadline.deadlines_for_courses( enrolled_course_keys) recent_verification_datetime = None for enrollment in course_enrollments: # If the user hasn't enrolled as verified, then the course # won't display state related to its verification status. if enrollment.mode in CourseMode.VERIFIED_MODES: # Retrieve the verification deadline associated with the course. # This could be None if the course doesn't have a deadline. deadline = course_deadlines.get(enrollment.course_id) relevant_verification = SoftwareSecurePhotoVerification.verification_for_datetime( deadline, verifications) # Picking the max verification datetime on each iteration only with approved status if relevant_verification is not None and relevant_verification.status == "approved": recent_verification_datetime = max( recent_verification_datetime if recent_verification_datetime is not None else relevant_verification.expiration_datetime, relevant_verification.expiration_datetime) # By default, don't show any status related to verification status = None # Check whether the user was approved or is awaiting approval if relevant_verification is not None: if relevant_verification.status == "approved": if verification_expiring_soon: status = VERIFY_STATUS_NEED_TO_REVERIFY else: status = VERIFY_STATUS_APPROVED elif relevant_verification.status == "submitted": if verification_expiring_soon: status = VERIFY_STATUS_RESUBMITTED else: status = VERIFY_STATUS_SUBMITTED # If the user didn't submit at all, then tell them they need to verify # If the deadline has already passed, then tell them they missed it. # If they submitted but something went wrong (error or denied), # then don't show any messaging next to the course, since we already # show messages related to this on the left sidebar. submitted = (relevant_verification is not None and relevant_verification.status not in ["created", "ready"]) if status is None and not submitted: if deadline is None or deadline > datetime.now(UTC): if SoftwareSecurePhotoVerification.user_is_verified(user): if verification_expiring_soon: # The user has an active verification, but the verification # is set to expire within "EXPIRING_SOON_WINDOW" days (default is 4 weeks). # Tell the student to reverify. status = VERIFY_STATUS_NEED_TO_REVERIFY else: status = VERIFY_STATUS_NEED_TO_VERIFY else: # If a user currently has an active or pending verification, # then they may have submitted an additional attempt after # the verification deadline passed. This can occur, # for example, when the support team asks a student # to reverify after the deadline so they can receive # a verified certificate. # In this case, we still want to show them as "verified" # on the dashboard. if has_active_or_pending: status = VERIFY_STATUS_APPROVED # Otherwise, the student missed the deadline, so show # them as "honor" (the kind of certificate they will receive). else: status = VERIFY_STATUS_MISSED_DEADLINE # Set the status for the course only if we're displaying some kind of message # Otherwise, leave the course out of the dictionary. if status is not None: days_until_deadline = None now = datetime.now(UTC) if deadline is not None and deadline > now: days_until_deadline = (deadline - now).days status_by_course[enrollment.course_id] = { 'status': status, 'days_until_deadline': days_until_deadline } if recent_verification_datetime: for key, value in status_by_course.iteritems(): # pylint: disable=unused-variable status_by_course[key][ 'verification_good_until'] = recent_verification_datetime.strftime( "%m/%d/%Y") return status_by_course