From d84f7648403b61c2c0b97ecf3cf2b26c70a1a63d Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Fri, 9 Aug 2013 12:14:29 -0400 Subject: [PATCH 1/8] Basic first commit of Photo ID Verification model and test code --- lms/djangoapps/verify_student/__init__.py | 0 lms/djangoapps/verify_student/api.py | 0 .../verify_student/migrations/__init__.py | 0 lms/djangoapps/verify_student/models.py | 322 ++++++++++++++++++ .../verify_student/tests/__init__.py | 0 .../verify_student/tests/test_models.py | 59 ++++ .../verify_student/tests/test_views.py | 37 ++ lms/djangoapps/verify_student/urls.py | 0 lms/djangoapps/verify_student/views.py | 13 + lms/envs/common.py | 3 + requirements/edx/base.txt | 1 + 11 files changed, 435 insertions(+) create mode 100644 lms/djangoapps/verify_student/__init__.py create mode 100644 lms/djangoapps/verify_student/api.py create mode 100644 lms/djangoapps/verify_student/migrations/__init__.py create mode 100644 lms/djangoapps/verify_student/models.py create mode 100644 lms/djangoapps/verify_student/tests/__init__.py create mode 100644 lms/djangoapps/verify_student/tests/test_models.py create mode 100644 lms/djangoapps/verify_student/tests/test_views.py create mode 100644 lms/djangoapps/verify_student/urls.py create mode 100644 lms/djangoapps/verify_student/views.py diff --git a/lms/djangoapps/verify_student/__init__.py b/lms/djangoapps/verify_student/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/verify_student/api.py b/lms/djangoapps/verify_student/api.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/verify_student/migrations/__init__.py b/lms/djangoapps/verify_student/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py new file mode 100644 index 0000000000..852bc4a50f --- /dev/null +++ b/lms/djangoapps/verify_student/models.py @@ -0,0 +1,322 @@ +# -*- coding: utf-8 -*- +""" +Models for Student Identity Verification + +Currently the only model is `PhotoVerificationAttempt`, but this is where we +would put any models relating to establishing the real-life identity of a +student over a period of time. +""" +from datetime import datetime +import functools +import logging +import uuid + +import pytz +from django.db import models +from django.contrib.auth.models import User + +from model_utils.models import StatusModel +from model_utils import Choices + +log = logging.getLogger(__name__) + + +class VerificationException(Exception): + pass + + +class IdVerifiedCourses(models.Model): + """ + A table holding all the courses that are eligible for ID Verification. + """ + course_id = models.CharField(blank=False, max_length=100) + + +def status_before_must_be(*valid_start_statuses): + """ + Decorator with arguments to make sure that an object with a `status` + attribute is in one of a list of acceptable status states before a method + is called. You could use it in a class definition like: + + @status_before_must_be("submitted", "approved", "denied") + def refund_user(self, user_id): + # Do logic here... + + If the object has a status that is not listed when the `refund_user` method + is invoked, it will throw a `VerificationException`. This is just to avoid + distracting boilerplate when looking at a Model that needs to go through a + workflow process. + """ + def decorator_func(fn): + @functools.wraps(fn) + def with_status_check(obj, *args, **kwargs): + if obj.status not in valid_start_statuses: + exception_msg = ( + u"Error calling {} {}: status is '{}', must be one of: {}" + ).format(fn, obj, obj.status, valid_start_statuses) + raise VerificationException(exception_msg) + return fn(obj, *args, **kwargs) + + return with_status_check + + return decorator_func + + +class PhotoVerificationAttempt(StatusModel): + """ + Each PhotoVerificationAttempt 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 + just construct one and start setting fields unless you really know what + you're doing. + + We track this attempt through various states: + + `created` + Initial creation and state we're in after uploading the images. + `ready` + The user has uploaded their images and checked that they can read the + images. There's a separate state here because it may be the case that we + don't actually submit this attempt for review until payment is made. + `submitted` + Submitted for review. The review may be done by a staff member or an + external service. The user cannot make changes once in this state. + `approved` + An admin or an external service has confirmed that the user's photo and + photo ID match up, and that the photo ID's name matches the user's. + `denied` + The request has been denied. See `error_msg` for details on why. An + admin might later override this and change to `approved`, but the + student cannot re-open this attempt -- they have to create another + attempt and submit it instead. + + Because this Model inherits from StatusModel, we can also do things like:: + + attempt.status == PhotoVerificationAttempt.STATUS.created + attempt.status == "created" + pending_requests = PhotoVerificationAttempt.submitted.all() + """ + ######################## Fields Set During Creation ######################## + # See class docstring for description of status states + STATUS = Choices('created', 'ready', 'submitted', 'approved', 'denied') + user = models.ForeignKey(User, db_index=True) + + # They can change their name later on, so we want to copy the value here so + # we always preserve what it was at the time they requested. We only copy + # this value during the mark_ready() step. Prior to that, you should be + # displaying the user's name from their user.profile.name. + name = models.CharField(blank=True, max_length=255) + + # Where we place the uploaded image files (e.g. S3 URLs) + face_image_url = models.URLField(blank=True, max_length=255) + photo_id_image_url = models.URLField(blank=True, max_length=255) + + # Randomly generated UUID so that external services can post back the + # results of checking a user's photo submission without use exposing actual + # user IDs or something too easily guessable. + receipt_id = models.CharField( + db_index=True, + default=uuid.uuid4, + max_length=255, + ) + + created_at = models.DateTimeField(auto_now_add=True, db_index=True) + updated_at = models.DateTimeField(auto_now=True, db_index=True) + + + ######################## Fields Set When Submitting ######################## + submitted_at = models.DateTimeField(null=True, db_index=True) + + + #################### Fields Set During Approval/Denial ##################### + # If the review was done by an internal staff member, mark who it was. + reviewing_user = models.ForeignKey( + User, + db_index=True, + default=None, + null=True, + related_name="photo_verifications_reviewed" + ) + + # Mark the name of the service used to evaluate this attempt (e.g + # Software Secure). + reviewing_service = models.CharField(blank=True, max_length=255) + + # If status is "denied", this should contain text explaining why. + error_msg = models.TextField(blank=True) + + # Non-required field. External services can add any arbitrary codes as time + # goes on. We don't try to define an exhuastive list -- this is just + # capturing it so that we can later query for the common problems. + error_code = models.CharField(blank=True, max_length=50) + + + ##### 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 + 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 + approved or denied). + + Should there only be one active at any given time for a user? Enforced + at the DB level? + """ + raise NotImplementedError + + + @status_before_must_be("created") + def upload_face_image(self, img): + raise NotImplementedError + + + @status_before_must_be("created") + def upload_photo_id_image(self, img): + raise NotImplementedError + + + @status_before_must_be("created") + def mark_ready(self): + """ + Mark that the user data in this attempt is correct. In order to + succeed, the user must have uploaded the necessary images + (`face_image_url`, `photo_id_image_url`). This method will also copy + their name from their user profile. Prior to marking it ready, we read + this value directly from their profile, since they're free to change it. + This often happens because people put in less formal versions of their + name on signup, but realize they want something different to go on a + formal document. + + Valid attempt statuses when calling this method: + `created` + + Status after method completes: `ready` + + Other fields that will be set by this method: + `name` + + State Transitions: + + `created` → `ready` + This is what happens when the user confirms to us that the pictures + they uploaded are good. Note that we don't actually do a submission + anywhere yet. + """ + if not self.face_image_url: + raise VerificationException("No face image was uploaded.") + if not self.photo_id_image_url: + raise VerificationException("No photo ID image was uploaded.") + + # At any point prior to this, they can change their names via their + # student dashboard. But at this point, we lock the value into the + # attempt. + self.name = self.user.profile.name + self.status = "ready" + self.save() + + + @status_before_must_be("ready", "submit") + def submit(self, reviewing_service=None): + if self.status == "submitted": + return + + if reviewing_service: + reviewing_service.submit(self) + self.submitted_at = datetime.now(pytz.UTC) + self.status = "submitted" + self.save() + + + @status_before_must_be("submitted", "approved", "denied") + def approve(self, user_id=None, service=""): + """ + Approve this attempt. `user_id` + + Valid attempt statuses when calling this method: + `submitted`, `approved`, `denied` + + Status after method completes: `approved` + + Other fields that will be set by this method: + `reviewed_by_user_id`, `reviewed_by_service`, `error_msg` + + State Transitions: + + `submitted` → `approved` + This is the usual flow, whether initiated by a staff user or an + external validation service. + `approved` → `approved` + No-op. First one to approve it wins. + `denied` → `approved` + This might happen if a staff member wants to override a decision + made by an external service or another staff member (say, in + response to a support request). In this case, the previous values + of `reviewed_by_user_id` and `reviewed_by_service` will be changed + to whoever is doing the approving, and `error_msg` will be reset. + The only record that this record was ever denied would be in our + logs. This should be a relatively rare occurence. + """ + # If someone approves an outdated version of this, the first one wins + if self.status == "approved": + return + + self.error_msg = "" # reset, in case this attempt was denied before + self.error_code = "" # reset, in case this attempt was denied before + self.reviewing_user = user_id + self.reviewing_service = service + self.status = "approved" + self.save() + + + @status_before_must_be("submitted", "approved", "denied") + def deny(self, + error_msg, + error_code="", + reviewing_user=None, + reviewing_service=""): + """ + Deny this attempt. + + Valid attempt statuses when calling this method: + `submitted`, `approved`, `denied` + + Status after method completes: `denied` + + Other fields that will be set by this method: + `reviewed_by_user_id`, `reviewed_by_service`, `error_msg`, `error_code` + + State Transitions: + + `submitted` → `denied` + This is the usual flow, whether initiated by a staff user or an + external validation service. + `approved` → `denied` + This might happen if a staff member wants to override a decision + made by an external service or another staff member, or just correct + a mistake made during the approval process. In this case, the + previous values of `reviewed_by_user_id` and `reviewed_by_service` + will be changed to whoever is doing the denying. The only record + that this record was ever approved would be in our logs. This should + be a relatively rare occurence. + `denied` → `denied` + Update the error message and reviewing_user/reviewing_service. Just + lets you amend the error message in case there were additional + details to be made. + """ + self.error_msg = error_msg + self.error_code = error_code + self.reviewing_user = reviewing_user + self.reviewing_service = reviewing_service + self.status = "denied" + self.save() + + diff --git a/lms/djangoapps/verify_student/tests/__init__.py b/lms/djangoapps/verify_student/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py new file mode 100644 index 0000000000..2c80447d6c --- /dev/null +++ b/lms/djangoapps/verify_student/tests/test_models.py @@ -0,0 +1,59 @@ +# -*- 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 + + +class TestPhotoVerificationAttempt(object): + + def test_state_transitions(self): + """Make sure we can't make unexpected status transitions. + + 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) + assert_equals(attempt.status, "created") + + # This should fail because we don't have the necessary fields filled out + assert_raises(VerificationException, attempt.mark_ready) + + # These should all fail because we're in the wrong starting state. + assert_raises(VerificationException, attempt.submit) + assert_raises(VerificationException, attempt.approve) + assert_raises(VerificationException, attempt.deny) + + # Now let's fill in some values so that we can pass the mark_ready() call + attempt.face_image_url = "http://fake.edx.org/face.jpg" + attempt.photo_id_image_url = "http://fake.edx.org/photo_id.jpg" + attempt.mark_ready() + assert_equals(attempt.name, user.profile.name) # Move this to another test + 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. + # the user has clicked on whatever agreements, or given payment, or done + # whatever the application requires before it agrees to process their + # attempt. + assert_raises(VerificationException, attempt.approve) + assert_raises(VerificationException, attempt.deny) + + # Now we submit + attempt.submit() + assert_equals(attempt.status, "submitted") + + # So we should be able to both approve and deny + attempt.approve() + assert_equals(attempt.status, "approved") + + attempt.deny("Could not read name on Photo ID") + assert_equals(attempt.status, "denied") + + diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py new file mode 100644 index 0000000000..47b08f7b35 --- /dev/null +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -0,0 +1,37 @@ +""" + + +verify_student/start?course_id=MITx/6.002x/2013_Spring # create + /upload_face?course_id=MITx/6.002x/2013_Spring + /upload_photo_id + /confirm # mark_ready() + + ---> To Payment + +""" +import urllib + +from django.test import TestCase +from django.test.utils import override_settings + +from xmodule.modulestore.tests.factories import CourseFactory +from student.tests.factories import UserFactory + + +class StartView(TestCase): + + def start_url(course_id=""): + return "/verify_student/start?course_id={0}".format(urllib.quote(course_id)) + + def test_start_new_verification(self): + """ + Test the case where the user has no pending `PhotoVerficiationAttempts`, + but is just starting their first. + """ + 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())) + + \ No newline at end of file diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py new file mode 100644 index 0000000000..964f8fa0f3 --- /dev/null +++ b/lms/djangoapps/verify_student/views.py @@ -0,0 +1,13 @@ +""" + + +""" + +@login_required +def start(request): + """ + 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. + """ + \ No newline at end of file diff --git a/lms/envs/common.py b/lms/envs/common.py index 3e65c70805..cbcf2d59e7 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -774,6 +774,9 @@ INSTALLED_APPS = ( # Different Course Modes 'course_modes' + + # Student Identity Verification + 'verify_student', ) ######################### MARKETING SITE ############################### diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 9179315797..070f0a060d 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -12,6 +12,7 @@ django-followit==0.0.3 django-keyedcache==1.4-6 django-kombu==0.9.4 django-mako==0.1.5pre +django-model-utils==1.4.0 django-masquerade==0.1.6 django-mptt==0.5.5 django-openid-auth==0.4 From 59c2bb18ef0ae3c7b7f806cb6d1be52b6e4ed381 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Thu, 15 Aug 2013 14:20:36 -0400 Subject: [PATCH 2/8] Bare bones outline of ID verification templates --- lms/djangoapps/verify_student/urls.py | 28 +++++++++++++++++++ lms/djangoapps/verify_student/views.py | 24 ++++++++++++++-- lms/envs/common.py | 5 +++- lms/envs/dev.py | 1 + lms/templates/courseware/course_about.html | 2 ++ lms/templates/verify_student/face_upload.html | 11 ++++++++ .../verify_student/final_verification.html | 10 +++++++ .../verify_student/photo_id_upload.html | 11 ++++++++ .../verify_student/show_requirements.html | 12 ++++++++ lms/urls.py | 6 ++++ 10 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 lms/templates/verify_student/face_upload.html create mode 100644 lms/templates/verify_student/final_verification.html create mode 100644 lms/templates/verify_student/photo_id_upload.html create mode 100644 lms/templates/verify_student/show_requirements.html diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py index e69de29bb2..a3644615e8 100644 --- a/lms/djangoapps/verify_student/urls.py +++ b/lms/djangoapps/verify_student/urls.py @@ -0,0 +1,28 @@ +from django.conf.urls import include, patterns, url +from django.views.generic import TemplateView + +from verify_student import views + +urlpatterns = patterns( + '', + url( + r'^show_requirements', + views.show_requirements, + name="verify_student/show_requirements" + ), + url( + r'^face_upload', + views.face_upload, + name="verify_student/face_upload" + ), + url( + r'^photo_id_upload', + views.photo_id_upload, + name="verify_student/photo_id_upload" + ), + url( + r'^final_verification', + views.final_verification, + name="verify_student/final_verification" + ), +) diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 964f8fa0f3..acaafb092d 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -2,12 +2,30 @@ """ +from mitxmako.shortcuts import render_to_response -@login_required -def start(request): +# @login_required +def start_or_resume_attempt(request): """ 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. """ - \ No newline at end of file + pass + +def show_requirements(request): + """This might just be a plain template without a view.""" + context = { "course_id" : "edX/Certs101/2013_Test" } + return render_to_response("verify_student/show_requirements.html", context) + +def face_upload(request): + context = { "course_id" : "edX/Certs101/2013_Test" } + return render_to_response("verify_student/face_upload.html", context) + +def photo_id_upload(request): + context = { "course_id" : "edX/Certs101/2013_Test" } + return render_to_response("verify_student/photo_id_upload.html", context) + +def final_verification(request): + context = { "course_id" : "edX/Certs101/2013_Test" } + return render_to_response("verify_student/final_verification.html", context) diff --git a/lms/envs/common.py b/lms/envs/common.py index cbcf2d59e7..930fd6dcab 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -154,6 +154,9 @@ MITX_FEATURES = { # Toggle to enable chat availability (configured on a per-course # basis in Studio) 'ENABLE_CHAT': False, + + # Allow users to enroll with methods other than just honor code certificates + 'MULTIPLE_ENROLLMENT_ROLES' : False } # Used for A/B testing @@ -773,7 +776,7 @@ INSTALLED_APPS = ( 'notification_prefs', # Different Course Modes - 'course_modes' + 'course_modes', # Student Identity Verification 'verify_student', diff --git a/lms/envs/dev.py b/lms/envs/dev.py index b9768554b1..889ba71e35 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -30,6 +30,7 @@ MITX_FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True MITX_FEATURES['ENABLE_INSTRUCTOR_BETA_DASHBOARD'] = True +MITX_FEATURES['MULTIPLE_ENROLLMENT_ROLES'] = True FEEDBACK_SUBMISSION_EMAIL = "dummy@dummy.org" diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index e4a453133d..39cf04e72c 100644 --- a/lms/templates/courseware/course_about.html +++ b/lms/templates/courseware/course_about.html @@ -96,6 +96,8 @@ %else: ${_("Register for {course.display_number_with_default}").format(course=course) | h} + Mock Verify Enrollment +
%endif diff --git a/lms/templates/verify_student/face_upload.html b/lms/templates/verify_student/face_upload.html new file mode 100644 index 0000000000..6338750c06 --- /dev/null +++ b/lms/templates/verify_student/face_upload.html @@ -0,0 +1,11 @@ +<%! from django.utils.translation import ugettext as _ %> +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../main.html" /> + +<%block name="content"> + +

