From 0dd581968328cec64e822f5dd5a7c3e472da147a Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Thu, 22 Aug 2013 13:56:23 -0400 Subject: [PATCH] basic flow from enroll mode selection to verify screen --- cms/envs/common.py | 1 + common/djangoapps/course_modes/models.py | 4 ++ common/djangoapps/course_modes/urls.py | 9 ++++ common/djangoapps/course_modes/views.py | 44 +++++++++++++++--- common/djangoapps/student/views.py | 10 +++-- lms/djangoapps/verify_student/models.py | 45 +++++++++++++++---- .../verify_student/tests/test_views.py | 2 - lms/djangoapps/verify_student/urls.py | 6 +-- lms/djangoapps/verify_student/views.py | 42 ++++++++++------- lms/envs/common.py | 5 +++ 10 files changed, 130 insertions(+), 38 deletions(-) create mode 100644 common/djangoapps/course_modes/urls.py diff --git a/cms/envs/common.py b/cms/envs/common.py index 29e99b2551..a06d5a36e1 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -363,6 +363,7 @@ INSTALLED_APPS = ( 'course_modes' ) + ################# EDX MARKETING SITE ################################## EDXMKTG_COOKIE_NAME = 'edxloggedin' diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index 721b587603..6790630e3b 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -52,6 +52,10 @@ class CourseMode(models.Model): modes = [cls.DEFAULT_MODE] return modes + @classmethod + def modes_for_course_dict(cls, course_id): + return { mode.slug : mode for mode in cls.modes_for_course(course_id) } + def __unicode__(self): return u"{} : {}, min={}, prices={}".format( self.course_id, self.mode_slug, self.min_price, self.suggested_prices diff --git a/common/djangoapps/course_modes/urls.py b/common/djangoapps/course_modes/urls.py new file mode 100644 index 0000000000..80ce7d4bda --- /dev/null +++ b/common/djangoapps/course_modes/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import include, patterns, url +from django.views.generic import TemplateView + +from course_modes import views + +urlpatterns = patterns( + '', + url(r'^choose', views.ChooseModeView.as_view(), name="course_modes_choose"), +) diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 2f7356a1a7..0a563a932b 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -1,9 +1,18 @@ -from django.http import HttpResponse +from django.core.urlresolvers import reverse +from django.http import ( + HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, Http404 +) +from django.shortcuts import redirect from django.views.generic.base import View +from django.utils.translation import ugettext as _ +from django.utils.http import urlencode from mitxmako.shortcuts import render_to_response from course_modes.models import CourseMode +from courseware.access import has_access +from student.models import CourseEnrollment +from student.views import course_from_id class ChooseModeView(View): @@ -11,17 +20,42 @@ class ChooseModeView(View): course_id = request.GET.get("course_id") context = { "course_id" : course_id, - "available_modes" : CourseMode.modes_for_course(course_id) + "modes" : CourseMode.modes_for_course_dict(course_id) } return render_to_response("course_modes/choose.html", context) + def post(self, request): course_id = request.GET.get("course_id") - mode_slug = request.POST.get("mode_slug") user = request.user # This is a bit redundant with logic in student.views.change_enrollement, # but I don't really have the time to refactor it more nicely and test. course = course_from_id(course_id) - if has_access(user, course, 'enroll'): - pass + if not has_access(user, course, 'enroll'): + return HttpResponseBadRequest(_("Enrollment is closed")) + + requested_mode = self.get_requested_mode(request.POST.get("mode")) + + allowed_modes = CourseMode.modes_for_course_dict(course_id) + if requested_mode not in allowed_modes: + return HttpResponseBadRequest(_("Enrollment mode not supported")) + + if requested_mode in ("audit", "honor"): + CourseEnrollment.enroll(user, course_id) + return redirect('dashboard') + + if requested_mode == "verified": + return redirect( + "{}?{}".format( + reverse('verify_student_verify'), + urlencode(dict(course_id=course_id)) + ) + ) + + def get_requested_mode(self, user_choice): + choices = { + "Select Audit" : "audit", + "Select Certificate" : "verified" + } + return choices.get(user_choice) \ No newline at end of file diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index b3f296080a..0808f56f3d 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -24,8 +24,7 @@ from django.db import IntegrityError, transaction from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, Http404 from django.shortcuts import redirect from django_future.csrf import ensure_csrf_cookie -from django.utils.http import cookie_date -from django.utils.http import base36_to_int +from django.utils.http import cookie_date, base36_to_int, urlencode from django.utils.translation import ugettext as _ from django.views.decorators.http import require_POST @@ -372,7 +371,12 @@ def change_enrollment(request): # where they can choose which mode they want. available_modes = CourseMode.modes_for_course(course_id) if len(available_modes) > 1: - return HttpResponse(reverse("course_modes.views.choose")) + return HttpResponse( + "{}?{}".format( + reverse("course_modes_choose"), + urlencode(dict(course_id=course_id)) + ) + ) org, course_num, run = course_id.split("/") statsd.increment("common.student.enrollment", diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py index 816f5da8d4..7d9b9799c8 100644 --- a/lms/djangoapps/verify_student/models.py +++ b/lms/djangoapps/verify_student/models.py @@ -8,7 +8,7 @@ of a student over a period of time. Right now, the only models are the abstract `SoftwareSecurePhotoVerification`. The hope is to keep as much of the photo verification process as generic as possible. """ -from datetime import datetime +from datetime import datetime, timedelta from hashlib import md5 import base64 import functools @@ -17,6 +17,7 @@ import uuid import pytz +from django.conf import settings from django.db import models from django.contrib.auth.models import User from model_utils.models import StatusModel @@ -69,10 +70,10 @@ class PhotoVerification(StatusModel): 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 - `PhotoVerification` object through the methods provided**. Do not - just construct one and start setting fields unless you really know what - you're doing. + for the querying facilities, **you should only edit a `PhotoVerification` + object through the methods provided**. Initialize them with a user: + + attempt = PhotoVerification(user=user) We track this attempt through various states: @@ -100,6 +101,9 @@ class PhotoVerification(StatusModel): attempt.status == "created" pending_requests = PhotoVerification.submitted.all() """ + # We can make this configurable later... + DAYS_GOOD_FOR = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"] + ######################## Fields Set During Creation ######################## # See class docstring for description of status states STATUS = Choices('created', 'ready', 'submitted', 'approved', 'denied') @@ -158,12 +162,37 @@ class PhotoVerification(StatusModel): ##### Methods listed in the order you'd typically call them @classmethod - def user_is_verified(cls, user): + def user_is_verified(cls, user, earliest_allowed_date=None): """ 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 + time, so a user might have to renew periodically. + """ + earliest_allowed_date = ( + earliest_allowed_date or + datetime.now(pytz.UTC) - timedelta(days=cls.DAYS_GOOD_FOR) + ) + return cls.objects.filter( + user=user, + status="approved", + created_at__lte=earliest_allowed_date + ).exists() + + @classmethod + def user_has_valid_or_pending(cls, user, earliest_allowed_date=None): + """ + TODO: eliminate duplication with user_is_verified + """ + valid_statuses = ['ready', 'submitted', 'approved'] + earliest_allowed_date = ( + earliest_allowed_date or + datetime.now(pytz.UTC) - timedelta(days=cls.DAYS_GOOD_FOR) + ) + return cls.objects.filter( + user=user, + status__in=valid_statuses, + created_at__lte=earliest_allowed_date + ).exists() @classmethod def active_for_user(cls, user): diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index 22426c811d..0d37aa0efd 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -31,8 +31,6 @@ class StartView(TestCase): user = UserFactory.create(username="rusty", password="test") self.client.login(username="rusty", password="test") - - def must_be_logged_in(self): self.assertHttpForbidden(self.client.get(self.start_url())) diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py index de08563970..43e258e912 100644 --- a/lms/djangoapps/verify_student/urls.py +++ b/lms/djangoapps/verify_student/urls.py @@ -35,9 +35,9 @@ urlpatterns = patterns( ), url( - r'^start_or_resume_attempt', - views.start_or_resume_attempt, - name="verify_student/start_or_resume_attempt" + r'^verify', + views.VerifyView.as_view(), + name="verify_student_verify" ) ) diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 5d113c68b1..4876399ed6 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -4,28 +4,36 @@ """ from mitxmako.shortcuts import render_to_response -from verify_student.models import SoftwareSecurePhotoVerification +from django.conf import settings +from django.core.urlresolvers import reverse +from django.shortcuts import redirect +from django.views.generic.base import View from course_modes.models import CourseMode +from verify_student.models import SoftwareSecurePhotoVerification -# @login_required -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. - """ - # 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 +class VerifyView(View): - attempt = SoftwareSecurePhotoVerification.active_for_user(request.user) - if not attempt: - # Redirect to show requirements - pass + def get(self, request): + """ + """ + # 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_has_valid_or_pending(request.user): + progress_state = "payment" + else: + # If they haven't completed a verification attempt, we have to + # restart with a new one. We can't reuse an older one because we + # won't be able to show them their encrypted photo_id -- it's easier + # bookkeeping-wise just to start over. + progress_state = "start" + + return render_to_response('verify_student/face_upload.html') + + + def post(request): + attempt = SoftwareSecurePhotoVerification(user=request.user) - # if attempt. def show_requirements(request): """This might just be a plain template without a view.""" diff --git a/lms/envs/common.py b/lms/envs/common.py index 22923de539..c178c1ca30 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -823,3 +823,8 @@ def enable_theme(theme_name): # avoid collisions with default edX static files STATICFILES_DIRS.append((u'themes/%s' % theme_name, theme_root / 'static')) + +################# Student Verification ################# +VERIFY_STUDENT = { + "DAYS_GOOD_FOR" : 365, # How many days is a verficiation good for? +}