Merge pull request #17835 from edx/bexline/sso_id_verification

ENT-942 Implement an abstraction layer for PhotoVerification class methods
This commit is contained in:
Brittney Exline
2018-04-06 07:49:45 -06:00
committed by GitHub
22 changed files with 589 additions and 503 deletions

View File

@@ -10,10 +10,10 @@ from datetime import datetime
import django
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import NoReverseMatch, reverse
from django.core.validators import ValidationError, validate_email
from django.contrib.auth import authenticate, load_backend, login, logout
from django.core.validators import ValidationError
from django.contrib.auth import load_backend
from django.contrib.auth.models import User
from django.db import IntegrityError, transaction
from django.utils import http
@@ -35,13 +35,14 @@ from lms.djangoapps.certificates.models import ( # pylint: disable=import-error
certificate_status_for_student
)
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline
from lms.djangoapps.verify_student.models import VerificationDeadline
from lms.djangoapps.verify_student.services import IDVerificationService
from lms.djangoapps.verify_student.utils import is_verification_expiring_soon, verification_for_datetime
from openedx.core.djangoapps.certificates.api import certificates_viewable_for_course
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming import helpers as theming_helpers
from openedx.core.djangoapps.theming.helpers import get_themes
from student.models import (
CourseEnrollment,
LinkedInAddToProfileConfiguration,
PasswordHistory,
Registration,
@@ -111,18 +112,18 @@ def check_verify_status_by_course(user, course_enrollments):
# Retrieve all verifications for the user, sorted in descending
# order by submission datetime
verifications = SoftwareSecurePhotoVerification.objects.filter(user=user)
verifications = IDVerificationService.verifications_for_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(
has_active_or_pending = IDVerificationService.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)
expiration_datetime = IDVerificationService.get_expiration_datetime(user, verifications)
verification_expiring_soon = is_verification_expiring_soon(expiration_datetime)
# Retrieve verification deadlines for the enrolled courses
enrolled_course_keys = [enrollment.course_id for enrollment in course_enrollments]
@@ -140,7 +141,7 @@ def check_verify_status_by_course(user, course_enrollments):
# 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)
relevant_verification = 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":
@@ -177,13 +178,12 @@ def check_verify_status_by_course(user, course_enrollments):
)
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:
if IDVerificationService.user_is_verified(user) and 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
elif not IDVerificationService.user_is_verified(user):
status = VERIFY_STATUS_NEED_TO_VERIFY
else:
# If a user currently has an active or pending verification,
@@ -525,7 +525,7 @@ def _cert_info(user, course_overview, cert_status):
'can_unenroll': status not in DISABLE_UNENROLL_CERT_STATES,
}
if not status == default_status and course_overview.end_of_course_survey_url is not None:
if status != default_status and course_overview.end_of_course_survey_url is not None:
status_dict.update({
'show_survey_button': True,
'survey_url': process_survey_link(course_overview.end_of_course_survey_url, user)})

View File

