diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py index 913d69f06b53d9cfc959e6e2fca35ed65572f069..816f5da8d44659c2e2ac77c990412a1860588051 100644 --- a/lms/djangoapps/verify_student/models.py +++ b/lms/djangoapps/verify_student/models.py @@ -4,8 +4,8 @@ Models for Student Identity Verification This is where we put any models relating to establishing the real-life identity of a student over a period of time. Right now, the only models are the abstract -`PhotoVerificationAttempt`, and its one concrete implementation -`SoftwareSecurePhotoVerificationAttempt`. The hope is to keep as much of the +`PhotoVerification`, and its one concrete implementation +`SoftwareSecurePhotoVerification`. The hope is to keep as much of the photo verification process as generic as possible. """ from datetime import datetime @@ -63,14 +63,14 @@ def status_before_must_be(*valid_start_statuses): return decorator_func -class PhotoVerificationAttempt(StatusModel): +class PhotoVerification(StatusModel): """ - Each PhotoVerificationAttempt represents a Student's attempt to establish + Each PhotoVerification represents a Student's attempt to establish their identity by uploading a photo of themselves and a picture ID. An attempt actually has a number of fields that need to be filled out at different steps of the approval process. While it's useful as a Django Model for the querying facilities, **you should only create and edit a - `PhotoVerificationAttempt` object through the methods provided**. Do not + `PhotoVerification` object through the methods provided**. Do not just construct one and start setting fields unless you really know what you're doing. @@ -96,9 +96,9 @@ class PhotoVerificationAttempt(StatusModel): Because this Model inherits from StatusModel, we can also do things like:: - attempt.status == PhotoVerificationAttempt.STATUS.created + attempt.status == PhotoVerification.STATUS.created attempt.status == "created" - pending_requests = PhotoVerificationAttempt.submitted.all() + pending_requests = PhotoVerification.submitted.all() """ ######################## Fields Set During Creation ######################## # See class docstring for description of status states @@ -154,24 +154,33 @@ class PhotoVerificationAttempt(StatusModel): class Meta: abstract = True + ordering = ['-created_at'] ##### Methods listed in the order you'd typically call them @classmethod - def user_is_verified(cls, user_id): - """Returns whether or not a user has satisfactorily proved their + def user_is_verified(cls, user): + """ + Returns 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.""" raise NotImplementedError @classmethod - def active_for_user(cls, user_id): - """Return all PhotoVerificationAttempts that are still active (i.e. not + def active_for_user(cls, user): + """ + Return all PhotoVerifications that are still active (i.e. not approved or denied). Should there only be one active at any given time for a user? Enforced at the DB level? """ - raise NotImplementedError + # 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='created') + if active_attempts: + return active_attempts[0] + else: + return None @status_before_must_be("created") def upload_face_image(self, img): @@ -315,10 +324,10 @@ class PhotoVerificationAttempt(StatusModel): self.save() -class SoftwareSecurePhotoVerificationAttempt(PhotoVerificationAttempt): +class SoftwareSecurePhotoVerification(PhotoVerification): """ Model to verify identity using a service provided by Software Secure. Much - of the logic is inherited from `PhotoVerificationAttempt`, but this class + of the logic is inherited from `PhotoVerification`, but this class encrypts the photos. Software Secure (http://www.softwaresecure.com/) is a remote proctoring @@ -368,4 +377,3 @@ class SoftwareSecurePhotoVerificationAttempt(PhotoVerificationAttempt): ) rsa_cipher = PKCS1_OAEP.new(key) rsa_encrypted_aes_key = rsa_cipher.encrypt(aes_key) - diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py index 2c80447d6ccf89c22595c5a0c1c0873c7b641195..838da26fbadd6779b30d2b8a3861f8e3bb151dae 100644 --- a/lms/djangoapps/verify_student/tests/test_models.py +++ b/lms/djangoapps/verify_student/tests/test_models.py @@ -1,12 +1,13 @@ -# -*- coding: utf-8 -*- -from nose.tools import assert_in, assert_is_none, assert_equals, \ - assert_raises, assert_not_equals +# -*- coding: utf-8 -*- +from nose.tools import ( + assert_in, assert_is_none, assert_equals, assert_raises, assert_not_equals +) from django.test import TestCase from student.tests.factories import UserFactory -from verify_student.models import PhotoVerificationAttempt, VerificationException +from verify_student.models import SoftwareSecurePhotoVerification, VerificationException -class TestPhotoVerificationAttempt(object): +class TestPhotoVerification(object): def test_state_transitions(self): """Make sure we can't make unexpected status transitions. @@ -14,12 +15,12 @@ class TestPhotoVerificationAttempt(object): The status transitions we expect are:: created → ready → submitted → approved - ↑ ↓ + ↑ ↓ → denied """ user = UserFactory.create() - attempt = PhotoVerificationAttempt(user=user) - assert_equals(attempt.status, PhotoVerificationAttempt.STATUS.created) + attempt = SoftwareSecurePhotoVerification(user=user) + assert_equals(attempt.status, SoftwareSecurePhotoVerification.STATUS.created) assert_equals(attempt.status, "created") # This should fail because we don't have the necessary fields filled out @@ -38,7 +39,7 @@ class TestPhotoVerificationAttempt(object): assert_equals(attempt.status, "ready") # Once again, state transitions should fail here. We can't approve or - # deny anything until it's been placed into the submitted state -- i.e. + # deny anything until it's been placed into the submitted state -- i.e. # the user has clicked on whatever agreements, or given payment, or done # whatever the application requires before it agrees to process their # attempt. diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py index a3644615e8a9b92e5ba3fda323fc14a09e2733fd..de085639701c4761ae3022ad9dc7b6fd1f1fb5e4 100644 --- a/lms/djangoapps/verify_student/urls.py +++ b/lms/djangoapps/verify_student/urls.py @@ -25,4 +25,19 @@ urlpatterns = patterns( views.final_verification, name="verify_student/final_verification" ), + + # The above are what we did for the design mockups, but what we're really + # looking at now is: + url( + r'^show_verification_page', + views.show_verification_page, + name="verify_student/show_verification_page" + ), + + url( + r'^start_or_resume_attempt', + views.start_or_resume_attempt, + name="verify_student/start_or_resume_attempt" + ) + ) diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index acaafb092de00e542403e64eb237d915a4a6b0a1..3da2dd3bbe4a3b31c7c3008d17c5b3cd11bed040 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -4,14 +4,26 @@ """ from mitxmako.shortcuts import render_to_response +from verify_student.models import SoftwareSecurePhotoVerification + # @login_required -def start_or_resume_attempt(request): +def start_or_resume_attempt(request, course_id): """ If they've already started a PhotoVerificationAttempt, we move to wherever they are in that process. If they've completed one, then we skip straight to payment. """ - pass + # If the user has already been verified within the given time period, + # redirect straight to the payment -- no need to verify again. + #if SoftwareSecurePhotoVerification.user_is_verified(user): + # pass + + attempt = SoftwareSecurePhotoVerification.active_for_user(request.user) + if not attempt: + # Redirect to show requirements + pass + + # if attempt. def show_requirements(request): """This might just be a plain template without a view.""" @@ -29,3 +41,9 @@ def photo_id_upload(request): def final_verification(request): context = { "course_id" : "edX/Certs101/2013_Test" } return render_to_response("verify_student/final_verification.html", context) + +# + +def show_verification_page(request): + pass +