Merge pull request #890 from edx/ormsbee/verifyuser3
User verification / validated certificates feature
This commit is contained in:
@@ -86,3 +86,10 @@ INSTALLED_APPS += ('lettuce.django',)
|
||||
LETTUCE_APPS = ('contentstore',)
|
||||
LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535)
|
||||
LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome')
|
||||
|
||||
#####################################################################
|
||||
# Lastly, see if the developer has any local overrides.
|
||||
try:
|
||||
from .private import * # pylint: disable=F0401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -374,6 +374,7 @@ INSTALLED_APPS = (
|
||||
'course_modes'
|
||||
)
|
||||
|
||||
|
||||
################# EDX MARKETING SITE ##################################
|
||||
|
||||
EDXMKTG_COOKIE_NAME = 'edxloggedin'
|
||||
|
||||
@@ -53,6 +53,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) }
|
||||
|
||||
@classmethod
|
||||
def mode_for_course(cls, course_id, mode_slug):
|
||||
"""
|
||||
@@ -67,3 +71,8 @@ class CourseMode(models.Model):
|
||||
return matched[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def __unicode__(self):
|
||||
return u"{} : {}, min={}, prices={}".format(
|
||||
self.course_id, self.mode_slug, self.min_price, self.suggested_prices
|
||||
)
|
||||
|
||||
0
common/djangoapps/course_modes/tests/__init__.py
Normal file
0
common/djangoapps/course_modes/tests/__init__.py
Normal file
13
common/djangoapps/course_modes/tests/factories.py
Normal file
13
common/djangoapps/course_modes/tests/factories.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from course_modes.models import CourseMode
|
||||
from factory import DjangoModelFactory
|
||||
|
||||
# Factories don't have __init__ methods, and are self documenting
|
||||
# pylint: disable=W0232
|
||||
class CourseModeFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = CourseMode
|
||||
|
||||
course_id = u'MITx/999/Robot_Super_Course'
|
||||
mode_slug = 'audit'
|
||||
mode_display_name = 'audit course'
|
||||
min_price = 0
|
||||
currency = 'usd'
|
||||
9
common/djangoapps/course_modes/urls.py
Normal file
9
common/djangoapps/course_modes/urls.py
Normal file
@@ -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/(?P<course_id>[^/]+/[^/]+/[^/]+)$', views.ChooseModeView.as_view(), name="course_modes_choose"),
|
||||
)
|
||||
@@ -1 +1,102 @@
|
||||
# Create your views here.
|
||||
import decimal
|
||||
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 django.contrib.auth.decorators import login_required
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
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
|
||||
from verify_student.models import SoftwareSecurePhotoVerification
|
||||
|
||||
class ChooseModeView(View):
|
||||
|
||||
@method_decorator(login_required)
|
||||
def get(self, request, course_id, error=None):
|
||||
if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified':
|
||||
return redirect(reverse('dashboard'))
|
||||
modes = CourseMode.modes_for_course_dict(course_id)
|
||||
context = {
|
||||
"course_id": course_id,
|
||||
"modes": modes,
|
||||
"course_name": course_from_id(course_id).display_name,
|
||||
"chosen_price": None,
|
||||
"error": error,
|
||||
}
|
||||
if "verified" in modes:
|
||||
context["suggested_prices"] = modes["verified"].suggested_prices.split(",")
|
||||
context["currency"] = modes["verified"].currency.upper()
|
||||
context["min_price"] = modes["verified"].min_price
|
||||
|
||||
return render_to_response("course_modes/choose.html", context)
|
||||
|
||||
@method_decorator(login_required)
|
||||
def post(self, request, course_id):
|
||||
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 not has_access(user, course, 'enroll'):
|
||||
error_msg = _("Enrollment is closed")
|
||||
return self.get(request, course_id, error=error_msg)
|
||||
|
||||
requested_mode = self.get_requested_mode(request.POST.get("mode"))
|
||||
if requested_mode == "verified" and request.POST.get("honor-code"):
|
||||
requested_mode = "honor"
|
||||
|
||||
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')
|
||||
|
||||
mode_info = allowed_modes[requested_mode]
|
||||
|
||||
if requested_mode == "verified":
|
||||
amount = request.POST.get("contribution") or \
|
||||
request.POST.get("contribution-other-amt") or 0
|
||||
|
||||
try:
|
||||
# validate the amount passed in and force it into two digits
|
||||
amount_value = decimal.Decimal(amount).quantize(decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN)
|
||||
except decimal.InvalidOperation:
|
||||
error_msg = _("Invalid amount selected.")
|
||||
return self.get(request, course_id, error=error_msg)
|
||||
|
||||
# Check for minimum pricing
|
||||
if amount_value < mode_info.min_price:
|
||||
error_msg = _("No selected price or selected price is too low.")
|
||||
return self.get(request, course_id, error=error_msg)
|
||||
|
||||
donation_for_course = request.session.get("donation_for_course", {})
|
||||
donation_for_course[course_id] = amount_value
|
||||
request.session["donation_for_course"] = donation_for_course
|
||||
if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user):
|
||||
return redirect(
|
||||
reverse('verify_student_verified',
|
||||
kwargs={'course_id': course_id})
|
||||
)
|
||||
|
||||
return redirect(
|
||||
reverse('verify_student_show_requirements',
|
||||
kwargs={'course_id': course_id}),
|
||||
)
|
||||
|
||||
def get_requested_mode(self, user_choice):
|
||||
choices = {
|
||||
"Select Audit": "audit",
|
||||
"Select Certificate": "verified"
|
||||
}
|
||||
return choices.get(user_choice)
|
||||
|
||||
@@ -843,6 +843,23 @@ class CourseEnrollment(models.Model):
|
||||
except cls.DoesNotExist:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def enrollment_mode_for_user(cls, user, course_id):
|
||||
"""
|
||||
Returns the enrollment mode for the given user for the given course
|
||||
|
||||
`user` is a Django User object
|
||||
`course_id` is our usual course_id string (e.g. "edX/Test101/2013_Fall)
|
||||
"""
|
||||
try:
|
||||
record = CourseEnrollment.objects.get(user=user, course_id=course_id)
|
||||
if record.is_active:
|
||||
return record.mode
|
||||
else:
|
||||
return None
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def enrollments_for_user(cls, user):
|
||||
return CourseEnrollment.objects.filter(user=user, is_active=1)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Student Views
|
||||
"""
|
||||
import datetime
|
||||
import feedparser
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
@@ -27,22 +26,20 @@ 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
|
||||
|
||||
from ratelimitbackend.exceptions import RateLimitException
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from student.models import (Registration, UserProfile, TestCenterUser, TestCenterUserForm,
|
||||
TestCenterRegistration, TestCenterRegistrationForm,
|
||||
PendingNameChange, PendingEmailChange,
|
||||
CourseEnrollment, unique_id_for_user,
|
||||
get_testcenter_registration, CourseEnrollmentAllowed)
|
||||
|
||||
from student.forms import PasswordResetFormNoActive
|
||||
|
||||
from certificates.models import CertificateStatuses, certificate_status_for_student
|
||||
@@ -269,7 +266,7 @@ def dashboard(request):
|
||||
courses = []
|
||||
for enrollment in CourseEnrollment.enrollments_for_user(user):
|
||||
try:
|
||||
courses.append(course_from_id(enrollment.course_id))
|
||||
courses.append((course_from_id(enrollment.course_id), enrollment))
|
||||
except ItemNotFoundError:
|
||||
log.error("User {0} enrolled in non-existent course {1}"
|
||||
.format(user.username, enrollment.course_id))
|
||||
@@ -288,12 +285,12 @@ def dashboard(request):
|
||||
staff_access = True
|
||||
errored_courses = modulestore().get_errored_courses()
|
||||
|
||||
show_courseware_links_for = frozenset(course.id for course in courses
|
||||
show_courseware_links_for = frozenset(course.id for course, _enrollment in courses
|
||||
if has_access(request.user, course, 'load'))
|
||||
|
||||
cert_statuses = {course.id: cert_info(request.user, course) for course in courses}
|
||||
cert_statuses = {course.id: cert_info(request.user, course) for course, _enrollment in courses}
|
||||
|
||||
exam_registrations = {course.id: exam_registration_info(request.user, course) for course in courses}
|
||||
exam_registrations = {course.id: exam_registration_info(request.user, course) for course, _enrollment in courses}
|
||||
|
||||
# get info w.r.t ExternalAuthMap
|
||||
external_auth_map = None
|
||||
@@ -335,10 +332,13 @@ def try_change_enrollment(request):
|
||||
enrollment_response.content
|
||||
)
|
||||
)
|
||||
if enrollment_response.content != '':
|
||||
return enrollment_response.content
|
||||
except Exception, e:
|
||||
log.exception("Exception automatically enrolling after login: {0}".format(str(e)))
|
||||
|
||||
|
||||
@require_POST
|
||||
def change_enrollment(request):
|
||||
"""
|
||||
Modify the enrollment status for the logged-in user.
|
||||
@@ -356,18 +356,16 @@ def change_enrollment(request):
|
||||
as a post-login/registration helper, so the error messages in the responses
|
||||
should never actually be user-visible.
|
||||
"""
|
||||
if request.method != "POST":
|
||||
return HttpResponseNotAllowed(["POST"])
|
||||
|
||||
user = request.user
|
||||
if not user.is_authenticated():
|
||||
return HttpResponseForbidden()
|
||||
|
||||
action = request.POST.get("enrollment_action")
|
||||
course_id = request.POST.get("course_id")
|
||||
if course_id is None:
|
||||
return HttpResponseBadRequest(_("Course id not specified"))
|
||||
|
||||
if not user.is_authenticated():
|
||||
return HttpResponseForbidden()
|
||||
|
||||
if action == "enroll":
|
||||
# Make sure the course exists
|
||||
# We don't do this check on unenroll, or a bad course id can't be unenrolled from
|
||||
@@ -381,6 +379,14 @@ def change_enrollment(request):
|
||||
if not has_access(user, course, 'enroll'):
|
||||
return HttpResponseBadRequest(_("Enrollment is closed"))
|
||||
|
||||
# If this course is available in multiple modes, redirect them to a page
|
||||
# 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_choose", kwargs={'course_id': course_id})
|
||||
)
|
||||
|
||||
org, course_num, run = course_id.split("/")
|
||||
statsd.increment("common.student.enrollment",
|
||||
tags=["org:{0}".format(org),
|
||||
@@ -463,10 +469,10 @@ def login_user(request, error=""):
|
||||
log.exception(e)
|
||||
raise
|
||||
|
||||
try_change_enrollment(request)
|
||||
redirect_url = try_change_enrollment(request)
|
||||
|
||||
statsd.increment("common.student.successful_login")
|
||||
response = HttpResponse(json.dumps({'success': True}))
|
||||
response = HttpResponse(json.dumps({'success': True, 'redirect_url': redirect_url}))
|
||||
|
||||
# set the login cookie for the edx marketing site
|
||||
# we want this cookie to be accessed via javascript
|
||||
@@ -732,14 +738,14 @@ def create_account(request, post_override=None):
|
||||
login_user.save()
|
||||
AUDIT_LOG.info(u"Login activated on extauth account - {0} ({1})".format(login_user.username, login_user.email))
|
||||
|
||||
try_change_enrollment(request)
|
||||
redirect_url = try_change_enrollment(request)
|
||||
|
||||
statsd.increment("common.student.account_created")
|
||||
|
||||
js = {'success': True}
|
||||
HttpResponse(json.dumps(js), mimetype="application/json")
|
||||
response_params = {'success': True,
|
||||
'redirect_url': redirect_url}
|
||||
|
||||
response = HttpResponse(json.dumps({'success': True}))
|
||||
response = HttpResponse(json.dumps(response_params))
|
||||
|
||||
# set the login cookie for the edx marketing site
|
||||
# we want this cookie to be accessed via javascript
|
||||
|
||||
@@ -5,6 +5,7 @@ and integration / BDD tests.
|
||||
'''
|
||||
import student.tests.factories as sf
|
||||
import xmodule.modulestore.tests.factories as xf
|
||||
import course_modes.tests.factories as cmf
|
||||
from lettuce import world
|
||||
|
||||
|
||||
@@ -51,6 +52,14 @@ class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowedFactory):
|
||||
pass
|
||||
|
||||
|
||||
@world.absorb
|
||||
class CourseModeFactory(cmf.CourseModeFactory):
|
||||
"""
|
||||
Course modes
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@world.absorb
|
||||
class CourseFactory(xf.CourseFactory):
|
||||
"""
|
||||
|
||||
32
common/templates/course_modes/_contribution.html
Normal file
32
common/templates/course_modes/_contribution.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<ul class="list-fields contribution-options">
|
||||
% for price in suggested_prices:
|
||||
<li class="field contribution-option">
|
||||
<input type="radio" name="contribution" value="${price|h}" ${'checked' if price == chosen_price else ''} id="contribution-${price|h}" />
|
||||
<label for="contribution-${price|h}">
|
||||
<span class="deco-denomination">$</span>
|
||||
<span class="label-value">${price}</span>
|
||||
<span class="denomination-name">${currency}</span>
|
||||
</label>
|
||||
</li>
|
||||
% endfor
|
||||
|
||||
<li class="field contribution-option">
|
||||
<ul class="field-group field-group-other">
|
||||
<li class="contribution-option contribution-option-other1">
|
||||
<input type="radio" id="contribution-other" name="contribution" value="" ${'checked' if (chosen_price and chosen_price not in suggested_prices) else ''} />
|
||||
<label for=" contribution-other"><span class="sr">Other</span></label>
|
||||
</li>
|
||||
|
||||
<li class="contribution-option contribution-option-other2">
|
||||
<label for="contribution-other-amt">
|
||||
<span class="sr">Other Amount</span>
|
||||
</label>
|
||||
<div class="wrapper">
|
||||
<span class="deco-denomination">$</span>
|
||||
<input type="text" size="9" name="contribution-other-amt" id="contribution-other-amt" value="${chosen_price if (chosen_price and chosen_price not in suggested_prices) else ''}"/>
|
||||
<span class="denomination-name">${currency}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
164
common/templates/course_modes/choose.html
Normal file
164
common/templates/course_modes/choose.html
Normal file
@@ -0,0 +1,164 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="../main.html" />
|
||||
|
||||
<%block name="bodyclass">register verification-process step-select-track</%block>
|
||||
<%block name="title"><title>${_("Register for {} | Choose Your Track").format(course_name)}</title></%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript">
|
||||
|
||||
$(document).ready(function() {
|
||||
$('.expandable-area').slideUp();
|
||||
$('.is-expandable').addClass('is-ready');
|
||||
$('.is-expandable .title-expand').click(function(e) {
|
||||
e.preventDefault();
|
||||
$(this).next('.expandable-area').slideToggle();
|
||||
$(this).parent().toggleClass('is-expanded');
|
||||
});
|
||||
|
||||
$('#contribution-other-amt').focus(function() {
|
||||
$('#contribution-other').attr('checked',true);
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
%if error:
|
||||
<div class="wrapper-msg wrapper-msg-error">
|
||||
<div class=" msg msg-error">
|
||||
<i class="msg-icon icon-warning-sign"></i>
|
||||
<div class="msg-content">
|
||||
<h3 class="title">${_("Sorry, there was an error when trying to register you")}</h3>
|
||||
<div class="copy">
|
||||
<p>${error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
<div class="container">
|
||||
<section class="wrapper">
|
||||
|
||||
<%include file="/verify_student/_verification_header.html" args="course_name=course_name" />
|
||||
|
||||
<div class="wrapper-register-choose wrapper-content-main">
|
||||
<article class="register-choose content-main">
|
||||
<h3 class="title">${_("Select your track:")}</h3>
|
||||
|
||||
<form class="form-register-choose" method="post" name="enrollment_mode_form" id="enrollment_mode_form">
|
||||
|
||||
% if "verified" in modes:
|
||||
<div class="register-choice register-choice-certificate">
|
||||
<div class="wrapper-copy">
|
||||
<span class="deco-ribbon"></span>
|
||||
<h4 class="title">${_("Certificate of Achievement (ID Verified)")}</h4>
|
||||
<div class="copy">
|
||||
<p>${_("Sign up and work toward a verified Certificate of Achievement.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field field-certificate-contribution">
|
||||
<h5 class="label">${_("Select your contribution for this course (min. $")} ${min_price} <span class="denomination-name">${currency}</span>${_("):")}</h5>
|
||||
|
||||
%if error:
|
||||
<div class="msg msg-error msg-inline">
|
||||
<div class="copy">
|
||||
<p><i class="msg-icon icon-warning-sign"></i> ${error}</p>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
<%include file="_contribution.html" args="suggested_prices=suggested_prices, currency=currency, chosen_price=chosen_price, min_price=min_price"/>
|
||||
|
||||
<div class="help-tip is-expandable">
|
||||
<h5 class="title title-expand"><i class="icon-caret-down expandable-icon"></i> ${_("Why do I have to pay? What if I don't meet all the requirements?")}</h5>
|
||||
|
||||
<div class="copy expandable-area">
|
||||
<dl class="list-faq">
|
||||
<dt class="faq-question">${_("Why do I have to pay?")}</dt>
|
||||
<dd class="faq-answer">
|
||||
<p>${_("As a not-for-profit, edX uses your contribution to support our mission to provide quality education to everyone around the world. While we have established a minimum fee, we ask that you contribute as much as you can.")}</p>
|
||||
</dd>
|
||||
|
||||
<dt class="faq-question">${_("I'd like to pay more than the minimum. Is my contribution tax deductible?")}</dt>
|
||||
<dd class="faq-answer">
|
||||
<p>${_("Please check with your tax advisor to determine whether your contribution is tax deductible.")}</p>
|
||||
</dd>
|
||||
|
||||
% if "honor" in modes:
|
||||
<dt class="faq-question">${_("What if I can't afford it or don't have the necessary equipment?")}</dt>
|
||||
<dd class="faq-answer">
|
||||
<p>${_("If you can't afford the minimum fee, don't have a webcam, credit card, debit card or acceptable ID, you can audit the course for free. You may also elect to pursue an Honor Code certificate, but you will need to tell us why you would like the fee waived below. Then click the 'Select Certificate' button to complete your registration.")}</p>
|
||||
|
||||
<ul class="list-fields">
|
||||
<li class="field field-honor-code checkbox">
|
||||
<input type="checkbox" name="honor-code" id="honor-code">
|
||||
<label for="honor-code">${_("Select Honor Code Certificate")}</label>
|
||||
</li>
|
||||
|
||||
<li class="field field-explain">
|
||||
<label for="explain"><span class="sr">${_("Explain your situation: ")}</span>${_("Please write a few sentences about why you would like the fee waived for this course")}</label>
|
||||
<textarea name="explain"></textarea>
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
% endif
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action action-select">
|
||||
<input type="submit" name="mode" value="Select Certificate" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="help help-register">
|
||||
<h3 class="title">${_("Verified Registration Requirements")}</h3>
|
||||
<div class="copy">
|
||||
<p>${_("To register for a Verified Certificate of Achievement option, you will need a webcam, a credit or debit card, and an ID.")}</p>
|
||||
</div>
|
||||
|
||||
<h3 class="title">${_("What is an ID Verified Certificate?")}</h3>
|
||||
<div class="copy">
|
||||
<p>${_("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if "audit" in modes:
|
||||
<span class="deco-divider">
|
||||
<span class="copy">${_("or")}</span>
|
||||
</span>
|
||||
<div class="register-choice register-choice-audit">
|
||||
<div class="wrapper-copy">
|
||||
<h4 class="title">${_("Audit This Course")}</h4>
|
||||
<div class="copy">
|
||||
<p>${_("Sign up to audit this course for free and track your own progress.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action action-select">
|
||||
<input type="submit" name="mode" value="Select Audit" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
</form>
|
||||
</article>
|
||||
</div> <!-- /wrapper-content-main -->
|
||||
|
||||
<%include file="/verify_student/_verification_support.html" />
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
80
lms/djangoapps/courseware/features/certificates.feature
Normal file
80
lms/djangoapps/courseware/features/certificates.feature
Normal file
@@ -0,0 +1,80 @@
|
||||
Feature: Verified certificates
|
||||
As a student,
|
||||
In order to earn a verified certificate
|
||||
I want to sign up for a verified certificate course.
|
||||
|
||||
Scenario: I can audit a verified certificate course
|
||||
Given I am logged in
|
||||
When I select the audit track
|
||||
Then I should see the course on my dashboard
|
||||
|
||||
Scenario: I can submit photos to verify my identity
|
||||
Given I am logged in
|
||||
When I select the verified track
|
||||
And I go to step "1"
|
||||
And I capture my "face" photo
|
||||
And I approve my "face" photo
|
||||
And I go to step "2"
|
||||
And I capture my "photo_id" photo
|
||||
And I approve my "photo_id" photo
|
||||
And I go to step "3"
|
||||
And I select a contribution amount
|
||||
And I confirm that the details match
|
||||
And I go to step "4"
|
||||
Then I am at the payment page
|
||||
|
||||
Scenario: I can pay for a verified certificate
|
||||
Given I have submitted photos to verify my identity
|
||||
When I submit valid payment information
|
||||
Then I see that my payment was successful
|
||||
|
||||
|
||||
# Not yet implemented LMS-982
|
||||
@skip
|
||||
Scenario: Verified courses display correctly on dashboard
|
||||
Given I have submitted photos to verify my identity
|
||||
When I submit valid payment information
|
||||
And I navigate to my dashboard
|
||||
Then I see the course on my dashboard
|
||||
And I see that I am on the verified track
|
||||
|
||||
# Not easily automated
|
||||
@skip
|
||||
Scenario: I can re-take photos
|
||||
Given I have submitted my "<PhotoType>" photo
|
||||
When I retake my "<PhotoType>" photo
|
||||
Then I see the new photo on the confirmation page.
|
||||
|
||||
Examples:
|
||||
| PhotoType |
|
||||
| face |
|
||||
| ID |
|
||||
|
||||
# Not yet implemented LMS-983
|
||||
@skip
|
||||
Scenario: I can edit identity information
|
||||
Given I have submitted face and ID photos
|
||||
When I edit my name
|
||||
Then I see the new name on the confirmation page.
|
||||
|
||||
# Currently broken LMS-1009
|
||||
@skip
|
||||
Scenario: I can return to the verify flow
|
||||
Given I have submitted photos to verify my identity
|
||||
When I leave the flow and return
|
||||
Then I am at the verified page
|
||||
|
||||
# Currently broken LMS-1009
|
||||
@skip
|
||||
Scenario: I can pay from the return flow
|
||||
Given I have submitted photos to verify my identity
|
||||
When I leave the flow and return
|
||||
And I press the payment button
|
||||
Then I am at the payment page
|
||||
|
||||
# Design not yet finalized
|
||||
@skip
|
||||
Scenario: I can take a verified certificate course for free
|
||||
Given I have submitted photos to verify my identity
|
||||
When I give a reason why I cannot pay
|
||||
Then I see that I am registered for a verified certificate course on my dashboard
|
||||
243
lms/djangoapps/courseware/features/certificates.py
Normal file
243
lms/djangoapps/courseware/features/certificates.py
Normal file
@@ -0,0 +1,243 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
from course_modes.models import CourseMode
|
||||
from nose.tools import assert_equal
|
||||
|
||||
def create_cert_course():
|
||||
world.clear_courses()
|
||||
org = 'edx'
|
||||
number = '999'
|
||||
name = 'Certificates'
|
||||
course_id = '{org}/{number}/{name}'.format(
|
||||
org=org, number=number, name=name)
|
||||
world.scenario_dict['COURSE'] = world.CourseFactory.create(
|
||||
org=org, number=number, display_name=name)
|
||||
|
||||
audit_mode = world.CourseModeFactory.create(
|
||||
course_id=course_id,
|
||||
mode_slug='audit',
|
||||
mode_display_name='audit course',
|
||||
min_price=0,
|
||||
)
|
||||
assert isinstance(audit_mode, CourseMode)
|
||||
|
||||
verfied_mode = world.CourseModeFactory.create(
|
||||
course_id=course_id,
|
||||
mode_slug='verified',
|
||||
mode_display_name='verified cert course',
|
||||
min_price=16,
|
||||
suggested_prices='32,64,128',
|
||||
currency='usd',
|
||||
)
|
||||
assert isinstance(verfied_mode, CourseMode)
|
||||
|
||||
|
||||
def register():
|
||||
url = 'courses/{org}/{number}/{name}/about'.format(
|
||||
org='edx', number='999', name='Certificates')
|
||||
world.browser.visit(django_url(url))
|
||||
|
||||
world.css_click('section.intro a.register')
|
||||
assert world.is_css_present('section.wrapper h3.title')
|
||||
|
||||
|
||||
@step(u'I select the audit track$')
|
||||
def select_the_audit_track(step):
|
||||
create_cert_course()
|
||||
register()
|
||||
btn_css = 'input[value="Select Audit"]'
|
||||
world.wait(1) # TODO remove this after troubleshooting JZ
|
||||
world.css_find(btn_css)
|
||||
world.css_click(btn_css)
|
||||
|
||||
|
||||
def select_contribution(amount=32):
|
||||
radio_css = 'input[value="{}"]'.format(amount)
|
||||
world.css_click(radio_css)
|
||||
assert world.css_find(radio_css).selected
|
||||
|
||||
|
||||
@step(u'I select the verified track$')
|
||||
def select_the_verified_track(step):
|
||||
create_cert_course()
|
||||
register()
|
||||
select_contribution(32)
|
||||
btn_css = 'input[value="Select Certificate"]'
|
||||
world.css_click(btn_css)
|
||||
assert world.is_css_present('section.progress')
|
||||
|
||||
|
||||
@step(u'I should see the course on my dashboard$')
|
||||
def should_see_the_course_on_my_dashboard(step):
|
||||
course_css = 'li.course-item'
|
||||
assert world.is_css_present(course_css)
|
||||
|
||||
|
||||
@step(u'I go to step "([^"]*)"$')
|
||||
def goto_next_step(step, step_num):
|
||||
btn_css = {
|
||||
'1': '#face_next_button',
|
||||
'2': '#face_next_button',
|
||||
'3': '#photo_id_next_button',
|
||||
'4': '#pay_button',
|
||||
}
|
||||
next_css = {
|
||||
'1': 'div#wrapper-facephoto.carousel-active',
|
||||
'2': 'div#wrapper-idphoto.carousel-active',
|
||||
'3': 'div#wrapper-review.carousel-active',
|
||||
'4': 'div#wrapper-review.carousel-active',
|
||||
}
|
||||
world.css_click(btn_css[step_num])
|
||||
|
||||
# Pressing the button will advance the carousel to the next item
|
||||
# and give the wrapper div the "carousel-active" class
|
||||
assert world.css_find(next_css[step_num])
|
||||
|
||||
|
||||
@step(u'I capture my "([^"]*)" photo$')
|
||||
def capture_my_photo(step, name):
|
||||
|
||||
# Draw a red rectangle in the image element
|
||||
snapshot_script = '"{}{}{}{}{}{}"'.format(
|
||||
"var canvas = $('#{}_canvas');".format(name),
|
||||
"var ctx = canvas[0].getContext('2d');",
|
||||
"ctx.fillStyle = 'rgb(200,0,0)';",
|
||||
"ctx.fillRect(0, 0, 640, 480);",
|
||||
"var image = $('#{}_image');".format(name),
|
||||
"image[0].src = canvas[0].toDataURL('image/png').replace('image/png', 'image/octet-stream');"
|
||||
)
|
||||
|
||||
# Mirror the javascript of the photo_verification.html page
|
||||
world.browser.execute_script(snapshot_script)
|
||||
world.browser.execute_script("$('#{}_capture_button').hide();".format(name))
|
||||
world.browser.execute_script("$('#{}_reset_button').show();".format(name))
|
||||
world.browser.execute_script("$('#{}_approve_button').show();".format(name))
|
||||
assert world.css_find('#{}_approve_button'.format(name))
|
||||
|
||||
|
||||
@step(u'I approve my "([^"]*)" photo$')
|
||||
def approve_my_photo(step, name):
|
||||
button_css = {
|
||||
'face': 'div#wrapper-facephoto li.control-approve',
|
||||
'photo_id': 'div#wrapper-idphoto li.control-approve',
|
||||
}
|
||||
wrapper_css = {
|
||||
'face': 'div#wrapper-facephoto',
|
||||
'photo_id': 'div#wrapper-idphoto',
|
||||
}
|
||||
|
||||
# Make sure that the carousel is in the right place
|
||||
assert world.css_has_class(wrapper_css[name], 'carousel-active')
|
||||
assert world.css_find(button_css[name])
|
||||
|
||||
# HACK: for now don't bother clicking the approve button for
|
||||
# id_photo, because it is sending you back to Step 1.
|
||||
# Come back and figure it out later. JZ Aug 29 2013
|
||||
if name=='face':
|
||||
world.css_click(button_css[name])
|
||||
|
||||
# Make sure you didn't advance the carousel
|
||||
assert world.css_has_class(wrapper_css[name], 'carousel-active')
|
||||
|
||||
|
||||
@step(u'I select a contribution amount$')
|
||||
def select_contribution_amount(step):
|
||||
select_contribution(32)
|
||||
|
||||
|
||||
@step(u'I confirm that the details match$')
|
||||
def confirm_details_match(step):
|
||||
# First you need to scroll down on the page
|
||||
# to make the element visible?
|
||||
# Currently chrome is failing with ElementNotVisibleException
|
||||
world.browser.execute_script("window.scrollTo(0,1024)")
|
||||
|
||||
cb_css = 'input#confirm_pics_good'
|
||||
world.css_click(cb_css)
|
||||
assert world.css_find(cb_css).checked
|
||||
|
||||
|
||||
@step(u'I am at the payment page')
|
||||
def at_the_payment_page(step):
|
||||
assert world.css_find('input[name=transactionSignature]')
|
||||
|
||||
|
||||
@step(u'I submit valid payment information$')
|
||||
def submit_payment(step):
|
||||
button_css = 'input[value=Submit]'
|
||||
world.css_click(button_css)
|
||||
|
||||
|
||||
@step(u'I have submitted photos to verify my identity')
|
||||
def submitted_photos_to_verify_my_identity(step):
|
||||
step.given('I am logged in')
|
||||
step.given('I select the verified track')
|
||||
step.given('I go to step "1"')
|
||||
step.given('I capture my "face" photo')
|
||||
step.given('I approve my "face" photo')
|
||||
step.given('I go to step "2"')
|
||||
step.given('I capture my "photo_id" photo')
|
||||
step.given('I approve my "photo_id" photo')
|
||||
step.given('I go to step "3"')
|
||||
step.given('I select a contribution amount')
|
||||
step.given('I confirm that the details match')
|
||||
step.given('I go to step "4"')
|
||||
|
||||
|
||||
@step(u'I see that my payment was successful')
|
||||
def see_that_my_payment_was_successful(step):
|
||||
title = world.css_find('div.wrapper-content-main h3.title')
|
||||
assert_equal(title.text, u'Congratulations! You are now verified on edX.')
|
||||
|
||||
|
||||
@step(u'I navigate to my dashboard')
|
||||
def navigate_to_my_dashboard(step):
|
||||
world.css_click('span.avatar')
|
||||
assert world.css_find('section.my-courses')
|
||||
|
||||
|
||||
@step(u'I see the course on my dashboard')
|
||||
def see_the_course_on_my_dashboard(step):
|
||||
course_link_css = 'section.my-courses a[href*="edx/999/Certificates"]'
|
||||
assert world.is_css_present(course_link_css)
|
||||
|
||||
|
||||
@step(u'I see that I am on the verified track')
|
||||
def see_that_i_am_on_the_verified_track(step):
|
||||
assert False, 'Implement this step after the design is done'
|
||||
|
||||
|
||||
@step(u'I leave the flow and return$')
|
||||
def leave_the_flow_and_return(step):
|
||||
world.browser.back()
|
||||
|
||||
|
||||
@step(u'I am at the verified page$')
|
||||
def see_the_payment_page(step):
|
||||
assert world.css_find('button#pay_button')
|
||||
|
||||
|
||||
@step(u'I press the payment button')
|
||||
def press_payment_button(step):
|
||||
assert False, 'This step must be implemented'
|
||||
@step(u'I have submitted face and ID photos')
|
||||
def submitted_face_and_id_photos(step):
|
||||
assert False, 'This step must be implemented'
|
||||
@step(u'I edit my name')
|
||||
def edit_my_name(step):
|
||||
assert False, 'This step must be implemented'
|
||||
@step(u'I see the new name on the confirmation page.')
|
||||
def sesee_the_new_name_on_the_confirmation_page(step):
|
||||
assert False, 'This step must be implemented'
|
||||
@step(u'I have submitted photos')
|
||||
def submitted_photos(step):
|
||||
assert False, 'This step must be implemented'
|
||||
@step(u'I am registered for the course')
|
||||
def seam_registered_for_the_course(step):
|
||||
assert False, 'This step must be implemented'
|
||||
@step(u'I return to the student dashboard')
|
||||
def return_to_the_student_dashboard(step):
|
||||
assert False, 'This step must be implemented'
|
||||
@@ -1,26 +1,28 @@
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
import logging
|
||||
import smtplib
|
||||
from datetime import datetime
|
||||
import textwrap
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.mail import send_mail
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db import transaction
|
||||
from model_utils.managers import InheritanceManager
|
||||
from courseware.courses import get_course_about_section
|
||||
from django.core.mail import send_mail
|
||||
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from courseware.courses import get_course_about_section
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from student.views import course_from_id
|
||||
from student.models import CourseEnrollment
|
||||
from statsd import statsd
|
||||
from .exceptions import *
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from .exceptions import InvalidCartItem, PurchasedCallbackException
|
||||
|
||||
log = logging.getLogger("shoppingcart")
|
||||
|
||||
@@ -116,6 +118,7 @@ class Order(models.Model):
|
||||
self.bill_to_ccnum = ccnum
|
||||
self.bill_to_cardtype = cardtype
|
||||
self.processor_reply_dump = processor_reply_dump
|
||||
|
||||
# save these changes on the order, then we can tell when we are in an
|
||||
# inconsistent state
|
||||
self.save()
|
||||
@@ -124,6 +127,7 @@ class Order(models.Model):
|
||||
orderitems = OrderItem.objects.filter(order=self).select_subclasses()
|
||||
for item in orderitems:
|
||||
item.purchase_item()
|
||||
|
||||
# send confirmation e-mail
|
||||
subject = _("Order Payment Confirmation")
|
||||
message = render_to_string('emails/order_confirmation_email.txt', {
|
||||
@@ -195,6 +199,30 @@ class OrderItem(models.Model):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def single_item_receipt_template(self):
|
||||
"""
|
||||
The template that should be used when there's only one item in the order
|
||||
"""
|
||||
return 'shoppingcart/receipt.html'
|
||||
|
||||
@property
|
||||
def single_item_receipt_context(self):
|
||||
"""
|
||||
Extra variables needed to render the template specified in
|
||||
`single_item_receipt_template`
|
||||
"""
|
||||
return {}
|
||||
|
||||
@property
|
||||
def additional_instruction_text(self):
|
||||
"""
|
||||
Individual instructions for this order item.
|
||||
|
||||
Currently, only used for e-mails.
|
||||
"""
|
||||
return ''
|
||||
|
||||
|
||||
class PaidCourseRegistration(OrderItem):
|
||||
"""
|
||||
@@ -311,6 +339,13 @@ class CertificateItem(OrderItem):
|
||||
course_enrollment = CourseEnrollment.objects.get(user=order.user, course_id=course_id)
|
||||
except ObjectDoesNotExist:
|
||||
course_enrollment = CourseEnrollment.create_enrollment(order.user, course_id, mode=mode)
|
||||
|
||||
# do some validation on the enrollment mode
|
||||
valid_modes = CourseMode.modes_for_course_dict(course_id)
|
||||
if mode in valid_modes:
|
||||
mode_info = valid_modes[mode]
|
||||
else:
|
||||
raise InvalidCartItem(_("Mode {mode} does not exist for {course_id}").format(mode=mode, course_id=course_id))
|
||||
item, _created = cls.objects.get_or_create(
|
||||
order=order,
|
||||
user=order.user,
|
||||
@@ -321,8 +356,9 @@ class CertificateItem(OrderItem):
|
||||
item.status = order.status
|
||||
item.qty = 1
|
||||
item.unit_cost = cost
|
||||
item.line_desc = _("{mode} certificate for course {course_id}").format(mode=item.mode,
|
||||
course_id=course_id)
|
||||
course_name = course_from_id(course_id).display_name
|
||||
item.line_desc = _("Certificate of Achievement, {mode_name} for course {course}").format(mode_name=mode_info.name,
|
||||
course=course_name)
|
||||
item.currency = currency
|
||||
order.currency = currency
|
||||
order.save()
|
||||
@@ -336,3 +372,17 @@ class CertificateItem(OrderItem):
|
||||
self.course_enrollment.mode = self.mode
|
||||
self.course_enrollment.save()
|
||||
self.course_enrollment.activate()
|
||||
|
||||
@property
|
||||
def single_item_receipt_template(self):
|
||||
if self.mode == 'verified':
|
||||
return 'shoppingcart/verified_cert_receipt.html'
|
||||
else:
|
||||
return super(CertificateItem, self).single_item_receipt_template
|
||||
|
||||
@property
|
||||
def additional_instruction_text(self):
|
||||
return textwrap.dedent(
|
||||
_("Note - you have up to 2 weeks into the course to unenroll from the Verified Certificate option \
|
||||
and receive a full refund. To receive your refund, contact {billing_email}.").format(
|
||||
billing_email=settings.PAYMENT_SUPPORT_EMAIL))
|
||||
|
||||
@@ -97,13 +97,19 @@ def verify_signatures(params, signed_fields_key='signedFields', full_sig_key='si
|
||||
if processor_hash(data) != returned_sig:
|
||||
raise CCProcessorSignatureException()
|
||||
|
||||
|
||||
def render_purchase_form_html(cart):
|
||||
"""
|
||||
Renders the HTML of the hidden POST form that must be used to initiate a purchase with CyberSource
|
||||
"""
|
||||
purchase_endpoint = settings.CC_PROCESSOR['CyberSource'].get('PURCHASE_ENDPOINT', '')
|
||||
return render_to_string('shoppingcart/cybersource_form.html', {
|
||||
'action': get_purchase_endpoint(),
|
||||
'params': get_signed_purchase_params(cart),
|
||||
})
|
||||
|
||||
def get_signed_purchase_params(cart):
|
||||
return sign(get_purchase_params(cart))
|
||||
|
||||
def get_purchase_params(cart):
|
||||
total_cost = cart.total_cost
|
||||
amount = "{0:0.2f}".format(total_cost)
|
||||
cart_items = cart.orderitem_set.all()
|
||||
@@ -112,13 +118,11 @@ def render_purchase_form_html(cart):
|
||||
params['currency'] = cart.currency
|
||||
params['orderPage_transactionType'] = 'sale'
|
||||
params['orderNumber'] = "{0:d}".format(cart.id)
|
||||
signed_param_dict = sign(params)
|
||||
|
||||
return render_to_string('shoppingcart/cybersource_form.html', {
|
||||
'action': purchase_endpoint,
|
||||
'params': signed_param_dict,
|
||||
})
|
||||
return params
|
||||
|
||||
def get_purchase_endpoint():
|
||||
return settings.CC_PROCESSOR['CyberSource'].get('PURCHASE_ENDPOINT', '')
|
||||
|
||||
def payment_accepted(params):
|
||||
"""
|
||||
|
||||
229
lms/djangoapps/shoppingcart/tests/payment_fake.py
Normal file
229
lms/djangoapps/shoppingcart/tests/payment_fake.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
Fake payment page for use in acceptance tests.
|
||||
This view is enabled in the URLs by the feature flag `ENABLE_PAYMENT_FAKE`.
|
||||
|
||||
Note that you will still need to configure this view as the payment
|
||||
processor endpoint in order for the shopping cart to use it:
|
||||
|
||||
settings.CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = "/shoppingcart/payment_fake"
|
||||
|
||||
You can configure the payment to indicate success or failure by sending a PUT
|
||||
request to the view with param "success"
|
||||
set to "success" or "failure". The view defaults to payment success.
|
||||
"""
|
||||
|
||||
from django.views.generic.base import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
|
||||
|
||||
# We use the same hashing function as the software under test,
|
||||
# because it mainly uses standard libraries, and I want
|
||||
# to avoid duplicating that code.
|
||||
from shoppingcart.processors.CyberSource import processor_hash
|
||||
|
||||
|
||||
class PaymentFakeView(View):
|
||||
"""
|
||||
Fake payment page for use in acceptance tests.
|
||||
"""
|
||||
|
||||
# We store the payment status to respond with in a class
|
||||
# variable. In a multi-process Django app, this wouldn't work,
|
||||
# since processes don't share memory. Since Lettuce
|
||||
# runs one Django server process, this works for acceptance testing.
|
||||
PAYMENT_STATUS_RESPONSE = "success"
|
||||
|
||||
@csrf_exempt
|
||||
def dispatch(self, *args, **kwargs):
|
||||
"""
|
||||
Disable CSRF for these methods.
|
||||
"""
|
||||
return super(PaymentFakeView, self).dispatch(*args, **kwargs)
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
Render a fake payment page.
|
||||
|
||||
This is an HTML form that:
|
||||
|
||||
* Triggers a POST to `postpay_callback()` on submit.
|
||||
|
||||
* Has hidden fields for all the data CyberSource sends to the callback.
|
||||
- Most of this data is duplicated from the request POST params (e.g. `amount` and `course_id`)
|
||||
- Other params contain fake data (always the same user name and address.
|
||||
- Still other params are calculated (signatures)
|
||||
|
||||
* Serves an error page (HTML) with a 200 status code
|
||||
if the signatures are invalid. This is what CyberSource does.
|
||||
|
||||
Since all the POST requests are triggered by HTML forms, this is
|
||||
equivalent to the CyberSource payment page, even though it's
|
||||
served by the shopping cart app.
|
||||
"""
|
||||
if self._is_signature_valid(request.POST):
|
||||
return self._payment_page_response(request.POST, '/shoppingcart/postpay_callback/')
|
||||
|
||||
else:
|
||||
return render_to_response('shoppingcart/test/fake_payment_error.html')
|
||||
|
||||
def put(self, request):
|
||||
"""
|
||||
Set the status of payment requests to success or failure.
|
||||
|
||||
Accepts one POST param "status" that can be either "success"
|
||||
or "failure".
|
||||
"""
|
||||
new_status = request.body
|
||||
|
||||
if not new_status in ["success", "failure"]:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
else:
|
||||
# Configure all views to respond with the new status
|
||||
PaymentFakeView.PAYMENT_STATUS_RESPONSE = new_status
|
||||
return HttpResponse()
|
||||
|
||||
@staticmethod
|
||||
def _is_signature_valid(post_params):
|
||||
"""
|
||||
Return a bool indicating whether the client sent
|
||||
us a valid signature in the payment page request.
|
||||
"""
|
||||
|
||||
# Calculate the fields signature
|
||||
fields_sig = processor_hash(post_params.get('orderPage_signedFields'))
|
||||
|
||||
# Retrieve the list of signed fields
|
||||
signed_fields = post_params.get('orderPage_signedFields').split(',')
|
||||
|
||||
# Calculate the public signature
|
||||
hash_val = ",".join([
|
||||
"{0}={1}".format(key, post_params[key])
|
||||
for key in signed_fields
|
||||
]) + ",signedFieldsPublicSignature={0}".format(fields_sig)
|
||||
|
||||
public_sig = processor_hash(hash_val)
|
||||
|
||||
return public_sig == post_params.get('orderPage_signaturePublic')
|
||||
|
||||
@classmethod
|
||||
def response_post_params(cls, post_params):
|
||||
"""
|
||||
Calculate the POST params we want to send back to the client.
|
||||
"""
|
||||
resp_params = {
|
||||
# Indicate whether the payment was successful
|
||||
"decision": "ACCEPT" if cls.PAYMENT_STATUS_RESPONSE == "success" else "REJECT",
|
||||
|
||||
# Reflect back whatever the client sent us,
|
||||
# defaulting to `None` if a paramter wasn't received
|
||||
"course_id": post_params.get('course_id'),
|
||||
"orderAmount": post_params.get('amount'),
|
||||
"ccAuthReply_amount": post_params.get('amount'),
|
||||
"orderPage_transactionType": post_params.get('orderPage_transactionType'),
|
||||
"orderPage_serialNumber": post_params.get('orderPage_serialNumber'),
|
||||
"orderNumber": post_params.get('orderNumber'),
|
||||
"orderCurrency": post_params.get('currency'),
|
||||
"match": post_params.get('match'),
|
||||
"merchantID": post_params.get('merchantID'),
|
||||
|
||||
# Send fake user data
|
||||
"billTo_firstName": "John",
|
||||
"billTo_lastName": "Doe",
|
||||
"billTo_street1": "123 Fake Street",
|
||||
"billTo_state": "MA",
|
||||
"billTo_city": "Boston",
|
||||
"billTo_postalCode": "02134",
|
||||
"billTo_country": "us",
|
||||
|
||||
# Send fake data for other fields
|
||||
"card_cardType": "001",
|
||||
"card_accountNumber": "############1111",
|
||||
"card_expirationMonth": "08",
|
||||
"card_expirationYear": "2019",
|
||||
"paymentOption": "card",
|
||||
"orderPage_environment": "TEST",
|
||||
"orderPage_requestToken": "unused",
|
||||
"reconciliationID": "39093601YKVO1I5D",
|
||||
"ccAuthReply_authorizationCode": "888888",
|
||||
"ccAuthReply_avsCodeRaw": "I1",
|
||||
"reasonCode": "100",
|
||||
"requestID": "3777139938170178147615",
|
||||
"ccAuthReply_reasonCode": "100",
|
||||
"ccAuthReply_authorizedDateTime": "2013-08-28T181954Z",
|
||||
"ccAuthReply_processorResponse": "100",
|
||||
"ccAuthReply_avsCode": "X",
|
||||
|
||||
# We don't use these signatures
|
||||
"transactionSignature": "unused=",
|
||||
"decision_publicSignature": "unused=",
|
||||
"orderAmount_publicSignature": "unused=",
|
||||
"orderNumber_publicSignature": "unused=",
|
||||
"orderCurrency_publicSignature": "unused=",
|
||||
}
|
||||
|
||||
# Indicate which fields we are including in the signature
|
||||
# Order is important
|
||||
signed_fields = [
|
||||
'billTo_lastName', 'orderAmount', 'course_id',
|
||||
'billTo_street1', 'card_accountNumber', 'orderAmount_publicSignature',
|
||||
'orderPage_serialNumber', 'orderCurrency', 'reconciliationID',
|
||||
'decision', 'ccAuthReply_processorResponse', 'billTo_state',
|
||||
'billTo_firstName', 'card_expirationYear', 'billTo_city',
|
||||
'billTo_postalCode', 'orderPage_requestToken', 'ccAuthReply_amount',
|
||||
'orderCurrency_publicSignature', 'orderPage_transactionType',
|
||||
'ccAuthReply_authorizationCode', 'decision_publicSignature',
|
||||
'match', 'ccAuthReply_avsCodeRaw', 'paymentOption',
|
||||
'billTo_country', 'reasonCode', 'ccAuthReply_reasonCode',
|
||||
'orderPage_environment', 'card_expirationMonth', 'merchantID',
|
||||
'orderNumber_publicSignature', 'requestID', 'orderNumber',
|
||||
'ccAuthReply_authorizedDateTime', 'card_cardType', 'ccAuthReply_avsCode'
|
||||
]
|
||||
|
||||
# Add the list of signed fields
|
||||
resp_params['signedFields'] = ",".join(signed_fields)
|
||||
|
||||
# Calculate the fields signature
|
||||
signed_fields_sig = processor_hash(resp_params['signedFields'])
|
||||
|
||||
# Calculate the public signature
|
||||
hash_val = ",".join([
|
||||
"{0}={1}".format(key, resp_params[key])
|
||||
for key in signed_fields
|
||||
]) + ",signedFieldsPublicSignature={0}".format(signed_fields_sig)
|
||||
|
||||
resp_params['signedDataPublicSignature'] = processor_hash(hash_val)
|
||||
|
||||
return resp_params
|
||||
|
||||
def _payment_page_response(self, post_params, callback_url):
|
||||
"""
|
||||
Render the payment page to a response. This is an HTML form
|
||||
that triggers a POST request to `callback_url`.
|
||||
|
||||
The POST params are described in the CyberSource documentation:
|
||||
http://apps.cybersource.com/library/documentation/dev_guides/HOP_UG/html/wwhelp/wwhimpl/js/html/wwhelp.htm
|
||||
|
||||
To figure out the POST params to send to the callback,
|
||||
we either:
|
||||
|
||||
1) Use fake static data (e.g. always send user name "John Doe")
|
||||
2) Use the same info we received (e.g. send the same `course_id` and `amount`)
|
||||
3) Dynamically calculate signatures using a shared secret
|
||||
"""
|
||||
|
||||
# Build the context dict used to render the HTML form,
|
||||
# filling in values for the hidden input fields.
|
||||
# These will be sent in the POST request to the callback URL.
|
||||
context_dict = {
|
||||
|
||||
# URL to send the POST request to
|
||||
"callback_url": callback_url,
|
||||
|
||||
# POST params embedded in the HTML form
|
||||
'post_params': self.response_post_params(post_params)
|
||||
}
|
||||
|
||||
return render_to_response('shoppingcart/test/fake_payment_page.html', context_dict)
|
||||
@@ -4,11 +4,12 @@ Tests for the Shopping Cart Models
|
||||
|
||||
from factory import DjangoModelFactory
|
||||
from mock import patch
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.core import mail
|
||||
from django.conf import settings
|
||||
from django.db import DatabaseError
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
@@ -19,50 +20,54 @@ from course_modes.models import CourseMode
|
||||
from shoppingcart.exceptions import PurchasedCallbackException
|
||||
|
||||
|
||||
class OrderTest(TestCase):
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class OrderTest(ModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory.create()
|
||||
self.course_id = "test/course"
|
||||
self.course_id = "org/test/Test_Course"
|
||||
CourseFactory.create(org='org', number='test', display_name='Test Course')
|
||||
for i in xrange(1, 5):
|
||||
CourseFactory.create(org='org', number='test', display_name='Test Course {0}'.format(i))
|
||||
self.cost = 40
|
||||
|
||||
def test_get_cart_for_user(self):
|
||||
# create a cart
|
||||
cart = Order.get_cart_for_user(user=self.user)
|
||||
# add something to it
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified')
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor')
|
||||
# should return the same cart
|
||||
cart2 = Order.get_cart_for_user(user=self.user)
|
||||
self.assertEquals(cart2.orderitem_set.count(), 1)
|
||||
|
||||
def test_cart_clear(self):
|
||||
cart = Order.get_cart_for_user(user=self.user)
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified')
|
||||
CertificateItem.add_to_order(cart, 'test/course1', self.cost, 'verified')
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor')
|
||||
CertificateItem.add_to_order(cart, 'org/test/Test_Course_1', self.cost, 'honor')
|
||||
self.assertEquals(cart.orderitem_set.count(), 2)
|
||||
cart.clear()
|
||||
self.assertEquals(cart.orderitem_set.count(), 0)
|
||||
|
||||
def test_add_item_to_cart_currency_match(self):
|
||||
cart = Order.get_cart_for_user(user=self.user)
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified', currency='eur')
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor', currency='eur')
|
||||
# verify that a new item has been added
|
||||
self.assertEquals(cart.orderitem_set.count(), 1)
|
||||
# verify that the cart's currency was updated
|
||||
self.assertEquals(cart.currency, 'eur')
|
||||
with self.assertRaises(InvalidCartItem):
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified', currency='usd')
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor', currency='usd')
|
||||
# assert that this item did not get added to the cart
|
||||
self.assertEquals(cart.orderitem_set.count(), 1)
|
||||
|
||||
def test_total_cost(self):
|
||||
cart = Order.get_cart_for_user(user=self.user)
|
||||
# add items to the order
|
||||
course_costs = [('test/course1', 30),
|
||||
('test/course2', 40),
|
||||
('test/course3', 10),
|
||||
('test/course4', 20)]
|
||||
course_costs = [('org/test/Test_Course_1', 30),
|
||||
('org/test/Test_Course_2', 40),
|
||||
('org/test/Test_Course_3', 10),
|
||||
('org/test/Test_Course_4', 20)]
|
||||
for course, cost in course_costs:
|
||||
CertificateItem.add_to_order(cart, course, cost, 'verified')
|
||||
CertificateItem.add_to_order(cart, course, cost, 'honor')
|
||||
self.assertEquals(cart.orderitem_set.count(), len(course_costs))
|
||||
self.assertEquals(cart.total_cost, sum(cost for _course, cost in course_costs))
|
||||
|
||||
@@ -72,7 +77,7 @@ class OrderTest(TestCase):
|
||||
# CertificateItem, which is not quite good unit test form. Sorry.
|
||||
cart = Order.get_cart_for_user(user=self.user)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id))
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified')
|
||||
item = CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor')
|
||||
# course enrollment object should be created but still inactive
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id))
|
||||
cart.purchase()
|
||||
@@ -83,12 +88,13 @@ class OrderTest(TestCase):
|
||||
self.assertEquals('Order Payment Confirmation', mail.outbox[0].subject)
|
||||
self.assertIn(settings.PAYMENT_SUPPORT_EMAIL, mail.outbox[0].body)
|
||||
self.assertIn(unicode(cart.total_cost), mail.outbox[0].body)
|
||||
self.assertIn(item.additional_instruction_text, mail.outbox[0].body)
|
||||
|
||||
def test_purchase_item_failure(self):
|
||||
# once again, we're testing against the specific implementation of
|
||||
# CertificateItem
|
||||
cart = Order.get_cart_for_user(user=self.user)
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified')
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor')
|
||||
with patch('shoppingcart.models.CertificateItem.save', side_effect=DatabaseError):
|
||||
with self.assertRaises(DatabaseError):
|
||||
cart.purchase()
|
||||
@@ -99,7 +105,7 @@ class OrderTest(TestCase):
|
||||
|
||||
def purchase_with_data(self, cart):
|
||||
""" purchase a cart with billing information """
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified')
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor')
|
||||
cart.purchase(
|
||||
first='John',
|
||||
last='Smith',
|
||||
@@ -145,6 +151,7 @@ class OrderTest(TestCase):
|
||||
self.assertEqual(cart.bill_to_ccnum, '')
|
||||
self.assertEqual(cart.bill_to_cardtype, '')
|
||||
|
||||
|
||||
class OrderItemTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory.create()
|
||||
@@ -222,14 +229,26 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id))
|
||||
|
||||
|
||||
class CertificateItemTest(TestCase):
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class CertificateItemTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for verifying specific CertificateItem functionality
|
||||
"""
|
||||
def setUp(self):
|
||||
self.user = UserFactory.create()
|
||||
self.course_id = "test/course"
|
||||
self.course_id = "org/test/Test_Course"
|
||||
self.cost = 40
|
||||
CourseFactory.create(org='org', number='test', run='course', display_name='Test Course')
|
||||
course_mode = CourseMode(course_id=self.course_id,
|
||||
mode_slug="honor",
|
||||
mode_display_name="honor cert",
|
||||
min_price=self.cost)
|
||||
course_mode.save()
|
||||
course_mode = CourseMode(course_id=self.course_id,
|
||||
mode_slug="verified",
|
||||
mode_display_name="verified cert",
|
||||
min_price=self.cost)
|
||||
course_mode.save()
|
||||
|
||||
def test_existing_enrollment(self):
|
||||
CourseEnrollment.enroll(self.user, self.course_id)
|
||||
@@ -240,3 +259,14 @@ class CertificateItemTest(TestCase):
|
||||
cart.purchase()
|
||||
enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_id)
|
||||
self.assertEquals(enrollment.mode, u'verified')
|
||||
|
||||
def test_single_item_template(self):
|
||||
cart = Order.get_cart_for_user(user=self.user)
|
||||
cert_item = CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified')
|
||||
|
||||
self.assertEquals(cert_item.single_item_receipt_template,
|
||||
'shoppingcart/verified_cert_receipt.html')
|
||||
|
||||
cert_item = CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor')
|
||||
self.assertEquals(cert_item.single_item_receipt_template,
|
||||
'shoppingcart/receipt.html')
|
||||
|
||||
112
lms/djangoapps/shoppingcart/tests/test_payment_fake.py
Normal file
112
lms/djangoapps/shoppingcart/tests/test_payment_fake.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Tests for the fake payment page used in acceptance tests.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from shoppingcart.processors.CyberSource import sign, verify_signatures, \
|
||||
CCProcessorSignatureException
|
||||
from shoppingcart.tests.payment_fake import PaymentFakeView
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
class PaymentFakeViewTest(TestCase):
|
||||
"""
|
||||
Test that the fake payment view interacts
|
||||
correctly with the shopping cart.
|
||||
"""
|
||||
|
||||
CLIENT_POST_PARAMS = OrderedDict([
|
||||
('match', 'on'),
|
||||
('course_id', 'edx/999/2013_Spring'),
|
||||
('amount', '25.00'),
|
||||
('currency', 'usd'),
|
||||
('orderPage_transactionType', 'sale'),
|
||||
('orderNumber', '33'),
|
||||
('merchantID', 'edx'),
|
||||
('djch', '012345678912'),
|
||||
('orderPage_version', 2),
|
||||
('orderPage_serialNumber', '1234567890'),
|
||||
])
|
||||
|
||||
def setUp(self):
|
||||
super(PaymentFakeViewTest, self).setUp()
|
||||
|
||||
# Reset the view state
|
||||
PaymentFakeView.PAYMENT_STATUS_RESPONSE = "success"
|
||||
|
||||
def test_accepts_client_signatures(self):
|
||||
|
||||
# Generate shoppingcart signatures
|
||||
post_params = sign(self.CLIENT_POST_PARAMS)
|
||||
|
||||
# Simulate a POST request from the payment workflow
|
||||
# page to the fake payment page.
|
||||
resp = self.client.post(
|
||||
'/shoppingcart/payment_fake', dict(post_params)
|
||||
)
|
||||
|
||||
# Expect that the response was successful
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Expect that we were served the payment page
|
||||
# (not the error page)
|
||||
self.assertIn("Payment Form", resp.content)
|
||||
|
||||
def test_rejects_invalid_signature(self):
|
||||
|
||||
# Generate shoppingcart signatures
|
||||
post_params = sign(self.CLIENT_POST_PARAMS)
|
||||
|
||||
# Tamper with the signature
|
||||
post_params['orderPage_signaturePublic'] = "invalid"
|
||||
|
||||
# Simulate a POST request from the payment workflow
|
||||
# page to the fake payment page.
|
||||
resp = self.client.post(
|
||||
'/shoppingcart/payment_fake', dict(post_params)
|
||||
)
|
||||
|
||||
# Expect that we got an error
|
||||
self.assertIn("Error", resp.content)
|
||||
|
||||
def test_sends_valid_signature(self):
|
||||
|
||||
# Generate shoppingcart signatures
|
||||
post_params = sign(self.CLIENT_POST_PARAMS)
|
||||
|
||||
# Get the POST params that the view would send back to us
|
||||
resp_params = PaymentFakeView.response_post_params(post_params)
|
||||
|
||||
# Check that the client accepts these
|
||||
try:
|
||||
verify_signatures(resp_params)
|
||||
|
||||
except CCProcessorSignatureException:
|
||||
self.fail("Client rejected signatures.")
|
||||
|
||||
def test_set_payment_status(self):
|
||||
|
||||
# Generate shoppingcart signatures
|
||||
post_params = sign(self.CLIENT_POST_PARAMS)
|
||||
|
||||
# Configure the view to fail payments
|
||||
resp = self.client.put(
|
||||
'/shoppingcart/payment_fake',
|
||||
data="failure", content_type='text/plain'
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Check that the decision is "REJECT"
|
||||
resp_params = PaymentFakeView.response_post_params(post_params)
|
||||
self.assertEqual(resp_params.get('decision'), 'REJECT')
|
||||
|
||||
# Configure the view to accept payments
|
||||
resp = self.client.put(
|
||||
'/shoppingcart/payment_fake',
|
||||
data="success", content_type='text/plain'
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Check that the decision is "ACCEPT"
|
||||
resp_params = PaymentFakeView.response_post_params(post_params)
|
||||
self.assertEqual(resp_params.get('decision'), 'ACCEPT')
|
||||
@@ -50,6 +50,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
mode_display_name="honor cert",
|
||||
min_price=self.cost)
|
||||
self.course_mode.save()
|
||||
self.verified_course_id = 'org/test/Test_Course'
|
||||
CourseFactory.create(org='org', number='test', run='course1', display_name='Test Course')
|
||||
self.cart = Order.get_cart_for_user(self.user)
|
||||
|
||||
def login_user(self):
|
||||
@@ -63,14 +65,14 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
PaidCourseRegistration.add_to_order(self.cart, self.course_id)
|
||||
self.login_user()
|
||||
resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id]))
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertIn(_('The course {0} is already in your cart.'.format(self.course_id)), resp.content)
|
||||
|
||||
def test_add_course_to_cart_already_registered(self):
|
||||
CourseEnrollment.enroll(self.user, self.course_id)
|
||||
self.login_user()
|
||||
resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id]))
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertIn(_('You are already registered in course {0}.'.format(self.course_id)), resp.content)
|
||||
|
||||
def test_add_nonexistent_course_to_cart(self):
|
||||
@@ -91,7 +93,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
def test_show_cart(self):
|
||||
self.login_user()
|
||||
reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id)
|
||||
cert_item = CertificateItem.add_to_order(self.cart, 'test/course1', self.cost, 'verified')
|
||||
cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_id, self.cost, 'honor')
|
||||
resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[]))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@@ -110,7 +112,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
def test_clear_cart(self):
|
||||
self.login_user()
|
||||
PaidCourseRegistration.add_to_order(self.cart, self.course_id)
|
||||
CertificateItem.add_to_order(self.cart, 'test/course1', self.cost, 'verified')
|
||||
CertificateItem.add_to_order(self.cart, self.verified_course_id, self.cost, 'honor')
|
||||
self.assertEquals(self.cart.orderitem_set.count(), 2)
|
||||
resp = self.client.post(reverse('shoppingcart.views.clear_cart', args=[]))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -120,7 +122,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
def test_remove_item(self, exception_log):
|
||||
self.login_user()
|
||||
reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id)
|
||||
cert_item = CertificateItem.add_to_order(self.cart, 'test/course1', self.cost, 'verified')
|
||||
cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_id, self.cost, 'honor')
|
||||
self.assertEquals(self.cart.orderitem_set.count(), 2)
|
||||
resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]),
|
||||
{'id': reg_item.id})
|
||||
@@ -166,7 +168,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
|
||||
def test_show_receipt_404s(self):
|
||||
PaidCourseRegistration.add_to_order(self.cart, self.course_id)
|
||||
CertificateItem.add_to_order(self.cart, 'test/course1', self.cost, 'verified')
|
||||
CertificateItem.add_to_order(self.cart, self.verified_course_id, self.cost, 'honor')
|
||||
self.cart.purchase()
|
||||
|
||||
user2 = UserFactory.create()
|
||||
@@ -184,7 +186,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
@patch('shoppingcart.views.render_to_response', render_mock)
|
||||
def test_show_receipt_success(self):
|
||||
reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id)
|
||||
cert_item = CertificateItem.add_to_order(self.cart, 'test/course1', self.cost, 'verified')
|
||||
cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_id, self.cost, 'honor')
|
||||
self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
|
||||
|
||||
self.login_user()
|
||||
@@ -196,14 +198,14 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
((template, context), _) = render_mock.call_args
|
||||
self.assertEqual(template, 'shoppingcart/receipt.html')
|
||||
self.assertEqual(context['order'], self.cart)
|
||||
self.assertIn(reg_item.orderitem_ptr, context['order_items'])
|
||||
self.assertIn(cert_item.orderitem_ptr, context['order_items'])
|
||||
self.assertIn(reg_item, context['order_items'])
|
||||
self.assertIn(cert_item, context['order_items'])
|
||||
self.assertFalse(context['any_refunds'])
|
||||
|
||||
@patch('shoppingcart.views.render_to_response', render_mock)
|
||||
def test_show_receipt_success_refund(self):
|
||||
reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id)
|
||||
cert_item = CertificateItem.add_to_order(self.cart, 'test/course1', self.cost, 'verified')
|
||||
cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_id, self.cost, 'honor')
|
||||
self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
|
||||
cert_item.status = "refunded"
|
||||
cert_item.save()
|
||||
@@ -213,9 +215,20 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIn('40.00', resp.content)
|
||||
|
||||
((template, context), _) = render_mock.call_args
|
||||
((template, context), _tmp) = render_mock.call_args
|
||||
self.assertEqual(template, 'shoppingcart/receipt.html')
|
||||
self.assertEqual(context['order'], self.cart)
|
||||
self.assertIn(reg_item.orderitem_ptr, context['order_items'])
|
||||
self.assertIn(cert_item.orderitem_ptr, context['order_items'])
|
||||
self.assertIn(reg_item, context['order_items'])
|
||||
self.assertIn(cert_item, context['order_items'])
|
||||
self.assertTrue(context['any_refunds'])
|
||||
|
||||
@patch('shoppingcart.views.render_to_response', render_mock)
|
||||
def test_show_receipt_success_custom_receipt_page(self):
|
||||
cert_item = CertificateItem.add_to_order(self.cart, self.course_id, self.cost, 'honor')
|
||||
self.cart.purchase()
|
||||
self.login_user()
|
||||
receipt_url = reverse('shoppingcart.views.show_receipt', args=[self.cart.id])
|
||||
resp = self.client.get(receipt_url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
((template, _context), _tmp) = render_mock.call_args
|
||||
self.assertEqual(template, cert_item.single_item_receipt_template)
|
||||
|
||||
@@ -13,3 +13,10 @@ if settings.MITX_FEATURES['ENABLE_SHOPPING_CART']:
|
||||
url(r'^remove_item/$', 'remove_item'),
|
||||
url(r'^add/course/(?P<course_id>[^/]+/[^/]+/[^/]+)/$', 'add_course_to_cart', name='add_course_to_cart'),
|
||||
)
|
||||
|
||||
if settings.MITX_FEATURES.get('ENABLE_PAYMENT_FAKE'):
|
||||
from shoppingcart.tests.payment_fake import PaymentFakeView
|
||||
urlpatterns += patterns(
|
||||
'shoppingcart.tests.payment_fake',
|
||||
url(r'^payment_fake', PaymentFakeView.as_view())
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound, HttpResponseForbidden, Http404
|
||||
from django.http import (HttpResponse, HttpResponseRedirect, HttpResponseNotFound,
|
||||
HttpResponseBadRequest, HttpResponseForbidden, Http404)
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -19,9 +20,10 @@ def add_course_to_cart(request, course_id):
|
||||
return HttpResponseForbidden(_('You must be logged-in to add to a shopping cart'))
|
||||
cart = Order.get_cart_for_user(request.user)
|
||||
if PaidCourseRegistration.part_of_order(cart, course_id):
|
||||
return HttpResponseNotFound(_('The course {0} is already in your cart.'.format(course_id)))
|
||||
return HttpResponseBadRequest(_('The course {0} is already in your cart.'.format(course_id)))
|
||||
if CourseEnrollment.is_enrolled(user=request.user, course_id=course_id):
|
||||
return HttpResponseNotFound(_('You are already registered in course {0}.'.format(course_id)))
|
||||
return HttpResponseBadRequest(_('You are already registered in course {0}.'.format(course_id)))
|
||||
|
||||
try:
|
||||
PaidCourseRegistration.add_to_order(cart, course_id)
|
||||
except ItemNotFoundError:
|
||||
@@ -98,8 +100,18 @@ def show_receipt(request, ordernum):
|
||||
if order.user != request.user or order.status != 'purchased':
|
||||
raise Http404('Order not found!')
|
||||
|
||||
order_items = order.orderitem_set.all()
|
||||
order_items = OrderItem.objects.filter(order=order).select_subclasses()
|
||||
any_refunds = any(i.status == "refunded" for i in order_items)
|
||||
return render_to_response('shoppingcart/receipt.html', {'order': order,
|
||||
'order_items': order_items,
|
||||
'any_refunds': any_refunds})
|
||||
receipt_template = 'shoppingcart/receipt.html'
|
||||
# we want to have the ability to override the default receipt page when
|
||||
# there is only one item in the order
|
||||
context = {
|
||||
'order': order,
|
||||
'order_items': order_items,
|
||||
'any_refunds': any_refunds,
|
||||
}
|
||||
|
||||
if order_items.count() == 1:
|
||||
receipt_template = order_items[0].single_item_receipt_template
|
||||
|
||||
return render_to_response(receipt_template, context)
|
||||
|
||||
0
lms/djangoapps/verify_student/__init__.py
Normal file
0
lms/djangoapps/verify_student/__init__.py
Normal file
0
lms/djangoapps/verify_student/api.py
Normal file
0
lms/djangoapps/verify_student/api.py
Normal file
96
lms/djangoapps/verify_student/migrations/0001_initial.py
Normal file
96
lms/djangoapps/verify_student/migrations/0001_initial.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'SoftwareSecurePhotoVerification'
|
||||
db.create_table('verify_student_softwaresecurephotoverification', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('status', self.gf('model_utils.fields.StatusField')(default='created', max_length=100, no_check_for_status=True)),
|
||||
('status_changed', self.gf('model_utils.fields.MonitorField')(default=datetime.datetime.now, monitor=u'status')),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
|
||||
('name', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
|
||||
('face_image_url', self.gf('django.db.models.fields.URLField')(max_length=255, blank=True)),
|
||||
('photo_id_image_url', self.gf('django.db.models.fields.URLField')(max_length=255, blank=True)),
|
||||
('receipt_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
|
||||
('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
|
||||
('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
|
||||
('submitted_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
|
||||
('reviewing_user', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='photo_verifications_reviewed', null=True, to=orm['auth.User'])),
|
||||
('reviewing_service', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
|
||||
('error_msg', self.gf('django.db.models.fields.TextField')(blank=True)),
|
||||
('error_code', self.gf('django.db.models.fields.CharField')(max_length=50, blank=True)),
|
||||
('photo_id_key', self.gf('django.db.models.fields.TextField')(max_length=1024)),
|
||||
))
|
||||
db.send_create_signal('verify_student', ['SoftwareSecurePhotoVerification'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'SoftwareSecurePhotoVerification'
|
||||
db.delete_table('verify_student_softwaresecurephotoverification')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'verify_student.softwaresecurephotoverification': {
|
||||
'Meta': {'ordering': "['-created_at']", 'object_name': 'SoftwareSecurePhotoVerification'},
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'error_code': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
|
||||
'error_msg': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'face_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'photo_id_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'photo_id_key': ('django.db.models.fields.TextField', [], {'max_length': '1024'}),
|
||||
'receipt_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'reviewing_service': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'reviewing_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'photo_verifications_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}),
|
||||
'status': ('model_utils.fields.StatusField', [], {'default': "'created'", 'max_length': '100', u'no_check_for_status': 'True'}),
|
||||
'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
|
||||
'submitted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['verify_student']
|
||||
408
lms/djangoapps/verify_student/models.py
Normal file
408
lms/djangoapps/verify_student/models.py
Normal file
@@ -0,0 +1,408 @@
|
||||
# -*- coding: utf-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
|
||||
`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, timedelta
|
||||
from hashlib import md5
|
||||
import base64
|
||||
import functools
|
||||
import logging
|
||||
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
|
||||
from model_utils import Choices
|
||||
|
||||
from verify_student.ssencrypt import (
|
||||
random_aes_key, decode_and_decrypt, encrypt_and_encode
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VerificationException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def status_before_must_be(*valid_start_statuses):
|
||||
"""
|
||||
Helper 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 PhotoVerification(StatusModel):
|
||||
"""
|
||||
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 edit a `PhotoVerification`
|
||||
object through the methods provided**. Initialize them with a user:
|
||||
|
||||
attempt = PhotoVerification(user=user)
|
||||
|
||||
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 == PhotoVerification.STATUS.created
|
||||
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')
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
ordering = ['-created_at']
|
||||
|
||||
##### Methods listed in the order you'd typically call them
|
||||
@classmethod
|
||||
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.
|
||||
"""
|
||||
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__gte=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__gte=earliest_allowed_date
|
||||
).exists()
|
||||
|
||||
@classmethod
|
||||
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?
|
||||
"""
|
||||
# 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):
|
||||
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()
|
||||
|
||||
|
||||
class SoftwareSecurePhotoVerification(PhotoVerification):
|
||||
"""
|
||||
Model to verify identity using a service provided by Software Secure. Much
|
||||
of the logic is inherited from `PhotoVerification`, but this class
|
||||
encrypts the photos.
|
||||
|
||||
Software Secure (http://www.softwaresecure.com/) is a remote proctoring
|
||||
service that also does identity verification. A student uses their webcam
|
||||
to upload two images: one of their face, one of a photo ID. Due to the
|
||||
sensitive nature of the data, the following security precautions are taken:
|
||||
|
||||
1. The snapshot of their face is encrypted using AES-256 in CBC mode. All
|
||||
face photos are encypted with the same key, and this key is known to
|
||||
both Software Secure and edx-platform.
|
||||
|
||||
2. The snapshot of a user's photo ID is also encrypted using AES-256, but
|
||||
the key is randomly generated using pycrypto's Random. Every verification
|
||||
attempt has a new key. The AES key is then encrypted using a public key
|
||||
provided by Software Secure. We store only the RSA-encryped AES key.
|
||||
Since edx-platform does not have Software Secure's private RSA key, it
|
||||
means that we can no longer even read photo ID.
|
||||
|
||||
3. The encrypted photos are base64 encoded and stored in an S3 bucket that
|
||||
edx-platform does not have read access to.
|
||||
"""
|
||||
# This is a base64.urlsafe_encode(rsa_encrypt(photo_id_aes_key), ss_pub_key)
|
||||
# So first we generate a random AES-256 key to encrypt our photo ID with.
|
||||
# Then we RSA encrypt it with Software Secure's public key. Then we base64
|
||||
# encode that. The result is saved here. Actual expected length is 344.
|
||||
photo_id_key = models.TextField(max_length=1024)
|
||||
|
||||
@status_before_must_be("created")
|
||||
def upload_face_image(self, img_data):
|
||||
aes_key_str = settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["FACE_IMAGE_AES_KEY"]
|
||||
aes_key = aes_key_str.decode("hex")
|
||||
encrypted_img_data = self._encrypt_image_data(img_data, aes_key)
|
||||
b64_encoded_img_data = base64.encodestring(encrypted_img_data)
|
||||
|
||||
# Upload it to S3
|
||||
|
||||
@status_before_must_be("created")
|
||||
def upload_photo_id_image(self, img_data):
|
||||
aes_key = random_aes_key()
|
||||
encrypted_img_data = self._encrypt_image_data(img_data, aes_key)
|
||||
b64_encoded_img_data = base64.encodestring(encrypted_img_data)
|
||||
|
||||
# Upload this to S3
|
||||
|
||||
rsa_key = RSA.importKey(
|
||||
settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["RSA_PUBLIC_KEY"]
|
||||
)
|
||||
rsa_cipher = PKCS1_OAEP.new(key)
|
||||
rsa_encrypted_aes_key = rsa_cipher.encrypt(aes_key)
|
||||
90
lms/djangoapps/verify_student/ssencrypt.py
Normal file
90
lms/djangoapps/verify_student/ssencrypt.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
NOTE: Anytime a `key` is passed into a function here, we assume it's a raw byte
|
||||
string. It should *not* be a string representation of a hex value. In other
|
||||
words, passing the `str` value of
|
||||
`"32fe72aaf2abb44de9e161131b5435c8d37cbdb6f5df242ae860b283115f2dae"` is bad.
|
||||
You want to pass in the result of calling .decode('hex') on that, so this instead:
|
||||
"'2\xfer\xaa\xf2\xab\xb4M\xe9\xe1a\x13\x1bT5\xc8\xd3|\xbd\xb6\xf5\xdf$*\xe8`\xb2\x83\x11_-\xae'"
|
||||
|
||||
The RSA functions take any key format that RSA.importKey() accepts, so...
|
||||
|
||||
An RSA public key can be in any of the following formats:
|
||||
* X.509 subjectPublicKeyInfo DER SEQUENCE (binary or PEM encoding)
|
||||
* PKCS#1 RSAPublicKey DER SEQUENCE (binary or PEM encoding)
|
||||
* OpenSSH (textual public key only)
|
||||
|
||||
An RSA private key can be in any of the following formats:
|
||||
* PKCS#1 RSAPrivateKey DER SEQUENCE (binary or PEM encoding)
|
||||
* PKCS#8 PrivateKeyInfo DER SEQUENCE (binary or PEM encoding)
|
||||
* OpenSSH (textual public key only)
|
||||
|
||||
In case of PEM encoding, the private key can be encrypted with DES or 3TDES
|
||||
according to a certain pass phrase. Only OpenSSL-compatible pass phrases are
|
||||
supported.
|
||||
"""
|
||||
from hashlib import md5
|
||||
import base64
|
||||
|
||||
from Crypto import Random
|
||||
from Crypto.Cipher import AES, PKCS1_OAEP
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
|
||||
def encrypt_and_encode(data, key):
|
||||
return base64.urlsafe_b64encode(aes_encrypt(data, key))
|
||||
|
||||
def decode_and_decrypt(encoded_data, key):
|
||||
return aes_decrypt(base64.urlsafe_b64decode(encoded_data), key)
|
||||
|
||||
def aes_encrypt(data, key):
|
||||
"""
|
||||
Return a version of the `data` that has been encrypted to
|
||||
"""
|
||||
cipher = aes_cipher_from_key(key)
|
||||
padded_data = pad(data)
|
||||
return cipher.encrypt(padded_data)
|
||||
|
||||
def aes_decrypt(encrypted_data, key):
|
||||
cipher = aes_cipher_from_key(key)
|
||||
padded_data = cipher.decrypt(encrypted_data)
|
||||
return unpad(padded_data)
|
||||
|
||||
def aes_cipher_from_key(key):
|
||||
"""
|
||||
Given an AES key, return a Cipher object that has `encrypt()` and
|
||||
`decrypt()` methods. It will create the cipher to use CBC mode, and create
|
||||
the initialization vector as Software Secure expects it.
|
||||
"""
|
||||
return AES.new(key, AES.MODE_CBC, generate_aes_iv(key))
|
||||
|
||||
def generate_aes_iv(key):
|
||||
"""
|
||||
Return the initialization vector Software Secure expects for a given AES
|
||||
key (they hash it a couple of times and take a substring).
|
||||
"""
|
||||
return md5(key + md5(key).hexdigest()).hexdigest()[:AES.block_size]
|
||||
|
||||
def random_aes_key():
|
||||
return Random.new().read(32)
|
||||
|
||||
def pad(data):
|
||||
bytes_to_pad = AES.block_size - len(data) % AES.block_size
|
||||
return data + (bytes_to_pad * chr(bytes_to_pad))
|
||||
|
||||
def unpad(padded_data):
|
||||
num_padded_bytes = ord(padded_data[-1])
|
||||
return padded_data[:-num_padded_bytes]
|
||||
|
||||
def rsa_encrypt(data, rsa_pub_key_str):
|
||||
"""
|
||||
`rsa_pub_key` is a string with the public key
|
||||
"""
|
||||
key = RSA.importKey(rsa_pub_key_str)
|
||||
cipher = PKCS1_OAEP.new(key)
|
||||
encrypted_data = cipher.encrypt(data)
|
||||
return encrypted_data
|
||||
|
||||
def rsa_decrypt(data, rsa_priv_key_str):
|
||||
key = RSA.importKey(rsa_priv_key_str)
|
||||
cipher = PKCS1_OAEP.new(key)
|
||||
return cipher.decrypt(data)
|
||||
0
lms/djangoapps/verify_student/tests/__init__.py
Normal file
0
lms/djangoapps/verify_student/tests/__init__.py
Normal file
60
lms/djangoapps/verify_student/tests/test_models.py
Normal file
60
lms/djangoapps/verify_student/tests/test_models.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- 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 SoftwareSecurePhotoVerification, VerificationException
|
||||
|
||||
|
||||
class TestPhotoVerification(TestCase):
|
||||
|
||||
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 = 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
|
||||
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")
|
||||
|
||||
|
||||
78
lms/djangoapps/verify_student/tests/test_ssencrypt.py
Normal file
78
lms/djangoapps/verify_student/tests/test_ssencrypt.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import base64
|
||||
|
||||
from nose.tools import assert_equals
|
||||
|
||||
from verify_student.ssencrypt import (
|
||||
aes_decrypt, aes_encrypt, encrypt_and_encode, decode_and_decrypt,
|
||||
rsa_decrypt, rsa_encrypt, random_aes_key
|
||||
)
|
||||
|
||||
def test_aes():
|
||||
key_str = "32fe72aaf2abb44de9e161131b5435c8d37cbdb6f5df242ae860b283115f2dae"
|
||||
key = key_str.decode("hex")
|
||||
|
||||
def assert_roundtrip(text):
|
||||
assert_equals(text, aes_decrypt(aes_encrypt(text, key), key))
|
||||
assert_equals(
|
||||
text,
|
||||
decode_and_decrypt(
|
||||
encrypt_and_encode(text, key),
|
||||
key
|
||||
)
|
||||
)
|
||||
|
||||
assert_roundtrip("Hello World!")
|
||||
assert_roundtrip("1234567890123456") # AES block size, padding corner case
|
||||
# Longer string
|
||||
assert_roundtrip("12345678901234561234567890123456123456789012345601")
|
||||
assert_roundtrip("")
|
||||
assert_roundtrip("\xe9\xe1a\x13\x1bT5\xc8") # Random, non-ASCII text
|
||||
|
||||
def test_rsa():
|
||||
# Make up some garbage keys for testing purposes.
|
||||
pub_key_str = """-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1hLVjP0oV0Uy/+jQ+Upz
|
||||
c+eYc4Pyflb/WpfgYATggkoQdnsdplmvPtQr85+utgqKPxOh+PvYGW8QNUzjLIu4
|
||||
5/GlmvBa82i1jRMgEAxGI95bz7j9DtH+7mnj+06zR5xHwT49jK0zMs5MjMaz5WRq
|
||||
BUNkz7dxWzDrYJZQx230sPp6upy1Y5H5O8SnJVdghsh8sNciS4Bo4ZONQ3giBwxz
|
||||
h5svjspz1MIsOoShjbAdfG+4VX7sVwYlw2rnQeRsMH5/xpnNeqtScyOMoz0N9UDG
|
||||
dtRMNGa2MihAg7zh7/zckbUrtf+o5wQtlCJL1Kdj4EjshqYvCxzWnSM+MaYAjb3M
|
||||
EQIDAQAB
|
||||
-----END PUBLIC KEY-----"""
|
||||
priv_key_str = """-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA1hLVjP0oV0Uy/+jQ+Upzc+eYc4Pyflb/WpfgYATggkoQdnsd
|
||||
plmvPtQr85+utgqKPxOh+PvYGW8QNUzjLIu45/GlmvBa82i1jRMgEAxGI95bz7j9
|
||||
DtH+7mnj+06zR5xHwT49jK0zMs5MjMaz5WRqBUNkz7dxWzDrYJZQx230sPp6upy1
|
||||
Y5H5O8SnJVdghsh8sNciS4Bo4ZONQ3giBwxzh5svjspz1MIsOoShjbAdfG+4VX7s
|
||||
VwYlw2rnQeRsMH5/xpnNeqtScyOMoz0N9UDGdtRMNGa2MihAg7zh7/zckbUrtf+o
|
||||
5wQtlCJL1Kdj4EjshqYvCxzWnSM+MaYAjb3MEQIDAQABAoIBAQCviuA87fdfoOoS
|
||||
OerrEacc20QDLaby/QoGUtZ2RmmHzY40af7FQ3PWFIw6Ca5trrTwxnuivXnWWWG0
|
||||
I2mCRM0Kvfgr1n7ubOW7WnyHTFlT3mnxK2Ov/HmNLZ36nO2cgkXA6/Xy3rBGMC9L
|
||||
nUE1kSLzT/Fh965ntfS9zmVNNBhb6no0rVkGx5nK3vTI6kUmaa0m+E7KL/HweO4c
|
||||
JodhN8CX4gpxSrkuwJ7IHEPYspqc0jInMYKLmD3d2g3BiOctjzFmaj3lV5AUlujW
|
||||
z7/LVe5WAEaaxjwaMvwqrJLv9ogxWU3etJf22+Yy7r5gbPtqpqJrCZ5+WpGnUHws
|
||||
3mMGP2QBAoGBAOc3pzLFgGUREVPSFQlJ06QFtfKYqg9fFHJCgWu/2B2aVZc2aO/t
|
||||
Zhuoz+AgOdzsw+CWv7K0FH9sUkffk2VKPzwwwufLK3avD9gI0bhmBAYvdhS6A3nO
|
||||
YM3W+lvmaJtFL00K6kdd+CzgRnBS9cZ70WbcbtqjdXI6+mV1WdGUTLhBAoGBAO0E
|
||||
xhD4z+GjubSgfHYEZPgRJPqyUIfDH+5UmFGpr6zlvNN/depaGxsbhW8t/V6xkxsG
|
||||
MCgic7GLMihEiUMx1+/snVs5bBUx7OT9API0d+vStHCFlTTe6aTdmiduFD4PbDsq
|
||||
6E4DElVRqZhpIYusdDh7Z3fO2hm5ad4FfMlx65/RAoGAPYEfV7ETs06z9kEG2X6q
|
||||
7pGaUZrsecRH8xDfzmKswUshg2S0y0WyCJ+CFFNeMPdGL4LKIWYnobGVvYqqcaIr
|
||||
af5qijAQMrTkmQnXh56TaXXMijzk2czdEUQjOrjykIL5zxudMDi94GoUMqLOv+qF
|
||||
zD/MuRoMDsPDgaOSrd4t/kECgYEAzwBNT8NOIz3P0Z4cNSJPYIvwpPaY+IkE2SyO
|
||||
vzuYj0Mx7/Ew9ZTueXVGyzv6PfqOhJqZ8mNscZIlIyAAVWwxsHwRTfvPlo882xzP
|
||||
97i1R4OFTYSNNFi+69sSZ/9utGjZ2K73pjJuj487tD2VK5xZAH9edTd2KeNSP7LB
|
||||
MlpJNBECgYAmIswPdldm+G8SJd5j9O2fcDVTURjKAoSXCv2j4gEZzzfudpLWNHYu
|
||||
l8N6+LEIVTMAytPk+/bImHvGHKZkCz5rEMSuYJWOmqKI92rUtI6fz5DUb3XSbrwT
|
||||
3W+sdGFUK3GH1NAX71VxbAlFVLUetcMwai1+wXmGkRw6A7YezVFnhw==
|
||||
-----END RSA PRIVATE KEY-----"""
|
||||
aes_key_str = "32fe72aaf2abb44de9e161131b5435c8d37cbdb6f5df242ae860b283115f2dae"
|
||||
|
||||
aes_key = aes_key_str.decode('hex')
|
||||
|
||||
encrypted_aes_key = rsa_encrypt(aes_key, pub_key_str)
|
||||
assert_equals(aes_key, rsa_decrypt(encrypted_aes_key, priv_key_str))
|
||||
|
||||
# Even though our AES key is only 32 bytes, RSA encryption will make it 256
|
||||
# bytes, and base64 encoding will blow that up to 344
|
||||
assert_equals(len(base64.urlsafe_b64encode(encrypted_aes_key)), 344)
|
||||
36
lms/djangoapps/verify_student/tests/test_views.py
Normal file
36
lms/djangoapps/verify_student/tests/test_views.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
|
||||
|
||||
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/{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()))
|
||||
|
||||
38
lms/djangoapps/verify_student/urls.py
Normal file
38
lms/djangoapps/verify_student/urls.py
Normal file
@@ -0,0 +1,38 @@
|
||||
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/(?P<course_id>[^/]+/[^/]+/[^/]+)$',
|
||||
views.show_requirements,
|
||||
name="verify_student_show_requirements"
|
||||
),
|
||||
|
||||
url(
|
||||
r'^verify/(?P<course_id>[^/]+/[^/]+/[^/]+)$',
|
||||
views.VerifyView.as_view(),
|
||||
name="verify_student_verify"
|
||||
),
|
||||
|
||||
url(
|
||||
r'^verified/(?P<course_id>[^/]+/[^/]+/[^/]+)$',
|
||||
views.VerifiedView.as_view(),
|
||||
name="verify_student_verified"
|
||||
),
|
||||
|
||||
url(
|
||||
r'^create_order',
|
||||
views.create_order,
|
||||
name="verify_student_create_order"
|
||||
),
|
||||
|
||||
url(
|
||||
r'^show_verification_page/(?P<course_id>[^/]+/[^/]+/[^/]+)$',
|
||||
views.show_verification_page,
|
||||
name="verify_student/show_verification_page"
|
||||
),
|
||||
|
||||
)
|
||||
221
lms/djangoapps/verify_student/views.py
Normal file
221
lms/djangoapps/verify_student/views.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import decimal
|
||||
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
from django.views.generic.base import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.http import urlencode
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from student.models import CourseEnrollment
|
||||
from student.views import course_from_id
|
||||
from shoppingcart.models import Order, CertificateItem
|
||||
from shoppingcart.processors.CyberSource import (
|
||||
get_signed_purchase_params, get_purchase_endpoint
|
||||
)
|
||||
from verify_student.models import SoftwareSecurePhotoVerification
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class VerifyView(View):
|
||||
|
||||
@method_decorator(login_required)
|
||||
def get(self, request, course_id):
|
||||
"""
|
||||
"""
|
||||
# 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):
|
||||
return redirect(
|
||||
reverse('verify_student_verified',
|
||||
kwargs={'course_id': course_id}))
|
||||
elif CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified':
|
||||
return redirect(reverse('dashboard'))
|
||||
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"
|
||||
|
||||
verify_mode = CourseMode.mode_for_course(course_id, "verified")
|
||||
if course_id in request.session.get("donation_for_course", {}):
|
||||
chosen_price = request.session["donation_for_course"][course_id]
|
||||
else:
|
||||
chosen_price = verify_mode.min_price
|
||||
context = {
|
||||
"progress_state": progress_state,
|
||||
"user_full_name": request.user.profile.name,
|
||||
"course_id": course_id,
|
||||
"course_name": course_from_id(course_id).display_name,
|
||||
"purchase_endpoint": get_purchase_endpoint(),
|
||||
"suggested_prices": [
|
||||
decimal.Decimal(price)
|
||||
for price in verify_mode.suggested_prices.split(",")
|
||||
],
|
||||
"currency": verify_mode.currency.upper(),
|
||||
"chosen_price": chosen_price,
|
||||
"min_price": verify_mode.min_price,
|
||||
}
|
||||
|
||||
return render_to_response('verify_student/photo_verification.html', context)
|
||||
|
||||
|
||||
class VerifiedView(View):
|
||||
"""
|
||||
View that gets shown once the user has already gone through the
|
||||
verification flow
|
||||
"""
|
||||
@method_decorator(login_required)
|
||||
def get(self, request, course_id):
|
||||
"""
|
||||
Handle the case where we have a get request
|
||||
"""
|
||||
if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified':
|
||||
return redirect(reverse('dashboard'))
|
||||
verify_mode = CourseMode.mode_for_course(course_id, "verified")
|
||||
if course_id in request.session.get("donation_for_course", {}):
|
||||
chosen_price = request.session["donation_for_course"][course_id]
|
||||
else:
|
||||
chosen_price = verify_mode.min_price.format("{:g}")
|
||||
|
||||
context = {
|
||||
"course_id": course_id,
|
||||
"course_name": course_from_id(course_id).display_name,
|
||||
"purchase_endpoint": get_purchase_endpoint(),
|
||||
"currency": verify_mode.currency.upper(),
|
||||
"chosen_price": chosen_price,
|
||||
}
|
||||
return render_to_response('verify_student/verified.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def create_order(request):
|
||||
"""
|
||||
Submit PhotoVerification and create a new Order for this verified cert
|
||||
"""
|
||||
if not SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user):
|
||||
attempt = SoftwareSecurePhotoVerification(user=request.user)
|
||||
attempt.status = "ready"
|
||||
attempt.save()
|
||||
|
||||
course_id = request.POST['course_id']
|
||||
donation_for_course = request.session.get('donation_for_course', {})
|
||||
current_donation = donation_for_course.get(course_id, decimal.Decimal(0))
|
||||
contribution = request.POST.get("contribution", donation_for_course.get(course_id, 0))
|
||||
try:
|
||||
amount = decimal.Decimal(contribution).quantize(decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN)
|
||||
except decimal.InvalidOperation:
|
||||
return HttpResponseBadRequest(_("Selected price is not valid number."))
|
||||
|
||||
if amount != current_donation:
|
||||
donation_for_course[course_id] = amount
|
||||
request.session['donation_for_course'] = donation_for_course
|
||||
|
||||
verified_mode = CourseMode.modes_for_course_dict(course_id).get('verified', None)
|
||||
|
||||
# make sure this course has a verified mode
|
||||
if not verified_mode:
|
||||
return HttpResponseBadRequest(_("This course doesn't support verified certificates"))
|
||||
|
||||
if amount < verified_mode.min_price:
|
||||
return HttpResponseBadRequest(_("No selected price or selected price is below minimum."))
|
||||
|
||||
# I know, we should check this is valid. All kinds of stuff missing here
|
||||
cart = Order.get_cart_for_user(request.user)
|
||||
cart.clear()
|
||||
CertificateItem.add_to_order(cart, course_id, amount, 'verified')
|
||||
|
||||
params = get_signed_purchase_params(cart)
|
||||
|
||||
return HttpResponse(json.dumps(params), content_type="text/json")
|
||||
|
||||
|
||||
@login_required
|
||||
def show_requirements(request, course_id):
|
||||
"""
|
||||
Show the requirements necessary for
|
||||
"""
|
||||
if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified':
|
||||
return redirect(reverse('dashboard'))
|
||||
context = {
|
||||
"course_id": course_id,
|
||||
"is_not_active": not request.user.is_active,
|
||||
"course_name": course_from_id(course_id).display_name,
|
||||
}
|
||||
return render_to_response("verify_student/show_requirements.html", context)
|
||||
|
||||
|
||||
def show_verification_page(request):
|
||||
pass
|
||||
|
||||
|
||||
def enroll(user, course_id, mode_slug):
|
||||
"""
|
||||
Enroll the user in a course for a certain mode.
|
||||
|
||||
This is the view you send folks to when they click on the enroll button.
|
||||
This does NOT cover changing enrollment modes -- it's intended for new
|
||||
enrollments only, and will just redirect to the dashboard if it detects
|
||||
that an enrollment already exists.
|
||||
"""
|
||||
# If the user is already enrolled, jump to the dashboard. Yeah, we could
|
||||
# do upgrades here, but this method is complicated enough.
|
||||
if CourseEnrollment.is_enrolled(user, course_id):
|
||||
return HttpResponseRedirect(reverse('dashboard'))
|
||||
|
||||
available_modes = CourseModes.modes_for_course(course_id)
|
||||
|
||||
# If they haven't chosen a mode...
|
||||
if not mode_slug:
|
||||
# Does this course support multiple modes of Enrollment? If so, redirect
|
||||
# to a page that lets them choose which mode they want.
|
||||
if len(available_modes) > 1:
|
||||
return HttpResponseRedirect(
|
||||
reverse('choose_enroll_mode', kwargs={'course_id': course_id})
|
||||
)
|
||||
# Otherwise, we use the only mode that's supported...
|
||||
else:
|
||||
mode_slug = available_modes[0].slug
|
||||
|
||||
# If the mode is one of the simple, non-payment ones, do the enrollment and
|
||||
# send them to their dashboard.
|
||||
if mode_slug in ("honor", "audit"):
|
||||
CourseEnrollment.enroll(user, course_id, mode=mode_slug)
|
||||
return HttpResponseRedirect(reverse('dashboard'))
|
||||
|
||||
if mode_slug == "verify":
|
||||
if SoftwareSecurePhotoVerification.has_submitted_recent_request(user):
|
||||
# Capture payment info
|
||||
# Create an order
|
||||
# Create a VerifiedCertificate order item
|
||||
return HttpResponse.Redirect(reverse('verified'))
|
||||
|
||||
|
||||
# There's always at least one mode available (default is "honor"). If they
|
||||
# haven't specified a mode, we just assume it's
|
||||
if not mode:
|
||||
mode = available_modes[0]
|
||||
|
||||
elif len(available_modes) == 1:
|
||||
if mode != available_modes[0]:
|
||||
raise Exception()
|
||||
|
||||
mode = available_modes[0]
|
||||
|
||||
if mode == "honor":
|
||||
CourseEnrollment.enroll(user, course_id)
|
||||
return HttpResponseRedirect(reverse('dashboard'))
|
||||
|
||||
@@ -19,6 +19,7 @@ import logging
|
||||
logging.disable(logging.ERROR)
|
||||
import os
|
||||
from random import choice, randint
|
||||
import string
|
||||
|
||||
|
||||
def seed():
|
||||
@@ -94,6 +95,23 @@ MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
|
||||
# Use the auto_auth workflow for creating users and logging them in
|
||||
MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
|
||||
|
||||
# Enable fake payment processing page
|
||||
MITX_FEATURES['ENABLE_PAYMENT_FAKE'] = True
|
||||
|
||||
# Configure the payment processor to use the fake processing page
|
||||
# Since both the fake payment page and the shoppingcart app are using
|
||||
# the same settings, we can generate this randomly and guarantee
|
||||
# that they are using the same secret.
|
||||
RANDOM_SHARED_SECRET = ''.join(
|
||||
choice(string.letters + string.digits + string.punctuation)
|
||||
for x in range(250)
|
||||
)
|
||||
|
||||
CC_PROCESSOR['CyberSource']['SHARED_SECRET'] = RANDOM_SHARED_SECRET
|
||||
CC_PROCESSOR['CyberSource']['MERCHANT_ID'] = "edx"
|
||||
CC_PROCESSOR['CyberSource']['SERIAL_NUMBER'] = "0123456789012345678901"
|
||||
CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = "/shoppingcart/payment_fake"
|
||||
|
||||
# HACK
|
||||
# Setting this flag to false causes imports to not load correctly in the lettuce python files
|
||||
# We do not yet understand why this occurs. Setting this to true is a stopgap measure
|
||||
@@ -107,3 +125,10 @@ INSTALLED_APPS += ('lettuce.django',)
|
||||
LETTUCE_APPS = ('courseware',)
|
||||
LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535)
|
||||
LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome')
|
||||
|
||||
#####################################################################
|
||||
# Lastly, see if the developer has any local overrides.
|
||||
try:
|
||||
from .private import * # pylint: disable=F0401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -161,11 +161,14 @@ MITX_FEATURES = {
|
||||
# basis in Studio)
|
||||
'ENABLE_CHAT': False,
|
||||
|
||||
# Allow users to enroll with methods other than just honor code certificates
|
||||
'MULTIPLE_ENROLLMENT_ROLES' : False,
|
||||
|
||||
# Toggle the availability of the shopping cart page
|
||||
'ENABLE_SHOPPING_CART': False,
|
||||
|
||||
# Toggle storing detailed billing information
|
||||
'STORE_BILLING_INFO': False
|
||||
'STORE_BILLING_INFO': False,
|
||||
}
|
||||
|
||||
# Used for A/B testing
|
||||
@@ -822,7 +825,10 @@ INSTALLED_APPS = (
|
||||
'notification_prefs',
|
||||
|
||||
# Different Course Modes
|
||||
'course_modes'
|
||||
'course_modes',
|
||||
|
||||
# Student Identity Verification
|
||||
'verify_student',
|
||||
)
|
||||
|
||||
######################### MARKETING SITE ###############################
|
||||
@@ -837,6 +843,9 @@ MKTG_URL_LINK_MAP = {
|
||||
'TOS': 'tos',
|
||||
'HONOR': 'honor',
|
||||
'PRIVACY': 'privacy_edx',
|
||||
|
||||
# Verified Certificates
|
||||
'WHAT_IS_VERIFIED_CERT' : 'verified-certificate',
|
||||
}
|
||||
|
||||
|
||||
@@ -867,6 +876,11 @@ def enable_theme(theme_name):
|
||||
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?
|
||||
}
|
||||
|
||||
######################## CAS authentication ###########################
|
||||
|
||||
if MITX_FEATURES.get('AUTH_USE_CAS'):
|
||||
|
||||
@@ -28,9 +28,9 @@ MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True
|
||||
MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard)
|
||||
MITX_FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True
|
||||
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
|
||||
MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True
|
||||
MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True
|
||||
MITX_FEATURES['ENABLE_INSTRUCTOR_BETA_DASHBOARD'] = True
|
||||
MITX_FEATURES['MULTIPLE_ENROLLMENT_ROLES'] = True
|
||||
MITX_FEATURES['ENABLE_SHOPPING_CART'] = True
|
||||
|
||||
FEEDBACK_SUBMISSION_EMAIL = "dummy@example.com"
|
||||
|
||||
@@ -155,6 +155,26 @@ OPENID_UPDATE_DETAILS_FROM_SREG = True
|
||||
OPENID_USE_AS_ADMIN_LOGIN = False
|
||||
OPENID_PROVIDER_TRUSTED_ROOTS = ['*']
|
||||
|
||||
###################### Payment ##############################3
|
||||
# Enable fake payment processing page
|
||||
MITX_FEATURES['ENABLE_PAYMENT_FAKE'] = True
|
||||
|
||||
# Configure the payment processor to use the fake processing page
|
||||
# Since both the fake payment page and the shoppingcart app are using
|
||||
# the same settings, we can generate this randomly and guarantee
|
||||
# that they are using the same secret.
|
||||
from random import choice
|
||||
import string
|
||||
RANDOM_SHARED_SECRET = ''.join(
|
||||
choice(string.letters + string.digits + string.punctuation)
|
||||
for x in range(250)
|
||||
)
|
||||
|
||||
CC_PROCESSOR['CyberSource']['SHARED_SECRET'] = RANDOM_SHARED_SECRET
|
||||
CC_PROCESSOR['CyberSource']['MERCHANT_ID'] = "edx"
|
||||
CC_PROCESSOR['CyberSource']['SERIAL_NUMBER'] = "0123456789012345678901"
|
||||
CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = "/shoppingcart/payment_fake"
|
||||
|
||||
################################# CELERY ######################################
|
||||
|
||||
CELERY_ALWAYS_EAGER = True
|
||||
|
||||
BIN
lms/static/images/icon-id.png
Normal file
BIN
lms/static/images/icon-id.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
lms/static/images/vcert-ribbon-s.png
Normal file
BIN
lms/static/images/vcert-ribbon-s.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
BIN
lms/static/images/vcert-steps.png
Normal file
BIN
lms/static/images/vcert-steps.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
4
lms/static/js/vendor/responsive-carousel/responsive-carousel.js
vendored
Normal file
4
lms/static/js/vendor/responsive-carousel/responsive-carousel.js
vendored
Normal file
File diff suppressed because one or more lines are too long
38
lms/static/js/vendor/responsive-carousel/responsive-carousel.keybd.js
vendored
Normal file
38
lms/static/js/vendor/responsive-carousel/responsive-carousel.keybd.js
vendored
Normal file
@@ -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));
|
||||
103
lms/static/js/verify_student/CameraCapture.as
Normal file
103
lms/static/js/verify_student/CameraCapture.as
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Simple Camera Capture application meant to be used where WebRTC is not supported
|
||||
* (e.g. Safari, Internet Explorer, Opera). All orchestration is assumed to happen
|
||||
* in JavaScript. The only function this application has is to capture a snapshot
|
||||
* and allow a 640x480 PNG of that snapshot to be made available to the JS as a
|
||||
* base64 encoded data URL.
|
||||
*
|
||||
* There are really only three methods:
|
||||
* snap() freezes the video and returns a PNG file as a data URL string. You can
|
||||
* assign this return value to an img's src attribute.
|
||||
* reset() restarts the the video.
|
||||
* imageDataUrl() returns the same thing as snap() --
|
||||
*/
|
||||
|
||||
package
|
||||
{
|
||||
import flash.display.BitmapData;
|
||||
import flash.display.PNGEncoderOptions;
|
||||
import flash.display.Sprite;
|
||||
import flash.events.Event;
|
||||
import flash.external.ExternalInterface;
|
||||
import flash.geom.Rectangle;
|
||||
import flash.media.Camera;
|
||||
import flash.media.Video;
|
||||
import flash.utils.ByteArray;
|
||||
import mx.utils.Base64Encoder;
|
||||
|
||||
[SWF(width="640", height="480")]
|
||||
public class CameraCapture extends Sprite
|
||||
{
|
||||
// We pick these values because that's captured by the WebRTC spec
|
||||
private const VIDEO_WIDTH:int = 640;
|
||||
private const VIDEO_HEIGHT:int = 480;
|
||||
|
||||
private var camera:Camera;
|
||||
private var video:Video;
|
||||
private var b64EncodedImage:String = null;
|
||||
|
||||
public function CameraCapture()
|
||||
{
|
||||
addEventListener(Event.ADDED_TO_STAGE, init);
|
||||
}
|
||||
|
||||
protected function init(e:Event):void {
|
||||
camera = Camera.getCamera();
|
||||
camera.setMode(VIDEO_WIDTH, VIDEO_HEIGHT, 30);
|
||||
|
||||
video = new Video(VIDEO_WIDTH, VIDEO_HEIGHT);
|
||||
video.attachCamera(camera);
|
||||
|
||||
addChild(video);
|
||||
|
||||
ExternalInterface.addCallback("snap", snap);
|
||||
ExternalInterface.addCallback("reset", reset);
|
||||
ExternalInterface.addCallback("imageDataUrl", imageDataUrl);
|
||||
|
||||
// Notify the container that the SWF is ready to be called.
|
||||
ExternalInterface.call("setSWFIsReady");
|
||||
}
|
||||
|
||||
public function snap():String {
|
||||
// If we already have a b64 encoded image, just return that. The user
|
||||
// is calling snap() multiple times in a row without reset()
|
||||
if (b64EncodedImage) {
|
||||
return imageDataUrl();
|
||||
}
|
||||
|
||||
var bitmapData:BitmapData = new BitmapData(video.width, video.height);
|
||||
bitmapData.draw(video); // Draw a snapshot of the video onto our bitmapData
|
||||
video.attachCamera(null); // Stop capturing video
|
||||
|
||||
// Convert to PNG
|
||||
var pngBytes:ByteArray = new ByteArray();
|
||||
bitmapData.encode(
|
||||
new Rectangle(0, 0, video.width, video.height),
|
||||
new PNGEncoderOptions(),
|
||||
pngBytes
|
||||
);
|
||||
|
||||
// Convert to Base64 encoding of PNG
|
||||
var b64Encoder:Base64Encoder = new Base64Encoder();
|
||||
b64Encoder.encodeBytes(pngBytes);
|
||||
b64EncodedImage = b64Encoder.toString();
|
||||
|
||||
return imageDataUrl();
|
||||
}
|
||||
|
||||
public function reset():String {
|
||||
video.attachCamera(camera);
|
||||
b64EncodedImage = null;
|
||||
|
||||
return imageDataUrl();
|
||||
}
|
||||
|
||||
public function imageDataUrl():String {
|
||||
if (b64EncodedImage) {
|
||||
return "data:image/png;base64," + b64EncodedImage;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
lms/static/js/verify_student/CameraCapture.swf
Normal file
BIN
lms/static/js/verify_student/CameraCapture.swf
Normal file
Binary file not shown.
224
lms/static/js/verify_student/photocapture.js
Normal file
224
lms/static/js/verify_student/photocapture.js
Normal file
@@ -0,0 +1,224 @@
|
||||
var onVideoFail = function(e) {
|
||||
console.log('Failed to get camera access!', e);
|
||||
};
|
||||
|
||||
// Returns true if we are capable of video capture (regardless of whether the
|
||||
// user has given permission).
|
||||
function initVideoCapture() {
|
||||
window.URL = window.URL || window.webkitURL;
|
||||
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
|
||||
navigator.mozGetUserMedia || navigator.msGetUserMedia;
|
||||
return !(navigator.getUserMedia == undefined);
|
||||
}
|
||||
|
||||
var submitToPaymentProcessing = function() {
|
||||
var contribution_input = $("input[name='contribution']:checked")
|
||||
var contribution = 0;
|
||||
if(contribution_input.attr('id') == 'contribution-other')
|
||||
{
|
||||
contribution = $("input[name='contribution-other-amt']").val();
|
||||
}
|
||||
else
|
||||
{
|
||||
contribution = contribution_input.val();
|
||||
}
|
||||
var course_id = $("input[name='course_id']").val();
|
||||
var xhr = $.post(
|
||||
"/verify_student/create_order",
|
||||
{
|
||||
"course_id" : course_id,
|
||||
"contribution": contribution
|
||||
},
|
||||
function(data) {
|
||||
for (prop in data) {
|
||||
$('<input>').attr({
|
||||
type: 'hidden',
|
||||
name: prop,
|
||||
value: data[prop]
|
||||
}).appendTo('#pay_form');
|
||||
}
|
||||
}
|
||||
)
|
||||
.done(function(data) {
|
||||
$("#pay_form").submit();
|
||||
})
|
||||
.fail(function(jqXhr,text_status, error_thrown) {
|
||||
alert(jqXhr.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
function doResetButton(resetButton, captureButton, approveButton, nextButton) {
|
||||
approveButton.removeClass('approved');
|
||||
nextButton.addClass('disabled');
|
||||
|
||||
captureButton.show();
|
||||
resetButton.hide();
|
||||
approveButton.hide();
|
||||
}
|
||||
|
||||
function doApproveButton(approveButton, nextButton) {
|
||||
approveButton.addClass('approved');
|
||||
nextButton.removeClass('disabled');
|
||||
}
|
||||
|
||||
function doSnapshotButton(captureButton, resetButton, approveButton) {
|
||||
captureButton.hide();
|
||||
resetButton.show();
|
||||
approveButton.show();
|
||||
}
|
||||
|
||||
|
||||
function submitNameChange(event) {
|
||||
event.preventDefault();
|
||||
var full_name = $('input[name="name"]').val();
|
||||
var xhr = $.post(
|
||||
"/change_name",
|
||||
{
|
||||
"new_name" : full_name,
|
||||
"rationale": "Want to match ID for ID Verified Certificates."
|
||||
},
|
||||
function(data) {
|
||||
$('#full-name').html(full_name);
|
||||
}
|
||||
)
|
||||
.fail(function(jqXhr,text_status, error_thrown) {
|
||||
$('.message-copy').html(jqXhr.responseText);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function initSnapshotHandler(names, hasHtml5CameraSupport) {
|
||||
var name = names.pop();
|
||||
if (name == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
var video = $('#' + name + '_video');
|
||||
var canvas = $('#' + name + '_canvas');
|
||||
var image = $('#' + name + "_image");
|
||||
var captureButton = $("#" + name + "_capture_button");
|
||||
var resetButton = $("#" + name + "_reset_button");
|
||||
var approveButton = $("#" + name + "_approve_button");
|
||||
var nextButton = $("#" + name + "_next_button");
|
||||
var flashCapture = $("#" + name + "_flash");
|
||||
|
||||
var ctx = null;
|
||||
if (hasHtml5CameraSupport) {
|
||||
ctx = canvas[0].getContext('2d');
|
||||
}
|
||||
var localMediaStream = null;
|
||||
|
||||
function snapshot(event) {
|
||||
if (hasHtml5CameraSupport) {
|
||||
if (localMediaStream) {
|
||||
ctx.drawImage(video[0], 0, 0);
|
||||
image[0].src = canvas[0].toDataURL('image/png');
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
video[0].pause();
|
||||
}
|
||||
else {
|
||||
image[0].src = flashCapture[0].snap();
|
||||
}
|
||||
|
||||
doSnapshotButton(captureButton, resetButton, approveButton);
|
||||
return false;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
image[0].src = "";
|
||||
|
||||
if (hasHtml5CameraSupport) {
|
||||
video[0].play();
|
||||
}
|
||||
else {
|
||||
flashCapture[0].reset();
|
||||
}
|
||||
|
||||
doResetButton(resetButton, captureButton, approveButton, nextButton);
|
||||
return false;
|
||||
}
|
||||
|
||||
function approve() {
|
||||
doApproveButton(approveButton, nextButton)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize state for this picture taker
|
||||
captureButton.show();
|
||||
resetButton.hide();
|
||||
approveButton.hide();
|
||||
nextButton.addClass('disabled');
|
||||
|
||||
// Connect event handlers...
|
||||
video.click(snapshot);
|
||||
captureButton.click(snapshot);
|
||||
resetButton.click(reset);
|
||||
approveButton.click(approve);
|
||||
|
||||
// If it's flash-based, we can just immediate initialize the next one.
|
||||
// If it's HTML5 based, we have to do it in the callback from getUserMedia
|
||||
// so that Firefox doesn't eat the second request.
|
||||
if (hasHtml5CameraSupport) {
|
||||
navigator.getUserMedia({video: true}, function(stream) {
|
||||
video[0].src = window.URL.createObjectURL(stream);
|
||||
localMediaStream = stream;
|
||||
|
||||
// We do this in a recursive call on success because Firefox seems to
|
||||
// simply eat the request if you stack up two on top of each other before
|
||||
// the user has a chance to approve the first one.
|
||||
initSnapshotHandler(names, hasHtml5CameraSupport);
|
||||
}, onVideoFail);
|
||||
}
|
||||
else {
|
||||
initSnapshotHandler(names, hasHtml5CameraSupport);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function objectTagForFlashCamera(name) {
|
||||
return '<object type="application/x-shockwave-flash" id="' +
|
||||
name + '" name="' + name + '" data=' +
|
||||
'"/static/js/verify_student/CameraCapture.swf"' +
|
||||
'width="500" height="375"><param name="quality" ' +
|
||||
'value="high"><param name="allowscriptaccess" ' +
|
||||
'value="sameDomain"></object>';
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$(".carousel-nav").addClass('sr');
|
||||
$("#pay_button").click(submitToPaymentProcessing);
|
||||
// prevent browsers from keeping this button checked
|
||||
$("#confirm_pics_good").prop("checked", false)
|
||||
$("#confirm_pics_good").change(function() {
|
||||
$("#pay_button").toggleClass('disabled');
|
||||
});
|
||||
|
||||
|
||||
// add in handlers to add/remove the correct classes to the body
|
||||
// when moving between steps
|
||||
$('#face_next_button').click(function(){
|
||||
$('body').addClass('step-photos-id').removeClass('step-photos-cam')
|
||||
})
|
||||
|
||||
$('#photo_id_next_button').click(function(){
|
||||
$('body').addClass('step-review').removeClass('step-photos-id')
|
||||
})
|
||||
|
||||
// set up edit information dialog
|
||||
$('#edit-name div[role="alert"]').hide();
|
||||
$('#edit-name .action-save').click(submitNameChange);
|
||||
|
||||
var hasHtml5CameraSupport = initVideoCapture();
|
||||
|
||||
// If HTML5 WebRTC capture is not supported, we initialize jpegcam
|
||||
if (!hasHtml5CameraSupport) {
|
||||
$("#face_capture_div").html(objectTagForFlashCamera("face_flash"));
|
||||
$("#photo_id_capture_div").html(objectTagForFlashCamera("photo_id_flash"));
|
||||
}
|
||||
|
||||
initSnapshotHandler(["photo_id", "face"], hasHtml5CameraSupport);
|
||||
|
||||
});
|
||||
@@ -130,7 +130,13 @@
|
||||
// ====================
|
||||
|
||||
// edx.org marketing site - registration iframe band-aid (poor form enough to isolate out)
|
||||
.view-iframe, .view-iframe-content {
|
||||
background: transparent !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.view-partial-mktgregister {
|
||||
background: transparent !important;
|
||||
|
||||
// dimensions needed for course about page on marketing site
|
||||
.wrapper-view {
|
||||
@@ -169,6 +175,10 @@
|
||||
&:hover .track {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
&.has-option-verified {
|
||||
padding-top: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// already registered but course not started or registration is closed
|
||||
|
||||
@@ -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,19 @@
|
||||
// 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';
|
||||
@import 'elements/controls';
|
||||
|
||||
// base - specific views
|
||||
@import 'views/verification';
|
||||
|
||||
// shared - course
|
||||
@import 'shared/forms';
|
||||
@import 'shared/footer';
|
||||
@import 'shared/header';
|
||||
@@ -67,7 +75,7 @@
|
||||
@import 'multicourse/help';
|
||||
@import 'multicourse/edge';
|
||||
|
||||
// applications
|
||||
// applications
|
||||
@import 'discussion';
|
||||
@import 'news';
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
// lms - utilities - mixins and extends
|
||||
// ====================
|
||||
|
||||
// mixins - font sizing
|
||||
@mixin font-size($sizeValue: 16){
|
||||
font-size: $sizeValue + px;
|
||||
font-size: ($sizeValue/10) + rem;
|
||||
// font-size: ($sizeValue/10) + rem;
|
||||
}
|
||||
|
||||
// mixins - line height
|
||||
@mixin line-height($fontSize: auto){
|
||||
line-height: ($fontSize*1.48) + px;
|
||||
line-height: (($fontSize/10)*1.48) + rem;
|
||||
// line-height: (($fontSize/10)*1.48) + rem;
|
||||
}
|
||||
|
||||
// image-replacement hidden text
|
||||
@@ -38,21 +41,38 @@
|
||||
@return #{$pxval / $base}em;
|
||||
}
|
||||
|
||||
// Line-height
|
||||
// line-height
|
||||
@function lh($amount: 1) {
|
||||
@return $body-line-height * $amount;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
//-----------------
|
||||
// Theme Mixin Styles
|
||||
//-----------------
|
||||
// theme mixin styles
|
||||
@mixin login_register_h1_style {}
|
||||
|
||||
@mixin footer_references_style {}
|
||||
|
||||
// ====================
|
||||
|
||||
// extends - UI - visual link
|
||||
.ui-fake-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// extends - UI - functional disable
|
||||
.ui-disabled {
|
||||
pointer-events: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// extends - UI - depth levels
|
||||
.ui-depth0 { z-index: 0; }
|
||||
.ui-depth1 { z-index: 10; }
|
||||
.ui-depth2 { z-index: 100; }
|
||||
.ui-depth3 { z-index: 1000; }
|
||||
.ui-depth4 { z-index: 10000; }
|
||||
.ui-depth5 { z-index: 100000; }
|
||||
|
||||
// extends -hidden elems - screenreaders
|
||||
.text-sr {
|
||||
border: 0;
|
||||
@@ -64,3 +84,39 @@
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
// extends - UI - removes list styling/spacing when using uls, ols for navigation and less content-centric cases
|
||||
.ui-no-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-indent: 0;
|
||||
|
||||
li, dt, dd {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// extends - text - image-replacement hidden text
|
||||
.text-hide {
|
||||
text-indent: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// extends - text - wrapping
|
||||
.text-wrap {
|
||||
text-wrap: wrap;
|
||||
white-space: pre-wrap;
|
||||
white-space: -moz-pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
// extends - text - text overflow by ellipsis
|
||||
.text-truncated {
|
||||
@include box-sizing(border-box);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
// base
|
||||
$baseline: 20px;
|
||||
|
||||
// ====================
|
||||
|
||||
// LAYOUT: grid
|
||||
$gw-column: 80px;
|
||||
$gw-gutter: 20px;
|
||||
|
||||
$fg-column: $gw-column;
|
||||
$fg-gutter: $gw-gutter;
|
||||
$fg-max-columns: 12;
|
||||
$fg-max-width: 1400px;
|
||||
$fg-min-width: 810px;
|
||||
|
||||
$sans-serif: 'Open Sans', $verdana;
|
||||
// ====================
|
||||
|
||||
// FONTS
|
||||
$sans-serif: 'Open Sans', $verdana, sans-serif;
|
||||
$monospace: Monaco, 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
|
||||
$body-font-family: $sans-serif;
|
||||
$serif: $georgia;
|
||||
|
||||
// ====================
|
||||
|
||||
// MISC: base fonts/colors
|
||||
$body-font-size: em(14);
|
||||
$body-line-height: golden-ratio(.875em, 1);
|
||||
$base-font-color: rgb(60,60,60);
|
||||
@@ -22,8 +31,21 @@ $base-font-color: rgb(60,60,60);
|
||||
$lighter-base-font-color: rgb(100,100,100);
|
||||
$very-light-text: #fff;
|
||||
|
||||
// ====================
|
||||
|
||||
// COLORS: misc.
|
||||
$white: rgb(255,255,255);
|
||||
$white-t0: rgba($white, 0.125);
|
||||
$white-t1: rgba($white, 0.25);
|
||||
$white-t2: rgba($white, 0.5);
|
||||
$white-t3: rgba($white, 0.75);
|
||||
|
||||
$black: rgb(0,0,0);
|
||||
$black-t0: rgba($black, 0.125);
|
||||
$black-t1: rgba($black, 0.25);
|
||||
$black-t2: rgba($black, 0.5);
|
||||
$black-t3: rgba($black, 0.75);
|
||||
|
||||
$blue: rgb(29,157,217);
|
||||
$pink: rgb(182,37,104);
|
||||
$yellow: rgb(255, 252, 221);
|
||||
@@ -35,9 +57,10 @@ $dark-gray: rgb(51, 51, 51);
|
||||
$border-color: rgb(200, 200, 200);
|
||||
$sidebar-color: rgb(246, 246, 246);
|
||||
$outer-border-color: rgb(170, 170, 170);
|
||||
|
||||
$green: rgb(37, 184, 90);
|
||||
|
||||
// old variables
|
||||
// COLORS: old variables
|
||||
$light-gray: #ddd;
|
||||
$dark-gray: #333;
|
||||
|
||||
@@ -60,24 +83,62 @@ $m-gray-d1: #7D7F83;
|
||||
$m-gray-d2: #707276;
|
||||
$m-gray-d3: #646668;
|
||||
$m-gray-d4: #050505;
|
||||
$m-gray-t0: rgba($m-gray,0.125);
|
||||
$m-gray-t1: rgba($m-gray,0.25);
|
||||
$m-gray-t2: rgba($m-gray,0.50);
|
||||
$m-gray-t3: rgba($m-gray,0.75);
|
||||
|
||||
$m-blue: #1AA1DE;
|
||||
$m-blue-l1: #2BACE6;
|
||||
$m-blue-l2: #42B5E9;
|
||||
$m-blue-l3: #59BEEC;
|
||||
$m-blue-l4: tint($m-blue,90%);
|
||||
$m-blue-l5: tint($m-blue,95%);
|
||||
$m-blue-d1: #1790C7;
|
||||
$m-blue-d2: #1580B0;
|
||||
$m-blue-d3: #126F9A;
|
||||
$m-blue-d4: #0A4A67;
|
||||
$m-blue-t0: rgba($m-blue,0.125);
|
||||
$m-blue-t1: rgba($m-blue,0.25);
|
||||
$m-blue-t2: rgba($m-blue,0.50);
|
||||
$m-blue-t3: rgba($m-blue,0.75);
|
||||
|
||||
$m-pink: #B52A67;
|
||||
$m-pink-l1: #CA2F73;
|
||||
$m-pink-l2: #D33F80;
|
||||
$m-pink-l3: #D7548E;
|
||||
$m-pink-l4: tint($m-pink,75%);
|
||||
$m-pink-l5: tint($m-pink,85%);
|
||||
$m-pink-d1: #A0255B;
|
||||
$m-pink-d2: #8C204F;
|
||||
$m-pink-d3: #771C44;
|
||||
|
||||
$m-green: rgb(0, 136, 1);
|
||||
$m-green-s1: rgb(96, 188, 97);
|
||||
$m-green-l1: tint($m-green,20%);
|
||||
$m-green-l2: tint($m-green,40%);
|
||||
$m-green-l3: tint($m-green,60%);
|
||||
$m-green-l4: tint($m-green,90%);
|
||||
$m-green-l5: tint($m-green,95%);
|
||||
$m-green-d1: shade($m-green,20%);
|
||||
$m-green-d2: shade($m-green,40%);
|
||||
$m-green-d3: shade($m-green,60%);
|
||||
$m-green-d4: shade($m-green,90%);
|
||||
$m-green-t0: rgba($m-green,0.125);
|
||||
$m-green-t1: rgba($m-green,0.25);
|
||||
$m-green-t2: rgba($m-green,0.50);
|
||||
$m-green-t3: rgba($m-green,0.75);
|
||||
|
||||
// ====================
|
||||
|
||||
// shadows
|
||||
$shadow: rgba(0,0,0,0.2);
|
||||
$shadow-l1: rgba(0,0,0,0.1);
|
||||
$shadow-l2: rgba(0,0,0,0.05);
|
||||
$shadow-d1: rgba(0,0,0,0.4);
|
||||
|
||||
// ====================
|
||||
|
||||
$m-base-font-size: em(15);
|
||||
|
||||
$base-font-color: rgb(60,60,60);
|
||||
@@ -98,57 +159,65 @@ $courseware-footer-border: none;
|
||||
$courseware-footer-shadow: none;
|
||||
$courseware-footer-margin: 0px;
|
||||
|
||||
// ====================
|
||||
|
||||
// actions
|
||||
// STATE: verified
|
||||
$verified-color-lvl1: $m-green;
|
||||
$verified-color-lvl2: $m-green-l1;
|
||||
$verified-color-lvl3: $m-green-l2;
|
||||
$verified-color-lvl4: $m-green-l3;
|
||||
$verified-color-lvl5: $m-green-l4;
|
||||
|
||||
// ====================
|
||||
|
||||
// ACTIONS: general
|
||||
$button-bg-image: linear-gradient(#fff 0%, rgb(250,250,250) 50%, rgb(237,237,237) 50%, rgb(220,220,220) 100%);
|
||||
$button-bg-color: transparent;
|
||||
$button-bg-hover-color: #fff;
|
||||
|
||||
// actions - primary
|
||||
// ACTIONS: primary
|
||||
$action-primary-bg: $m-blue-d3;
|
||||
$action-primary-fg: $white;
|
||||
$action-primary-shadow: $m-blue-d4;
|
||||
|
||||
// focused - hover/active pseudo states
|
||||
// ACTIONS: primary - focused - hover/active pseudo states
|
||||
$action-primary-focused-bg: $m-blue-d1;
|
||||
$action-primary-focused-fg: $white;
|
||||
|
||||
// current or active navigation item
|
||||
// ACTIONS: primary - current or active navigation item
|
||||
$action-primary-active-bg: $m-blue;
|
||||
$action-primary-active-fg: $m-blue-d3;
|
||||
$action-primary-active-shadow: $m-blue-d2;
|
||||
$action-primary-active-focused-fg: $m-blue-d4;
|
||||
$action-primary-active-focused-shadow: $m-blue-d3;
|
||||
|
||||
// disabled
|
||||
// ACTIONS: disabled
|
||||
$action-primary-disabled-bg: $m-gray-d3;
|
||||
$action-prmary-disabled-fg: $white;
|
||||
|
||||
|
||||
|
||||
// actions - secondary
|
||||
// ACTIONS: secondary
|
||||
$action-secondary-bg: $m-pink;
|
||||
$action-secondary-fg: $white;
|
||||
$action-secondary-shadow: $m-pink-d2;
|
||||
|
||||
// focused - hover/active pseudo states
|
||||
// ACTIONS: secondary - focused - hover/active pseudo states
|
||||
$action-secondary-focused-bg: $m-pink-l3;
|
||||
$action-secondary-focused-fg: $white;
|
||||
|
||||
// current or active navigation item
|
||||
// ACTIONS: secondary - current or active navigation item
|
||||
$action-secondary-active-bg: $m-pink-l2;
|
||||
$action-secondary-active-fg: $m-pink-d1;
|
||||
$action-secondary-active-shadow: $m-pink-d1;
|
||||
$action-secondary-active-focused-fg: $m-pink-d3;
|
||||
$action-secondary-active-focused-shadow: $m-pink-d2;
|
||||
|
||||
// disabled
|
||||
// ACTIONS: secondary - disabled
|
||||
$action-secondary-disabled-bg: $m-gray-d3;
|
||||
$action-secondary-disabled-fg: $white;
|
||||
|
||||
// ====================
|
||||
|
||||
|
||||
|
||||
// MISC: visual horizontal rules
|
||||
$faded-hr-image-1: linear-gradient(180deg, rgba(200,200,200, 0) 0%, rgba(200,200,200, 1) 50%, rgba(200,200,200, 0));
|
||||
$faded-hr-image-2: linear-gradient(180deg, rgba(200,200,200, 0) 0%, rgba(200,200,200, 1));
|
||||
$faded-hr-image-3: linear-gradient(180deg, rgba(200,200,200, 1) 0%, rgba(200,200,200, 0));
|
||||
@@ -156,54 +225,70 @@ $faded-hr-image-4: linear-gradient(180deg, rgba(240,240,240, 0) 0%, rgba(240,240
|
||||
$faded-hr-image-5: linear-gradient(180deg, rgba(255,255,255, 0) 0%, rgba(255,255,255, 0.8) 50%, rgba(255,255,255, 0));
|
||||
$faded-hr-image-6: linear-gradient(90deg, rgba(255,255,255, 0) 0%, rgba(255,255,255, 0.6) 50%, rgba(255,255,255, 0));
|
||||
|
||||
// MISC: dashboard
|
||||
$dashboard-profile-header-image: linear-gradient(-90deg, rgb(255,255,255), rgb(245,245,245));
|
||||
$dashboard-profile-header-color: transparent;
|
||||
$dashboard-profile-color: rgb(252,252,252);
|
||||
$dot-color: $light-gray;
|
||||
|
||||
// MISC: course assets
|
||||
$content-wrapper-bg: $white;
|
||||
$course-bg-color: #d6d6d6;
|
||||
$course-bg-image: url(../images/bg-texture.png);
|
||||
$account-content-wrapper-bg: shade($body-bg, 2%);
|
||||
|
||||
$course-profile-bg: rgb(245,245,245);
|
||||
$course-header-bg: rgba(255,255,255, 0.93);
|
||||
|
||||
// MISC: borders
|
||||
$border-color-1: rgb(190,190,190);
|
||||
$border-color-2: rgb(200,200,200);
|
||||
$border-color-3: rgb(100,100,100);
|
||||
$border-color-4: rgb(252,252,252);
|
||||
$border-color-l1: $m-gray-l1;
|
||||
$border-color-l2: $m-gray-l2;
|
||||
$border-color-l3: $m-gray-l3;
|
||||
$border-color-l4: $m-gray-l4;
|
||||
|
||||
// MISC: links and buttons
|
||||
$link-color: $blue;
|
||||
$link-color-d1: $m-blue-d2;
|
||||
$link-hover: $pink;
|
||||
$site-status-color: $pink;
|
||||
|
||||
$button-color: $blue;
|
||||
$button-archive-color: #eee;
|
||||
|
||||
// MISC: shadow, form, modal
|
||||
$shadow-color: $blue;
|
||||
$form-bg-color: #fff;
|
||||
$modal-bg-color: rgb(245,245,245);
|
||||
|
||||
// MISC: sidebar
|
||||
$sidebar-chapter-bg-top: rgba(255, 255, 255, .6);
|
||||
$sidebar-chapter-bg-bottom: rgba(255, 255, 255, 0);
|
||||
$sidebar-chapter-bg: #eee;
|
||||
$sidebar-active-image: linear-gradient(top, #e6e6e6, #d6d6d6);
|
||||
|
||||
$form-bg-color: #fff;
|
||||
$modal-bg-color: rgb(245,245,245);
|
||||
|
||||
//TOP HEADER IMAGE MARGIN
|
||||
// TOP HEADER IMAGE MARGIN
|
||||
$header_image_margin: -69px;
|
||||
|
||||
//FOOTER MARGIN
|
||||
$footer_margin: ($baseline/4) 0 ($baseline*1.5) 0;
|
||||
|
||||
//-----------------
|
||||
// CSS BG Images
|
||||
//-----------------
|
||||
// ====================
|
||||
|
||||
// IMAGES: backgrounds
|
||||
$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';
|
||||
|
||||
// ====================
|
||||
|
||||
// SPLINT: new standards
|
||||
|
||||
// SPLINT: 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;
|
||||
|
||||
218
lms/static/sass/elements/_controls.scss
Normal file
218
lms/static/sass/elements/_controls.scss
Normal file
@@ -0,0 +1,218 @@
|
||||
// lms - elements - controls
|
||||
// ====================
|
||||
|
||||
.btn {
|
||||
@include box-sizing(border-box);
|
||||
@include transition(color 0.25s ease-in-out, background 0.25s ease-in-out, box-shadow 0.25s ease-in-out);
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover, &:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.disabled, &[disabled] {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-pill {
|
||||
border-radius: $baseline/5;
|
||||
}
|
||||
|
||||
.btn-rounded {
|
||||
border-radius: ($baseline/2);
|
||||
}
|
||||
|
||||
.btn-edged {
|
||||
border-radius: ($baseline/10);
|
||||
}
|
||||
|
||||
// primary button
|
||||
.btn-primary {
|
||||
@extend .t-weight3;
|
||||
@extend .btn;
|
||||
@extend .btn-edged;
|
||||
border: none;
|
||||
padding: ($baseline*0.75) $baseline;
|
||||
text-align: center;
|
||||
|
||||
&:hover, &:active {
|
||||
|
||||
}
|
||||
|
||||
&.current, &.active {
|
||||
|
||||
&:hover, &:active {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled, &.is-disabled, &[disabled] {
|
||||
background: $m-gray-l2;
|
||||
color: $white-t3;
|
||||
}
|
||||
}
|
||||
|
||||
// blue primary gray
|
||||
.btn-primary-gray {
|
||||
@extend .btn-primary;
|
||||
box-shadow: 0 2px 1px 0 $m-gray-d2;
|
||||
background: $m-gray;
|
||||
color: $white;
|
||||
|
||||
&:hover, &:active {
|
||||
background: $m-gray-l1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.current, &.active {
|
||||
box-shadow: inset 0 2px 1px 1px $m-gray-d2;
|
||||
background: $m-gray;
|
||||
color: $m-gray-l1;
|
||||
|
||||
&:hover, &:active {
|
||||
box-shadow: inset 0 2px 1px 1px $m-gray-d3;
|
||||
color: $m-gray-d3;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled, &[disabled] {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// blue primary button
|
||||
.btn-primary-blue {
|
||||
@extend .btn-primary;
|
||||
box-shadow: 0 2px 1px 0 $m-blue-d4;
|
||||
background: $m-blue-d3;
|
||||
color: $white;
|
||||
|
||||
&:hover, &:active {
|
||||
background: $m-blue-d1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.current, &.active {
|
||||
box-shadow: inset 0 2px 1px 1px $m-blue-d2;
|
||||
background: $m-blue;
|
||||
color: $m-blue-d2;
|
||||
|
||||
&:hover, &:active {
|
||||
box-shadow: inset 0 2px 1px 1px $m-blue-d3;
|
||||
color: $m-blue-d3;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled, &[disabled] {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// pink primary button
|
||||
.btn-primary-pink {
|
||||
@extend .btn-primary;
|
||||
box-shadow: 0 2px 1px 0 $m-pink-d2;
|
||||
background: $m-pink;
|
||||
color: $white;
|
||||
|
||||
&:hover, &:active {
|
||||
background: $m-pink-l3;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.current, &.active {
|
||||
box-shadow: inset 0 2px 1px 1px $m-pink-d1;
|
||||
background: $m-pink-l2;
|
||||
color: $m-pink-d1;
|
||||
|
||||
&:hover, &:active {
|
||||
box-shadow: inset 0 2px 1px 1px $m-pink-d2;
|
||||
color: $m-pink-d3;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled, &[disabled] {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// green primary button
|
||||
.btn-primary-green {
|
||||
@extend .btn-primary;
|
||||
box-shadow: 0 2px 1px 0 $m-green-d2;
|
||||
background: $m-green-d1;
|
||||
color: $white;
|
||||
|
||||
&:hover, &:active {
|
||||
background: $m-green-s1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.current, &.active {
|
||||
box-shadow: inset 0 2px 1px 1px $m-green;
|
||||
background: $m-green-l2;
|
||||
color: $m-green;
|
||||
|
||||
&:hover, &:active {
|
||||
box-shadow: inset 0 2px 1px 1px $m-green-d1;
|
||||
color: $m-green-d1;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled, &[disabled] {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// disabled primary button - used for more manual approaches
|
||||
.btn-primary-disabled {
|
||||
background: $m-gray-l2;
|
||||
color: $white-t3;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// application: canned actions
|
||||
.btn {
|
||||
font-family: $f-sans-serif;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
@extend .t-action1;
|
||||
@extend .t-weight3;
|
||||
display: block;
|
||||
padding:($baseline*0.75) ($baseline*1.5);
|
||||
}
|
||||
|
||||
.btn-avg {
|
||||
@extend .t-action2;
|
||||
@extend .t-weight3;
|
||||
}
|
||||
|
||||
.btn-blue {
|
||||
@extend .btn-primary-blue;
|
||||
margin-bottom: $baseline;
|
||||
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-pink {
|
||||
@extend .btn-primary-pink;
|
||||
margin-bottom: $baseline;
|
||||
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: none;
|
||||
}
|
||||
}
|
||||
196
lms/static/sass/elements/_typography.scss
Normal file
196
lms/static/sass/elements/_typography.scss
Normal file
@@ -0,0 +1,196 @@
|
||||
// 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);
|
||||
}
|
||||
|
||||
.t-icon2 {
|
||||
@include font-size(36);
|
||||
}
|
||||
|
||||
.t-icon3 {
|
||||
@include font-size(24);
|
||||
}
|
||||
|
||||
.t-icon4 {
|
||||
@include font-size(18);
|
||||
}
|
||||
|
||||
.t-icon5 {
|
||||
@include font-size(16);
|
||||
}
|
||||
|
||||
.t-icon6 {
|
||||
@include font-size(14);
|
||||
}
|
||||
|
||||
.t-icon7 {
|
||||
@include font-size(12);
|
||||
}
|
||||
|
||||
.t-icon8 {
|
||||
@include font-size(11);
|
||||
}
|
||||
|
||||
.t-icon9 {
|
||||
@include font-size(10);
|
||||
}
|
||||
|
||||
.t-icon-solo {
|
||||
@include line-height(0);
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// typography weights
|
||||
.t-weight1 {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.t-weight2 {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.t-weight3 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.t-weight4 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.t-weight5 {
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// lms - views - user/student dashboard
|
||||
// ====================
|
||||
|
||||
.dashboard {
|
||||
@include clearfix;
|
||||
padding: 60px 0 0 0;
|
||||
@@ -224,6 +227,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// course listings
|
||||
.my-courses {
|
||||
float: left;
|
||||
margin: 0px;
|
||||
@@ -268,21 +272,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
.my-course {
|
||||
clear: both;
|
||||
@include clearfix;
|
||||
margin-right: flex-gutter();
|
||||
margin-bottom: 50px;
|
||||
padding-bottom: 50px;
|
||||
border-bottom: 1px solid $border-color-1;
|
||||
position: relative;
|
||||
width: flex-grid(12);
|
||||
z-index: 20;
|
||||
@include transition(all 0.15s linear 0s);
|
||||
// UI: course list
|
||||
.listing-courses {
|
||||
@extend .ui-no-list;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: none;
|
||||
.course-item {
|
||||
margin-bottom: ($baseline*2.5);
|
||||
border-bottom: 4px solid $border-color-l4;
|
||||
padding-bottom: ($baseline*2.5);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI: individual course item
|
||||
.course {
|
||||
@include box-sizing(box);
|
||||
@include transition(all 0.15s linear 0s);
|
||||
@include clearfix();
|
||||
@extend .ui-depth2;
|
||||
position: relative;
|
||||
|
||||
.cover {
|
||||
@include box-sizing(border-box);
|
||||
@@ -402,6 +415,51 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: course mode - verified
|
||||
&.verified {
|
||||
@extend .ui-depth2;
|
||||
margin-top: ($baseline*2.5);
|
||||
border-top: 1px solid $verified-color-lvl3;
|
||||
padding-top: ($baseline*1.25);
|
||||
background: $white;
|
||||
|
||||
// FIXME: bad, but needed selector!
|
||||
.info > hgroup .date-block {
|
||||
top: ($baseline*1.25);
|
||||
}
|
||||
|
||||
// course enrollment status message
|
||||
.sts-enrollment {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
right: ($baseline/2);
|
||||
text-align: center;
|
||||
|
||||
.label {
|
||||
@extend .text-sr;
|
||||
}
|
||||
|
||||
.deco-graphic {
|
||||
@extend .ui-depth3;
|
||||
width: 40px;
|
||||
position: absolute;
|
||||
left: -30px;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
.sts-enrollment-value {
|
||||
@extend .ui-depth1;
|
||||
@extend .copy-badge;
|
||||
border-radius: 0;
|
||||
padding: ($baseline/4) ($baseline/2) ($baseline/4) $baseline;
|
||||
color: $white;
|
||||
background: $verified-color-lvl3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.message-status {
|
||||
|
||||
20
lms/static/sass/vendor/responsive-carousel/_responsive-carousel.scss
vendored
Normal file
20
lms/static/sass/vendor/responsive-carousel/_responsive-carousel.scss
vendored
Normal file
@@ -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;
|
||||
}
|
||||
61
lms/static/sass/vendor/responsive-carousel/_responsive-carousel.slide.scss
vendored
Normal file
61
lms/static/sass/vendor/responsive-carousel/_responsive-carousel.slide.scss
vendored
Normal file
@@ -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;
|
||||
}
|
||||
1846
lms/static/sass/views/_verification.scss
Normal file
1846
lms/static/sass/views/_verification.scss
Normal file
@@ -0,0 +1,1846 @@
|
||||
// lms - views - verification flow
|
||||
// ====================
|
||||
|
||||
// MISC: extends - type
|
||||
// application: canned headings
|
||||
.hd-lv1 {
|
||||
@extend .t-title1;
|
||||
@extend .t-weight1;
|
||||
color: $m-gray-d4;
|
||||
margin: 0 0 ($baseline*2) 0;
|
||||
}
|
||||
|
||||
.hd-lv2 {
|
||||
@extend .t-title4;
|
||||
@extend .t-weight1;
|
||||
margin: 0 0 ($baseline*0.75) 0;
|
||||
border-bottom: 1px solid $m-gray-l3;
|
||||
padding-bottom: ($baseline/2);
|
||||
color: $m-gray-d4;
|
||||
}
|
||||
|
||||
.hd-lv3 {
|
||||
@extend .t-title6;
|
||||
@extend .t-weight4;
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
color: $m-gray-d4;
|
||||
}
|
||||
|
||||
.hd-lv4 {
|
||||
@extend .t-title6;
|
||||
@extend .t-weight2;
|
||||
margin: 0 0 $baseline 0;
|
||||
color: $m-gray-d4;
|
||||
}
|
||||
|
||||
.hd-lv5 {
|
||||
@extend .t-title7;
|
||||
@extend .t-weight4;
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
color: $m-gray-d4;
|
||||
}
|
||||
|
||||
// application: canned copy
|
||||
.copy-base {
|
||||
@extend .t-copy-base;
|
||||
color: $m-gray-d2;
|
||||
}
|
||||
|
||||
.copy-lead1 {
|
||||
@extend .t-copy-lead2;
|
||||
color: $m-gray;
|
||||
}
|
||||
|
||||
.copy-detail {
|
||||
@extend .t-copy-sub1;
|
||||
@extend .t-weight3;
|
||||
color: $m-gray-d1;
|
||||
}
|
||||
|
||||
.copy-metadata {
|
||||
@extend .t-copy-sub2;
|
||||
color: $m-gray-d1;
|
||||
|
||||
|
||||
.copy-metadata-value {
|
||||
@extend .t-weight2;
|
||||
}
|
||||
|
||||
.copy-metadata-value {
|
||||
@extend .t-weight4;
|
||||
}
|
||||
}
|
||||
|
||||
// application: canned links
|
||||
.copy-link {
|
||||
border-bottom: 1px dotted transparent;
|
||||
|
||||
&:hover, &:active {
|
||||
border-color: $link-color-d1;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-badge {
|
||||
@extend .t-title8;
|
||||
@extend .t-weight5;
|
||||
border-radius: ($baseline/5);
|
||||
padding: ($baseline/2) $baseline;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
.btn-verify-primary {
|
||||
@extend .btn-primary-green;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// MISC: extends - UI - window
|
||||
.ui-window {
|
||||
@include clearfix();
|
||||
border-radius: ($baseline/10);
|
||||
box-shadow: 0 1px 2px 1px $shadow-l1;
|
||||
margin-bottom: $baseline;
|
||||
border: 1px solid $m-gray-l3;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// MISC: extends - UI - well
|
||||
.ui-well {
|
||||
box-shadow: inset 0 1px 2px 1px $shadow-l1;
|
||||
padding: ($baseline*0.75) $baseline;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// MISC: expandable UI
|
||||
.is-expandable {
|
||||
|
||||
.title-expand {
|
||||
|
||||
}
|
||||
|
||||
.expandable-icon {
|
||||
@include transition(all 0.25s ease-in-out 0s);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.expandable-area {
|
||||
// @include transition(opacity 0.25s ease-in-out 0s, height 0.25s ease-in-out 0s);
|
||||
}
|
||||
|
||||
// STATE: active
|
||||
&.is-ready {
|
||||
|
||||
.expandable-icon {
|
||||
@include transform(rotate(-90deg));
|
||||
}
|
||||
|
||||
.title-expand {
|
||||
@extend .ui-fake-link;
|
||||
color: $m-blue-d2;
|
||||
|
||||
&:hover {
|
||||
color: $m-blue;
|
||||
}
|
||||
}
|
||||
|
||||
/* using jquery
|
||||
.expandable-area {
|
||||
visibility: none;
|
||||
height: 0;
|
||||
opacity: 0.0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// STATE: expanded
|
||||
&.is-expanded {
|
||||
|
||||
.expandable-icon {
|
||||
@include transform(rotate(0));
|
||||
@include transform-origin(50% 50%);
|
||||
}
|
||||
|
||||
/*
|
||||
.expandable-area {
|
||||
visibility: visible;
|
||||
height: ($baseline*16);
|
||||
opacity: 1.0;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// VIEW: all verification steps
|
||||
.register.verification-process {
|
||||
|
||||
// reset: box-sizing (making things so right its scary)
|
||||
* {
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
// reset: typography
|
||||
font-family: $sans-serif;
|
||||
|
||||
// reset: typography - heading
|
||||
h1, h2, h3, h4, h5 ,h6 {
|
||||
@extend .t-title;
|
||||
color: $m-gray-d4;
|
||||
}
|
||||
|
||||
// reset: typography - copy
|
||||
p, ol, ul, dl, input, select, textarea {
|
||||
font-family: $f-sans-serif;
|
||||
color: $m-gray-d1;
|
||||
}
|
||||
|
||||
.copy {
|
||||
|
||||
p, ul, li, dl, blockquote, input, select {
|
||||
margin-bottom: ($baseline*0.75);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reset: copy/text
|
||||
|
||||
|
||||
// reset: forms
|
||||
input {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
margin-right: ($baseline/5);
|
||||
}
|
||||
|
||||
label {
|
||||
@extend .t-weight4;
|
||||
font-family: $sans-serif;
|
||||
font-style: normal;
|
||||
color: $m-gray-d4;
|
||||
}
|
||||
|
||||
// HACK: nasty override due to our bad input/button styling
|
||||
button, input[type="submit"], input[type="button"], button[type="submit"] {
|
||||
@include font-size(16);
|
||||
@extend .t-weight4;
|
||||
text-transform: none;
|
||||
text-shadow: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.checkbox label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// reset: lists
|
||||
.list-actions, .list-steps, .progress-steps, .list-controls, .list-fields, .list-help, .list-faq, .nav-wizard, .list-reqs, .list-faq, .review-tasks, .list-tips, .wrapper-photos, .field-group, .list-info {
|
||||
@extend .ui-no-list;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// elements: layout
|
||||
.content-wrapper {
|
||||
background: $m-gray-l4;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: $white;
|
||||
padding: ($baseline*1.5) ($baseline*1.5) ($baseline*2) ($baseline*1.5);
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// elements: common copy
|
||||
.title {
|
||||
@extend .t-title5;
|
||||
@extend .t-weight1;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend .t-weight1;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// elements - controls
|
||||
.action-primary {
|
||||
@extend .btn-primary-blue;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-confirm {
|
||||
@extend .btn-verify-primary;
|
||||
border: none;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// elements: page depth
|
||||
|
||||
// ====================
|
||||
|
||||
// UI: reports/tables
|
||||
.wrapper-report {
|
||||
|
||||
.report {
|
||||
@extend .ui-window;
|
||||
width: flex-grid(12,12);
|
||||
border-color: $m-gray-t3;
|
||||
border-collapse:collapse;
|
||||
|
||||
tr {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
@extend .t-weight3;
|
||||
vertical-align: middle;
|
||||
padding: ($baseline*0.75) $baseline;
|
||||
color: $m-gray-d3;
|
||||
}
|
||||
|
||||
th {
|
||||
@extend .t-weight2;
|
||||
padding: ($baseline/2) $baseline;
|
||||
}
|
||||
|
||||
thead {
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid $m-gray-l3;
|
||||
}
|
||||
|
||||
th[scope="col"] {
|
||||
@extend .t-title7;
|
||||
@extend .t-weight2;
|
||||
vertical-align: middle;
|
||||
color: $m-gray-l1;
|
||||
background: $m-gray-l4;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid $m-gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
tfoot {
|
||||
background: $m-blue-t0;
|
||||
|
||||
th[scope="row"] {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI: help
|
||||
.help-item {
|
||||
|
||||
.title {
|
||||
@extend .hd-lv4;
|
||||
margin-bottom: ($baseline/4);
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend .copy-detail;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI : message
|
||||
.wrapper-msg {
|
||||
width: flex-grid(12,12);
|
||||
margin: 0 auto ($baseline*1.5) auto;
|
||||
border-bottom: ($baseline/4) solid $m-blue;
|
||||
padding: $baseline ($baseline*1.5);
|
||||
background: tint($m-blue, 95%);
|
||||
|
||||
.msg-content, .msg-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.msg-content {
|
||||
width: flex-grid(11,12);
|
||||
|
||||
.title {
|
||||
@extend .t-title6;
|
||||
@extend .t-weight4;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend .t-copy-sub1;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-icon {
|
||||
width: flex-grid(1,12);
|
||||
@extend .t-icon2;
|
||||
text-align: center;
|
||||
color: $m-blue;
|
||||
}
|
||||
}
|
||||
|
||||
// UI: error
|
||||
.wrapper-msg-error {
|
||||
border-bottom-color: $red;
|
||||
background: tint($red, 95%);
|
||||
|
||||
.msg-icon {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.msg-content {
|
||||
|
||||
.title {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI: error
|
||||
.wrapper-msg-activate {
|
||||
border-bottom-color: $m-pink;
|
||||
background: tint($m-pink, 95%);
|
||||
|
||||
.msg-icon {
|
||||
color: $m-pink;
|
||||
}
|
||||
|
||||
.msg-content {
|
||||
|
||||
.title {
|
||||
color: $m-pink;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI: inline messages
|
||||
.msg-inline {
|
||||
|
||||
&.msg-error {
|
||||
|
||||
.copy, .copy p {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI: page header
|
||||
.page-header {
|
||||
width: flex-grid(12,12);
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
border-bottom: ($baseline/4) solid $m-gray-l4;
|
||||
|
||||
.title {
|
||||
@include clearfix();
|
||||
width: flex-grid(12,12);
|
||||
|
||||
.wrapper-sts, .sts-track {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wrapper-sts {
|
||||
width: flex-grid(9,12);
|
||||
}
|
||||
|
||||
.sts-track {
|
||||
width: flex-grid(3,12);
|
||||
text-align: right;
|
||||
|
||||
.sts-track-value {
|
||||
@extend .copy-badge;
|
||||
color: $white;
|
||||
background: $verified-color-lvl3;
|
||||
|
||||
.context {
|
||||
margin-right: ($baseline/4);
|
||||
opacity: 0.80;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sts {
|
||||
@extend .t-title7;
|
||||
display: block;
|
||||
color: $m-gray;
|
||||
}
|
||||
|
||||
.sts-course {
|
||||
@extend .t-title;
|
||||
@include font-size(28);
|
||||
@include line-height(28);
|
||||
@extend .t-weight4;
|
||||
display: block;
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI : progress
|
||||
.wrapper-progress {
|
||||
position: relative;
|
||||
margin-bottom: ($baseline*1.5);
|
||||
}
|
||||
|
||||
.progress-sts {
|
||||
@include size(($baseline/4));
|
||||
@extend .ui-depth1;
|
||||
position: absolute;
|
||||
top: 43px;
|
||||
left: 7%;
|
||||
display: block;
|
||||
width: 83%;
|
||||
margin: 0 auto;
|
||||
background: $m-gray-l4;
|
||||
|
||||
.progress-sts-value {
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
background: $verified-color-lvl4;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: flex-grid(12,12);
|
||||
margin: 0 auto;
|
||||
border-bottom: ($baseline/4) solid $m-gray-l4;
|
||||
|
||||
.progress-steps {
|
||||
@include clearfix();
|
||||
position: relative;
|
||||
top: ($baseline/4);
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
@extend .ui-depth2;
|
||||
position: relative;
|
||||
display: table-cell;
|
||||
height: ($baseline*6);
|
||||
padding: $baseline $baseline ($baseline*1.5) $baseline;
|
||||
text-align: center;
|
||||
|
||||
.wrapper-step-number, .step-number, .step-name {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wrapper-step-number {
|
||||
@include size(($baseline*2) ($baseline*2));
|
||||
margin: 0 auto ($baseline/2) auto;
|
||||
border-radius: ($baseline*10);
|
||||
border: ($baseline/5) solid $m-gray-l4;
|
||||
background: $white;
|
||||
|
||||
.step-number {
|
||||
@extend .t-title7;
|
||||
@extend .t-weight4;
|
||||
@include line-height(0);
|
||||
margin: 16px auto 0 auto;
|
||||
color: $m-gray-l1;
|
||||
}
|
||||
}
|
||||
|
||||
.step-name {
|
||||
@extend .t-title7;
|
||||
@extend .t-weight4;
|
||||
color: $m-gray-l1;
|
||||
}
|
||||
|
||||
// confirmation step w/ icon
|
||||
&.progress-step-icon {
|
||||
|
||||
.step-number {
|
||||
margin-top: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is completed
|
||||
&.is-completed {
|
||||
border-bottom: ($baseline/5) solid $verified-color-lvl3;
|
||||
|
||||
.wrapper-step-number {
|
||||
border-color: $verified-color-lvl3;
|
||||
}
|
||||
|
||||
.step-number, .step-name {
|
||||
color: $m-gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is current
|
||||
&.is-current {
|
||||
border-bottom: ($baseline/5) solid $m-blue-d1;
|
||||
opacity: 1.0;
|
||||
|
||||
.wrapper-step-number {
|
||||
border-color: $m-blue-d1;
|
||||
}
|
||||
|
||||
.step-number, .step-name {
|
||||
color: $m-gray-d3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI: slides
|
||||
.carousel {
|
||||
|
||||
// carousel item
|
||||
.carousel-item {
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
// STATE: active
|
||||
.carousel-active {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
// indiv slides
|
||||
.wrapper-view {
|
||||
|
||||
}
|
||||
|
||||
.view {
|
||||
width: flex-grid(12,12);
|
||||
|
||||
> .title {
|
||||
@extend .hd-lv2;
|
||||
color: $m-blue-d1;
|
||||
}
|
||||
|
||||
.instruction {
|
||||
@extend .t-copy-lead1;
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-task {
|
||||
@include clearfix();
|
||||
width: flex-grid(12,12);
|
||||
margin: $baseline 0;
|
||||
|
||||
.wrapper-help {
|
||||
float: right;
|
||||
width: flex-grid(6,12);
|
||||
padding: 0 $baseline;
|
||||
|
||||
.help {
|
||||
margin-bottom: ($baseline*1.5);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
@extend .hd-lv3;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend .copy-detail;
|
||||
}
|
||||
|
||||
// help - general list
|
||||
.list-help {
|
||||
margin-top: ($baseline/2);
|
||||
|
||||
.help-item {
|
||||
margin-bottom: ($baseline/4);
|
||||
border-bottom: 1px solid $m-gray-l4;
|
||||
padding-bottom: ($baseline/4);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// help - faq
|
||||
.list-faq {
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task {
|
||||
@extend .ui-window;
|
||||
float: left;
|
||||
width: flex-grid(6,12);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.controls {
|
||||
padding: ($baseline*0.75) $baseline;
|
||||
background: $m-gray-l4;
|
||||
|
||||
.list-controls {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.control {
|
||||
position: absolute;
|
||||
|
||||
.action {
|
||||
@extend .btn-primary-blue;
|
||||
padding: ($baseline/2) ($baseline*0.75);
|
||||
|
||||
*[class^="icon-"] {
|
||||
@extend .t-icon4;
|
||||
padding: ($baseline*.25) ($baseline*.5);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: hidden
|
||||
&.is-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
// STATE: shown
|
||||
&.is-shown {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
// STATE: approved
|
||||
&.approved {
|
||||
|
||||
.action {
|
||||
@extend .btn-verify-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// control - redo
|
||||
.control-redo {
|
||||
position: absolute;
|
||||
left: ($baseline/2);
|
||||
}
|
||||
|
||||
// control - take/do
|
||||
.control-do {
|
||||
left: 45%;
|
||||
}
|
||||
|
||||
// control - approve
|
||||
.control-approve {
|
||||
position: absolute;
|
||||
right: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
.msg {
|
||||
@include clearfix();
|
||||
margin-top: ($baseline*2);
|
||||
|
||||
.copy {
|
||||
float: left;
|
||||
width: flex-grid(8,12);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
position: relative;
|
||||
top: -($baseline/2);
|
||||
float: left;
|
||||
width: flex-grid(4,12);
|
||||
text-align: right;
|
||||
|
||||
.action-retakephotos a {
|
||||
@extend .btn-primary-blue;
|
||||
@include font-size(14);
|
||||
padding: ($baseline/2) ($baseline*.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.msg-followup {
|
||||
border-top: ($baseline/10) solid $m-gray-t0;
|
||||
padding-top: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
// indiv slides - photo
|
||||
#wrapper-facephoto {
|
||||
|
||||
}
|
||||
|
||||
// indiv slides - ID
|
||||
#wrapper-idphoto {
|
||||
|
||||
}
|
||||
|
||||
// indiv slides - review
|
||||
#wrapper-review {
|
||||
|
||||
.review-task {
|
||||
margin-bottom: ($baseline*1.5);
|
||||
padding: ($baseline*0.75) $baseline;
|
||||
border-radius: ($baseline/10);
|
||||
background: $m-gray-l4;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> .title {
|
||||
@extend .hd-lv3;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend .copy-base;
|
||||
|
||||
strong {
|
||||
@extend .t-weight5;
|
||||
color: $m-gray-d4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// individual task - photos
|
||||
.review-task-photos {
|
||||
|
||||
.wrapper-photos {
|
||||
@include clearfix();
|
||||
width: flex-grid(12,12);
|
||||
margin: $baseline 0;
|
||||
|
||||
.wrapper-photo {
|
||||
float: left;
|
||||
width: flex-grid(6,12);
|
||||
margin-right: flex-gutter();
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.placeholder-photo {
|
||||
@extend .ui-window;
|
||||
padding: ($baseline*0.75) $baseline;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.help-tips {
|
||||
|
||||
.title {
|
||||
@extend .hd-lv5;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend .copy-detail;
|
||||
}
|
||||
|
||||
// help - general list
|
||||
.list-tips {
|
||||
|
||||
.tip {
|
||||
margin-bottom: ($baseline/4);
|
||||
border-bottom: 1px solid $m-gray-t0;
|
||||
padding-bottom: ($baseline/4);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// individual task - name
|
||||
.review-task-name {
|
||||
@include clearfix();
|
||||
|
||||
.copy {
|
||||
float: left;
|
||||
width: flex-grid(8,12);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
position: relative;
|
||||
top: -($baseline);
|
||||
float: left;
|
||||
width: flex-grid(4,12);
|
||||
text-align: right;
|
||||
|
||||
.action-editname a {
|
||||
@extend .btn-primary-blue;
|
||||
@include font-size(14);
|
||||
padding: ($baseline/2) ($baseline*.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// individual task - contribution
|
||||
.review-task-contribution {
|
||||
|
||||
.list-fields {
|
||||
@include clearfix();
|
||||
margin: $baseline 0;
|
||||
|
||||
.field {
|
||||
float: left;
|
||||
margin-right: ($baseline/2);
|
||||
padding: ($baseline/2) ($baseline*0.75);
|
||||
background: $m-gray-t0;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.field-group-other {
|
||||
|
||||
.contribution-option {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
padding: 3px 0;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.contribution-option-other1 label, .contribution-option-other2 label {
|
||||
@extend .text-sr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// UI: camera states
|
||||
.cam {
|
||||
|
||||
.placeholder-cam {
|
||||
position: relative;
|
||||
width: 95%;
|
||||
height: 375px;
|
||||
margin: ($baseline/2) auto;
|
||||
background: $m-blue-l5;
|
||||
|
||||
// placeholders
|
||||
.placeholder-art {
|
||||
opacity: 0.7;
|
||||
z-index: 100;
|
||||
|
||||
.copy {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
margin: 0 40px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// previously defined in HTML
|
||||
video, canvas {
|
||||
position: relative;
|
||||
display: block;
|
||||
@include size(100% 100%);
|
||||
z-index: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
height: ($baseline*4);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI: deco - divider
|
||||
.deco-divider {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: $baseline 0 ($baseline*2) 0;
|
||||
border-top: ($baseline/5) solid $m-gray-l4;
|
||||
|
||||
.copy {
|
||||
@extend .t-copy-lead1;
|
||||
@extend .t-weight4;
|
||||
position: absolute;
|
||||
top: -($baseline*1.25);
|
||||
left: 45%;
|
||||
padding: ($baseline/2) ($baseline*1.5);
|
||||
background: white;
|
||||
text-align: center;
|
||||
color: $m-gray-l2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ====================
|
||||
|
||||
// UI: nav - wizard
|
||||
.nav-wizard {
|
||||
@extend .ui-well;
|
||||
@include clearfix;
|
||||
width: flex-grid(12,12);
|
||||
border-radius: ($baseline/10);
|
||||
background: $m-gray-l4;
|
||||
|
||||
.help-inline, .wizard-steps {
|
||||
|
||||
}
|
||||
|
||||
.help-inline {
|
||||
@extend .t-copy-sub1;
|
||||
float: left;
|
||||
width: flex-grid(6,12);
|
||||
margin: ($baseline*0.75) flex-gutter() 0 0;
|
||||
}
|
||||
|
||||
.wizard-steps {
|
||||
float: right;
|
||||
width: flex-grid(6,12);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// STATE: is ready
|
||||
&.is-ready {
|
||||
background: $m-blue-l4;
|
||||
}
|
||||
|
||||
&.is-not-ready {
|
||||
background: $m-gray-l4;
|
||||
|
||||
.action-primary {
|
||||
@extend .btn-primary-disabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI: forms - payment
|
||||
.contribution-options {
|
||||
|
||||
.contribution-option {
|
||||
border-radius: ($baseline/5);
|
||||
|
||||
.label, label, input {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.label, label {
|
||||
margin-bottom: 0;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.deco-denomination, .label-value, .denomination-name {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.deco-denomination {
|
||||
}
|
||||
|
||||
.label-value {
|
||||
@extend .t-weight4;
|
||||
}
|
||||
|
||||
.denomination-name {
|
||||
@include font-size(14);
|
||||
color: $m-gray-l1;
|
||||
}
|
||||
|
||||
// specific fields
|
||||
#contribution-other-amt {
|
||||
width: ($baseline*4.5);
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// help - faq
|
||||
.list-faq {
|
||||
|
||||
.faq-question {
|
||||
@extend .hd-lv3;
|
||||
border-bottom: 1px solid $m-gray-l4;
|
||||
padding-bottom: ($baseline/4);
|
||||
}
|
||||
|
||||
.faq-answer {
|
||||
margin-bottom: ($baseline*1.25);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI: main content
|
||||
.wrapper-content-main {
|
||||
|
||||
}
|
||||
|
||||
.content-main {
|
||||
width: flex-grid(12,12);
|
||||
|
||||
> .title {
|
||||
@extend .hd-lv2;
|
||||
color: $m-blue-d1;
|
||||
}
|
||||
|
||||
.instruction {
|
||||
@extend .t-copy-lead1;
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI: supplemental content
|
||||
.wrapper-content-supplementary {
|
||||
margin: ($baseline*1.5) 0;
|
||||
border-top: ($baseline/4) solid $m-gray-l4;
|
||||
}
|
||||
|
||||
.content-supplementary {
|
||||
width: flex-grid(12,12);
|
||||
|
||||
.list-help {
|
||||
@include clearfix();
|
||||
|
||||
.help-item {
|
||||
width: flex-grid(4,12);
|
||||
float: left;
|
||||
margin-right: flex-gutter();
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// VIEW: select a track
|
||||
&.step-select-track {
|
||||
|
||||
.sts-track {
|
||||
@extend .text-sr;
|
||||
}
|
||||
|
||||
.form-register-choose {
|
||||
@include clearfix();
|
||||
width: flex-grid(12,12);
|
||||
margin: $baseline 0;
|
||||
|
||||
.deco-divider {
|
||||
width: flex-grid(8,12);
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.register-choice, .help-register {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.register-choice {
|
||||
width: flex-grid(8,12);
|
||||
margin: 0 flex-gutter() $baseline 0;
|
||||
border-top: ($baseline/4) solid $m-gray-d4;
|
||||
padding: $baseline ($baseline*1.5);
|
||||
background: $m-gray-l4;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: none;
|
||||
}
|
||||
|
||||
.wrapper-copy, .list-actions {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wrapper-copy {
|
||||
width: flex-grid(8,8);
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
width: flex-grid(8,8);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.title {
|
||||
@extend .t-title5;
|
||||
@extend .t-weight5;
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend .t-copy-base;
|
||||
}
|
||||
|
||||
.action-select input {
|
||||
@extend .t-weight4;
|
||||
padding: ($baseline/2) ($baseline*0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.register-choice-audit {
|
||||
border-color: $m-blue-d1;
|
||||
|
||||
.wrapper-copy {
|
||||
width: flex-grid(5,8);
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
width: flex-grid(3,8);
|
||||
}
|
||||
|
||||
.action-select input {
|
||||
@extend .btn-primary-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.register-choice-certificate {
|
||||
border-color: $verified-color-lvl3;
|
||||
position: relative;
|
||||
|
||||
.deco-ribbon {
|
||||
position: absolute;
|
||||
top: -($baseline*1.5);
|
||||
right: $baseline;
|
||||
display: block;
|
||||
width: ($baseline*3);
|
||||
height: ($baseline*4);
|
||||
background: transparent url('../images/vcert-ribbon-s.png') no-repeat 0 0;
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
margin: ($baseline/2) 0;
|
||||
border-top: ($baseline/10) solid $m-gray-t1;
|
||||
padding-top: $baseline;
|
||||
}
|
||||
|
||||
.action-intro, .action-select {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.action-intro {
|
||||
@extend .copy-detail;
|
||||
width: flex-grid(3,8);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.action-select {
|
||||
width: flex-grid(5,8);
|
||||
}
|
||||
|
||||
.action-select input {
|
||||
@extend .btn-verify-primary;
|
||||
}
|
||||
|
||||
.action-intro {
|
||||
|
||||
}
|
||||
|
||||
// extra register options/info
|
||||
.title-expand {
|
||||
@extend .t-copy-sub1;
|
||||
font-weight: 500 !important;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.expandable-area {
|
||||
margin: $baseline 0;
|
||||
}
|
||||
}
|
||||
|
||||
.help-register {
|
||||
width: flex-grid(4,12);
|
||||
|
||||
.title {
|
||||
@extend .hd-lv4;
|
||||
@extend .t-weight4;
|
||||
margin-top: $baseline;
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend .copy-detail;
|
||||
}
|
||||
}
|
||||
|
||||
// progress indicator
|
||||
.progress-sts-value {
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
// contribution selection
|
||||
.field-certificate-contribution {
|
||||
margin: $baseline 0;
|
||||
|
||||
.label {
|
||||
@extend .hd-lv4;
|
||||
@extend .t-weight4;
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
.contribution-options {
|
||||
@include clearfix();
|
||||
margin: $baseline 0;
|
||||
|
||||
.field {
|
||||
float: left;
|
||||
margin-right: ($baseline/2);
|
||||
padding: ($baseline/2) ($baseline*0.75);
|
||||
background: $m-gray-t0;
|
||||
|
||||
input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#contribution-other-amt {
|
||||
width: ($baseline*4.5);
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
}
|
||||
|
||||
.field-group-other {
|
||||
|
||||
.contribution-option {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
padding: 1px 0;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.contribution-option-other1 label, .contribution-option-other2 label {
|
||||
@extend .text-sr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VIEW: requirements
|
||||
&.step-requirements {
|
||||
|
||||
// progress nav
|
||||
.progress .progress-step {
|
||||
|
||||
// STATE: is current
|
||||
&#progress-step0 {
|
||||
border-bottom: ($baseline/5) solid $m-blue-d1;
|
||||
opacity: 1.0;
|
||||
|
||||
.wrapper-step-number {
|
||||
border-color: $m-blue-d1;
|
||||
}
|
||||
|
||||
.step-number, .step-name {
|
||||
color: $m-gray-d3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// progress indicator
|
||||
.progress-sts-value {
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.list-reqs {
|
||||
@include clearfix();
|
||||
width: flex-grid(12,12);
|
||||
|
||||
.req {
|
||||
@extend .ui-window;
|
||||
width: flex-grid(4,12);
|
||||
min-height: ($baseline*15);
|
||||
float: left;
|
||||
margin-right: flex-gutter();
|
||||
border-color: $verified-color-lvl4;
|
||||
text-align: center;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
@extend .t-title5;
|
||||
@extend .t-weight4;
|
||||
padding: $baseline;
|
||||
border-bottom: 1px solid $verified-color-lvl4;
|
||||
background: $verified-color-lvl5;
|
||||
|
||||
}
|
||||
|
||||
.placeholder-art {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: $baseline 0 ($baseline/2) 0;
|
||||
padding: $baseline;
|
||||
background: $verified-color-lvl3;
|
||||
border-radius: ($baseline*10);
|
||||
|
||||
*[class^="icon"] {
|
||||
@extend .t-icon1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.icon-over, .icon-under {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-under {
|
||||
@extend .ui-depth1;
|
||||
}
|
||||
|
||||
.icon-over {
|
||||
@extend .ui-depth2;
|
||||
@extend .t-icon5;
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
top: 34px;
|
||||
background: $verified-color-lvl3;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.copy {
|
||||
padding: ($baseline/2) $baseline;
|
||||
}
|
||||
|
||||
.copy-super, .copy-sub {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.copy-super {
|
||||
@extend .t-copy-base;
|
||||
margin-bottom: ($baseline/2);
|
||||
color: $verified-color-lvl1;
|
||||
}
|
||||
|
||||
.copy-sub {
|
||||
@extend .t-copy-sub2;
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: ($baseline/2) $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
// specific requirements
|
||||
.req-activate {
|
||||
float: left;
|
||||
text-align: center;
|
||||
border-color: $m-pink-l3;
|
||||
|
||||
.title {
|
||||
@extend .t-title4;
|
||||
@extend .t-weight4;
|
||||
border-bottom-color: $m-pink-l3;
|
||||
background: tint($m-pink, 95%);
|
||||
color: $m-pink;
|
||||
}
|
||||
|
||||
.placeholder-art {
|
||||
background: $m-pink-l1;
|
||||
}
|
||||
|
||||
.copy-super {
|
||||
@extend .t-copy-lead1;
|
||||
color: $m-pink;
|
||||
}
|
||||
|
||||
.copy-sub {
|
||||
@extend .t-copy-base;
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: account not activated
|
||||
&.account-not-activated {
|
||||
|
||||
.req {
|
||||
width: flex-grid(3,12);
|
||||
min-height: ($baseline*18);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VIEW: take and review photos
|
||||
&.step-photos {
|
||||
|
||||
}
|
||||
|
||||
// VIEW: take cam photo
|
||||
&.step-photos-cam {
|
||||
|
||||
// progress nav
|
||||
.progress .progress-step {
|
||||
|
||||
// STATE: is completed
|
||||
&#progress-step0 {
|
||||
border-bottom: ($baseline/5) solid $verified-color-lvl3;
|
||||
|
||||
.wrapper-step-number {
|
||||
border-color: $verified-color-lvl3;
|
||||
}
|
||||
|
||||
.step-number, .step-name {
|
||||
color: $m-gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is current
|
||||
&#progress-step1 {
|
||||
border-bottom: ($baseline/5) solid $m-blue-d1;
|
||||
opacity: 1.0;
|
||||
|
||||
.wrapper-step-number {
|
||||
border-color: $m-blue-d1;
|
||||
}
|
||||
|
||||
.step-number, .step-name {
|
||||
color: $m-gray-d3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// progress indicator
|
||||
.progress-sts-value {
|
||||
width: 14%;
|
||||
}
|
||||
}
|
||||
|
||||
// VIEW: take id photo
|
||||
&.step-photos-id {
|
||||
|
||||
// progress nav
|
||||
.progress .progress-step {
|
||||
|
||||
// STATE: is completed
|
||||
&#progress-step0, &#progress-step1 {
|
||||
border-bottom: ($baseline/5) solid $verified-color-lvl3;
|
||||
|
||||
.wrapper-step-number {
|
||||
border-color: $verified-color-lvl3;
|
||||
}
|
||||
|
||||
.step-number, .step-name {
|
||||
color: $m-gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is current
|
||||
&#progress-step2 {
|
||||
border-bottom: ($baseline/5) solid $m-blue-d1;
|
||||
opacity: 1.0;
|
||||
|
||||
.wrapper-step-number {
|
||||
border-color: $m-blue-d1;
|
||||
}
|
||||
|
||||
.step-number, .step-name {
|
||||
color: $m-gray-d3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// progress indicator
|
||||
.progress-sts-value {
|
||||
width: 36%;
|
||||
}
|
||||
}
|
||||
|
||||
// VIEW: review photos
|
||||
&.step-review {
|
||||
|
||||
.nav-wizard {
|
||||
|
||||
.help-inline {
|
||||
width: flex-grid(4,12);
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
.wizard-steps {
|
||||
float: right;
|
||||
width: flex-grid(8,12);
|
||||
|
||||
.wizard-step {
|
||||
width: flex-grid(4,8);
|
||||
margin-right: flex-gutter();
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.step-match {
|
||||
|
||||
label {
|
||||
@extend .t-copy-sub1;
|
||||
}
|
||||
}
|
||||
|
||||
.step-proceed {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// progress nav
|
||||
.progress .progress-step {
|
||||
|
||||
// STATE: is completed
|
||||
&#progress-step0, &#progress-step1, &#progress-step2 {
|
||||
border-bottom: ($baseline/5) solid $verified-color-lvl3;
|
||||
|
||||
.wrapper-step-number {
|
||||
border-color: $verified-color-lvl3;
|
||||
}
|
||||
|
||||
.step-number, .step-name {
|
||||
color: $m-gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is current
|
||||
&#progress-step3 {
|
||||
border-bottom: ($baseline/5) solid $m-blue-d1;
|
||||
opacity: 1.0;
|
||||
|
||||
.wrapper-step-number {
|
||||
border-color: $m-blue-d1;
|
||||
}
|
||||
|
||||
.step-number, .step-name {
|
||||
color: $m-gray-d3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// progress indicator
|
||||
.progress-sts-value {
|
||||
width: 55%;
|
||||
}
|
||||
}
|
||||
|
||||
// VIEW: confirmation/receipt
|
||||
&.step-confirmation {
|
||||
|
||||
// progress nav
|
||||
.progress .progress-step {
|
||||
|
||||
// STATE: is completed
|
||||
&#progress-step0, &#progress-step1, &#progress-step2, &#progress-step3, &#progress-step4 {
|
||||
border-bottom: ($baseline/5) solid $verified-color-lvl3;
|
||||
|
||||
.wrapper-step-number {
|
||||
border-color: $verified-color-lvl3;
|
||||
}
|
||||
|
||||
.step-number, .step-name {
|
||||
color: $m-gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is current
|
||||
&#progress-step5 {
|
||||
border-bottom: ($baseline/5) solid $m-blue-d1;
|
||||
opacity: 1.0;
|
||||
|
||||
.wrapper-step-number {
|
||||
border-color: $m-blue-d1;
|
||||
}
|
||||
|
||||
.step-number, .step-name {
|
||||
color: $m-gray-d3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// progress indicator
|
||||
.progress-sts-value {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// information elements
|
||||
.list-info {
|
||||
|
||||
.info-item {
|
||||
margin-bottom: ($baseline*1.5);
|
||||
border-bottom: ($baseline/4) solid $m-gray-l4;
|
||||
padding-bottom: ($baseline*1.5);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: none;
|
||||
}
|
||||
|
||||
> .title {
|
||||
@extend .hd-lv3;
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend .copy-base;
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// information - course
|
||||
.course-info {
|
||||
|
||||
.options {
|
||||
|
||||
.action-course {
|
||||
@extend .btn-primary-blue;
|
||||
@include font-size(14);
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.course-actions {
|
||||
|
||||
.action-dashboard {
|
||||
@extend .btn-primary-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// information - verification
|
||||
.verification-info {
|
||||
|
||||
}
|
||||
|
||||
// information - payment
|
||||
.payment-info {
|
||||
|
||||
}
|
||||
|
||||
// UI: table/report
|
||||
.wrapper-report {
|
||||
|
||||
.report {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: already verified
|
||||
.register.is-verified {
|
||||
|
||||
.nav-wizard .price-value {
|
||||
@extend .t-weight4;
|
||||
@include font-size(16);
|
||||
margin-top: 18px;
|
||||
color: $m-blue-d1;
|
||||
}
|
||||
|
||||
// progress nav
|
||||
.progress .progress-step {
|
||||
|
||||
// STATE: is completed
|
||||
&#progress-step1 {
|
||||
border-bottom: ($baseline/5) solid $verified-color-lvl3;
|
||||
|
||||
.wrapper-step-number {
|
||||
border-color: $verified-color-lvl3;
|
||||
}
|
||||
|
||||
.step-number, .step-name {
|
||||
color: $m-gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is current
|
||||
&#progress-step2 {
|
||||
border-bottom: ($baseline/5) solid $m-blue-d1;
|
||||
opacity: 1.0;
|
||||
|
||||
.wrapper-step-number {
|
||||
border-color: $m-blue-d1;
|
||||
}
|
||||
|
||||
.step-number, .step-name {
|
||||
color: $m-gray-d3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// progress indicator
|
||||
.progress-sts {
|
||||
width: 75%;
|
||||
left: 13%;
|
||||
}
|
||||
|
||||
.progress-sts-value {
|
||||
width: 32% !important;
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,12 @@
|
||||
|
||||
$('#class_enroll_form').on('ajax:complete', function(event, xhr) {
|
||||
if(xhr.status == 200) {
|
||||
location.href = "${reverse('dashboard')}";
|
||||
if (xhr.responseText == "") {
|
||||
location.href = "${reverse('dashboard')}";
|
||||
}
|
||||
else {
|
||||
location.href = xhr.responseText;
|
||||
}
|
||||
} else if (xhr.status == 403) {
|
||||
location.href = "${reverse('register_user')}?course_id=${course.id}&enrollment_action=enroll";
|
||||
} else {
|
||||
@@ -95,7 +100,6 @@
|
||||
|
||||
%else:
|
||||
<a href="#" class="register">${_("Register for {course.display_number_with_default}").format(course=course) | h}</a>
|
||||
|
||||
<div id="register_error"></div>
|
||||
%endif
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<%block name="title"><title>${_("About {course_id}").format(course_id=course_id)}</title></%block>
|
||||
|
||||
<%block name="bodyclass">view-partial-mktgregister</%block>
|
||||
<%block name="bodyclass">view-iframe-content view-partial-mktgregister</%block>
|
||||
|
||||
|
||||
<%block name="headextra">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<%block name="title"><title>${_("About {course_number}").format(course_number=course.display_number_with_default) | h}</title></%block>
|
||||
|
||||
<%block name="bodyclass">view-partial-mktgregister</%block>
|
||||
<%block name="bodyclass">view-iframe-content view-partial-mktgregister</%block>
|
||||
|
||||
|
||||
<%block name="headextra">
|
||||
@@ -27,7 +27,12 @@
|
||||
|
||||
$('#class_enroll_form').on('ajax:complete', function(event, xhr) {
|
||||
if(xhr.status == 200) {
|
||||
window.top.location.href = "${reverse('dashboard')}";
|
||||
if (xhr.responseText != "") {
|
||||
window.top.location.href = xhr.responseText;
|
||||
}
|
||||
else {
|
||||
window.top.location.href = "${reverse('dashboard')}";
|
||||
}
|
||||
} else if (xhr.status == 403) {
|
||||
window.top.location.href = "${reverse('register_user')}?course_id=${course.id}&enrollment_action=enroll";
|
||||
} else {
|
||||
@@ -52,7 +57,7 @@
|
||||
<div class="action is-registered">${_("You Are Registered")}</div>
|
||||
%endif
|
||||
%elif allow_registration:
|
||||
<a class="action action-register register" href="#">${_("Register for")} <strong>${course.display_number_with_default | h}</strong>
|
||||
<a class="action action-register register ${'has-option-verified' if len(course_modes) > 1 else ''}" href="#">${_("Register for")} <strong>${course.display_number_with_default | h}</strong>
|
||||
%if len(course_modes) > 1:
|
||||
<span class="track">
|
||||
and choose your student track
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<%block name="title"><title>${_("Dashboard")}</title></%block>
|
||||
<%block name="bodyclass">view-dashboard is-authenticated</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript">
|
||||
@@ -101,7 +102,7 @@
|
||||
},
|
||||
error: function(xhr, textStatus, error) {
|
||||
if (xhr.status == 403) {
|
||||
location.href = "${reverse('signin_user')}";
|
||||
location.href = "${reverse('signin_user')}";
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -157,167 +158,178 @@
|
||||
</header>
|
||||
|
||||
% if len(courses) > 0:
|
||||
% for course in courses:
|
||||
|
||||
<article class="my-course">
|
||||
<%
|
||||
course_target = reverse('info', args=[course.id])
|
||||
%>
|
||||
|
||||
|
||||
|
||||
% if course.id in show_courseware_links_for:
|
||||
<a href="${course_target}" class="cover">
|
||||
<img src="${course_image_url(course)}" alt="${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) |h}" />
|
||||
</a>
|
||||
% else:
|
||||
<div class="cover">
|
||||
<img src="${course_image_url(course)}" alt="${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) | h}" />
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<section class="info">
|
||||
<hgroup>
|
||||
<p class="date-block">
|
||||
% if course.has_ended():
|
||||
${_("Course Completed - {end_date}").format(end_date=course.end_date_text)}
|
||||
% elif course.has_started():
|
||||
${_("Course Started - {start_date}").format(start_date=course.start_date_text)}
|
||||
% else: # hasn't started yet
|
||||
${_("Course Starts - {start_date}").format(start_date=course.start_date_text)}
|
||||
% endif
|
||||
</p>
|
||||
<h2 class="university">${get_course_about_section(course, 'university')}</h2>
|
||||
<h3>
|
||||
% if course.id in show_courseware_links_for:
|
||||
<a href="${course_target}">${course.display_number_with_default | h} ${course.display_name_with_default}</a>
|
||||
% else:
|
||||
<span>${course.display_number_with_default | h} ${course.display_name_with_default}</span>
|
||||
% endif
|
||||
</h3>
|
||||
</hgroup>
|
||||
|
||||
<ul class="listing-courses">
|
||||
% for course, enrollment in courses:
|
||||
<li class="course-item">
|
||||
<article class="course ${enrollment.mode}">
|
||||
<%
|
||||
testcenter_exam_info = course.current_test_center_exam
|
||||
registration = exam_registrations.get(course.id)
|
||||
testcenter_register_target = reverse('begin_exam_registration', args=[course.id])
|
||||
course_target = reverse('info', args=[course.id])
|
||||
%>
|
||||
% if testcenter_exam_info is not None:
|
||||
|
||||
% if registration is None and testcenter_exam_info.is_registering():
|
||||
<div class="message message-status is-shown exam-register">
|
||||
<a href="${testcenter_register_target}" class="button exam-button" id="exam_register_button">${_("Register for Pearson exam")}</a>
|
||||
<p class="message-copy">${_("Registration for the Pearson exam is now open and will close on {end_date}").format(end_date="<strong>{}</strong>".format(testcenter_exam_info.registration_end_date_text))}</p>
|
||||
</div>
|
||||
% endif
|
||||
<!-- display a registration for a current exam, even if the registration period is over -->
|
||||
% if registration is not None:
|
||||
% if registration.is_accepted:
|
||||
<div class="message message-status is-shown exam-schedule">
|
||||
<a href="${registration.registration_signup_url}" class="button exam-button">${_("Schedule Pearson exam")}</a>
|
||||
<p class="exam-registration-number">${_("{link_start}Registration{link_end} number: {number}").format(
|
||||
link_start='<a href="{url}" id="exam_register_link">'.format(url=testcenter_register_target),
|
||||
link_end='</a>',
|
||||
number=registration.client_candidate_id,
|
||||
)}</p>
|
||||
<p class="message-copy">${_("Write this down! You'll need it to schedule your exam.")}</p>
|
||||
</div>
|
||||
% endif
|
||||
% if registration.is_rejected:
|
||||
<div class="message message-status is-shown exam-schedule">
|
||||
<p class="message-copy">
|
||||
<strong>${_("Your registration for the Pearson exam has been rejected. Please {link_start}see your registration status details{link_end}.").format(
|
||||
link_start='<a href="{url}" id="exam_register_link">'.format(url=testcenter_register_target),
|
||||
link_end='</a>')}</strong>
|
||||
${_("Otherwise {link_start}contact edX at {email}{link_end} for further help.").format(
|
||||
link_start='<a class="contact-link" href="mailto:{email}?subject=Pearson VUE Exam - {about} {number}">'.format(email="exam-help@edx.org", about=get_course_about_section(course, 'university'), number=course.display_number_with_default),
|
||||
link_end='</a>',
|
||||
email="exam-help@edx.org",
|
||||
)}
|
||||
</div>
|
||||
% endif
|
||||
% if not registration.is_accepted and not registration.is_rejected:
|
||||
<div class="message message-status is-shown">
|
||||
<p class="message-copy"><strong>${_("Your {link_start}registration for the Pearson exam{link_end} is pending.").format(link_start='<a href="{url}" id="exam_register_link">'.format(url=testcenter_register_target), link_end='</a>')}</strong>
|
||||
${_("Within a few days, you should see a confirmation number here, which can be used to schedule your exam.")}
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
% endif
|
||||
|
||||
<%
|
||||
cert_status = cert_statuses.get(course.id)
|
||||
%>
|
||||
% if course.has_ended() and cert_status:
|
||||
<%
|
||||
if cert_status['status'] == 'generating':
|
||||
status_css_class = 'course-status-certrendering'
|
||||
elif cert_status['status'] == 'ready':
|
||||
status_css_class = 'course-status-certavailable'
|
||||
elif cert_status['status'] == 'notpassing':
|
||||
status_css_class = 'course-status-certnotavailable'
|
||||
else:
|
||||
status_css_class = 'course-status-processing'
|
||||
%>
|
||||
<div class="message message-status ${status_css_class} is-shown">
|
||||
|
||||
% if cert_status['status'] == 'processing':
|
||||
<p class="message-copy">${_("Final course details are being wrapped up at this time. Your final standing will be available shortly.")}</p>
|
||||
% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted'):
|
||||
<p class="message-copy">${_("Your final grade:")}
|
||||
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
|
||||
% if cert_status['status'] == 'notpassing':
|
||||
${_("Grade required for a certificate:")} <span class="grade-value">
|
||||
${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>.
|
||||
% elif cert_status['status'] == 'restricted':
|
||||
<p class="message-copy">
|
||||
${_("Your certificate is being held pending confirmation that the issuance of your certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {email}.").format(email='<a class="contact-link" href="mailto:{email}">{email}</a>.'.format(email=settings.CONTACT_EMAIL))}
|
||||
</p>
|
||||
% endif
|
||||
</p>
|
||||
% endif
|
||||
|
||||
% if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']:
|
||||
<ul class="actions">
|
||||
% if cert_status['show_disabled_download_button']:
|
||||
<li class="action"><span class="disabled">
|
||||
${_("Your Certificate is Generating")}</span></li>
|
||||
% elif cert_status['show_download_url']:
|
||||
<li class="action">
|
||||
<a class="btn" href="${cert_status['download_url']}"
|
||||
title="${_('This link will open/download a PDF document')}">
|
||||
Download Your PDF Certificate</a></li>
|
||||
% endif
|
||||
|
||||
% if cert_status['show_survey_button']:
|
||||
<li class="action"><a class="cta" href="${cert_status['survey_url']}">
|
||||
${_('Complete our course feedback survey')}</a></li>
|
||||
% endif
|
||||
</ul>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
% endif
|
||||
|
||||
% if course.id in show_courseware_links_for:
|
||||
% if course.has_ended():
|
||||
<a href="${course_target}" class="enter-course archived">${_('View Archived Course')}</a>
|
||||
% else:
|
||||
<a href="${course_target}" class="enter-course">${_('View Course')}</a>
|
||||
% endif
|
||||
<a href="${course_target}" class="cover">
|
||||
<img src="${course_image_url(course)}" alt="${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) |h}" />
|
||||
</a>
|
||||
% else:
|
||||
<div class="cover">
|
||||
<img src="${course_image_url(course)}" alt="${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) | h}" />
|
||||
</div>
|
||||
% endif
|
||||
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">${_('Unregister')}</a>
|
||||
% if settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and modulestore().get_modulestore_type(course.id) == MONGO_MODULESTORE_TYPE:
|
||||
<!-- Only show the Email Settings link/modal if this course has bulk email feature enabled -->
|
||||
<a href="#email-settings-modal" class="email-settings" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}" data-optout="${course.id in course_optouts}">${_('Email Settings')}</a>
|
||||
% endif
|
||||
|
||||
% if enrollment.mode == "verified":
|
||||
<span class="sts-enrollment">
|
||||
<span class="label">${_("Enrolled as: ")}</span>
|
||||
<img class="deco-graphic" src="${static.url('images/vcert-ribbon-s.png')}" alt="ID Verified Ribbon/Badge">
|
||||
<span class="sts-enrollment-value">${_("ID Verified")}</span>
|
||||
</span>
|
||||
% endif
|
||||
|
||||
<section class="info">
|
||||
<hgroup>
|
||||
<p class="date-block">
|
||||
% if course.has_ended():
|
||||
${_("Course Completed - {end_date}").format(end_date=course.end_date_text)}
|
||||
% elif course.has_started():
|
||||
${_("Course Started - {start_date}").format(start_date=course.start_date_text)}
|
||||
% else: # hasn't started yet
|
||||
${_("Course Starts - {start_date}").format(start_date=course.start_date_text)}
|
||||
% endif
|
||||
</p>
|
||||
<h2 class="university">${get_course_about_section(course, 'university')}</h2>
|
||||
<h3>
|
||||
% if course.id in show_courseware_links_for:
|
||||
<a href="${course_target}">${course.display_number_with_default | h} ${course.display_name_with_default}</a>
|
||||
% else:
|
||||
<span>${course.display_number_with_default | h} ${course.display_name_with_default}</span>
|
||||
% endif
|
||||
</h3>
|
||||
</hgroup>
|
||||
|
||||
<%
|
||||
testcenter_exam_info = course.current_test_center_exam
|
||||
registration = exam_registrations.get(course.id)
|
||||
testcenter_register_target = reverse('begin_exam_registration', args=[course.id])
|
||||
%>
|
||||
% if testcenter_exam_info is not None:
|
||||
|
||||
% if registration is None and testcenter_exam_info.is_registering():
|
||||
<div class="message message-status is-shown exam-register">
|
||||
<a href="${testcenter_register_target}" class="button exam-button" id="exam_register_button">${_("Register for Pearson exam")}</a>
|
||||
<p class="message-copy">${_("Registration for the Pearson exam is now open and will close on {end_date}").format(end_date="<strong>{}</strong>".format(testcenter_exam_info.registration_end_date_text))}</p>
|
||||
</div>
|
||||
% endif
|
||||
<!-- display a registration for a current exam, even if the registration period is over -->
|
||||
% if registration is not None:
|
||||
% if registration.is_accepted:
|
||||
<div class="message message-status is-shown exam-schedule">
|
||||
<a href="${registration.registration_signup_url}" class="button exam-button">${_("Schedule Pearson exam")}</a>
|
||||
<p class="exam-registration-number">${_("{link_start}Registration{link_end} number: {number}").format(
|
||||
link_start='<a href="{url}" id="exam_register_link">'.format(url=testcenter_register_target),
|
||||
link_end='</a>',
|
||||
number=registration.client_candidate_id,
|
||||
)}</p>
|
||||
<p class="message-copy">${_("Write this down! You'll need it to schedule your exam.")}</p>
|
||||
</div>
|
||||
% endif
|
||||
% if registration.is_rejected:
|
||||
<div class="message message-status is-shown exam-schedule">
|
||||
<p class="message-copy">
|
||||
<strong>${_("Your registration for the Pearson exam has been rejected. Please {link_start}see your registration status details{link_end}.").format(
|
||||
link_start='<a href="{url}" id="exam_register_link">'.format(url=testcenter_register_target),
|
||||
link_end='</a>')}</strong>
|
||||
${_("Otherwise {link_start}contact edX at {email}{link_end} for further help.").format(
|
||||
link_start='<a class="contact-link" href="mailto:{email}?subject=Pearson VUE Exam - {about} {number}">'.format(email="exam-help@edx.org", about=get_course_about_section(course, 'university'), number=course.display_number_with_default),
|
||||
link_end='</a>',
|
||||
email="exam-help@edx.org",
|
||||
)}
|
||||
</div>
|
||||
% endif
|
||||
% if not registration.is_accepted and not registration.is_rejected:
|
||||
<div class="message message-status is-shown">
|
||||
<p class="message-copy"><strong>${_("Your {link_start}registration for the Pearson exam{link_end} is pending.").format(link_start='<a href="{url}" id="exam_register_link">'.format(url=testcenter_register_target), link_end='</a>')}</strong>
|
||||
${_("Within a few days, you should see a confirmation number here, which can be used to schedule your exam.")}
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
% endif
|
||||
|
||||
<%
|
||||
cert_status = cert_statuses.get(course.id)
|
||||
%>
|
||||
% if course.has_ended() and cert_status:
|
||||
<%
|
||||
if cert_status['status'] == 'generating':
|
||||
status_css_class = 'course-status-certrendering'
|
||||
elif cert_status['status'] == 'ready':
|
||||
status_css_class = 'course-status-certavailable'
|
||||
elif cert_status['status'] == 'notpassing':
|
||||
status_css_class = 'course-status-certnotavailable'
|
||||
else:
|
||||
status_css_class = 'course-status-processing'
|
||||
%>
|
||||
<div class="message message-status ${status_css_class} is-shown">
|
||||
|
||||
% if cert_status['status'] == 'processing':
|
||||
<p class="message-copy">${_("Final course details are being wrapped up at this time. Your final standing will be available shortly.")}</p>
|
||||
% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted'):
|
||||
<p class="message-copy">${_("Your final grade:")}
|
||||
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
|
||||
% if cert_status['status'] == 'notpassing':
|
||||
${_("Grade required for a certificate:")} <span class="grade-value">
|
||||
${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>.
|
||||
% elif cert_status['status'] == 'restricted':
|
||||
<p class="message-copy">
|
||||
${_("Your certificate is being held pending confirmation that the issuance of your certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {email}.").format(email='<a class="contact-link" href="mailto:{email}">{email}</a>.'.format(email=settings.CONTACT_EMAIL))}
|
||||
</p>
|
||||
% endif
|
||||
</p>
|
||||
% endif
|
||||
|
||||
% if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']:
|
||||
<ul class="actions">
|
||||
% if cert_status['show_disabled_download_button']:
|
||||
<li class="action"><span class="disabled">
|
||||
${_("Your Certificate is Generating")}</span></li>
|
||||
% elif cert_status['show_download_url']:
|
||||
<li class="action">
|
||||
<a class="btn" href="${cert_status['download_url']}"
|
||||
title="${_('This link will open/download a PDF document')}">
|
||||
Download Your PDF Certificate</a></li>
|
||||
% endif
|
||||
|
||||
% if cert_status['show_survey_button']:
|
||||
<li class="action"><a class="cta" href="${cert_status['survey_url']}">
|
||||
${_('Complete our course feedback survey')}</a></li>
|
||||
% endif
|
||||
</ul>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
% endif
|
||||
|
||||
% if course.id in show_courseware_links_for:
|
||||
% if course.has_ended():
|
||||
<a href="${course_target}" class="enter-course archived">${_('View Archived Course')}</a>
|
||||
% else:
|
||||
<a href="${course_target}" class="enter-course">${_('View Course')}</a>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
|
||||
|
||||
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">${_('Unregister')}</a>
|
||||
|
||||
% if settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and modulestore().get_modulestore_type(course.id) == MONGO_MODULESTORE_TYPE:
|
||||
<!-- Only show the Email Settings link/modal if this course has bulk email feature enabled -->
|
||||
<a href="#email-settings-modal" class="email-settings" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}" data-optout="${course.id in course_optouts}">${_('Email Settings')}</a>
|
||||
% endif
|
||||
</section>
|
||||
</article>
|
||||
|
||||
|
||||
|
||||
</li>
|
||||
% endfor
|
||||
|
||||
</ul>
|
||||
% else:
|
||||
<section class="empty-dashboard-message">
|
||||
<p>${_("Looks like you haven't registered for any courses yet.")}</p>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
${_("Hi {name}").format(name=order.user.profile.name)}
|
||||
|
||||
${_("Thank you for your order! Payment was successful, and you should be able to see the results on your dashboard.")}
|
||||
${_("Your payment was successful. You will see the charge below on your next credit or debit card statement. The charge will show up on your statement under the company name {platform_name}. If you have billing questions, please read the FAQ or contact {billing_email}. We hope you enjoy your order.").format(platform_name=settings.PLATFORM_NAME,billing_email=settings.PAYMENT_SUPPORT_EMAIL)}
|
||||
|
||||
${_("-The {platform_name} Team").format(platform_name=settings.PLATFORM_NAME)}
|
||||
|
||||
${_("Your order number is: {order_number}").format(order_number=order.id)}
|
||||
|
||||
${_("Items in your order:")}
|
||||
${_("The items in your order are:")}
|
||||
|
||||
${_("Quantity - Description - Price")}
|
||||
%for order_item in order_items:
|
||||
${order_item.qty} - ${order_item.line_desc} - $(order_item.line_cost}
|
||||
${order_item.qty} - ${order_item.line_desc} - ${order_item.line_cost}
|
||||
%endfor
|
||||
${_("Total: {total_cost}").format(total_cost=order.total_cost)}
|
||||
${_("Total billed to credit/debit card: {total_cost}").format(total_cost=order.total_cost)}
|
||||
|
||||
${_("If you have any issues, please contact us at {billing_email}").format(billing_email=settings.PAYMENT_SUPPORT_EMAIL)}
|
||||
%for order_item in order_items:
|
||||
${order_item.additional_instruction_text}
|
||||
%endfor
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</nav>
|
||||
|
||||
<div class="colophon-about">
|
||||
<img src="${MITX_ROOT_URL}/static/images/header-logo.png" />
|
||||
<img src="${MITX_ROOT_URL}/lms/static/images/header-logo.png" />
|
||||
|
||||
<p>${_("{platform_name} is a non-profit created by founding partners {Harvard} and {MIT} whose mission is to bring the best of higher education to students of all ages anywhere in the world, wherever there is Internet access. {platform_name}'s free online MOOCs are interactive and subjects include computer science, public health, and artificial intelligence.").format(platform_name="EdX", Harvard="Harvard", MIT="MIT")}</p>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,9 @@
|
||||
next=u.split("next=")[1];
|
||||
if (next && !isExternal(next)) {
|
||||
location.href=next;
|
||||
} else {
|
||||
} else if(json.redirect_url){
|
||||
location.href=json.redirect_url;
|
||||
} else {
|
||||
location.href="${reverse('dashboard')}";
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
</%def>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--[if lt IE 8]><html class="ie"><![endif]-->
|
||||
<!--[if IE 8]><html class="ie8"><![endif]-->
|
||||
<!--[if gte IE 9]><!--><html><!--<![endif]-->
|
||||
<head>
|
||||
<%block name="title">
|
||||
% if stanford_theme_enabled():
|
||||
@@ -25,6 +27,9 @@
|
||||
## "edX" should not be translated
|
||||
<title>edX</title>
|
||||
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<script type="text/javascript">
|
||||
/* immediately break out of an iframe if coming from the marketing website */
|
||||
(function(window) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -53,7 +53,12 @@
|
||||
|
||||
$('#register-form').on('ajax:success', function(event, json, xhr) {
|
||||
if(json.success) {
|
||||
location.href="${reverse('dashboard')}";
|
||||
if(json.redirect_url){
|
||||
location.href=json.redirect_url;
|
||||
}
|
||||
else {
|
||||
location.href="${reverse('dashboard')}";
|
||||
}
|
||||
} else {
|
||||
toggleSubmitButton(true);
|
||||
$('.status.message.submission-error').addClass('is-shown').focus();
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
<%! from django.conf import settings %>
|
||||
|
||||
<%inherit file="../main.html" />
|
||||
<%block name="bodyclass">register verification-process step-requirements</%block>
|
||||
|
||||
<%block name="title"><title>${_("Receipt for Order")} ${order.id}</title></%block>
|
||||
<%block name="title"><title>${_("Register for [Course Name] | Receipt (Order")} ${order.id})</title></%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
% if notification is not UNDEFINED:
|
||||
<section class="notification">
|
||||
@@ -12,8 +15,15 @@
|
||||
</section>
|
||||
% endif
|
||||
|
||||
<section class="container cart-list">
|
||||
<p><h1>${_(settings.PLATFORM_NAME + " (" + settings.SITE_NAME + ")" + " Electronic Receipt")}</h1></p>
|
||||
<div class="container">
|
||||
<section class="wrapper cart-list">
|
||||
|
||||
<div class="wrapper-content-main">
|
||||
<article class="content-main">
|
||||
<h3 class="title">${_(settings.PLATFORM_NAME + " (" + settings.SITE_NAME + ")" + " Electronic Receipt")}</h3>
|
||||
|
||||
|
||||
|
||||
<h2>${_("Order #")}${order.id}</h2>
|
||||
<h2>${_("Date:")} ${order.purchase_time.date().isoformat()}</h2>
|
||||
<h2>${_("Items ordered:")}</h2>
|
||||
@@ -57,4 +67,6 @@
|
||||
${order.bill_to_country.upper()}<br />
|
||||
</p>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
9
lms/templates/shoppingcart/test/fake_payment_error.html
Normal file
9
lms/templates/shoppingcart/test/fake_payment_error.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Payment Error</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>An error occurred while you submitted your order.
|
||||
If you are trying to make a purchase, please contact the merchant.</p>
|
||||
</body>
|
||||
</html>
|
||||
12
lms/templates/shoppingcart/test/fake_payment_page.html
Normal file
12
lms/templates/shoppingcart/test/fake_payment_page.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<html>
|
||||
<head><title>Payment Form</title></head>
|
||||
<body>
|
||||
<p>Payment page</p>
|
||||
<form name="input" action="${callback_url}" method="post">
|
||||
% for name, value in post_params.items():
|
||||
<input type="hidden" name="${name}" value="${value}">
|
||||
% endfor
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
258
lms/templates/shoppingcart/verified_cert_receipt.html
Normal file
258
lms/templates/shoppingcart/verified_cert_receipt.html
Normal file
@@ -0,0 +1,258 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%! from student.views import course_from_id %>
|
||||
<%! from datetime import datetime %>
|
||||
<%! import pytz %>
|
||||
|
||||
<%inherit file="../main.html" />
|
||||
<%block name="bodyclass">register verification-process step-confirmation</%block>
|
||||
|
||||
<%block name="title"><title>${_("Receipt (Order")} ${order.id})</title></%block>
|
||||
|
||||
<%block name="content">
|
||||
% if notification is not UNDEFINED:
|
||||
<section class="notification">
|
||||
${notification}
|
||||
</section>
|
||||
% endif
|
||||
<% course_id = order_items[0].course_id %>
|
||||
<% course = course_from_id(course_id) %>
|
||||
|
||||
<div class="container">
|
||||
<section class="wrapper cart-list">
|
||||
|
||||
<header class="page-header">
|
||||
<h2 class="title">
|
||||
<span class="wrapper-sts">
|
||||
<span class="sts">${_("You are now registered for")}</span>
|
||||
<span class="sts-course">${course.display_name}</span>
|
||||
</span>
|
||||
<span class="sts-track">
|
||||
<span class="sts-track-value">
|
||||
<span class="context">${_("Registered as: ")}</span> ${_("ID Verified")}
|
||||
</span>
|
||||
</span>
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="wrapper-progress">
|
||||
<section class="progress">
|
||||
<h3 class="sr title">${_("Your Progress")}</h3>
|
||||
|
||||
<ol class="progress-steps">
|
||||
<li class="progress-step is-current" id="progress-step0">
|
||||
<span class="wrapper-step-number"><span class="step-number">0</span></span>
|
||||
<span class="step-name"><span class="sr">${_("Current Step: ")}</span>${_("Intro")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step" id="progress-step1">
|
||||
<span class="wrapper-step-number"><span class="step-number">1</span></span>
|
||||
<span class="step-name">${_("Take Photo")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step" id="progress-step2">
|
||||
<span class="wrapper-step-number"><span class="step-number">2</span></span>
|
||||
<span class="step-name">${_("Take ID Photo")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step" id="progress-step3">
|
||||
<span class="wrapper-step-number"><span class="step-number">3</span></span>
|
||||
<span class="step-name">${_("Review")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step" id="progress-step4">
|
||||
<span class="wrapper-step-number"><span class="step-number">4</span></span>
|
||||
<span class="step-name">${_("Make Payment")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step progress-step-icon" id="progress-step5">
|
||||
<span class="wrapper-step-number"><span class="step-number">
|
||||
<i class="icon-ok"></i>
|
||||
</span></span>
|
||||
<span class="step-name"><span class="sr">${_("Current Step: ")}</span>${_("Confirmation")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<span class="progress-sts">
|
||||
<span class="progress-sts-value"></span>
|
||||
</span>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content-main">
|
||||
<article class="content-main">
|
||||
<h3 class="title">${_("Congratulations! You are now verified on ")} ${_(settings.PLATFORM_NAME)}.</h3>
|
||||
|
||||
<div class="instruction">
|
||||
<p>${_("You are now registered as a verified student! Your registration details are below.")}</p>
|
||||
</div>
|
||||
|
||||
<ul class="list-info">
|
||||
<li class="info-item course-info">
|
||||
<h4 class="title">${_("You are registered for:")}</h4>
|
||||
|
||||
<div class="wrapper-report">
|
||||
<table class="report report-course">
|
||||
<caption class="sr">${_("A list of courses you have just registered for as a verified student")}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" >${_("Course")}</th>
|
||||
<th scope="col" >${_("Status")}</th>
|
||||
<th scope="col" >${_("Options")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
% for item in order_items:
|
||||
<tr>
|
||||
<td>${item.line_desc}</td>
|
||||
<td>
|
||||
${_("Starts: {start_date}").format(start_date=course.start_date_text)}
|
||||
</td>
|
||||
<td class="options">
|
||||
%if course.start > datetime.today().replace(tzinfo=pytz.utc):
|
||||
${_("Starts: {start_date}").format(start_date=course.start_date_text)}
|
||||
%else:
|
||||
<a class="action action-course" href="${reverse('course_root', kwargs={'course_id': item.course_id})}">${_("Go to Course")}</a>
|
||||
%endif
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="course-actions">
|
||||
<td colspan="3">
|
||||
<a class="action action-dashboard" href="${reverse('dashboard')}">${_("Go to your Dashboard")}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="info-item verification-info">
|
||||
<h4 class="title">${_("Verified Status")}</h4>
|
||||
|
||||
<div class="copy">
|
||||
<p>${_("We have received your identification details to verify your identity. If there is a problem with any of the items, we will contact you to resubmit. You can now register for any of the verified certificate courses this semester without having to re-verify.")}</p>
|
||||
|
||||
<p>${_("The professor will ask you to periodically submit a new photo to verify your work during the course (usually at exam times).")}</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="info-item payment-info">
|
||||
<h4 class="title">${_("Payment Details")}</h4>
|
||||
|
||||
<div class="copy">
|
||||
<p>${_("Please print this page for your records; it serves as your receipt. You will also receive an email with the same information.")}</p>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-report">
|
||||
<table class="report report-receipt">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" >${_("Order No.")}</th>
|
||||
<th scope="col" >${_("Description")}</th>
|
||||
<th scope="col" >${_("Date")}</th>
|
||||
<th scope="col" >${_("Description")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
% for item in order_items:
|
||||
<tr>
|
||||
% if item.status == "purchased":
|
||||
<td>${order.id}</td>
|
||||
<td>${item.line_desc}</td>
|
||||
<td>${order.purchase_time.date().isoformat()}</td>
|
||||
<td>${"{0:0.2f}".format(item.line_cost)} (${item.currency.upper()})</td>
|
||||
|
||||
% elif item.status == "refunded":
|
||||
<td><del>${order.id}</del></td>
|
||||
<td><del>${item.line_desc}</del></td>
|
||||
<td><del>${order.purchase_time.date().isoformat()}</del></td>
|
||||
<td><del>${"{0:0.2f}".format(item.line_cost)} (${item.currency.upper()})</del></td>
|
||||
% endif
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th scope="row" class="total-label" colspan="1">${_("Total")}</th>
|
||||
<td claass="total-value" colspan="3">
|
||||
<span class="value-amount">${"{0:0.2f}".format(order.total_cost)} </span>
|
||||
<span class="value-currency">(${item.currency.upper()})</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
% if any_refunds:
|
||||
<div class="msg msg-refunds">
|
||||
<h4 class="title sr">Please Note:</h4>
|
||||
<div class="copy">
|
||||
<p>${_("Note: items with strikethough like ")}<del>${_("this")}</del>${_(" have been refunded.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="info-item billing-info">
|
||||
<h4 class="title">${_("Billing Information")}</h4>
|
||||
|
||||
<div class="wrapper-report">
|
||||
<table class="report report-billing">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">${_("Billed To")}</th>
|
||||
<th scope="col">${_("Billing Address")}</th>
|
||||
<th scope="col">${_("Payment Method Type")}</th>
|
||||
<th scope="col">${_("Payment Method Details")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="billing-to">
|
||||
${order.bill_to_first} ${order.bill_to_last}
|
||||
</td>
|
||||
|
||||
<td class="billing-address">
|
||||
<span class="address-street1">${order.bill_to_street1}</span>
|
||||
<span class="address-street2">${order.bill_to_street2}</span>
|
||||
|
||||
<span class="address-city">${order.bill_to_street2}</span>,
|
||||
<span class="address-state">${order.bill_to_state}</span>
|
||||
<span class="address-postalcode">${order.bill_to_postalcode}</span>
|
||||
|
||||
<span class="address-country">${order.bill_to_country.upper()}</span>
|
||||
</td>
|
||||
|
||||
<td class="billing-methodtype">
|
||||
${order.bill_to_cardtype}
|
||||
</td>
|
||||
<td class="method-details">
|
||||
${order.bill_to_ccnum}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th scope="row" class="total-label" colspan="1">${_("Total")}</th>
|
||||
<td class="total-value" colspan="3"><span class="value-amount">${"{0:0.2f}".format(order.total_cost)}</span> <span class="value-currency">(${item.currency.upper()})</span></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
26
lms/templates/verify_student/_modal_editname.html
Normal file
26
lms/templates/verify_student/_modal_editname.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<section id="edit-name" class="modal">
|
||||
<header>
|
||||
<h4>${_("Edit Your Full Name")}</h4>
|
||||
</header>
|
||||
<form id="course-checklists" class="course-checklists" method="post" action="">
|
||||
<div role="alert" class="status message submission-error" tabindex="-1">
|
||||
<p class="message-title">${_("The following error occured while editing your name:")}
|
||||
<span class="message-copy"> </span>
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
<label for="name">${_('Full Name')}</label>
|
||||
<input id="name" type="text" name="name" value="" placeholder="${user_full_name}" required aria-required="true" />
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<button class="action action-primary action-save">${_("Save")}</button>
|
||||
</div>
|
||||
</form>
|
||||
<a href="#" data-dismiss="leanModal" rel="view" class="action action-close action-editname-close">
|
||||
<i class="icon-remove-sign"></i>
|
||||
<span class="label">${_("close")}</span>
|
||||
</a>
|
||||
</section>
|
||||
15
lms/templates/verify_student/_verification_header.html
Normal file
15
lms/templates/verify_student/_verification_header.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<header class="page-header">
|
||||
<h2 class="title">
|
||||
<span class="wrapper-sts">
|
||||
<span class="sts">${_("You are registering for")}</span>
|
||||
<span class="sts-course">${course_name}</span>
|
||||
</span>
|
||||
<span class="sts-track">
|
||||
<span class="sts-track-value">
|
||||
<span class="context">${_("Registering as: ")}</span> ${_("ID Verified")}
|
||||
</span>
|
||||
</span>
|
||||
</h2>
|
||||
</header>
|
||||
21
lms/templates/verify_student/_verification_support.html
Normal file
21
lms/templates/verify_student/_verification_support.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<div class="wrapper-content-supplementary">
|
||||
<aside class="content-supplementary">
|
||||
<ul class="list-help">
|
||||
<li class="help-item">
|
||||
<h3 class="title">${_("Have questions?")}</h3>
|
||||
<div class="copy">
|
||||
<p>${_("Please read {a_start}our FAQs to view common questions about our certificates{a_end}.").format(a_start='<a rel="external" href="'+ marketing_link('WHAT_IS_VERIFIED_CERT') + '">', a_end="</a>")}</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="help-item">
|
||||
<h3 class="title">${_("Change your mind?")}</h3>
|
||||
<div class="copy">
|
||||
<p>${_("You can always {a_start} audit the course for free {a_end} without verifying.").format(a_start='<a rel="external" href="/course_modes/choose/' + course_id + '">', a_end="</a>")}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div> <!-- /wrapper-content-supplementary -->
|
||||
325
lms/templates/verify_student/face_upload.html
Normal file
325
lms/templates/verify_student/face_upload.html
Normal file
@@ -0,0 +1,325 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="../main.html" />
|
||||
|
||||
<%block name="bodyclass">register verification photos</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<!-- please move link to js/vendor/responsive-carousel/responsive-carousel.js from main.html to here -->
|
||||
<script type="text/javascript">
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
$( ".carousel-nav" ).addClass('sr');
|
||||
|
||||
$('.block-photo .control-redo').addClass('is-hidden');
|
||||
$('.block-photo .control-approve').addClass('is-hidden');
|
||||
$('.block-photo .m-btn-primary').addClass('disabled');
|
||||
|
||||
|
||||
$( "#wrapper-facephoto .control-do" ).click(function(e) {
|
||||
e.preventDefault();
|
||||
$(this).toggleClass('is-hidden');
|
||||
$('#wrapper-facephoto .control-redo').toggleClass('is-shown');
|
||||
$('#wrapper-facephoto .control-approve').toggleClass('is-shown');
|
||||
});
|
||||
|
||||
$( "#wrapper-facephoto .control-approve" ).click(function(e) {
|
||||
e.preventDefault();
|
||||
$(this).addClass('approved');
|
||||
$('#wrapper-facephoto .m-btn-primary').removeClass('disabled');
|
||||
});
|
||||
|
||||
|
||||
$( "#wrapper-idphoto .control-do" ).click(function(e) {
|
||||
e.preventDefault();
|
||||
$(this).toggleClass('is-hidden');
|
||||
$('#wrapper-idphoto .control-redo').toggleClass('is-shown');
|
||||
$('#wrapper-idphoto .control-approve').toggleClass('is-shown');
|
||||
});
|
||||
|
||||
$( "#wrapper-idphoto .control-approve" ).click(function(e) {
|
||||
e.preventDefault();
|
||||
$(this).addClass('approved');
|
||||
$('#wrapper-idphoto .m-btn-primary').removeClass('disabled');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
|
||||
<%block name="content">
|
||||
<div class="container">
|
||||
|
||||
<header class="page-header">
|
||||
<h2 class="title">You are registering for [coursename] (ID Verified)</h2>
|
||||
</header>
|
||||
|
||||
<section class="progress">
|
||||
<h3 class="sr">Your Progress</h3>
|
||||
<ol>
|
||||
<li class="progress-step current" id="progress-step1"><span class="sr">Current: </span>Step 1 Take Your Photo</li>
|
||||
<li class="progress-step" id="progress-step2">Step 2 ID Photo</li>
|
||||
<li class="progress-step" id="progress-step3">Step 3 Review</li>
|
||||
<li class="progress-step" id="progress-step4">Step 4 Payment</li>
|
||||
<li class="progress-step" id="progress-step5">Finished Confirmation</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<section class="wrapper carousel" data-transition="slide">
|
||||
<div id="wrapper-facephoto" class="block block-photo">
|
||||
<h3 class="title">Take Your Photo</h3>
|
||||
<p>Use your webcam to take a picture of your face so we can match it with the picture on your ID.</p>
|
||||
|
||||
<div class="wrapper-up">
|
||||
|
||||
<div id="facecam" class="cam">
|
||||
<div class="placeholder-cam">
|
||||
<!-- cam image -->
|
||||
<p>cam image</p>
|
||||
</div>
|
||||
|
||||
<div class="controls photo-controls">
|
||||
<ul class="controls-list">
|
||||
<li class="control control-redo">
|
||||
<a class="action action-redo" href=""><i class="icon-undo"></i> <span class="sr">Retake</span></a>
|
||||
</li>
|
||||
<li class="control control-do">
|
||||
<a class="action action-do" href=""><i class="icon-camera"></i> <span class="sr">Take photo</span></a>
|
||||
</li>
|
||||
<li class="control control-approve">
|
||||
<a class="action action-approve" href=""><i class="icon-ok"></i> <span class="sr">Looks good</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="photo-tips facetips">
|
||||
<h4>Tips on taking a successful photo</h4>
|
||||
<ul>
|
||||
<li>Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.</li>
|
||||
<li>Maecenas faucibus mollis interdum.</li>
|
||||
<li>Nullam id dolor id nibh ultricies vehicula ut id elit.</li>
|
||||
<li>Cras mattis consectetur purus sit amet fermentum.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-down">
|
||||
<div class="faq facefaq">
|
||||
<h4 class="sr">Common Questions</h4>
|
||||
<dl>
|
||||
<dt>Why do you need my photo?</dt>
|
||||
<dd>We need your photo to confirm that you are you.</dd>
|
||||
|
||||
<dt>What do you do with this picture?</dt>
|
||||
<dd>We only use it to verify your identity. It is not displayed anywhere.</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="next-step">
|
||||
<p class="m-btn-primary" id="face_next_button">
|
||||
<a class="next" href="#next" aria-hidden="true" title="Next">Go to Step 2: Take ID Photo</a>
|
||||
</p>
|
||||
<p class="tip">Once you verify your photo looks good, you can move on to step 2.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div id="wrapper-idphoto" class="block block-photo">
|
||||
<h3 class="title">Take Your Photo</h3>
|
||||
<p>Use your webcam to take a picture of your face so we can match it with the picture on your ID.</p>
|
||||
|
||||
<div class="wrapper-up">
|
||||
|
||||
<div id="idcam" class="cam">
|
||||
|
||||
<div class="placeholder-cam">
|
||||
<!-- cam image -->
|
||||
<p>cam image</p>
|
||||
</div>
|
||||
|
||||
<div class="controls photo-controls">
|
||||
<ul class="controls-list">
|
||||
<li class="control control-redo">
|
||||
<a class="action action-redo" href=""><i class="icon-undo"></i> <span class="sr">Retake</span></a>
|
||||
</li>
|
||||
<li class="control control-do">
|
||||
<a class="action action-do" href=""><i class="icon-camera"></i> <span class="sr">Take photo</span></a>
|
||||
</li>
|
||||
<li class="control control-approve">
|
||||
<a class="action action-approve" href=""><i class="icon-ok"></i> <span class="sr">Looks good</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="photo-tips idtips">
|
||||
<h4>Tips on taking a successful photo</h4>
|
||||
<ul>
|
||||
<li>Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.</li>
|
||||
<li>Maecenas faucibus mollis interdum.</li>
|
||||
<li>Nullam id dolor id nibh ultricies vehicula ut id elit.</li>
|
||||
<li>Cras mattis consectetur purus sit amet fermentum.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="wrapper-down">
|
||||
|
||||
<div class="faq idfaq">
|
||||
<h4 class="sr">Common Questions</h4>
|
||||
<dl class="faq">
|
||||
<dt>Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.</dt>
|
||||
<dd>Aenean eu leo quam.</dd>
|
||||
<dt>Pellentesque ornare sem lacinia quam venenatis vestibulum.</dt>
|
||||
<dd>Maecenas faucibus mollis interdum.</dd>
|
||||
<dt>Cras justo odio, dapibus ac facilisis in, egestas eget quam.</dt>
|
||||
<dd>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</dd>
|
||||
<dt>Vestibulum id ligula porta felis euismod semper.</dt>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="next-step">
|
||||
<p class="m-btn-primary">
|
||||
<a class="next" href="#next" aria-hidden="true" title="Next">Go to Step 3: Review Your Info</a>
|
||||
</p>
|
||||
<p class="tip">Once you verify your ID photo looks good, you can move on to step 3.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="wrapper-review" class="block block-photo">
|
||||
<h3 class="title">Verify Your Submission</h3>
|
||||
<p>Make sure we can verify your identity with the photos and information below.</p>
|
||||
|
||||
<div class="review-name">
|
||||
<h3>Check Your Name</h3>
|
||||
<p>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.</p>
|
||||
<p><a href="#">Edit my name</a></p>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-up">
|
||||
<div id="review-facephoto" class="review-photo">
|
||||
<div class="placeholder-photo">
|
||||
<!-- photo image -->
|
||||
</div>
|
||||
|
||||
<h4>The photo above needs to meet the following requirements:</h4>
|
||||
<ul>
|
||||
<li>Be well lit</li>
|
||||
<li>Show your whole face</li>
|
||||
<li>Match your ID</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="review-idphoto" class="review-photo">
|
||||
<div class="placeholder-photo">
|
||||
<!-- photo image -->
|
||||
</div>
|
||||
|
||||
<h4>The photo above needs to meet the following requirements:</h4>
|
||||
<ul>
|
||||
<li>Be readable (not too far away, no glare)</li>
|
||||
<li>Show your name</li>
|
||||
<li>Match the photo of your face and your name above</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="wrapper-down">
|
||||
<h3>Check Your Contribution</h3>
|
||||
|
||||
<dl>
|
||||
<dt>
|
||||
Select your contribution for this course:
|
||||
</dt>
|
||||
<dd>
|
||||
<ul class="pay-options">
|
||||
<li>
|
||||
<input type="radio" id="contribution-25" name="contribution"> <label for="contribution-25">$25 USD</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="radio" id="contribution-50" name="contribution"> <label for="contribution-50">$50 USD</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="radio" id="contribution-100" name="contribution"> <label for="contribution-100">$100 USD</label>
|
||||
</li>
|
||||
<li class="other1">
|
||||
<input type="radio" id="contribution-other" name="contribution"> <label for="contribution-other"><span class="sr">Other</span></label>
|
||||
</li>
|
||||
<li class="other2">
|
||||
<label for="contribution-other-amt"><span class="sr">Other Amount</span> $</label> <input type="text" size="5" name="contribution-other-amt" id="contribution-other-amt">
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="next-step">
|
||||
|
||||
<p>Photos don't meet the requirements? <a href="">Retake the photos</a>.</p>
|
||||
<input type="checkbox" name="match" /> <label for="match">Yes! My details all match.</label>
|
||||
|
||||
<p class="m-btn-primary"><a href="#">Go to Step 4: Secure Payment</a></p>
|
||||
<p class="tip">Once you verify your details match the requirements, you can move on to step 4, payment on our secure server.</p>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<section class="support">
|
||||
<p class="tip">More questions? <a rel="external" href="">Check out our FAQs.</a></p>
|
||||
<p class="tip">Change your mind? <a href="">You can always Audit the course for free without verifying.</a></p>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<section id="edit-name" class="modal">
|
||||
<header>
|
||||
<h4>${_("Edit Your Full Name")}</h4>
|
||||
</header>
|
||||
<form id="course-checklists" class="course-checklists" method="post" action="">
|
||||
<div role="alert" class="status message submission-error" tabindex="-1">
|
||||
<p class="message-title">${_("The following error occured while editing your name:")}
|
||||
<span class="message-copy"> </span>
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
<label for="name">${_('Full Name')}</label>
|
||||
<input id="name" type="text" name="name" value="" placeholder="${_('example: Jane Doe')}" required aria-required="true" />
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<button class="action action-primary" type="submit">${_("Save")}</button>
|
||||
<button class="action action-secondary action-cancel">${_("Cancel")}</button>
|
||||
</div>
|
||||
</form>
|
||||
<a href="#" data-dismiss="modal" rel="view" class="action action-close action-editname-close">
|
||||
<i class="icon-remove-sign"></i>
|
||||
<span class="label">close</span>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
|
||||
</%block>
|
||||
10
lms/templates/verify_student/final_verification.html
Normal file
10
lms/templates/verify_student/final_verification.html
Normal file
@@ -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!
|
||||
|
||||
|
||||
</%block>
|
||||
145
lms/templates/verify_student/photo_id_upload.html
Normal file
145
lms/templates/verify_student/photo_id_upload.html
Normal file
@@ -0,0 +1,145 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="../main.html" />
|
||||
|
||||
<%block name="bodyclass">register verification select-track</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript">
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
$( ".more" ).slideUp();
|
||||
|
||||
$( ".expand" ).click(function(e) {
|
||||
e.preventDefault();
|
||||
$(this).next().slideToggle();
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="container">
|
||||
<section class="wrapper">
|
||||
|
||||
<header class="page-header header-white">
|
||||
<h2 class="title header-white-title">You are registering for [coursename] (ID Verified)</h2>
|
||||
</header>
|
||||
|
||||
<h3 class="title">Select your track:</h3>
|
||||
|
||||
<div class="select">
|
||||
<div class="block block-audit">
|
||||
<div class="wrap-copy">
|
||||
<h4 class="title">Audit This Course</h4>
|
||||
<p>Sign up to audit this course for free and track your own progress.</p>
|
||||
</div>
|
||||
|
||||
<div class="wrap-action">
|
||||
<p class="m-btn-primary">
|
||||
<a href="">Select Audit</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"><p>or</p></div>
|
||||
|
||||
<div class="select">
|
||||
<div class="block block-cert">
|
||||
<h4 class="title">Certificate of Achievement</h4>
|
||||
<span class="ribbon"></span>
|
||||
<p>Sign up as a verified student and work toward a Certificate of Achievement.</p>
|
||||
|
||||
<form>
|
||||
<dl>
|
||||
<dt>
|
||||
Select your contribution for this course:
|
||||
</dt>
|
||||
<dd>
|
||||
<ul class="pay-options">
|
||||
<li>
|
||||
<input type="radio" id="contribution-25" name="contribution"> <label for="contribution-25">$25 USD</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="radio" id="contribution-50" name="contribution"> <label for="contribution-50">$50 USD</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="radio" id="contribution-100" name="contribution"> <label for="contribution-100">$100 USD</label>
|
||||
</li>
|
||||
<li class="other1">
|
||||
<input type="radio" id="contribution-other" name="contribution"> <label for="contribution-other">$<span class="sr">Other</span></label>
|
||||
</li>
|
||||
<li class="other2">
|
||||
<label for="contribution-other-amt"><span class="sr">Other Amount</span> </label> <input type="text" size="5" name="contribution-other-amt" id="contribution-other-amt">
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<p class="tip tip-input expand">
|
||||
<a href="">Why do I have to pay? What if I don't meet all the requirements?</a>
|
||||
</p>
|
||||
|
||||
<div class="more">
|
||||
<dl class="faq">
|
||||
<dt>Why do I have to pay?</dt>
|
||||
<dd>Your payment helps cover the costs of verification. As a non-profit, edX keeps these costs as low as possible, Your payment will also help edX with our mission to provide quality education to anyone.</dd>
|
||||
<dt>What if I can't afford it?</dt>
|
||||
<dd>If you cannot afford the minimum payment, you can always work towards a free Honor Code Certificate of Achievement for this course.
|
||||
<!--Enter $0 above and explain why you would like the fee waived below. Then click Select Certificate button to move on to the next step.
|
||||
<dl>
|
||||
<dt><label class="sr" for="explain">Explain your situation:</label></dt>
|
||||
<dd><p>Tell us why you need help paying for this course in 180 characters or more.</p>
|
||||
<textarea name="explain" rows="5" cols="50"></textarea>
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
-->
|
||||
|
||||
</dd>
|
||||
|
||||
<dt>I'd like to pay more than the minimum. Is my contribution tax deductible?</dt>
|
||||
<dd>Please check with your tax advisor to determine whether your contribution is tax deductible.</dd>
|
||||
|
||||
<dt>What if I don't meet all of the requirements for financial assistance but I still want to work toward a certificate?</dt>
|
||||
<dd>If you don't have a webcam, credit or debit card or acceptable ID, you can opt to simply audit this course, or select to work towards a free Honor Code Certificate of Achievement for this course by checking the box below. Then click the Select Certificate button to complete registration. We won't ask you to verify your identity.
|
||||
<p><input type="checkbox" name="honor-code"> <label for="honor-code">Select Honor Code Certificate</label></p>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
<hr />
|
||||
|
||||
<p class="tip">
|
||||
<a href="">What is an ID Verified Certificate?</a>
|
||||
</p>
|
||||
|
||||
<p class="m-btn-primary green">
|
||||
<a href="${reverse('verify_student_show_requirements')}">Select Certificate</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tips">
|
||||
<p>
|
||||
To register for a Verified Certificate of Achievement option, you will need a webcam, a credit or debit card, and an ID. <a href="">View requirements</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<p class="tip"><i class="icon-question-sign"></i> Have questions? <a href="">Check out our FAQs.</a></p>
|
||||
<p class="tip">Not the course you wanted? <a href="">Return to our course listings</a>.</p>
|
||||
|
||||
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</%block>
|
||||
357
lms/templates/verify_student/photo_verification.html
Normal file
357
lms/templates/verify_student/photo_verification.html
Normal file
@@ -0,0 +1,357 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="../main.html" />
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
<%block name="bodyclass">register verification-process step-photos</%block>
|
||||
<%block name="title"><title>${_("Register for {} | Verification").format(course_name)}</title></%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.js')}"></script>
|
||||
<script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.keybd.js')}"></script>
|
||||
<script src="${static.url('js/verify_student/photocapture.js')}"></script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="container">
|
||||
<section class="wrapper">
|
||||
|
||||
<%include file="_verification_header.html" args="course_name=course_name" />
|
||||
|
||||
<div class="wrapper-progress">
|
||||
<section class="progress">
|
||||
<h3 class="sr title">${_("Your Progress")}</h3>
|
||||
|
||||
<!-- FIXME: Move the "Current Step: " text to the right DOM element -->
|
||||
<ol class="progress-steps">
|
||||
<li class="progress-step is-completed" id="progress-step0">
|
||||
<span class="wrapper-step-number"><span class="step-number">0</span></span>
|
||||
<span class="step-name">${_("Intro")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step is-current" id="progress-step1">
|
||||
<span class="wrapper-step-number"><span class="step-number">1</span></span>
|
||||
<span class="step-name"><span class="sr">${_("Current Step: ")}</span>${_("Take Photo")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step" id="progress-step2">
|
||||
<span class="wrapper-step-number"><span class="step-number">2</span></span>
|
||||
<span class="step-name">${_("Take ID Photo")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step" id="progress-step3">
|
||||
<span class="wrapper-step-number"><span class="step-number">3</span></span>
|
||||
<span class="step-name">${_("Review")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step" id="progress-step4">
|
||||
<span class="wrapper-step-number"><span class="step-number">4</span></span>
|
||||
<span class="step-name">${_("Make Payment")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step progress-step-icon" id="progress-step5">
|
||||
<span class="wrapper-step-number"><span class="step-number">
|
||||
<i class="icon-ok"></i>
|
||||
</span></span>
|
||||
<span class="step-name">${_("Confirmation")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<span class="progress-sts">
|
||||
<span class="progress-sts-value"></span>
|
||||
</span>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content-main">
|
||||
<article class="content-main">
|
||||
|
||||
<section class="wrapper carousel" data-transition="slide">
|
||||
<div id="wrapper-facephoto" class="wrapper-view block-photo">
|
||||
<div class="facephoto view">
|
||||
<h3 class="title">${_("Take Your Photo")}</h3>
|
||||
<div class="instruction">
|
||||
<p>${_("Use your webcam to take a picture of your face so we can match it with the picture on your ID.")}</p>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-task">
|
||||
<div id="facecam" class="task cam">
|
||||
<div class="placeholder-cam" id="face_capture_div">
|
||||
|
||||
<div class="placeholder-art">
|
||||
<p class="copy">${_("Don't see your picture? Make sure to allow your browser to use your camera when it asks for permission. <br />Still not working? You can {a_start} audit the course {a_end} without verifying.").format(a_start='<a rel="external" href="/course_modes/choose/' + course_id + '">', a_end="</a>")}</p>
|
||||
</div>
|
||||
|
||||
<video id="face_video" autoplay></video><br/>
|
||||
<canvas id="face_canvas" style="display:none;" width="640" height="480"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="controls photo-controls">
|
||||
<ul class="list-controls">
|
||||
<li class="control control-redo" id="face_reset_button">
|
||||
<a class="action action-redo" href="">
|
||||
<i class="icon-undo"></i> <span class="sr">${_("Retake")}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="control control-do" id="face_capture_button">
|
||||
<a class="action action-do" href="">
|
||||
<i class="icon-camera"></i><span class="sr">${_("Take photo")}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="control control-approve" id="face_approve_button">
|
||||
<a class="action action-approve" href="">
|
||||
<i class="icon-ok"></i> <span class="sr">${_("Looks good")}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-help">
|
||||
<div class="help help-task photo-tips facetips">
|
||||
<h4 class="title">${_("Tips on taking a successful photo")}</h4>
|
||||
|
||||
<div class="copy">
|
||||
<ul class="list-help">
|
||||
<li class="help-item">${_("Make sure your face is well-lit")}</li>
|
||||
<li class="help-item">${_("Be sure your entire face is inside the frame")}</li>
|
||||
<li class="help-item">${_("Can we match the photo you took with the one on your ID?")}</li>
|
||||
<li class="help-item">${_("Click the checkmark once you are happy with the photo")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help help-faq facefaq">
|
||||
<h4 class="sr title">${_("Common Questions")}</h4>
|
||||
|
||||
<div class="copy">
|
||||
<dl class="list-faq">
|
||||
<dt class="faq-question">${_("Why do you need my photo?")}</dt>
|
||||
<dd class="faq-answer">${_("As part of the verification process, we need your photo to confirm that you are you.")}</dd>
|
||||
|
||||
<dt class="faq-question">${_("What do you do with this picture?")}</dt>
|
||||
<dd class="faq-answer">${_("We only use it to verify your identity. It is not displayed anywhere.")}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wizard"> <!-- FIXME: Additional class is-ready, is-not-ready -->
|
||||
<span class="help help-inline">${_("Once you verify your photo looks good, you can move on to step 2.")}</span>
|
||||
|
||||
<ol class="wizard-steps">
|
||||
<li class="wizard-step">
|
||||
<a class="next action-primary" id="face_next_button" href="#next" aria-hidden="true" title="Next">${_("Go to Step 2: Take ID Photo")}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
</div> <!-- /view -->
|
||||
</div> <!-- /wrapper-view -->
|
||||
|
||||
<div id="wrapper-idphoto" class="wrapper-view block-photo">
|
||||
<div class="idphoto view">
|
||||
<h3 class="title">${_("Show Us Your ID")}</h3>
|
||||
<div class="instruction">
|
||||
<p>${_("Use your webcam to take a picture of your ID so we can match it with your photo and the name on your account.")}</p>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-task">
|
||||
<div id="idcam" class="task cam">
|
||||
<div class="placeholder-cam" id="photo_id_capture_div">
|
||||
|
||||
<div class="placeholder-art">
|
||||
<p class="copy">${_("Don't see your picture? Make sure to allow your browser to use your camera when it asks for permission. Still not working? You can {a_start} audit the course {a_end} without verifying.").format(a_start='<a rel="external" href="/course_modes/choose/' + course_id + '">', a_end="</a>")}</p>
|
||||
</div>
|
||||
|
||||
<video id="photo_id_video" autoplay></video><br/>
|
||||
<canvas id="photo_id_canvas" style="display:none;" width="640" height="480"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="controls photo-controls">
|
||||
<ul class="list-controls">
|
||||
<li class="control control-redo" id="photo_id_reset_button">
|
||||
<a class="action action-redo" href="">
|
||||
<i class="icon-undo"></i> <span class="sr">${_("Retake")}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="control control-do" id="photo_id_capture_button">
|
||||
<a class="action action-do" href="">
|
||||
<i class="icon-camera"></i> <span class="sr">${_("Take photo")}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="control control-approve" id="photo_id_approve_button">
|
||||
<a class="action action-approve" href="">
|
||||
<i class="icon-ok"></i> <span class="sr">${_("Looks good")}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-help">
|
||||
<div class="help help-task photo-tips idtips">
|
||||
<h4 class="title">${_("Tips on taking a successful photo")}</h4>
|
||||
|
||||
<div class="copy">
|
||||
<ul class="list-help">
|
||||
<li class="help-item">${_("Make sure your ID is well-lit")}</li>
|
||||
<li class="help-item">${_("Check that there isn't any glare")}</li>
|
||||
<li class="help-item">${_("Ensure that you can see your photo and read your name")}</li>
|
||||
<li class="help-item">${_("Try to keep your fingers at the edge to avoid covering important information")}</li>
|
||||
<li class="help-item">${_("Acceptable IDs include drivers licenses, passports, or other goverment-issued IDs that include your name and photo")}</li>
|
||||
<li class="help-item">${_("Click the checkmark once you are happy with the photo")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help help-faq facefaq">
|
||||
<h4 class="sr title">${_("Common Questions")}</h4>
|
||||
|
||||
<div class="copy">
|
||||
<dl class="list-faq">
|
||||
<dt class="faq-question">${_("Why do you need a photo of my ID?")}</dt>
|
||||
<dd class="faq-answer">${_("We need to match your ID with your photo and name to confirm that you are you.")}</dd>
|
||||
|
||||
<dt class="faq-question">${_("What do you do with this picture?")}</dt>
|
||||
<dd class="faq-answer">${_("We encrypt it and send it to our secure authorization service for review. We use the highest levels of security and do not save the photo or information anywhere once the match has been completed.")}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wizard">
|
||||
<span class="help help-inline">${_("Once you verify your ID photo looks good, you can move on to step 3.")}</span>
|
||||
|
||||
<ol class="wizard-steps">
|
||||
<li class="wizard-step">
|
||||
<a class="next action-primary" id="photo_id_next_button" href="#next" aria-hidden="true" title="Next">${_("Go to Step 3: Review Your Info")}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div> <!-- /view -->
|
||||
</div> <!-- /wrapper-view -->
|
||||
|
||||
<div id="wrapper-review" class="wrapper-view">
|
||||
<div class="review view">
|
||||
<h3 class="title">${_("Verify Your Submission")}</h3>
|
||||
<div class="instruction">
|
||||
<p>${_("Make sure we can verify your identity with the photos and information below.")}</p>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-task">
|
||||
<ol class="review-tasks">
|
||||
<li class="review-task review-task-name">
|
||||
<h4 class="title">${_("Check Your Name")}</h4>
|
||||
|
||||
<div class="copy">
|
||||
<p>${_("Make sure your full name on your edX account ({full_name}) matches your ID. We will also use this as the name on your certificate.").format(full_name="<span id='full-name'>" + user_full_name + "</span>")}</p>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action action-editname">
|
||||
<a class="edit-name" rel="leanModal" href="#edit-name">${_("Edit your name")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="review-task review-task-photos">
|
||||
<h4 class="title">${_("Review the Photos You've Taken")}</h4>
|
||||
|
||||
<div class="copy">
|
||||
<p>${_("Please review the photos and verify that they meet the requirements listed below.")}</p>
|
||||
</div>
|
||||
|
||||
<ol class="wrapper-photos">
|
||||
<li class="wrapper-photo">
|
||||
<div class="placeholder-photo">
|
||||
<img id="face_image" src=""/>
|
||||
</div>
|
||||
|
||||
<div class="help-tips">
|
||||
<h5 class="title">${_("The photo above needs to meet the following requirements:")}</h5>
|
||||
<ul class="list-help list-tips copy">
|
||||
<li class="tip">${_("Be well lit")}</li>
|
||||
<li class="tip">${_("Show your whole face")}</li>
|
||||
<li class="tip">${_("The photo on your ID must match the photo of your face")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="wrapper-photo">
|
||||
<div class="placeholder-photo">
|
||||
<img id="photo_id_image" src=""/>
|
||||
</div>
|
||||
|
||||
<div class="help-tips">
|
||||
<h5 class="title">${_("The photo above needs to meet the following requirements:")}</h5>
|
||||
<ul class="list-help list-tips copy">
|
||||
<li class="tip">${_("Be readable (not too far away, no glare)")}</li>
|
||||
<li class="tip">${_("The photo on your ID must match the photo of your face")}</li>
|
||||
<li class="tip">${_("The name on your ID must match the name on your account above")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="msg msg-retake msg-followup">
|
||||
<div class="copy">
|
||||
<p>${_("Photos don't meet the requirements?")}</p>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action action-retakephotos">
|
||||
<a class="retake-photos" href="javascript:void(0);" onclick="document.location.reload(true);">${_("Retake Your Photos")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="review-task review-task-contribution">
|
||||
<h4 class="title">${_("Check Your Contribution Level")}</h4>
|
||||
|
||||
<div class="copy">
|
||||
<p>${_("Please confirm your contribution for this course (min. $")} ${min_price} <span class="denomination-name">${currency}</span>${_("):")}</p>
|
||||
</div>
|
||||
|
||||
<%include file="/course_modes/_contribution.html" args="suggested_prices=suggested_prices, currency=currency, chosen_price=chosen_price, min_price=min_price"/>
|
||||
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wizard">
|
||||
<span class="help help-inline">${_("Once you verify your details match the requirements, you can move on to step 4, payment on our secure server.")}</span>
|
||||
|
||||
<ol class="wizard-steps">
|
||||
<li class="wizard-step step-match">
|
||||
<input type="checkbox" name="match" id="confirm_pics_good" />
|
||||
<label for="confirm_pics_good">${_("Yes! My details all match.")}</label>
|
||||
</li>
|
||||
<li class="wizard-step step-proceed">
|
||||
<form id="pay_form" method="post" action="${purchase_endpoint}">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
<input type="hidden" name="course_id" value="${course_id | h}" />
|
||||
<input class="action-primary disabled" type="button" id="pay_button" value="Go to Step 4: Secure Payment" name="payment">
|
||||
</form>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div> <!-- /view -->
|
||||
</div> <!-- /wrapper-view -->
|
||||
</section>
|
||||
</article>
|
||||
</div> <!-- /wrapper-content-main -->
|
||||
|
||||
<%include file="_verification_support.html" />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<%include file="_modal_editname.html" />
|
||||
</%block>
|
||||
156
lms/templates/verify_student/show_requirements.html
Normal file
156
lms/templates/verify_student/show_requirements.html
Normal file
@@ -0,0 +1,156 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="../main.html" />
|
||||
<%block name="bodyclass">register verification-process step-requirements</%block>
|
||||
<%block name="title"><title>${_("Register for {}").format(course_name)}</title></%block>
|
||||
|
||||
<%block name="content">
|
||||
%if is_not_active:
|
||||
<div class="wrapper-msg wrapper-msg-activate">
|
||||
<div class=" msg msg-activate">
|
||||
<i class="msg-icon icon-warning-sign"></i>
|
||||
<div class="msg-content">
|
||||
<h3 class="title">${_("You need to activate your edX account before proceeding")}</h3>
|
||||
<div class="copy">
|
||||
<p>${_("Please check your email for further instructions on activating your new account.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
<div class="container">
|
||||
<section class="wrapper">
|
||||
|
||||
<%include file="_verification_header.html" args="course_name=course_name"/>
|
||||
|
||||
<div class="wrapper-progress">
|
||||
<section class="progress">
|
||||
<h3 class="sr title">${_("Your Progress")}</h3>
|
||||
|
||||
<ol class="progress-steps">
|
||||
<li class="progress-step is-current" id="progress-step0">
|
||||
<span class="wrapper-step-number"><span class="step-number">0</span></span>
|
||||
<span class="step-name"><span class="sr">${_("Current Step: ")}</span>${_("Intro")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step" id="progress-step1">
|
||||
<span class="wrapper-step-number"><span class="step-number">1</span></span>
|
||||
<span class="step-name">${_("Take Photo")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step" id="progress-step2">
|
||||
<span class="wrapper-step-number"><span class="step-number">2</span></span>
|
||||
<span class="step-name">${_("Take ID Photo")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step" id="progress-step3">
|
||||
<span class="wrapper-step-number"><span class="step-number">3</span></span>
|
||||
<span class="step-name">${_("Review")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step" id="progress-step4">
|
||||
<span class="wrapper-step-number"><span class="step-number">4</span></span>
|
||||
<span class="step-name">${_("Make Payment")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step progress-step-icon" id="progress-step5">
|
||||
<span class="wrapper-step-number"><span class="step-number">
|
||||
<i class="icon-ok"></i>
|
||||
</span></span>
|
||||
<span class="step-name">${_("Confirmation")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<span class="progress-sts">
|
||||
<span class="progress-sts-value"></span>
|
||||
</span>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="wrapper-content-main">
|
||||
<article class="content-main">
|
||||
<h3 class="title">${_("What You Will Need to Register")}</h3>
|
||||
|
||||
<div class="instruction">
|
||||
<p>${_("There are three things you will need to register as an ID verified student:")}</p>
|
||||
</div>
|
||||
|
||||
<ul class="list-reqs ${"account-not-activated" if is_not_active else ""}">
|
||||
%if is_not_active:
|
||||
<li class="req req-0 req-activate">
|
||||
<h4 class="title">${_("Activate Your Account")}</h4>
|
||||
<div class="placeholder-art">
|
||||
<i class="icon-envelope-alt"></i>
|
||||
</div>
|
||||
|
||||
<div class="copy">
|
||||
<p>
|
||||
<span class="copy-super">${_("Check Your Email")}</span>
|
||||
<span class="copy-sub">${_("you need an active edX account before registering - check your email for instructions")}</span>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
%endif
|
||||
|
||||
<li class="req req-1 req-id">
|
||||
<h4 class="title">${_("Identification")}</h4>
|
||||
<div class="placeholder-art">
|
||||
<i class="icon-list-alt icon-under"></i>
|
||||
<i class="icon-user icon-over"></i>
|
||||
</div>
|
||||
|
||||
<div class="copy">
|
||||
<p>
|
||||
<span class="copy-super">${_("A photo identification document")}</span>
|
||||
<span class="copy-sub">${_("a drivers license, passport, or other goverment-issued ID with your name and picture on it")}</span>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="req req-2 req-webcam">
|
||||
<h4 class="title">${_("Webcam")}</h4>
|
||||
<div class="placeholder-art">
|
||||
<i class="icon-facetime-video"></i>
|
||||
</div>
|
||||
|
||||
<div class="copy">
|
||||
<p>
|
||||
<span class="copy-super">${_("A webcam and a modern browser")}</span>
|
||||
<span class="copy-sub">${_("Firefox, Chrome, Safari, IE9+")}</span>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="req req-3 req-payment">
|
||||
<h4 class="title">${_("Credit or Debit Card")}</h4>
|
||||
<div class="placeholder-art">
|
||||
<i class="icon-credit-card"></i>
|
||||
</div>
|
||||
|
||||
<div class="copy">
|
||||
<p>
|
||||
<span class="copy-super">${_("A major credit or debit card")}</span>
|
||||
<span class="copy-sub">${_("Visa, Master Card, American Express, Discover, Diners Club, JCB with Discover logo")}</span>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<nav class="nav-wizard ${"is-not-ready" if is_not_active else "is-ready"}">
|
||||
<span class="help help-inline">${_("Missing something? You can always {a_start} audit this course instead {a_end}").format(a_start='<a href="/course_modes/choose/' + course_id + '">', a_end="</a>")}</span>
|
||||
|
||||
<ol class="wizard-steps">
|
||||
<li class="wizard-step">
|
||||
<a class="next action-primary ${"disabled" if is_not_active else ""}" id="face_next_button" href="${reverse('verify_student_verify', kwargs={'course_id': course_id})}">${_("Go to Step 1: Take my Photo")}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</article>
|
||||
</div> <!-- /wrapper-content-main -->
|
||||
|
||||
<%include file="_verification_support.html" />
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
106
lms/templates/verify_student/verified.html
Normal file
106
lms/templates/verify_student/verified.html
Normal file
@@ -0,0 +1,106 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="../main.html" />
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
<%block name="bodyclass">register verification-process is-verified</%block>
|
||||
<%block name="title"><title>${_("Register for {} | Verification").format(course_name)}</title></%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript">
|
||||
var submitToPaymentProcessing = function(event) {
|
||||
event.preventDefault();
|
||||
var xhr = $.post(
|
||||
"/verify_student/create_order",
|
||||
{
|
||||
"course_id" : "${course_id}",
|
||||
},
|
||||
function(data) {
|
||||
for (prop in data) {
|
||||
$('<input>').attr({
|
||||
type: 'hidden',
|
||||
name: prop,
|
||||
value: data[prop]
|
||||
}).appendTo('#pay_form');
|
||||
}
|
||||
}
|
||||
)
|
||||
.done(function(data) {
|
||||
$("#pay_form").submit();
|
||||
})
|
||||
.fail(function(jqXhr,text_status, error_thrown) { alert(jqXhr.responseText); });
|
||||
}
|
||||
$(document).ready(function() {
|
||||
$("#pay_button").click(submitToPaymentProcessing);
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="container">
|
||||
<section class="wrapper">
|
||||
|
||||
<%include file="_verification_header.html" />
|
||||
|
||||
<div class="wrapper-progress">
|
||||
<section class="progress">
|
||||
<h3 class="sr title">${_("Your Progress")}</h3>
|
||||
|
||||
<ol class="progress-steps">
|
||||
<li class="progress-step is-completed" id="progress-step1">
|
||||
<span class="wrapper-step-number"><span class="step-number">1</span></span>
|
||||
<span class="step-name">${_("ID Verification")}</span>
|
||||
</li>
|
||||
<li class="progress-step is-current" id="progress-step2">
|
||||
<span class="wrapper-step-number"><span class="step-number">2</span></span>
|
||||
<span class="step-name"><span class="sr">${_("Current Step: ")}</span>${_("Review")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step" id="progress-step3">
|
||||
<span class="wrapper-step-number"><span class="step-number">3</span></span>
|
||||
<span class="step-name">${_("Make Payment")}</span>
|
||||
</li>
|
||||
|
||||
<li class="progress-step progress-step-icon" id="progress-step4">
|
||||
<span class="wrapper-step-number"><span class="step-number">
|
||||
<i class="icon-ok"></i>
|
||||
</span></span>
|
||||
<span class="step-name">${_("Confirmation")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<span class="progress-sts">
|
||||
<span class="progress-sts-value"></span>
|
||||
</span>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content-main">
|
||||
<article class="content-main">
|
||||
<h3 class="title">${_("You've Been Verified Previously")}</h3>
|
||||
|
||||
<div class="instruction">
|
||||
<p>${_("We've already verified your identity (through the photos of you and your ID you provided earlier). You can proceed to make your secure payment and complete registration.")}</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wizard is-ready">
|
||||
|
||||
<span class="help help-inline price-value">${_("You have decided to pay $ ")} <strong>${chosen_price}</strong></span>
|
||||
|
||||
<ol class="wizard-steps">
|
||||
<li class="wizard-step step-proceed">
|
||||
<form id="pay_form" method="post" action="${purchase_endpoint}">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
<input type="hidden" name="course_id" value="${course_id | h}" />
|
||||
<button type="submit" class="action-primary" id="pay_button">Go to Secure Payment</button>
|
||||
</form>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</article>
|
||||
</div> <!-- /wrapper-content-main -->
|
||||
|
||||
<%include file="_verification_support.html" />
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -58,8 +58,16 @@ 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')),
|
||||
url(r'^course_modes/', include('course_modes.urls')),
|
||||
)
|
||||
|
||||
|
||||
js_info_dict = {
|
||||
'domain': 'djangojs',
|
||||
'packages': ('lms',),
|
||||
@@ -338,6 +346,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<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard$',
|
||||
|
||||
@@ -18,6 +18,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
|
||||
@@ -60,7 +61,6 @@ South==0.7.6
|
||||
sympy==0.7.1
|
||||
xmltodict==0.4.1
|
||||
django-ratelimit-backend==0.6
|
||||
django-model-utils==1.4.0
|
||||
|
||||
# Used for debugging
|
||||
ipython==0.13.1
|
||||
|
||||
Reference in New Issue
Block a user