Face Upload!

+ +Upload Photo ID + + diff --git a/lms/templates/verify_student/final_verification.html b/lms/templates/verify_student/final_verification.html new file mode 100644 index 0000000000..c9abf876ae --- /dev/null +++ b/lms/templates/verify_student/final_verification.html @@ -0,0 +1,10 @@ +<%! from django.utils.translation import ugettext as _ %> +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../main.html" /> + +<%block name="content"> + +Final Verification! + + + diff --git a/lms/templates/verify_student/photo_id_upload.html b/lms/templates/verify_student/photo_id_upload.html new file mode 100644 index 0000000000..b6724656f4 --- /dev/null +++ b/lms/templates/verify_student/photo_id_upload.html @@ -0,0 +1,11 @@ +<%! from django.utils.translation import ugettext as _ %> +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../main.html" /> + +<%block name="content"> + +

Photo ID Upload!

+ +Final Verification + + diff --git a/lms/templates/verify_student/show_requirements.html b/lms/templates/verify_student/show_requirements.html new file mode 100644 index 0000000000..5fa00a0145 --- /dev/null +++ b/lms/templates/verify_student/show_requirements.html @@ -0,0 +1,12 @@ +<%! from django.utils.translation import ugettext as _ %> +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../main.html" /> + +<%block name="content"> + +