@@ -11,10 +11,10 @@ from completion.utilities import get_key_to_last_completed_course_block
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import NoReverseMatch, reverse, reverse_lazy
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
from django.views.decorators.csrf import ensure_csrf_cookie
from opaque_keys.edx.keys import CourseKey
from pytz import UTC
@@ -27,7 +27,7 @@ from courseware.access import has_access
from edxmako.shortcuts import render_to_response, render_to_string
from entitlements.models import CourseEntitlement
from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=import-error
from lms.djangoapps.verify_student.services import IDVerificationService
from openedx.core.djangoapps import monitoring_utils
from openedx.core.djangoapps.catalog.utils import (
get_programs,
@@ -339,6 +339,9 @@ def is_course_blocked(request, redeemed_registration_codes, course_key):
def get_verification_error_reasons_for_display(verification_error_codes):
"""
Returns the display text for the given verification error codes.
"""
verification_errors = []
verification_error_map = {
'photos_mismatched': _('Photos are mismatched'),
@@ -713,7 +716,7 @@ def student_dashboard(request):
# Verification Attempts
# Used to generate the "you must reverify for course x" banner
verification_status, verification_error_codes = SoftwareSecurePhotoVerification.user_status(user)
verification_status, verification_error_codes = IDVerificationService.user_status(user)
verification_errors = get_verification_error_reasons_for_display(verification_error_codes)
# Gets data for midcourse reverifications, if any are necessary or have failed

View File

@@ -21,7 +21,7 @@ from lms.djangoapps.certificates.models import (
)
from course_modes.models import CourseMode
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from lms.djangoapps.verify_student.services import IDVerificationService
from student.models import CourseEnrollment, UserProfile
from xmodule.modulestore.django import modulestore
@@ -271,7 +271,7 @@ class XQueueCertInterface(object):
course_grade = CourseGradeFactory().read(student, course)
enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id)
mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES
user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
user_is_verified = IDVerificationService.user_is_verified(student)
cert_mode = enrollment_mode
is_eligible_for_certificate = is_whitelisted or CourseMode.is_eligible_for_certificate(enrollment_mode)
unverified = False

View File

@@ -13,7 +13,7 @@ from lms.djangoapps.certificates.models import (
)
from lms.djangoapps.certificates.tasks import generate_certificate
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from lms.djangoapps.verify_student.services import IDVerificationService
from openedx.core.djangoapps.certificates.api import auto_certificate_generation_enabled
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.content.course_overviews.signals import COURSE_PACING_CHANGED
@@ -82,7 +82,7 @@ def _listen_for_id_verification_status_changed(sender, user, **kwargs): # pylin
user_enrollments = CourseEnrollment.enrollments_for_user(user=user)
grade_factory = CourseGradeFactory()
expected_verification_status, _ = SoftwareSecurePhotoVerification.user_status(user)
expected_verification_status, _ = IDVerificationService.user_status(user)
for enrollment in user_enrollments:
if grade_factory.read(user=user, course=enrollment.course_overview).passed:
if fire_ungenerated_certificate_task(user, enrollment.course_id, expected_verification_status):

View File

@@ -3,7 +3,7 @@ from logging import getLogger
from celery_utils.persist_on_failure import LoggedPersistOnFailureTask
from django.contrib.auth.models import User
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from lms.djangoapps.verify_student.services import IDVerificationService
from opaque_keys.edx.keys import CourseKey
from .api import generate_user_certificates
@@ -31,7 +31,7 @@ def generate_certificate(self, **kwargs):
course_key = CourseKey.from_string(kwargs.pop('course_key'))
expected_verification_status = kwargs.pop('expected_verification_status', None)
if expected_verification_status:
actual_verification_status, _ = SoftwareSecurePhotoVerification.user_status(student)
actual_verification_status, _ = IDVerificationService.user_status(student)
if expected_verification_status != actual_verification_status:
raise self.retry(kwargs=original_kwargs)
generate_user_certificates(student=student, course_key=course_key, **kwargs)

View File

@@ -40,7 +40,7 @@ class GenerateUserCertificateTest(TestCase):
generate_certificate.apply_async(kwargs=kwargs).get()
@patch('lms.djangoapps.certificates.tasks.generate_user_certificates')
@patch('lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_status')
@patch('lms.djangoapps.verify_student.services.IDVerificationService.user_status')
def test_retry_until_verification_status_updates(self, user_status_mock, generate_user_certs_mock):
course_key = 'course-v1:edX+CS101+2017_T2'
student = UserFactory()

View File

@@ -12,7 +12,7 @@ from opaque_keys.edx.locator import CourseLocator
from course_modes.models import CourseMode
from edxmako.shortcuts import render_to_response
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from lms.djangoapps.verify_student.services import IDVerificationService
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming.helpers import is_request_in_themed_site
from shoppingcart.processors.CyberSource2 import is_user_payment_error
@@ -92,7 +92,7 @@ def checkout_receipt(request):
'page_title': page_title,
'is_payment_complete': is_payment_complete,
'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
'verified': SoftwareSecurePhotoVerification.verification_valid_or_pending(request.user).exists(),
'verified': IDVerificationService.verification_valid_or_pending(request.user).exists(),
'error_summary': error_summary,
'error_text': error_text,
'for_help_text': for_help_text,

View File

@@ -19,7 +19,8 @@ from pytz import utc
from course_modes.models import CourseMode, get_cosmetic_verified_display_price
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline
from lms.djangoapps.verify_student.models import VerificationDeadline
from lms.djangoapps.verify_student.services import IDVerificationService
from openedx.core.djangoapps.certificates.api import can_show_certificate_available_date_field
from openedx.core.djangolib.markup import HTML, Text
from openedx.features.course_experience import CourseHomeMessages, UPGRADE_DEADLINE_MESSAGE
@@ -626,7 +627,8 @@ class VerificationDeadlineDate(DateSummary):
@lazy
def verification_status(self):
"""Return the verification status for this user."""
return SoftwareSecurePhotoVerification.user_status(self.user)[0]
status, _ = IDVerificationService.user_status(self.user)
return status
def must_retry(self):
"""Return True if the user must re-submit verification, False otherwise."""

View File

@@ -46,7 +46,7 @@ from lms.djangoapps.grades.signals.signals import SCORE_PUBLISHED
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem
from lms.djangoapps.verify_student.services import VerificationService
from lms.djangoapps.verify_student.services import XBlockVerificationService
from openedx.core.djangoapps.bookmarks.services import BookmarksService
from openedx.core.djangoapps.crawlers.models import CrawlersConfig
from openedx.core.djangoapps.credit.services import CreditService
@@ -756,7 +756,7 @@ def get_module_system_for_user(
'fs': FSService(),
'field-data': field_data,
'user': DjangoXBlockUserService(user, user_is_staff=user_is_staff),
'verification': VerificationService(),
'verification': XBlockVerificationService(),
'proctoring': ProctoringService(),
'milestones': milestones_helpers.get_service(),
'credit': CreditService(),

View File

@@ -1363,7 +1363,7 @@ class ProgressPageTests(ProgressPageBaseTests):
self.store.update_item(self.course, self.user.id)
CourseEnrollment.enroll(self.user, self.course.id, mode="verified")
with patch(
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified'
) as user_verify:
user_verify.return_value = True
@@ -1412,7 +1412,7 @@ class ProgressPageTests(ProgressPageBaseTests):
CourseEnrollment.enroll(self.user, self.course.id, mode="verified")
with patch(
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified'
) as user_verify:
user_verify.return_value = True
@@ -1472,7 +1472,7 @@ class ProgressPageTests(ProgressPageBaseTests):
certs_api.set_cert_generation_enabled(self.course.id, True)
CourseEnrollment.enroll(self.user, self.course.id, mode=course_mode)
with patch(
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified'
) as user_verify:
user_verify.return_value = user_verified
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create:
@@ -1520,7 +1520,7 @@ class ProgressPageTests(ProgressPageBaseTests):
self.store.update_item(self.course, self.user.id)
CourseEnrollment.enroll(self.user, self.course.id, mode="verified")
with patch(
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified'
) as user_verify:
user_verify.return_value = True
@@ -1568,7 +1568,7 @@ class ProgressPageTests(ProgressPageBaseTests):
)
CourseEnrollment.enroll(self.user, self.course.id, mode="verified")
with patch(
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified'
) as user_verify:
user_verify.return_value = True
@@ -1593,7 +1593,7 @@ class ProgressPageTests(ProgressPageBaseTests):
)
CourseEnrollment.enroll(self.user, self.course.id, mode="verified")
with patch(
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified'
) as user_verify:
user_verify.return_value = True
@@ -1675,7 +1675,7 @@ class ProgressPageTests(ProgressPageBaseTests):
)
CourseEnrollment.enroll(self.user, self.course.id, mode="verified")
with patch(
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified'
) as user_verify:
user_verify.return_value = True
@@ -1723,7 +1723,7 @@ class ProgressPageTests(ProgressPageBaseTests):
)
CourseEnrollment.enroll(self.user, self.course.id, mode="verified")
with patch(
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified'
) as user_verify:
user_verify.return_value = True
with patch('lms.djangoapps.certificates.api.certificate_downloadable_status',

View File

@@ -70,7 +70,7 @@ from lms.djangoapps.experiments.utils import get_experiment_user_metadata_contex
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.instructor.enrollment import uses_shib
from lms.djangoapps.instructor.views.api import require_global_staff
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from lms.djangoapps.verify_student.services import IDVerificationService
from openedx.core.djangoapps.catalog.utils import get_programs, get_programs_with_type
from openedx.core.djangoapps.certificates import api as auto_certs_api
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
@@ -1017,7 +1017,7 @@ def _downloadable_certificate_message(course, cert_downloadable_status):
def _missing_required_verification(student, enrollment_mode):
return (
enrollment_mode in CourseMode.VERIFIED_MODES and not SoftwareSecurePhotoVerification.user_is_verified(student)
enrollment_mode in CourseMode.VERIFIED_MODES and not IDVerificationService.user_is_verified(student)
)

View File

@@ -32,7 +32,7 @@ from lms.djangoapps.certificates.tests.factories import (
from course_modes.models import CourseMode
from courseware.tests.factories import GlobalStaffFactory, InstructorFactory, UserFactory
from lms.djangoapps.grades.tests.utils import mock_passing_grade
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from lms.djangoapps.verify_student.services import IDVerificationService
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
from student.models import CourseEnrollment
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
@@ -398,10 +398,9 @@ class CertificatesInstructorApiTest(SharedModuleStoreTestCase):
# Create and assert user's ID verification record.
SoftwareSecurePhotoVerificationFactory.create(user=self.user, status=id_verification_status)
actual_verification_status = SoftwareSecurePhotoVerification.verification_status_for_user(
actual_verification_status = IDVerificationService.verification_status_for_user(
self.user,
self.course.id,
enrollment.mode,
enrollment.mode
)
self.assertEquals(actual_verification_status, verification_output)

View File

@@ -20,7 +20,7 @@ import xmodule.graders as xmgraders
from lms.djangoapps.certificates.models import CertificateStatuses, GeneratedCertificate
from courseware.models import StudentModule
from lms.djangoapps.grades.context import grading_context_for_course
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from lms.djangoapps.verify_student.services import IDVerificationService
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from shoppingcart.models import (
CouponRedemption,
@@ -286,9 +286,8 @@ def enrolled_students_features(course_key, features):
if include_enrollment_mode or include_verification_status:
enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_key)[0]
if include_verification_status:
student_dict['verification_status'] = SoftwareSecurePhotoVerification.verification_status_for_user(
student_dict['verification_status'] = IDVerificationService.verification_status_for_user(
student,
course_key,
enrollment_mode
)
if include_enrollment_mode:

View File

@@ -184,7 +184,7 @@ class TestAnalyticsBasic(ModuleStoreTestCase):
# is returned by verification and enrollment code
with patch("student.models.CourseEnrollment.enrollment_mode_for_user") as enrollment_patch:
with patch(
"lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.verification_status_for_user"
"lms.djangoapps.verify_student.services.IDVerificationService.verification_status_for_user"
) as verify_patch:
enrollment_patch.return_value = ["verified"]
verify_patch.return_value = "dummy verification status"

View File

@@ -20,7 +20,7 @@ from lms.djangoapps.grades.context import grading_context, grading_context_for_c
from lms.djangoapps.grades.models import PersistentCourseGrade
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.teams.models import CourseTeamMembership
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from lms.djangoapps.verify_student.services import IDVerificationService
from openedx.core.djangoapps.content.block_structure.api import get_course_in_cache
from openedx.core.djangoapps.course_groups.cohorts import bulk_cache_cohorts, get_cohort, is_course_cohorted
from openedx.core.djangoapps.user_api.course_tag.api import BulkCourseTags
@@ -170,8 +170,7 @@ class _EnrollmentBulkContext(object):
def __init__(self, context, users):
CourseEnrollment.bulk_fetch_enrollment_states(users, context.course_id)
self.verified_users = [
verified.user.id for verified in
SoftwareSecurePhotoVerification.verified_query().filter(user__in=users).select_related('user')
verified.user.id for verified in IDVerificationService.get_verified_users(users)
]
@@ -386,9 +385,8 @@ class CourseGradeReport(object):
given user.
"""
enrollment_mode = CourseEnrollment.enrollment_mode_for_user(user, context.course_id)[0]
verification_status = SoftwareSecurePhotoVerification.verification_status_for_user(
verification_status = IDVerificationService.verification_status_for_user(
user,
context.course_id,
enrollment_mode,
user_is_verified=user.id in bulk_enrollments.verified_users,
)

View File

@@ -27,13 +27,11 @@ from django.core.urlresolvers import reverse
from django.db import models
from django.dispatch import receiver
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from model_utils import Choices
from model_utils.models import StatusModel, TimeStampedModel
from opaque_keys.edx.django.models import CourseKeyField
from course_modes.models import CourseMode
from lms.djangoapps.verify_student.ssencrypt import (
encrypt_and_encode,
generate_signed_message,
@@ -41,9 +39,9 @@ from lms.djangoapps.verify_student.ssencrypt import (
rsa_encrypt
)
from openedx.core.djangoapps.signals.signals import LEARNER_NOW_VERIFIED
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.storage import get_storage
from .utils import earliest_allowed_verification_date
log = logging.getLogger(__name__)
@@ -193,213 +191,6 @@ class PhotoVerification(StatusModel):
abstract = True
ordering = ['-created_at']
##### Methods listed in the order you'd typically call them
@classmethod
def _earliest_allowed_date(cls):
"""
Returns the earliest allowed date given the settings
"""
days_good_for = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
return datetime.now(pytz.UTC) - timedelta(days=days_good_for)
@classmethod
def user_is_verified(cls, user, earliest_allowed_date=None):
"""
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.
This will check for the user's *initial* verification.
"""
return cls.verified_query(earliest_allowed_date).filter(user=user).exists()
@classmethod
def verified_query(cls, earliest_allowed_date=None):
"""
Return a query set for all records with 'approved' state
that are still valid according to the earliest_allowed_date
value or policy settings.
"""
return cls.objects.filter(
status="approved",
created_at__gte=(earliest_allowed_date or cls._earliest_allowed_date()),
)
@classmethod
def verification_valid_or_pending(cls, user, earliest_allowed_date=None, queryset=None):
"""
Check whether the user has a complete verification attempt that is
or *might* be good. This means that it's approved, been submitted,
or would have been submitted but had an non-user error when it was
being submitted.
It's basically any situation in which the user has signed off on
the contents of the attempt, and we have not yet received a denial.
This will check for the user's *initial* verification.
Arguments:
user:
earliest_allowed_date: earliest allowed date given in the
settings
queryset: If a queryset is provided, that will be used instead
of hitting the database.
Returns:
queryset: queryset of 'PhotoVerification' sorted by 'created_at' in
descending order.
"""
valid_statuses = ['submitted', 'approved', 'must_retry']
if queryset is None:
queryset = cls.objects.filter(user=user)
return queryset.filter(
status__in=valid_statuses,
created_at__gte=(
earliest_allowed_date
or cls._earliest_allowed_date()
)
).order_by('-created_at')
@classmethod
def get_expiration_datetime(cls, user, queryset=None):
"""
Check whether the user has an approved verification and return the
"expiration_datetime" of most recent "approved" verification.
Arguments:
user (Object): User
queryset: If a queryset is provided, that will be used instead
of hitting the database.
Returns:
expiration_datetime: expiration_datetime of most recent "approved"
verification.
"""
if queryset is None:
queryset = cls.objects.filter(user=user)
photo_verification = queryset.filter(status='approved').first()
if photo_verification:
return photo_verification.expiration_datetime
@classmethod
def user_has_valid_or_pending(cls, user, earliest_allowed_date=None, queryset=None):
"""
Check whether the user has an active or pending verification attempt
Returns:
bool: True or False according to existence of valid verifications
"""
return cls.verification_valid_or_pending(user, earliest_allowed_date, queryset).exists()
@classmethod
def active_for_user(cls, user):
"""
Return the most recent PhotoVerification that is marked ready (i.e. the
user has said they're set, but we haven't submitted anything yet).
This checks for the original verification.
"""
# This should only be one at the most, but just in case we create more
# by mistake, we'll grab the most recently created one.
active_attempts = cls.objects.filter(user=user, status='ready').order_by('-created_at')
if active_attempts:
return active_attempts[0]
else:
return None
@classmethod
def user_status(cls, user):
"""
Returns the status of the user based on their past verification attempts
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 initial verifications
"""
status = 'none'
error_msg = ''
if cls.user_is_verified(user):
status = 'approved'
elif cls.user_has_valid_or_pending(user):
# user_has_valid_or_pending does include 'approved', but if we are
# here, we know that the attempt is still pending
status = 'pending'
else:
# we need to check the most recent attempt to see if we need to ask them to do
# a retry
try:
attempts = cls.objects.filter(user=user).order_by('-updated_at')
attempt = attempts[0]
except IndexError:
# we return 'none'
return ('none', error_msg)
if attempt.created_at < cls._earliest_allowed_date():
return (
'expired',
_("Your {platform_name} verification has expired.").format(
platform_name=configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
)
)
# If someone is denied their original verification attempt, they can try to reverify.
if attempt.status == 'denied':
status = 'must_reverify'
if attempt.error_msg:
error_msg = attempt.parsed_error_msg()
return (status, error_msg)
@classmethod
def verification_for_datetime(cls, deadline, candidates):
"""Find a verification in a set that applied during a particular datetime.
A verification is considered "active" during a datetime if:
1) The verification was created before the datetime, and
2) The verification is set to expire after the datetime.
Note that verification status is *not* considered here,
just the start/expire dates.
If multiple verifications were active at the deadline,
returns the most recently created one.
Arguments:
deadline (datetime): The datetime at which the verification applied.
If `None`, then return the most recently created candidate.
candidates (list of `PhotoVerification`s): Potential verifications to search through.
Returns:
PhotoVerification: A photo verification that was active at the deadline.
If no verification was active, return None.
"""
if len(candidates) == 0:
return None
# If there's no deadline, then return the most recently created verification
if deadline is None:
return candidates[0]
# Otherwise, look for a verification that was in effect at the deadline,
# preferring recent verifications.
# If no such verification is found, implicitly return `None`
for verification in candidates:
if verification.active_at_datetime(deadline):
return verification
@property
def expiration_datetime(self):
"""Datetime that the verification will expire. """
@@ -642,7 +433,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
user=user,
status__in=["submitted", "approved"],
created_at__gte=(
earliest_allowed_date or cls._earliest_allowed_date()
earliest_allowed_date or earliest_allowed_verification_date()
)
).exclude(photo_id_key='')
@@ -974,34 +765,6 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
return response
@classmethod
def verification_status_for_user(cls, user, course_id, 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 is_verification_expiring_soon(cls, expiration_datetime):
"""
Returns True if verification is expiring within EXPIRING_SOON_WINDOW.
"""
if expiration_datetime:
if (expiration_datetime - datetime.now(pytz.UTC)).days <= settings.VERIFY_STUDENT.get(
"EXPIRING_SOON_WINDOW"):
return True
return False
class VerificationDeadline(TimeStampedModel):
"""

View File

@@ -1,21 +1,26 @@
"""
Implementation of "reverification" service to communicate with Reverification XBlock
Implementation of abstraction layer for other parts of the system to make queries related to ID Verification.
"""
import logging
from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from course_modes.models import CourseMode
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from student.models import User
from .models import SoftwareSecurePhotoVerification
from .utils import earliest_allowed_verification_date
log = logging.getLogger(__name__)
class VerificationService(object):
class XBlockVerificationService(object):
"""
Learner verification XBlock service
Learner verification XBlock service.
"""
def get_status(self, user_id):
@@ -33,12 +38,209 @@ class VerificationService(object):
'must_reverify' - verification has been denied and user must resubmit photos
"""
user = User.objects.get(id=user_id)
# TODO: provide a photo verification abstraction so that this
# isn't hard-coded to use Software Secure.
return SoftwareSecurePhotoVerification.user_status(user)
return IDVerificationService.user_status(user)
def reverify_url(self):
"""
Returns the URL for a user to verify themselves.
"""
return reverse('verify_student_reverify')
class IDVerificationService(object):
"""
Learner verification service interface for callers within edx-platform.
"""
@classmethod
def user_is_verified(cls, user, earliest_allowed_date=None):
"""
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.
This will check for the user's *initial* verification.
"""
return cls.verified_query(earliest_allowed_date).filter(user=user).exists()
@classmethod
def verified_query(cls, earliest_allowed_date=None):
"""
Return a query set for all records with 'approved' state
that are still valid according to the earliest_allowed_date
value or policy settings.
"""
return SoftwareSecurePhotoVerification.objects.filter(
status="approved",
created_at__gte=(earliest_allowed_date or earliest_allowed_verification_date()),
)
@classmethod
def verifications_for_user(cls, user):
"""
Return a query set for all records associated with the given user.
"""
return SoftwareSecurePhotoVerification.objects.filter(user=user)
@classmethod
def get_verified_users(cls, users):
"""
Return the list of user ids that have non expired verifications from the given list of users.
"""
return cls.verified_query().filter(user__in=users).select_related('user')
@classmethod
def verification_valid_or_pending(cls, user, earliest_allowed_date=None, queryset=None):
"""
Check whether the user has a complete verification attempt that is
or *might* be good. This means that it's approved, been submitted,
or would have been submitted but had an non-user error when it was
being submitted.
It's basically any situation in which the user has signed off on
the contents of the attempt, and we have not yet received a denial.
This will check for the user's *initial* verification.
Arguments:
user:
earliest_allowed_date: earliest allowed date given in the
settings
queryset: If a queryset is provided, that will be used instead
of hitting the database.
Returns:
queryset: queryset of 'PhotoVerification' sorted by 'created_at' in
descending order.
"""
valid_statuses = ['submitted', 'approved', 'must_retry']
if queryset is None:
queryset = SoftwareSecurePhotoVerification.objects.filter(user=user)
return queryset.filter(
status__in=valid_statuses,
created_at__gte=(
earliest_allowed_date
or earliest_allowed_verification_date()
)
).order_by('-created_at')
@classmethod
def get_expiration_datetime(cls, user, queryset=None):
"""
Check whether the user has an approved verification and return the
"expiration_datetime" of most recent "approved" verification.
Arguments:
user (Object): User
queryset: If a queryset is provided, that will be used instead
of hitting the database.
Returns:
expiration_datetime: expiration_datetime of most recent "approved"
verification.
"""
if queryset is None:
queryset = SoftwareSecurePhotoVerification.objects.filter(user=user)
photo_verification = queryset.filter(status='approved').first()
if photo_verification:
return photo_verification.expiration_datetime
@classmethod
def user_has_valid_or_pending(cls, user, earliest_allowed_date=None, queryset=None):
"""
Check whether the user has an active or pending verification attempt
Returns:
bool: True or False according to existence of valid verifications
"""
return cls.verification_valid_or_pending(user, earliest_allowed_date, queryset).exists()
@classmethod
def active_for_user(cls, user):
"""
Return the most recent PhotoVerification that is marked ready (i.e. the
user has said they're set, but we haven't submitted anything yet).
This checks for the original verification.
"""
# This should only be one at the most, but just in case we create more
# by mistake, we'll grab the most recently created one.
active_attempts = SoftwareSecurePhotoVerification.objects.filter(
user=user,
status='ready'
).order_by('-created_at')
if active_attempts:
return active_attempts[0]
else:
return None
@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 initial verifications
"""
status = 'none'
error_msg = ''
if cls.user_is_verified(user):
status = 'approved'
elif cls.user_has_valid_or_pending(user):
# user_has_valid_or_pending does include 'approved', but if we are
# here, we know that the attempt is still pending
status = 'pending'
else:
# we need to check the most recent attempt to see if we need to ask them to do
# a retry
try:
attempts = SoftwareSecurePhotoVerification.objects.filter(user=user).order_by('-updated_at')
attempt = attempts[0]
except IndexError:
# we return 'none'
return ('none', error_msg)
if attempt.created_at < earliest_allowed_verification_date():
return (
'expired',
_("Your {platform_name} verification has expired.").format(
platform_name=configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
)
)
# If someone is denied their original verification attempt, they can try to reverify.
if attempt.status == 'denied':
status = 'must_reverify'
if attempt.error_msg:
error_msg = attempt.parsed_error_msg()
return (status, error_msg)
@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'

View File

@@ -12,8 +12,6 @@ from freezegun import freeze_time
from mock import patch
from nose.tools import ( # pylint: disable=no-name-in-module
assert_equals,
assert_false,
assert_is_none,
assert_raises,
assert_true
)
@@ -29,7 +27,6 @@ from lms.djangoapps.verify_student.models import (
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
FAKE_SETTINGS = {
"SOFTWARE_SECURE": {
@@ -233,116 +230,6 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase):
self.assertEqual(attempt.photo_id_key, "fake-photo-id-key")
def test_active_for_user(self):
"""
Make sure we can retrive a user's active (in progress) verification
attempt.
"""
user = UserFactory.create()
# This user has no active at the moment...
assert_is_none(SoftwareSecurePhotoVerification.active_for_user(user))
# Create an attempt and mark it ready...
attempt = SoftwareSecurePhotoVerification(user=user)
attempt.mark_ready()
assert_equals(attempt, SoftwareSecurePhotoVerification.active_for_user(user))
# A new user won't see this...
user2 = UserFactory.create()
user2.save()
assert_is_none(SoftwareSecurePhotoVerification.active_for_user(user2))
# If it's got a different status, it doesn't count
for status in ["submitted", "must_retry", "approved", "denied"]:
attempt.status = status
attempt.save()
assert_is_none(SoftwareSecurePhotoVerification.active_for_user(user))
# But if we create yet another one and mark it ready, it passes again.
attempt_2 = SoftwareSecurePhotoVerification(user=user)
attempt_2.mark_ready()
assert_equals(attempt_2, SoftwareSecurePhotoVerification.active_for_user(user))
# And if we add yet another one with a later created time, we get that
# one instead. We always want the most recent attempt marked ready()
attempt_3 = SoftwareSecurePhotoVerification(
user=user,
created_at=attempt_2.created_at + timedelta(days=1)
)
attempt_3.save()
# We haven't marked attempt_3 ready yet, so attempt_2 still wins
assert_equals(attempt_2, SoftwareSecurePhotoVerification.active_for_user(user))
# Now we mark attempt_3 ready and expect it to come back
attempt_3.mark_ready()
assert_equals(attempt_3, SoftwareSecurePhotoVerification.active_for_user(user))
def test_user_is_verified(self):
"""
Test to make sure we correctly answer whether a user has been verified.
"""
user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user)
attempt.save()
# If it's any of these, they're not verified...
for status in ["created", "ready", "denied", "submitted", "must_retry"]:
attempt.status = status
attempt.save()
assert_false(SoftwareSecurePhotoVerification.user_is_verified(user), status)
attempt.status = "approved"
attempt.save()
assert_true(SoftwareSecurePhotoVerification.user_is_verified(user), attempt.status)
def test_user_has_valid_or_pending(self):
"""
Determine whether we have to prompt this user to verify, or if they've
already at least initiated a verification submission.
"""
user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user)
# If it's any of these statuses, they don't have anything outstanding
for status in ["created", "ready", "denied"]:
attempt.status = status
attempt.save()
assert_false(SoftwareSecurePhotoVerification.user_has_valid_or_pending(user), status)
# Any of these, and we are. Note the benefit of the doubt we're giving
# -- must_retry, and submitted both count until we hear otherwise
for status in ["submitted", "must_retry", "approved"]:
attempt.status = status
attempt.save()
assert_true(SoftwareSecurePhotoVerification.user_has_valid_or_pending(user), status)
def test_user_status(self):
# test for correct status when no error returned
user = UserFactory.create()
status = SoftwareSecurePhotoVerification.user_status(user)
self.assertEquals(status, ('none', ''))
# test for when one has been created
attempt = SoftwareSecurePhotoVerification.objects.create(user=user, status='approved')
status = SoftwareSecurePhotoVerification.user_status(user)
self.assertEquals(status, ('approved', ''))
# create another one for the same user, make sure the right one is
# returned
SoftwareSecurePhotoVerification.objects.create(
user=user, status='denied', error_msg='[{"photoIdReasons": ["Not provided"]}]'
)
status = SoftwareSecurePhotoVerification.user_status(user)
self.assertEquals(status, ('approved', ''))
# now delete the first one and verify that the denial is being handled
# properly
attempt.delete()
status = SoftwareSecurePhotoVerification.user_status(user)
self.assertEquals(status, ('must_reverify', ['id_image_missing']))
# pylint: disable=line-too-long
def test_parse_error_msg_success(self):
user = UserFactory.create()
@@ -384,87 +271,6 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase):
attempt.save()
self.assertFalse(attempt.active_at_datetime(datetime.now(pytz.UTC) + timedelta(days=1)))
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)
@ddt.unpack
@ddt.data(
{'enrollment_mode': 'honor', 'status': None, 'output': 'N/A'},
{'enrollment_mode': 'audit', 'status': None, 'output': 'N/A'},
{'enrollment_mode': 'verified', 'status': False, 'output': 'Not ID Verified'},
{'enrollment_mode': 'verified', 'status': True, 'output': 'ID Verified'},
)
def test_verification_status_for_user(self, enrollment_mode, status, output):
"""
Verify verification_status_for_user returns correct status.
"""
user = UserFactory.create()
course = CourseFactory.create()
with patch(
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
) as mock_verification:
mock_verification.return_value = status
status = SoftwareSecurePhotoVerification.verification_status_for_user(user, course.id, enrollment_mode)
self.assertEqual(status, output)
def test_initial_verification_for_user(self):
"""Test that method 'get_initial_verification' of model
'SoftwareSecurePhotoVerification' always returns the initial

View File

@@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
"""
Tests for the service classes in verify_student.
"""
from datetime import timedelta
import ddt
from django.conf import settings
from mock import patch
from nose.tools import (
assert_equals,
assert_false,
assert_is_none,
assert_true
)
from common.test.utils import MockS3Mixin
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from lms.djangoapps.verify_student.services import IDVerificationService
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
FAKE_SETTINGS = {
"DAYS_GOOD_FOR": 10,
}
@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
@ddt.ddt
class TestIDVerificationService(MockS3Mixin, ModuleStoreTestCase):
"""
Tests for IDVerificationService.
"""
def test_active_for_user(self):
"""
Make sure we can retrive a user's active (in progress) verification
attempt.
"""
user = UserFactory.create()
# This user has no active at the moment...
assert_is_none(IDVerificationService.active_for_user(user))
# Create an attempt and mark it ready...
attempt = SoftwareSecurePhotoVerification(user=user)
attempt.mark_ready()
assert_equals(attempt, IDVerificationService.active_for_user(user))
# A new user won't see this...
user2 = UserFactory.create()
user2.save()
assert_is_none(IDVerificationService.active_for_user(user2))
# If it's got a different status, it doesn't count
for status in ["submitted", "must_retry", "approved", "denied"]:
attempt.status = status
attempt.save()
assert_is_none(IDVerificationService.active_for_user(user))
# But if we create yet another one and mark it ready, it passes again.
attempt_2 = SoftwareSecurePhotoVerification(user=user)
attempt_2.mark_ready()
assert_equals(attempt_2, IDVerificationService.active_for_user(user))
# And if we add yet another one with a later created time, we get that
# one instead. We always want the most recent attempt marked ready()
attempt_3 = SoftwareSecurePhotoVerification(
user=user,
created_at=attempt_2.created_at + timedelta(days=1)
)
attempt_3.save()
# We haven't marked attempt_3 ready yet, so attempt_2 still wins
assert_equals(attempt_2, IDVerificationService.active_for_user(user))
# Now we mark attempt_3 ready and expect it to come back
attempt_3.mark_ready()
assert_equals(attempt_3, IDVerificationService.active_for_user(user))
def test_user_is_verified(self):
"""
Test to make sure we correctly answer whether a user has been verified.
"""
user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user)
attempt.save()
# If it's any of these, they're not verified...
for status in ["created", "ready", "denied", "submitted", "must_retry"]:
attempt.status = status
attempt.save()
assert_false(IDVerificationService.user_is_verified(user), status)
attempt.status = "approved"
attempt.save()
assert_true(IDVerificationService.user_is_verified(user), attempt.status)
def test_user_has_valid_or_pending(self):
"""
Determine whether we have to prompt this user to verify, or if they've
already at least initiated a verification submission.
"""
user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user)
# If it's any of these statuses, they don't have anything outstanding
for status in ["created", "ready", "denied"]:
attempt.status = status
attempt.save()
assert_false(IDVerificationService.user_has_valid_or_pending(user), status)
# Any of these, and we are. Note the benefit of the doubt we're giving
# -- must_retry, and submitted both count until we hear otherwise
for status in ["submitted", "must_retry", "approved"]:
attempt.status = status
attempt.save()
assert_true(IDVerificationService.user_has_valid_or_pending(user), status)
def test_user_status(self):
# test for correct status when no error returned
user = UserFactory.create()
status = IDVerificationService.user_status(user)
self.assertEquals(status, ('none', ''))
# test for when one has been created
attempt = SoftwareSecurePhotoVerification.objects.create(user=user, status='approved')
status = IDVerificationService.user_status(user)
self.assertEquals(status, ('approved', ''))
# create another one for the same user, make sure the right one is
# returned
SoftwareSecurePhotoVerification.objects.create(
user=user, status='denied', error_msg='[{"photoIdReasons": ["Not provided"]}]'
)
status = IDVerificationService.user_status(user)
self.assertEquals(status, ('approved', ''))
# now delete the first one and verify that the denial is being handled
# properly
attempt.delete()
status = IDVerificationService.user_status(user)
self.assertEquals(status, ('must_reverify', ['id_image_missing']))
@ddt.unpack
@ddt.data(
{'enrollment_mode': 'honor', 'status': None, 'output': 'N/A'},
{'enrollment_mode': 'audit', 'status': None, 'output': 'N/A'},
{'enrollment_mode': 'verified', 'status': False, 'output': 'Not ID Verified'},
{'enrollment_mode': 'verified', 'status': True, 'output': 'ID Verified'},
)
def test_verification_status_for_user(self, enrollment_mode, status, output):
"""
Verify verification_status_for_user returns correct status.
"""
user = UserFactory.create()
CourseFactory.create()
with patch(
'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified'
) as mock_verification:
mock_verification.return_value = status
status = IDVerificationService.verification_status_for_user(user, enrollment_mode)
self.assertEqual(status, output)

View File

@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
"""
Tests for verify_student utility functions.
"""
from datetime import datetime, timedelta
import unittest
import pytz
from mock import patch
from pytest import mark
from django.conf import settings
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from lms.djangoapps.verify_student.utils import verification_for_datetime
from student.tests.factories import UserFactory
FAKE_SETTINGS = {
"DAYS_GOOD_FOR": 10,
}
@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
@mark.django_db
class TestVerifyStudentUtils(unittest.TestCase):
"""
Tests for utility functions in verify_student.
"""
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 = verification_for_datetime(now, query)
self.assertIs(result, None)
# Should also return None if no deadline specified
query = SoftwareSecurePhotoVerification.objects.filter(user=user)
result = 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 = 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 = 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 = 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 = 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 = 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 = verification_for_datetime(deadline, query)
self.assertEqual(result, second_attempt)

View File

@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
"""
Common Utilities for the verify_student application.
"""
import datetime
import logging
import pytz
from django.conf import settings
from django.core.mail import send_mail
@@ -43,3 +45,61 @@ def send_verification_status_email(context):
subject=context['subject'],
user=context['user'].id
))
def is_verification_expiring_soon(expiration_datetime):
"""
Returns True if verification is expiring within EXPIRING_SOON_WINDOW.
"""
if expiration_datetime:
if (expiration_datetime - datetime.datetime.now(pytz.UTC)).days <= settings.VERIFY_STUDENT.get(
"EXPIRING_SOON_WINDOW"):
return True
return False
def earliest_allowed_verification_date():
"""
Returns the earliest allowed date given the settings
"""
days_good_for = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
return datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=days_good_for)
def verification_for_datetime(deadline, candidates):
"""Find a verification in a set that applied during a particular datetime.
A verification is considered "active" during a datetime if:
1) The verification was created before the datetime, and
2) The verification is set to expire after the datetime.
Note that verification status is *not* considered here,
just the start/expire dates.
If multiple verifications were active at the deadline,
returns the most recently created one.
Arguments:
deadline (datetime): The datetime at which the verification applied.
If `None`, then return the most recently created candidate.
candidates (list of `PhotoVerification`s): Potential verifications to search through.
Returns:
PhotoVerification: A photo verification that was active at the deadline.
If no verification was active, return None.
"""
if not candidates:
return None
# If there's no deadline, then return the most recently created verification
if deadline is None:
return candidates[0]
# Otherwise, look for a verification that was in effect at the deadline,
# preferring recent verifications.
# If no such verification is found, implicitly return `None`
for verification in candidates:
if verification.active_at_datetime(deadline):
return verification

View File

@@ -35,8 +35,9 @@ from edxmako.shortcuts import render_to_response, render_to_string
from lms.djangoapps.commerce.utils import EcommerceService, is_account_activation_requirement_disabled
from lms.djangoapps.verify_student.image import InvalidImageData, decode_image_data
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline
from lms.djangoapps.verify_student.utils import send_verification_status_email
from lms.djangoapps.verify_student.services import IDVerificationService
from lms.djangoapps.verify_student.ssencrypt import has_valid_signature
from lms.djangoapps.verify_student.utils import is_verification_expiring_soon, send_verification_status_email
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
from openedx.core.djangoapps.embargo import api as embargo_api
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
@@ -645,7 +646,7 @@ class PayAndVerifyView(View):
Returns:
datetime object in string format
"""
photo_verifications = SoftwareSecurePhotoVerification.verification_valid_or_pending(user)
photo_verifications = IDVerificationService.verification_valid_or_pending(user)
# return 'expiration_datetime' of latest photo verification if found,
# otherwise implicitly return ''
if photo_verifications:
@@ -664,7 +665,7 @@ class PayAndVerifyView(View):
submitted photos within the expiration period.
"""
return SoftwareSecurePhotoVerification.user_has_valid_or_pending(user)
return IDVerificationService.user_has_valid_or_pending(user)
def _check_enrollment(self, user, course_key):
"""Check whether the user has an active enrollment and has paid.
@@ -1202,12 +1203,12 @@ class ReverifyView(View):
Most of the work is done client-side by composing the same
Backbone views used in the initial verification flow.
"""
status, __ = SoftwareSecurePhotoVerification.user_status(request.user)
status, __ = IDVerificationService.user_status(request.user)
expiration_datetime = SoftwareSecurePhotoVerification.get_expiration_datetime(request.user)
expiration_datetime = IDVerificationService.get_expiration_datetime(request.user)
can_reverify = False
if expiration_datetime:
if SoftwareSecurePhotoVerification.is_verification_expiring_soon(expiration_datetime):
if is_verification_expiring_soon(expiration_datetime):
# The user has an active verification, but the verification
# is set to expire within "EXPIRING_SOON_WINDOW" days (default is 4 weeks).
# In this case user can resubmit photos for reverification.