Requirements Page!

+ +Upload Face + + + diff --git a/lms/urls.py b/lms/urls.py index b32c0263d0..58c2cd3b55 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -61,8 +61,13 @@ urlpatterns = ('', # nopep8 url(r'^heartbeat$', include('heartbeat.urls')), url(r'^user_api/', include('user_api.urls')), + ) +if settings.MITX_FEATURES.get("MULTIPLE_ENROLLMENT_ROLES"): + urlpatterns += (url(r'^verify_student/', include('verify_student.urls')),) + + js_info_dict = { 'domain': 'djangojs', 'packages': ('lms',), @@ -340,6 +345,7 @@ if settings.COURSEWARE_ENABLED: name='submission_history'), ) + if settings.COURSEWARE_ENABLED and settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'): urlpatterns += ( url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/instructor_dashboard$', From 94c442c9ccaef9b11e99b4858c02810a049d21ce Mon Sep 17 00:00:00 2001 From: Frances Botsford Date: Mon, 19 Aug 2013 16:48:36 -0400 Subject: [PATCH 3/8] initial rough skeleton for verification flow --- lms/templates/verify_student/face_upload.html | 244 +++++++++++++++++- 1 file changed, 242 insertions(+), 2 deletions(-) diff --git a/lms/templates/verify_student/face_upload.html b/lms/templates/verify_student/face_upload.html index 6338750c06..662934c20d 100644 --- a/lms/templates/verify_student/face_upload.html +++ b/lms/templates/verify_student/face_upload.html @@ -3,9 +3,249 @@ <%inherit file="../main.html" /> <%block name="content"> +
-

Face Upload!

+ -Upload Photo ID +
+

Your Progress

+
    +
  1. Current: Step 1 Take Your Photo
  2. +
  3. Step 2 ID Photo
  4. +
  5. Step 3 Review
  6. +
  7. Step 4 Payment
  8. +
  9. Finished Confirmation
  10. +
+
+ + + +
+
+

Take Your Photo

+

Use your webcam to take a picture of your face so we can match it with the picture on your ID.

+ +
+ +
+ +
+ +
+ + + + + +
+ +
+ +
+

Tips on taking a successful photo

+
    +
  • Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
  • +
  • Maecenas faucibus mollis interdum.
  • +
  • Nullam id dolor id nibh ultricies vehicula ut id elit.
  • +
  • Cras mattis consectetur purus sit amet fermentum.
  • +
+
+ +
+

Common Questions

+
+
Cras justo odio, dapibus ac facilisis in, egestas eget quam.
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+
Vestibulum id ligula porta felis euismod semper.
+
Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
+
Aenean eu leo quam.
+
Pellentesque ornare sem lacinia quam venenatis vestibulum.
+
Maecenas faucibus mollis interdum.
+
+
+ +
+ +
+
+ + + + +
+

Take Your Photo

+

Use your webcam to take a picture of your face so we can match it with the picture on your ID.

+ +
+ +
+ +
+ + + +
+ +
+

Tips on taking a successful photo

+
    +
  • Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
  • +
  • Maecenas faucibus mollis interdum.
  • +
  • Nullam id dolor id nibh ultricies vehicula ut id elit.
  • +
  • Cras mattis consectetur purus sit amet fermentum.
  • +
+
+ +
+

Common Questions

+
+
Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
+
Aenean eu leo quam.
+
Pellentesque ornare sem lacinia quam venenatis vestibulum.
+
Maecenas faucibus mollis interdum.
+
Cras justo odio, dapibus ac facilisis in, egestas eget quam.
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+
Vestibulum id ligula porta felis euismod semper.
+
+
+ +
+ +
+
+ + + +
+

Verify Your Submission

+

Make sure we can verify your identity with the photos and information below.

+ +
+

Check Your Name

+

Make sure your full name on your edX account, [User Name], matches your ID. We will also use this as the name on your certificate.

+

Edit my name

+ + +
+ +
+
+ +
+ +

The photo above needs to meet the following requirements:

+
    +
  • Be well lit
  • +
  • Show your whole face
  • +
  • Match your ID
  • +
+
+ +
+
+ +
+ +

The photo above needs to meet the following requirements:

+
    +
  • Be readable (not too far away, no glare)
  • +
  • Show your name
  • +
  • Match the photo of your face and your name above
  • +
+
+ + + + + +
+ + + +
+

More questions? Check out our FAQs.

+

Change your mind? You can always Audit the course for free without verifying.

+
+ + + + +
From 2e821be65fa5f00d237ef6eb36bc5061fccee533 Mon Sep 17 00:00:00 2001 From: Frances Botsford Date: Tue, 20 Aug 2013 16:04:48 -0400 Subject: [PATCH 4/8] adding some standard sass bits to LMS and bare bones sass for verification --- lms/static/sass/base/_mixins.scss | 6 + lms/static/sass/elements/_typography.scss | 178 ++++++++++++++++++ lms/static/sass/views/_verification.scss | 28 +++ lms/templates/verify_student/face_upload.html | 84 +++++---- 4 files changed, 259 insertions(+), 37 deletions(-) create mode 100644 lms/static/sass/elements/_typography.scss create mode 100644 lms/static/sass/views/_verification.scss diff --git a/lms/static/sass/base/_mixins.scss b/lms/static/sass/base/_mixins.scss index e2074f1976..8ee4559e36 100644 --- a/lms/static/sass/base/_mixins.scss +++ b/lms/static/sass/base/_mixins.scss @@ -1,3 +1,6 @@ +// lms - utilities - mixins and extends +// ==================== + // mixins - font sizing @mixin font-size($sizeValue: 16){ font-size: $sizeValue + px; @@ -44,6 +47,9 @@ } + + + //----------------- // Theme Mixin Styles //----------------- diff --git a/lms/static/sass/elements/_typography.scss b/lms/static/sass/elements/_typography.scss new file mode 100644 index 0000000000..157d3d60df --- /dev/null +++ b/lms/static/sass/elements/_typography.scss @@ -0,0 +1,178 @@ +// lms - elements - typography +// ==================== + +// Scale - (6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 21, 24, 36, 48, 60, 72) + +// headings/titles +.t-title { + font-family: $f-sans-serif; +} + +.t-title1 { + @extend .t-title; + @include font-size(60); + @include line-height(60); +} + +.t-title2 { + @extend .t-title; + @include font-size(48); + @include line-height(48); +} + +.t-title3 { + @include font-size(36); + @include line-height(36); +} + +.t-title4 { + @extend .t-title; + @include font-size(24); + @include line-height(24); +} + +.t-title5 { + @extend .t-title; + @include font-size(18); + @include line-height(18); +} + +.t-title6 { + @extend .t-title; + @include font-size(16); + @include line-height(16); +} + +.t-title7 { + @extend .t-title; + @include font-size(14); + @include line-height(14); +} + +.t-title8 { + @extend .t-title; + @include font-size(12); + @include line-height(12); +} + +.t-title9 { + @extend .t-title; + @include font-size(11); + @include line-height(11); +} + +// ==================== + +// copy +.t-copy { + font-family: $f-sans-serif; +} + +.t-copy-base { + @extend .t-copy; + @include font-size(16); + @include line-height(16); +} + +.t-copy-lead1 { + @extend .t-copy; + @include font-size(18); + @include line-height(18); +} + +.t-copy-lead2 { + @extend .t-copy; + @include font-size(24); + @include line-height(24); +} + +.t-copy-sub1 { + @extend .t-copy; + @include font-size(14); + @include line-height(14); +} + +.t-copy-sub2 { + @extend .t-copy; + @include font-size(12); + @include line-height(12); +} + +// ==================== + +// actions/labels +.t-action1 { + @include font-size(18); + @include line-height(18); +} + +.t-action2 { + @include font-size(16); + @include line-height(16); +} + +.t-action3 { + @include font-size(14); + @include line-height(14); +} + +.t-action4 { + @include font-size(12); + @include line-height(12); +} + + +// ==================== + +// code +.t-code { + font-family: $f-monospace; +} + +// ==================== + +// icons +.t-icon1 { + @include font-size(48); + @include line-height(48); +} + +.t-icon2 { + @include font-size(36); + @include line-height(36); +} + +.t-icon3 { + @include font-size(24); + @include line-height(24); +} + +.t-icon4 { + @include font-size(18); + @include line-height(18); +} + +.t-icon5 { + @include font-size(16); + @include line-height(16); +} + +.t-icon6 { + @include font-size(14); + @include line-height(14); +} + +.t-icon7 { + @include font-size(12); + @include line-height(12); +} + +.t-icon8 { + @include font-size(11); + @include line-height(11); +} + +.t-icon9 { + @include font-size(10); + @include line-height(10); +} diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss new file mode 100644 index 0000000000..e65901ab50 --- /dev/null +++ b/lms/static/sass/views/_verification.scss @@ -0,0 +1,28 @@ +// lms - views - verification flow +// ==================== + +body.register.verification { + + .page-header { + @extend .t-title3; + + + + .title { + + } + } + + .progress { + + } + + .block-photo { + + + } + + + + +} diff --git a/lms/templates/verify_student/face_upload.html b/lms/templates/verify_student/face_upload.html index 662934c20d..5f2c7959bc 100644 --- a/lms/templates/verify_student/face_upload.html +++ b/lms/templates/verify_student/face_upload.html @@ -1,6 +1,14 @@ <%! from django.utils.translation import ugettext as _ %> <%! from django.core.urlresolvers import reverse %> <%inherit file="../main.html" /> +<%block name="bodyclass">register verification + +<%block name="js_extra"> + + + <%block name="content">
@@ -12,19 +20,19 @@

Your Progress

    -
  1. Current: Step 1 Take Your Photo
  2. -
  3. Step 2 ID Photo
  4. -
  5. Step 3 Review
  6. -
  7. Step 4 Payment
  8. -
  9. Finished Confirmation
  10. +
  11. Current: Step 1 Take Your Photo
  12. +
  13. Step 2 ID Photo
  14. +
  15. Step 3 Review
  16. +
  17. Step 4 Payment
  18. +
  19. Finished Confirmation
-
-

Take Your Photo

+
+

Take Your Photo

Use your webcam to take a picture of your face so we can match it with the picture on your ID.

@@ -97,8 +105,8 @@ -
-

Take Your Photo

+
+

Take Your Photo

Use your webcam to take a picture of your face so we can match it with the picture on your ID.

@@ -153,40 +161,14 @@ -
-

Verify Your Submission

+
+

Verify Your Submission

Make sure we can verify your identity with the photos and information below.

Check Your Name

Make sure your full name on your edX account, [User Name], matches your ID. We will also use this as the name on your certificate.

Edit my name

- -
@@ -248,4 +230,32 @@
+ + + + From 07b94f7dd4cc356e8c64d2e18392423bea24359e Mon Sep 17 00:00:00 2001 From: Frances Botsford Date: Tue, 20 Aug 2013 21:59:23 -0400 Subject: [PATCH 5/8] added font variables for LMS, more sass for verification --- lms/static/sass/base/_variables.scss | 13 +- lms/static/sass/views/_verification.scss | 101 ++++++- lms/templates/verify_student/face_upload.html | 264 ++++++++++-------- 3 files changed, 257 insertions(+), 121 deletions(-) diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss index 93297f4043..2028b95efb 100644 --- a/lms/static/sass/base/_variables.scss +++ b/lms/static/sass/base/_variables.scss @@ -9,6 +9,7 @@ $fg-max-columns: 12; $fg-max-width: 1400px; $fg-min-width: 810px; +// fonts $sans-serif: 'Open Sans', $verdana; $monospace: Monaco, 'Bitstream Vera Sans Mono', 'Lucida Console', monospace; $body-font-family: $sans-serif; @@ -197,4 +198,14 @@ $homepage-bg-image: '../images/homepage-bg.jpg'; $login-banner-image: url(../images/bg-banner-login.png); $register-banner-image: url(../images/bg-banner-register.png); -$video-thumb-url: '../images/courses/video-thumb.jpg'; \ No newline at end of file +$video-thumb-url: '../images/courses/video-thumb.jpg'; + +//----------------- +// Newer variables ported from Studio +//----------------- + +// fonts +$f-serif: 'Bree Serif', Georgia, Cambria, 'Times New Roman', Times, serif; +$f-sans-serif: 'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif; +$f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace; + diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index e65901ab50..a0eebf0fea 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -2,27 +2,120 @@ // ==================== body.register.verification { + font-family: 'Open Sans', sans-serif; + + h1, h2, h3, h4, h5, h6, p { + font-family: 'Open Sans', sans-serif; + } + .page-header { - @extend .t-title3; - - .title { - + @extend .t-title5; } } .progress { + .progress-step { + border: 1px solid #eee; + display: inline-block; + padding: ($baseline/2) $baseline; + } + } + + + // for dev placement only + .placeholder-cam, + .placeholder-photo { + height: 300px; + background-color: #eee; + position: relative; + + p { + position: absolute; + top: 40%; + left: 40%; + color: #ccc; + } + } + + + #wrapper { + overflow: hidden; } .block-photo { + @include clearfix(); + + .title { + font-weight: bold; + } + + .wrapper-up, + .wrapper-down { + @include clearfix(); + } + + .cam { + width: 45%; + float: left; + padding-right: $baseline; + } + + .photo-controls { + background-color: #ddd; + + .controls-list { + margin: 0; + padding: 0; + list-style-type: none; + + .control { + display: inline-block; + } + } + } + + .faq { + width: 45%; + float: left; + padding-right: $baseline; + + dt { + font-weight: bold; + padding: 0 0 ($baseline/2) 0; + } + + dd { + margin: 0; + padding: 0 0 $baseline 0; + } + } + } + .photo-tips { + width: 45%; + float: left; + } + .actions { + width: 45%; + float: right; + } + + .review-photo { + width: 45%; + float: left; + } + + #review-facephoto { + margin-right: $baseline; + } } diff --git a/lms/templates/verify_student/face_upload.html b/lms/templates/verify_student/face_upload.html index 5f2c7959bc..2010ec53fe 100644 --- a/lms/templates/verify_student/face_upload.html +++ b/lms/templates/verify_student/face_upload.html @@ -20,7 +20,7 @@

Your Progress

    -
  1. Current: Step 1 Take Your Photo
  2. +
  3. Current: Step 1 Take Your Photo
  4. Step 2 ID Photo
  5. Step 3 Review
  6. Step 4 Payment
  7. @@ -31,137 +31,167 @@
    -
    +

    Take Your Photo

    Use your webcam to take a picture of your face so we can match it with the picture on your ID.

    -
    +
    -
    - +
    +
    + +

    cam image

    +
    + +
    + + + + + +
    -
    +
    +

    Tips on taking a successful photo

      -
    • - Take photo -
    • +
    • Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
    • +
    • Maecenas faucibus mollis interdum.
    • +
    • Nullam id dolor id nibh ultricies vehicula ut id elit.
    • +
    • Cras mattis consectetur purus sit amet fermentum.
    +
    +
    - +
    +
    +

    Common Questions

    +
    +
    Cras justo odio, dapibus ac facilisis in, egestas eget quam.
    +
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    +
    Vestibulum id ligula porta felis euismod semper.
    +
    Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
    +
    Aenean eu leo quam.
    +
    Pellentesque ornare sem lacinia quam venenatis vestibulum.
    +
    Maecenas faucibus mollis interdum.
    +
    +
    +
    - -
    - -
    -

    Tips on taking a successful photo

    -
      -
    • Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
    • -
    • Maecenas faucibus mollis interdum.
    • -
    • Nullam id dolor id nibh ultricies vehicula ut id elit.
    • -
    • Cras mattis consectetur purus sit amet fermentum.
    • -
    -
    - -
    -

    Common Questions

    -
    -
    Cras justo odio, dapibus ac facilisis in, egestas eget quam.
    -
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    -
    Vestibulum id ligula porta felis euismod semper.
    -
    Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
    -
    Aenean eu leo quam.
    -
    Pellentesque ornare sem lacinia quam venenatis vestibulum.
    -
    Maecenas faucibus mollis interdum.
    -
    -
    - -
    -
    -
    +

    Take Your Photo

    Use your webcam to take a picture of your face so we can match it with the picture on your ID.

    -
    +
    + +
    + +
    + +

    cam image

    +
    + +
    + + + + + +
    -
    -
    -
    - Take photo - - Retake - Looks good - - Looks good +
    +

    Tips on taking a successful photo

    +
      +
    • Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
    • +
    • Maecenas faucibus mollis interdum.
    • +
    • Nullam id dolor id nibh ultricies vehicula ut id elit.
    • +
    • Cras mattis consectetur purus sit amet fermentum.
    • +
    -
    -

    Tips on taking a successful photo

    -
      -
    • Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
    • -
    • Maecenas faucibus mollis interdum.
    • -
    • Nullam id dolor id nibh ultricies vehicula ut id elit.
    • -
    • Cras mattis consectetur purus sit amet fermentum.
    • -
    +
    + +
    +

    Common Questions

    +
    +
    Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
    +
    Aenean eu leo quam.
    +
    Pellentesque ornare sem lacinia quam venenatis vestibulum.
    +
    Maecenas faucibus mollis interdum.
    +
    Cras justo odio, dapibus ac facilisis in, egestas eget quam.
    +
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    +
    Vestibulum id ligula porta felis euismod semper.
    +
    +
    + +
    + +
    -
    -

    Common Questions

    -
    -
    Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
    -
    Aenean eu leo quam.
    -
    Pellentesque ornare sem lacinia quam venenatis vestibulum.
    -
    Maecenas faucibus mollis interdum.
    -
    Cras justo odio, dapibus ac facilisis in, egestas eget quam.
    -
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    -
    Vestibulum id ligula porta felis euismod semper.
    -
    -
    - -
    - -
    -
    +

    Verify Your Submission

    Make sure we can verify your identity with the photos and information below.

    @@ -171,30 +201,32 @@

    Edit my name

    -
    -
    - +
    +
    +
    + +
    + +

    The photo above needs to meet the following requirements:

    +
      +
    • Be well lit
    • +
    • Show your whole face
    • +
    • Match your ID
    • +
    -

    The photo above needs to meet the following requirements:

    -
      -
    • Be well lit
    • -
    • Show your whole face
    • -
    • Match your ID
    • -
    -
    +
    +
    + +
    -
    -
    - +

    The photo above needs to meet the following requirements:

    +
      +
    • Be readable (not too far away, no glare)
    • +
    • Show your name
    • +
    • Match the photo of your face and your name above
    • +
    - -

    The photo above needs to meet the following requirements:

    -
      -
    • Be readable (not too far away, no glare)
    • -
    • Show your name
    • -
    • Match the photo of your face and your name above
    • -
    From abd678e46476b199c4b82d6138a81afa08139e43 Mon Sep 17 00:00:00 2001 From: Frances Botsford Date: Wed, 21 Aug 2013 15:26:14 -0400 Subject: [PATCH 6/8] initial step interaction for verification --- .../responsive-carousel.js | 4 ++ .../responsive-carousel.keybd.js | 38 ++++++++++++ lms/static/sass/application.scss.mako | 15 +++-- .../_responsive-carousel.scss | 20 ++++++ .../_responsive-carousel.slide.scss | 61 +++++++++++++++++++ lms/static/sass/views/_verification.scss | 6 +- lms/templates/main.html | 3 + lms/templates/verify_student/face_upload.html | 14 ++++- 8 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 lms/static/js/vendor/responsive-carousel/responsive-carousel.js create mode 100644 lms/static/js/vendor/responsive-carousel/responsive-carousel.keybd.js create mode 100644 lms/static/sass/vendor/responsive-carousel/_responsive-carousel.scss create mode 100644 lms/static/sass/vendor/responsive-carousel/_responsive-carousel.slide.scss diff --git a/lms/static/js/vendor/responsive-carousel/responsive-carousel.js b/lms/static/js/vendor/responsive-carousel/responsive-carousel.js new file mode 100644 index 0000000000..2c421a14e8 --- /dev/null +++ b/lms/static/js/vendor/responsive-carousel/responsive-carousel.js @@ -0,0 +1,4 @@ +/*! Responsive Carousel - v0.1.0 - 2013-07-15 +* https://github.com/filamentgroup/responsive-carousel +* Copyright (c) 2013 Filament Group, Inc.; Licensed MIT, GPL */ +(function(e){var t="carousel",n="."+t,r="data-transition",i=t+"-transitioning",s=t+"-item",o=t+"-active",u=t+"-item-prev",a=t+"-item-next",f=t+"-in",l=t+"-out",c=t+"-nav",h=function(){var e="webkit Moz O Ms".split(" "),t=!1,n;while(e.length){n=e.shift()+"Transition";if(n in document.documentElement.style!==undefined&&n in document.documentElement.style!=0){t=!0;break}}return t}(),p={_create:function(){e(this).trigger("beforecreate."+t)[t]("_init")[t]("_addNextPrev").trigger("create."+t)},_init:function(){var n=e(this).attr(r);n||(h=!1),e(this).addClass(t+" "+(n?t+"-"+n:"")+" ").children().addClass(s).first().addClass(o),e(this)[t]("_addNextPrevClasses")},_addNextPrevClasses:function(){var t=e(this).find("."+s),n=t.filter("."+o),r=n.next("."+s),i=n.prev("."+s);r.length||(r=t.first().not("."+o)),i.length||(i=t.last().not("."+o)),t.removeClass(u+" "+a),i.addClass(u),r.addClass(a)},next:function(){e(this)[t]("goTo","+1")},prev:function(){e(this)[t]("goTo","-1")},goTo:function(n){var i=e(this),u=i.attr(r),a=" "+t+"-"+u+"-reverse";e(this).find("."+s).removeClass([l,f,a].join(" "));var c=e(this).find("."+o),p=c.index(),d=(p<0?0:p)+1,v=typeof n=="number"?n:d+parseFloat(n),m=e(this).find(".carousel-item").eq(v-1),g=typeof n=="string"&&!parseFloat(n)||v>d?"":a;m.length||(m=e(this).find("."+s)[g.length?"last":"first"]()),h?i[t]("_transitionStart",c,m,g):(m.addClass(o),i[t]("_transitionEnd",c,m,g)),i.trigger("goto."+t,m)},update:function(){return e(this).children().not("."+c).addClass(s),e(this).trigger("update."+t)},_transitionStart:function(n,r,i){var s=e(this);r.one(navigator.userAgent.indexOf("AppleWebKit")>-1?"webkitTransitionEnd":"transitionend otransitionend",function(){s[t]("_transitionEnd",n,r,i)}),e(this).addClass(i),n.addClass(l),r.addClass(f)},_transitionEnd:function(n,r,i){e(this).removeClass(i),n.removeClass(l+" "+o),r.removeClass(f).addClass(o),e(this)[t]("_addNextPrevClasses")},_bindEventListeners:function(){var n=e(this).bind("click",function(r){var i=e(r.target).closest("a[href='#next'],a[href='#prev']");i.length&&(n[t](i.is("[href='#next']")?"next":"prev"),r.preventDefault())});return this},_addNextPrev:function(){return e(this).append("")[t]("_bindEventListeners")},destroy:function(){}};e.fn[t]=function(n,r,i,s){return this.each(function(){if(n&&typeof n=="string")return e.fn[t].prototype[n].call(this,r,i,s);if(e(this).data(t+"data"))return e(this);e(this).data(t+"active",!0),e.fn[t].prototype._create.call(this)})},e.extend(e.fn[t].prototype,p)})(jQuery),function(e){var t="carousel",n="."+t,r=t+"-no-transition",i=/iPhone|iPad|iPod/.test(navigator.platform)&&navigator.userAgent.indexOf("AppleWebKit")>-1,s={_dragBehavior:function(){var t=e(this),s,o={},u,a,f=function(t){var r=t.touches||t.originalEvent.touches,i=e(t.target).closest(n);t.type==="touchstart"&&(s={x:r[0].pageX,y:r[0].pageY}),r[0]&&r[0].pageX&&(o.touches=r,o.deltaX=r[0].pageX-s.x,o.deltaY=r[0].pageY-s.y,o.w=i.width(),o.h=i.height(),o.xPercent=o.deltaX/o.w,o.yPercent=o.deltaY/o.h,o.srcEvent=t)},l=function(t){f(t),o.touches.length===1&&e(t.target).closest(n).trigger("drag"+t.type.split("touch")[1],o)};e(this).bind("touchstart",function(t){e(this).addClass(r),l(t)}).bind("touchmove",function(e){f(e),l(e),i||(e.preventDefault(),window.scrollBy(0,-o.deltaY))}).bind("touchend",function(t){e(this).removeClass(r),l(t)})}};e.extend(e.fn[t].prototype,s),e(document).on("create."+t,n,function(){e(this)[t]("_dragBehavior")})}(jQuery),function(e){var t="carousel",n="."+t,r=t+"-active",i=t+"-item",s=function(e){return Math.abs(e)>4},o=function(e,n){var r=e.find("."+t+"-active"),s=r.prevAll().length+1,o=n<0,u=s+(o?1:-1),a=e.find("."+i).eq(u-1);return a.length||(a=e.find("."+i)[o?"first":"last"]()),[r,a]};e(document).on("dragmove",n,function(t,n){if(!s(n.deltaX))return;var r=o(e(this),n.deltaX);r[0].css("left",n.deltaX+"px"),r[1].css("left",n.deltaX<0?n.w+n.deltaX+"px":-n.w+n.deltaX+"px")}).on("dragend",n,function(n,i){if(!s(i.deltaX))return;var u=o(e(this),i.deltaX),a=Math.abs(i.deltaX)>45;e(this).one(navigator.userAgent.indexOf("AppleWebKit")?"webkitTransitionEnd":"transitionEnd",function(){u[0].add(u[1]).css("left",""),e(this).trigger("goto."+t,u[1])}),a?(u[0].removeClass(r).css("left",i.deltaX>0?i.w+"px":-i.w+"px"),u[1].addClass(r).css("left",0)):(u[0].css("left",0),u[1].css("left",i.deltaX>0?-i.w+"px":i.w+"px"))})}(jQuery),function(e,t){var n="carousel",r="."+n+"[data-paginate]",i=n+"-pagination",s=n+"-active-page",o={_createPagination:function(){var t=e(this).find("."+n+"-nav"),r=e(this).find("."+n+"-item"),s=e("
      "),o,u,a;t.find("."+i).remove(),r.each(function(t){o=t+1,u=e(this).attr("data-thumb"),a=o,u&&(a=""),s.append("
    1. "+a+"")}),u&&s.addClass(n+"-nav-thumbs"),t.addClass(n+"-nav-paginated").find("a").first().after(s)},_bindPaginationEvents:function(){e(this).bind("click",function(t){var r=e(t.target);t.target.nodeName==="IMG"&&(r=r.parent()),r=r.closest("a");var s=r.attr("href");r.closest("."+i).length&&s&&(e(this)[n]("goTo",parseFloat(s.split("#")[1])),t.preventDefault())}).bind("goto."+n,function(t,n){var r=n?e(n).index():0;e(this).find("ol."+i+" li").removeClass(s).eq(r).addClass(s)}).trigger("goto."+n)}};e.extend(e.fn[n].prototype,o),e(document).on("create."+n,r,function(){e(this)[n]("_createPagination")[n]("_bindPaginationEvents")}).on("update."+n,r,function(){e(this)[n]("_createPagination")})}(jQuery),function(e){e(function(){e(".carousel").carousel()})}(jQuery); diff --git a/lms/static/js/vendor/responsive-carousel/responsive-carousel.keybd.js b/lms/static/js/vendor/responsive-carousel/responsive-carousel.keybd.js new file mode 100644 index 0000000000..c6b5993124 --- /dev/null +++ b/lms/static/js/vendor/responsive-carousel/responsive-carousel.keybd.js @@ -0,0 +1,38 @@ +/* + * responsive-carousel keyboard extension + * https://github.com/filamentgroup/responsive-carousel + * + * Copyright (c) 2012 Filament Group, Inc. + * Licensed under the MIT, GPL licenses. + */ + +(function($) { + var pluginName = "carousel", + initSelector = "." + pluginName, + navSelector = "." + pluginName + "-nav a", + buffer, + keyNav = function( e ) { + clearTimeout( buffer ); + buffer = setTimeout(function() { + var $carousel = $( e.target ).closest( initSelector ); + + if( e.keyCode === 39 || e.keyCode === 40 ){ + $carousel[ pluginName ]( "next" ); + } + else if( e.keyCode === 37 || e.keyCode === 38 ){ + $carousel[ pluginName ]( "prev" ); + } + }, 200 ); + + if( 37 <= e.keyCode <= 40 ) { + e.preventDefault(); + } + }; + + // Touch handling + $( document ) + .on( "click", navSelector, function( e ) { + $( e.target )[ 0 ].focus(); + }) + .on( "keydown", navSelector, keyNav ); +}(jQuery)); diff --git a/lms/static/sass/application.scss.mako b/lms/static/sass/application.scss.mako index fb7b4b1db2..c17b23b0dc 100644 --- a/lms/static/sass/application.scss.mako +++ b/lms/static/sass/application.scss.mako @@ -9,7 +9,8 @@ @import 'base/reset'; @import 'vendor/font-awesome'; - +@import 'vendor/responsive-carousel/responsive-carousel'; +@import 'vendor/responsive-carousel/responsive-carousel.slide'; // BASE *default edX offerings* // ==================== @@ -36,12 +37,18 @@ // base - assets @import 'base/font_face'; @import 'base/extends'; -@import 'base/animations'; +@import 'base/animations'; // base - starter @import 'base/base'; -// shared - course +// base - elements +@import 'elements/typography'; + +// base - specific views +@import 'views/verification'; + +// shared - course @import 'shared/forms'; @import 'shared/footer'; @import 'shared/header'; @@ -67,7 +74,7 @@ @import 'multicourse/help'; @import 'multicourse/edge'; -// applications +// applications @import 'discussion'; @import 'news'; diff --git a/lms/static/sass/vendor/responsive-carousel/_responsive-carousel.scss b/lms/static/sass/vendor/responsive-carousel/_responsive-carousel.scss new file mode 100644 index 0000000000..cbd8d701de --- /dev/null +++ b/lms/static/sass/vendor/responsive-carousel/_responsive-carousel.scss @@ -0,0 +1,20 @@ +/* + * responsive-carousel + * https://github.com/filamentgroup/responsive-carousel + * + * Copyright (c) 2012 Filament Group, Inc. + * Licensed under the MIT, GPL licenses. + */ +.carousel { + width: 100%; + position: relative; +} +.carousel .carousel-item { + display: none; +} +.carousel .carousel-active { + display: block; +} +.carousel .carousel-nav:nth-child(2) { + display: none; +} diff --git a/lms/static/sass/vendor/responsive-carousel/_responsive-carousel.slide.scss b/lms/static/sass/vendor/responsive-carousel/_responsive-carousel.slide.scss new file mode 100644 index 0000000000..b902bc6575 --- /dev/null +++ b/lms/static/sass/vendor/responsive-carousel/_responsive-carousel.slide.scss @@ -0,0 +1,61 @@ +/* + * responsive-carousel + * https://github.com/filamentgroup/responsive-carousel + * + * Copyright (c) 2012 Filament Group, Inc. + * Licensed under the MIT, GPL licenses. +*/ +.carousel-slide { + position: relative; + overflow: hidden; + -webkit-transform: translate3d(0, 0, 0); + -moz-transform: translate3d(0, 0, 0); + -ms-transform: translate3d(0, 0, 0); + -o-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} +.carousel-slide .carousel-item { + position: absolute; + left: 100%; + top: 0; + width: 100%; /* necessary for non-active slides */ + display: block; /* overrides basic carousel styles */ + z-index: 1; + -webkit-transition: left .2s ease; + -moz-transition: left .2s ease; + -ms-transition: left .2s ease; + -o-transition: left .2s ease; + transition: left .2s ease; +} +.carousel-no-transition .carousel-item { + -webkit-transition: none; + -moz-transition: none; + -ms-transition: none; + -o-transition: none; + transition: none; +} +.carousel-slide .carousel-active { + left: 0; + position: relative; + z-index: 2; +} +.carousel-slide .carousel-in { + left: 0; +} +.carousel-slide-reverse .carousel-out { + left: 100%; +} +.carousel-slide .carousel-out, +.carousel-slide-reverse .carousel-in { + left: -100%; +} +.carousel-slide-reverse .carousel-item { + -webkit-transition: left .1s ease; + -moz-transition: left .1s ease; + -ms-transition: left .1s ease; + -o-transition: left .1s ease; + transition: left .1s ease; +} +.carousel-slide-reverse .carousel-active { + left: 0; +} diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index a0eebf0fea..8da7a8d69e 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -41,13 +41,9 @@ body.register.verification { } } - - #wrapper { - overflow: hidden; - } - .block-photo { @include clearfix(); + background-color: $white; .title { font-weight: bold; diff --git a/lms/templates/main.html b/lms/templates/main.html index a25e8f5261..179a84f7d3 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -92,6 +92,9 @@ <%static:js group='application'/> <%static:js group='module-js'/> + + + <%block name="js_extra"/> diff --git a/lms/templates/verify_student/face_upload.html b/lms/templates/verify_student/face_upload.html index 2010ec53fe..ccf0e76bfa 100644 --- a/lms/templates/verify_student/face_upload.html +++ b/lms/templates/verify_student/face_upload.html @@ -4,8 +4,16 @@ <%block name="bodyclass">register verification <%block name="js_extra"> + @@ -30,7 +38,7 @@ -
      +
    2. From 0086a054d68b46a38d0d67dfd0414f29a5f09138 Mon Sep 17 00:00:00 2001 From: Frances Botsford Date: Thu, 22 Aug 2013 11:37:36 -0400 Subject: [PATCH 8/8] first pass at select a track for verification --- lms/static/sass/views/_verification.scss | 49 ++++++++++++ lms/templates/verify_student/face_upload.html | 17 +++-- .../verify_student/photo_id_upload.html | 76 ++++++++++++++++++- 3 files changed, 132 insertions(+), 10 deletions(-) diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index 7c30b44bd2..691652063c 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -13,9 +13,45 @@ body.register.verification { .title { @extend .t-title5; + margin-bottom: $baseline; + font-weight: bold; } } + .title { + @extend .t-title9; + margin-bottom: ($baseline/2); + font-weight: bold; + } + + .select { + @include clearfix(); + + + .block { + float: left; + margin: 0 $baseline $baseline 0; + background-color: #eee; + padding: $baseline; + width: 60%; + + .title { + @extend .t-title7; + } + + } + + .tips { + float: right; + width: 32%; + } + + } + + + + + .progress { .progress-step { @@ -70,6 +106,19 @@ body.register.verification { .control { display: inline-block; + + .action { + @extend .button-primary; + display: block; + background-color: $blue; + color: $white; + padding: ($baseline*.25) ($baseline*.5); + border: none; + + &:hover { + + } + } } } } diff --git a/lms/templates/verify_student/face_upload.html b/lms/templates/verify_student/face_upload.html index c6cfaf3b35..42328eba0a 100644 --- a/lms/templates/verify_student/face_upload.html +++ b/lms/templates/verify_student/face_upload.html @@ -1,7 +1,7 @@ <%! from django.utils.translation import ugettext as _ %> <%! from django.core.urlresolvers import reverse %> <%inherit file="../main.html" /> -<%block name="bodyclass">register verification +<%block name="bodyclass">register verification photos <%block name="js_extra"> @@ -54,16 +54,17 @@ $(document).ready(function() { +
      @@ -121,13 +122,13 @@ $(document).ready(function() { @@ -268,7 +269,7 @@ $(document).ready(function() {
      - + close diff --git a/lms/templates/verify_student/photo_id_upload.html b/lms/templates/verify_student/photo_id_upload.html index b6724656f4..c5fa6d462e 100644 --- a/lms/templates/verify_student/photo_id_upload.html +++ b/lms/templates/verify_student/photo_id_upload.html @@ -1,11 +1,83 @@ <%! from django.utils.translation import ugettext as _ %> <%! from django.core.urlresolvers import reverse %> <%inherit file="../main.html" /> +<%block name="bodyclass">register verification select <%block name="content"> +
      +
      -

      Photo ID Upload!

      + -Final Verification +

      Select your track:

      +
      +
      +

      Audit

      +

      Sign up to audit this course for free and track your own progress.

      + +

      + Select Audit +

      +
      + +
      +
      +

      Certificate of Achievement

      +

      Sign up as a verified student and work toward a Certificate of Achievement.

      +
      +
      + Select your contribution for this course (in USD): +
      +
      +
        +
      • + +
      • +
      • + +
      • +
      • + +
      • +
      • + +
      • +
      • + +
      • +
      +
      +
      + + Why do I have to pay? What if I don't meet all the requirements? + + + +

      + What is an ID Verified Certificate? +

      + +

      + Select Certificate +

      +
      + +
      +

      + To register for a Verified Certificate of Achievement option, you will need a webcam, a credit or debit card, and an ID. View requirements +

      +
      + +
      + + +

      Have questions? Check out our FAQs.

      +

      Not the course you wanted? Return to our course listings.

      + + +
      +