Commit 34d0fe15 by David Ormsbee

Merge pull request #890 from edx/ormsbee/verifyuser3

User verification / validated certificates feature
parents 0052b87c d3454e58
...@@ -86,3 +86,10 @@ INSTALLED_APPS += ('lettuce.django',) ...@@ -86,3 +86,10 @@ INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('contentstore',) LETTUCE_APPS = ('contentstore',)
LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535) LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535)
LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome') 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 = ( ...@@ -374,6 +374,7 @@ INSTALLED_APPS = (
'course_modes' 'course_modes'
) )
################# EDX MARKETING SITE ################################## ################# EDX MARKETING SITE ##################################
EDXMKTG_COOKIE_NAME = 'edxloggedin' EDXMKTG_COOKIE_NAME = 'edxloggedin'
......
...@@ -54,6 +54,10 @@ class CourseMode(models.Model): ...@@ -54,6 +54,10 @@ class CourseMode(models.Model):
return modes return modes
@classmethod @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): def mode_for_course(cls, course_id, mode_slug):
""" """
Returns the mode for the course corresponding to mode_slug. Returns the mode for the course corresponding to mode_slug.
...@@ -67,3 +71,8 @@ class CourseMode(models.Model): ...@@ -67,3 +71,8 @@ class CourseMode(models.Model):
return matched[0] return matched[0]
else: else:
return None return None
def __unicode__(self):
return u"{} : {}, min={}, prices={}".format(
self.course_id, self.mode_slug, self.min_price, self.suggested_prices
)
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'
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"),
)
# 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)
...@@ -844,6 +844,23 @@ class CourseEnrollment(models.Model): ...@@ -844,6 +844,23 @@ class CourseEnrollment(models.Model):
return False return False
@classmethod @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): def enrollments_for_user(cls, user):
return CourseEnrollment.objects.filter(user=user, is_active=1) return CourseEnrollment.objects.filter(user=user, is_active=1)
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
Student Views Student Views
""" """
import datetime import datetime
import feedparser
import json import json
import logging import logging
import random import random
...@@ -27,22 +26,20 @@ from django.db import IntegrityError, transaction ...@@ -27,22 +26,20 @@ from django.db import IntegrityError, transaction
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, Http404 from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.utils.http import cookie_date from django.utils.http import cookie_date, base36_to_int, urlencode
from django.utils.http import base36_to_int
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from ratelimitbackend.exceptions import RateLimitException from ratelimitbackend.exceptions import RateLimitException
from mitxmako.shortcuts import render_to_response, render_to_string 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, from student.models import (Registration, UserProfile, TestCenterUser, TestCenterUserForm,
TestCenterRegistration, TestCenterRegistrationForm, TestCenterRegistration, TestCenterRegistrationForm,
PendingNameChange, PendingEmailChange, PendingNameChange, PendingEmailChange,
CourseEnrollment, unique_id_for_user, CourseEnrollment, unique_id_for_user,
get_testcenter_registration, CourseEnrollmentAllowed) get_testcenter_registration, CourseEnrollmentAllowed)
from student.forms import PasswordResetFormNoActive from student.forms import PasswordResetFormNoActive
from certificates.models import CertificateStatuses, certificate_status_for_student from certificates.models import CertificateStatuses, certificate_status_for_student
...@@ -269,7 +266,7 @@ def dashboard(request): ...@@ -269,7 +266,7 @@ def dashboard(request):
courses = [] courses = []
for enrollment in CourseEnrollment.enrollments_for_user(user): for enrollment in CourseEnrollment.enrollments_for_user(user):
try: try:
courses.append(course_from_id(enrollment.course_id)) courses.append((course_from_id(enrollment.course_id), enrollment))
except ItemNotFoundError: except ItemNotFoundError:
log.error("User {0} enrolled in non-existent course {1}" log.error("User {0} enrolled in non-existent course {1}"
.format(user.username, enrollment.course_id)) .format(user.username, enrollment.course_id))
...@@ -288,12 +285,12 @@ def dashboard(request): ...@@ -288,12 +285,12 @@ def dashboard(request):
staff_access = True staff_access = True
errored_courses = modulestore().get_errored_courses() 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')) 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 # get info w.r.t ExternalAuthMap
external_auth_map = None external_auth_map = None
...@@ -335,10 +332,13 @@ def try_change_enrollment(request): ...@@ -335,10 +332,13 @@ def try_change_enrollment(request):
enrollment_response.content enrollment_response.content
) )
) )
if enrollment_response.content != '':
return enrollment_response.content
except Exception, e: except Exception, e:
log.exception("Exception automatically enrolling after login: {0}".format(str(e))) log.exception("Exception automatically enrolling after login: {0}".format(str(e)))
@require_POST
def change_enrollment(request): def change_enrollment(request):
""" """
Modify the enrollment status for the logged-in user. Modify the enrollment status for the logged-in user.
...@@ -356,18 +356,16 @@ def change_enrollment(request): ...@@ -356,18 +356,16 @@ def change_enrollment(request):
as a post-login/registration helper, so the error messages in the responses as a post-login/registration helper, so the error messages in the responses
should never actually be user-visible. should never actually be user-visible.
""" """
if request.method != "POST":
return HttpResponseNotAllowed(["POST"])
user = request.user user = request.user
if not user.is_authenticated():
return HttpResponseForbidden()
action = request.POST.get("enrollment_action") action = request.POST.get("enrollment_action")
course_id = request.POST.get("course_id") course_id = request.POST.get("course_id")
if course_id is None: if course_id is None:
return HttpResponseBadRequest(_("Course id not specified")) return HttpResponseBadRequest(_("Course id not specified"))
if not user.is_authenticated():
return HttpResponseForbidden()
if action == "enroll": if action == "enroll":
# Make sure the course exists # Make sure the course exists
# We don't do this check on unenroll, or a bad course id can't be unenrolled from # 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): ...@@ -381,6 +379,14 @@ def change_enrollment(request):
if not has_access(user, course, 'enroll'): if not has_access(user, course, 'enroll'):
return HttpResponseBadRequest(_("Enrollment is closed")) 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("/") org, course_num, run = course_id.split("/")
statsd.increment("common.student.enrollment", statsd.increment("common.student.enrollment",
tags=["org:{0}".format(org), tags=["org:{0}".format(org),
...@@ -463,10 +469,10 @@ def login_user(request, error=""): ...@@ -463,10 +469,10 @@ def login_user(request, error=""):
log.exception(e) log.exception(e)
raise raise
try_change_enrollment(request) redirect_url = try_change_enrollment(request)
statsd.increment("common.student.successful_login") 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 # set the login cookie for the edx marketing site
# we want this cookie to be accessed via javascript # we want this cookie to be accessed via javascript
...@@ -732,14 +738,14 @@ def create_account(request, post_override=None): ...@@ -732,14 +738,14 @@ def create_account(request, post_override=None):
login_user.save() login_user.save()
AUDIT_LOG.info(u"Login activated on extauth account - {0} ({1})".format(login_user.username, login_user.email)) 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") statsd.increment("common.student.account_created")
js = {'success': True} response_params = {'success': True,
HttpResponse(json.dumps(js), mimetype="application/json") '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 # set the login cookie for the edx marketing site
# we want this cookie to be accessed via javascript # we want this cookie to be accessed via javascript
......
...@@ -5,6 +5,7 @@ and integration / BDD tests. ...@@ -5,6 +5,7 @@ and integration / BDD tests.
''' '''
import student.tests.factories as sf import student.tests.factories as sf
import xmodule.modulestore.tests.factories as xf import xmodule.modulestore.tests.factories as xf
import course_modes.tests.factories as cmf
from lettuce import world from lettuce import world
...@@ -52,6 +53,14 @@ class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowedFactory): ...@@ -52,6 +53,14 @@ class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowedFactory):
@world.absorb @world.absorb
class CourseModeFactory(cmf.CourseModeFactory):
"""
Course modes
"""
pass
@world.absorb
class CourseFactory(xf.CourseFactory): class CourseFactory(xf.CourseFactory):
""" """
Courseware courses Courseware courses
......
<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>
<%! 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>
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
#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'
from datetime import datetime
import pytz import pytz
import logging import logging
import smtplib import smtplib
from datetime import datetime import textwrap
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.mail import send_mail
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.db import transaction from django.db import transaction
from model_utils.managers import InheritanceManager 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 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.views import course_from_id
from student.models import CourseEnrollment from student.models import CourseEnrollment
from statsd import statsd 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") log = logging.getLogger("shoppingcart")
...@@ -116,6 +118,7 @@ class Order(models.Model): ...@@ -116,6 +118,7 @@ class Order(models.Model):
self.bill_to_ccnum = ccnum self.bill_to_ccnum = ccnum
self.bill_to_cardtype = cardtype self.bill_to_cardtype = cardtype
self.processor_reply_dump = processor_reply_dump self.processor_reply_dump = processor_reply_dump
# save these changes on the order, then we can tell when we are in an # save these changes on the order, then we can tell when we are in an
# inconsistent state # inconsistent state
self.save() self.save()
...@@ -124,6 +127,7 @@ class Order(models.Model): ...@@ -124,6 +127,7 @@ class Order(models.Model):
orderitems = OrderItem.objects.filter(order=self).select_subclasses() orderitems = OrderItem.objects.filter(order=self).select_subclasses()
for item in orderitems: for item in orderitems:
item.purchase_item() item.purchase_item()
# send confirmation e-mail # send confirmation e-mail
subject = _("Order Payment Confirmation") subject = _("Order Payment Confirmation")
message = render_to_string('emails/order_confirmation_email.txt', { message = render_to_string('emails/order_confirmation_email.txt', {
...@@ -195,6 +199,30 @@ class OrderItem(models.Model): ...@@ -195,6 +199,30 @@ class OrderItem(models.Model):
""" """
raise NotImplementedError 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): class PaidCourseRegistration(OrderItem):
""" """
...@@ -311,6 +339,13 @@ class CertificateItem(OrderItem): ...@@ -311,6 +339,13 @@ class CertificateItem(OrderItem):
course_enrollment = CourseEnrollment.objects.get(user=order.user, course_id=course_id) course_enrollment = CourseEnrollment.objects.get(user=order.user, course_id=course_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
course_enrollment = CourseEnrollment.create_enrollment(order.user, course_id, mode=mode) 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( item, _created = cls.objects.get_or_create(
order=order, order=order,
user=order.user, user=order.user,
...@@ -321,8 +356,9 @@ class CertificateItem(OrderItem): ...@@ -321,8 +356,9 @@ class CertificateItem(OrderItem):
item.status = order.status item.status = order.status
item.qty = 1 item.qty = 1
item.unit_cost = cost item.unit_cost = cost
item.line_desc = _("{mode} certificate for course {course_id}").format(mode=item.mode, course_name = course_from_id(course_id).display_name
course_id=course_id) item.line_desc = _("Certificate of Achievement, {mode_name} for course {course}").format(mode_name=mode_info.name,
course=course_name)
item.currency = currency item.currency = currency
order.currency = currency order.currency = currency
order.save() order.save()
...@@ -336,3 +372,17 @@ class CertificateItem(OrderItem): ...@@ -336,3 +372,17 @@ class CertificateItem(OrderItem):
self.course_enrollment.mode = self.mode self.course_enrollment.mode = self.mode
self.course_enrollment.save() self.course_enrollment.save()
self.course_enrollment.activate() 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 ...@@ -97,13 +97,19 @@ def verify_signatures(params, signed_fields_key='signedFields', full_sig_key='si
if processor_hash(data) != returned_sig: if processor_hash(data) != returned_sig:
raise CCProcessorSignatureException() raise CCProcessorSignatureException()
def render_purchase_form_html(cart): def render_purchase_form_html(cart):
""" """
Renders the HTML of the hidden POST form that must be used to initiate a purchase with CyberSource 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 total_cost = cart.total_cost
amount = "{0:0.2f}".format(total_cost) amount = "{0:0.2f}".format(total_cost)
cart_items = cart.orderitem_set.all() cart_items = cart.orderitem_set.all()
...@@ -112,13 +118,11 @@ def render_purchase_form_html(cart): ...@@ -112,13 +118,11 @@ def render_purchase_form_html(cart):
params['currency'] = cart.currency params['currency'] = cart.currency
params['orderPage_transactionType'] = 'sale' params['orderPage_transactionType'] = 'sale'
params['orderNumber'] = "{0:d}".format(cart.id) params['orderNumber'] = "{0:d}".format(cart.id)
signed_param_dict = sign(params)
return render_to_string('shoppingcart/cybersource_form.html', { return params
'action': purchase_endpoint,
'params': signed_param_dict,
})
def get_purchase_endpoint():
return settings.CC_PROCESSOR['CyberSource'].get('PURCHASE_ENDPOINT', '')
def payment_accepted(params): def payment_accepted(params):
""" """
......
"""
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 ...@@ -4,11 +4,12 @@ Tests for the Shopping Cart Models
from factory import DjangoModelFactory from factory import DjangoModelFactory
from mock import patch from mock import patch
from django.test import TestCase
from django.test.utils import override_settings
from django.core import mail from django.core import mail
from django.conf import settings from django.conf import settings
from django.db import DatabaseError 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.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
...@@ -19,50 +20,54 @@ from course_modes.models import CourseMode ...@@ -19,50 +20,54 @@ from course_modes.models import CourseMode
from shoppingcart.exceptions import PurchasedCallbackException from shoppingcart.exceptions import PurchasedCallbackException
class OrderTest(TestCase): @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class OrderTest(ModuleStoreTestCase):
def setUp(self): def setUp(self):
self.user = UserFactory.create() 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 self.cost = 40
def test_get_cart_for_user(self): def test_get_cart_for_user(self):
# create a cart # create a cart
cart = Order.get_cart_for_user(user=self.user) cart = Order.get_cart_for_user(user=self.user)
# add something to it # 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 # should return the same cart
cart2 = Order.get_cart_for_user(user=self.user) cart2 = Order.get_cart_for_user(user=self.user)
self.assertEquals(cart2.orderitem_set.count(), 1) self.assertEquals(cart2.orderitem_set.count(), 1)
def test_cart_clear(self): def test_cart_clear(self):
cart = Order.get_cart_for_user(user=self.user) 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')
CertificateItem.add_to_order(cart, 'test/course1', self.cost, 'verified') CertificateItem.add_to_order(cart, 'org/test/Test_Course_1', self.cost, 'honor')
self.assertEquals(cart.orderitem_set.count(), 2) self.assertEquals(cart.orderitem_set.count(), 2)
cart.clear() cart.clear()
self.assertEquals(cart.orderitem_set.count(), 0) self.assertEquals(cart.orderitem_set.count(), 0)
def test_add_item_to_cart_currency_match(self): def test_add_item_to_cart_currency_match(self):
cart = Order.get_cart_for_user(user=self.user) 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 # verify that a new item has been added
self.assertEquals(cart.orderitem_set.count(), 1) self.assertEquals(cart.orderitem_set.count(), 1)
# verify that the cart's currency was updated # verify that the cart's currency was updated
self.assertEquals(cart.currency, 'eur') self.assertEquals(cart.currency, 'eur')
with self.assertRaises(InvalidCartItem): 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 # assert that this item did not get added to the cart
self.assertEquals(cart.orderitem_set.count(), 1) self.assertEquals(cart.orderitem_set.count(), 1)
def test_total_cost(self): def test_total_cost(self):
cart = Order.get_cart_for_user(user=self.user) cart = Order.get_cart_for_user(user=self.user)
# add items to the order # add items to the order
course_costs = [('test/course1', 30), course_costs = [('org/test/Test_Course_1', 30),
('test/course2', 40), ('org/test/Test_Course_2', 40),
('test/course3', 10), ('org/test/Test_Course_3', 10),
('test/course4', 20)] ('org/test/Test_Course_4', 20)]
for course, cost in course_costs: 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.orderitem_set.count(), len(course_costs))
self.assertEquals(cart.total_cost, sum(cost for _course, cost in course_costs)) self.assertEquals(cart.total_cost, sum(cost for _course, cost in course_costs))
...@@ -72,7 +77,7 @@ class OrderTest(TestCase): ...@@ -72,7 +77,7 @@ class OrderTest(TestCase):
# CertificateItem, which is not quite good unit test form. Sorry. # CertificateItem, which is not quite good unit test form. Sorry.
cart = Order.get_cart_for_user(user=self.user) cart = Order.get_cart_for_user(user=self.user)
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id)) 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 # course enrollment object should be created but still inactive
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id)) self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id))
cart.purchase() cart.purchase()
...@@ -83,12 +88,13 @@ class OrderTest(TestCase): ...@@ -83,12 +88,13 @@ class OrderTest(TestCase):
self.assertEquals('Order Payment Confirmation', mail.outbox[0].subject) self.assertEquals('Order Payment Confirmation', mail.outbox[0].subject)
self.assertIn(settings.PAYMENT_SUPPORT_EMAIL, mail.outbox[0].body) self.assertIn(settings.PAYMENT_SUPPORT_EMAIL, mail.outbox[0].body)
self.assertIn(unicode(cart.total_cost), 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): def test_purchase_item_failure(self):
# once again, we're testing against the specific implementation of # once again, we're testing against the specific implementation of
# CertificateItem # CertificateItem
cart = Order.get_cart_for_user(user=self.user) 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 patch('shoppingcart.models.CertificateItem.save', side_effect=DatabaseError):
with self.assertRaises(DatabaseError): with self.assertRaises(DatabaseError):
cart.purchase() cart.purchase()
...@@ -99,7 +105,7 @@ class OrderTest(TestCase): ...@@ -99,7 +105,7 @@ class OrderTest(TestCase):
def purchase_with_data(self, cart): def purchase_with_data(self, cart):
""" purchase a cart with billing information """ """ 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( cart.purchase(
first='John', first='John',
last='Smith', last='Smith',
...@@ -145,6 +151,7 @@ class OrderTest(TestCase): ...@@ -145,6 +151,7 @@ class OrderTest(TestCase):
self.assertEqual(cart.bill_to_ccnum, '') self.assertEqual(cart.bill_to_ccnum, '')
self.assertEqual(cart.bill_to_cardtype, '') self.assertEqual(cart.bill_to_cardtype, '')
class OrderItemTest(TestCase): class OrderItemTest(TestCase):
def setUp(self): def setUp(self):
self.user = UserFactory.create() self.user = UserFactory.create()
...@@ -222,14 +229,26 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): ...@@ -222,14 +229,26 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id)) 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 Tests for verifying specific CertificateItem functionality
""" """
def setUp(self): def setUp(self):
self.user = UserFactory.create() self.user = UserFactory.create()
self.course_id = "test/course" self.course_id = "org/test/Test_Course"
self.cost = 40 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): def test_existing_enrollment(self):
CourseEnrollment.enroll(self.user, self.course_id) CourseEnrollment.enroll(self.user, self.course_id)
...@@ -240,3 +259,14 @@ class CertificateItemTest(TestCase): ...@@ -240,3 +259,14 @@ class CertificateItemTest(TestCase):
cart.purchase() cart.purchase()
enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_id) enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_id)
self.assertEquals(enrollment.mode, u'verified') 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')
"""
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): ...@@ -50,6 +50,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
mode_display_name="honor cert", mode_display_name="honor cert",
min_price=self.cost) min_price=self.cost)
self.course_mode.save() 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) self.cart = Order.get_cart_for_user(self.user)
def login_user(self): def login_user(self):
...@@ -63,14 +65,14 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -63,14 +65,14 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
PaidCourseRegistration.add_to_order(self.cart, self.course_id) PaidCourseRegistration.add_to_order(self.cart, self.course_id)
self.login_user() self.login_user()
resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id])) 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) 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): def test_add_course_to_cart_already_registered(self):
CourseEnrollment.enroll(self.user, self.course_id) CourseEnrollment.enroll(self.user, self.course_id)
self.login_user() self.login_user()
resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id])) 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) self.assertIn(_('You are already registered in course {0}.'.format(self.course_id)), resp.content)
def test_add_nonexistent_course_to_cart(self): def test_add_nonexistent_course_to_cart(self):
...@@ -91,7 +93,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -91,7 +93,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
def test_show_cart(self): def test_show_cart(self):
self.login_user() self.login_user()
reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) 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=[])) resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[]))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
...@@ -110,7 +112,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -110,7 +112,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
def test_clear_cart(self): def test_clear_cart(self):
self.login_user() self.login_user()
PaidCourseRegistration.add_to_order(self.cart, self.course_id) 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) self.assertEquals(self.cart.orderitem_set.count(), 2)
resp = self.client.post(reverse('shoppingcart.views.clear_cart', args=[])) resp = self.client.post(reverse('shoppingcart.views.clear_cart', args=[]))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
...@@ -120,7 +122,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -120,7 +122,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
def test_remove_item(self, exception_log): def test_remove_item(self, exception_log):
self.login_user() self.login_user()
reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) 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) self.assertEquals(self.cart.orderitem_set.count(), 2)
resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]), resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]),
{'id': reg_item.id}) {'id': reg_item.id})
...@@ -166,7 +168,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -166,7 +168,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
def test_show_receipt_404s(self): def test_show_receipt_404s(self):
PaidCourseRegistration.add_to_order(self.cart, self.course_id) 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() self.cart.purchase()
user2 = UserFactory.create() user2 = UserFactory.create()
...@@ -184,7 +186,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -184,7 +186,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
@patch('shoppingcart.views.render_to_response', render_mock) @patch('shoppingcart.views.render_to_response', render_mock)
def test_show_receipt_success(self): def test_show_receipt_success(self):
reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) 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.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
self.login_user() self.login_user()
...@@ -196,14 +198,14 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -196,14 +198,14 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
((template, context), _) = render_mock.call_args ((template, context), _) = render_mock.call_args
self.assertEqual(template, 'shoppingcart/receipt.html') self.assertEqual(template, 'shoppingcart/receipt.html')
self.assertEqual(context['order'], self.cart) self.assertEqual(context['order'], self.cart)
self.assertIn(reg_item.orderitem_ptr, context['order_items']) self.assertIn(reg_item, context['order_items'])
self.assertIn(cert_item.orderitem_ptr, context['order_items']) self.assertIn(cert_item, context['order_items'])
self.assertFalse(context['any_refunds']) self.assertFalse(context['any_refunds'])
@patch('shoppingcart.views.render_to_response', render_mock) @patch('shoppingcart.views.render_to_response', render_mock)
def test_show_receipt_success_refund(self): def test_show_receipt_success_refund(self):
reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) 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.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
cert_item.status = "refunded" cert_item.status = "refunded"
cert_item.save() cert_item.save()
...@@ -213,9 +215,20 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -213,9 +215,20 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertIn('40.00', resp.content) 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(template, 'shoppingcart/receipt.html')
self.assertEqual(context['order'], self.cart) self.assertEqual(context['order'], self.cart)
self.assertIn(reg_item.orderitem_ptr, context['order_items']) self.assertIn(reg_item, context['order_items'])
self.assertIn(cert_item.orderitem_ptr, context['order_items']) self.assertIn(cert_item, context['order_items'])
self.assertTrue(context['any_refunds']) 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']: ...@@ -13,3 +13,10 @@ if settings.MITX_FEATURES['ENABLE_SHOPPING_CART']:
url(r'^remove_item/$', 'remove_item'), url(r'^remove_item/$', 'remove_item'),
url(r'^add/course/(?P<course_id>[^/]+/[^/]+/[^/]+)/$', 'add_course_to_cart', name='add_course_to_cart'), 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())
)
import logging 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.utils.translation import ugettext as _
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -19,9 +20,10 @@ def add_course_to_cart(request, course_id): ...@@ -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')) return HttpResponseForbidden(_('You must be logged-in to add to a shopping cart'))
cart = Order.get_cart_for_user(request.user) cart = Order.get_cart_for_user(request.user)
if PaidCourseRegistration.part_of_order(cart, course_id): 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): 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: try:
PaidCourseRegistration.add_to_order(cart, course_id) PaidCourseRegistration.add_to_order(cart, course_id)
except ItemNotFoundError: except ItemNotFoundError:
...@@ -98,8 +100,18 @@ def show_receipt(request, ordernum): ...@@ -98,8 +100,18 @@ def show_receipt(request, ordernum):
if order.user != request.user or order.status != 'purchased': if order.user != request.user or order.status != 'purchased':
raise Http404('Order not found!') 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) any_refunds = any(i.status == "refunded" for i in order_items)
return render_to_response('shoppingcart/receipt.html', {'order': order, receipt_template = 'shoppingcart/receipt.html'
'order_items': order_items, # we want to have the ability to override the default receipt page when
'any_refunds': any_refunds}) # 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)
# -*- 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']
\ No newline at end of file
"""
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)
# -*- 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")
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)
"""
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()))
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"
),
)
"""
"""
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 ...@@ -19,6 +19,7 @@ import logging
logging.disable(logging.ERROR) logging.disable(logging.ERROR)
import os import os
from random import choice, randint from random import choice, randint
import string
def seed(): def seed():
...@@ -94,6 +95,23 @@ MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True ...@@ -94,6 +95,23 @@ MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
# Use the auto_auth workflow for creating users and logging them in # Use the auto_auth workflow for creating users and logging them in
MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True 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 # HACK
# Setting this flag to false causes imports to not load correctly in the lettuce python files # 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 # We do not yet understand why this occurs. Setting this to true is a stopgap measure
...@@ -107,3 +125,10 @@ INSTALLED_APPS += ('lettuce.django',) ...@@ -107,3 +125,10 @@ INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('courseware',) LETTUCE_APPS = ('courseware',)
LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535) LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535)
LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome') 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 = { ...@@ -161,11 +161,14 @@ MITX_FEATURES = {
# basis in Studio) # basis in Studio)
'ENABLE_CHAT': False, '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 # Toggle the availability of the shopping cart page
'ENABLE_SHOPPING_CART': False, 'ENABLE_SHOPPING_CART': False,
# Toggle storing detailed billing information # Toggle storing detailed billing information
'STORE_BILLING_INFO': False 'STORE_BILLING_INFO': False,
} }
# Used for A/B testing # Used for A/B testing
...@@ -822,7 +825,10 @@ INSTALLED_APPS = ( ...@@ -822,7 +825,10 @@ INSTALLED_APPS = (
'notification_prefs', 'notification_prefs',
# Different Course Modes # Different Course Modes
'course_modes' 'course_modes',
# Student Identity Verification
'verify_student',
) )
######################### MARKETING SITE ############################### ######################### MARKETING SITE ###############################
...@@ -837,6 +843,9 @@ MKTG_URL_LINK_MAP = { ...@@ -837,6 +843,9 @@ MKTG_URL_LINK_MAP = {
'TOS': 'tos', 'TOS': 'tos',
'HONOR': 'honor', 'HONOR': 'honor',
'PRIVACY': 'privacy_edx', 'PRIVACY': 'privacy_edx',
# Verified Certificates
'WHAT_IS_VERIFIED_CERT' : 'verified-certificate',
} }
...@@ -867,6 +876,11 @@ def enable_theme(theme_name): ...@@ -867,6 +876,11 @@ def enable_theme(theme_name):
STATICFILES_DIRS.append((u'themes/%s' % theme_name, STATICFILES_DIRS.append((u'themes/%s' % theme_name,
theme_root / 'static')) theme_root / 'static'))
################# Student Verification #################
VERIFY_STUDENT = {
"DAYS_GOOD_FOR" : 365, # How many days is a verficiation good for?
}
######################## CAS authentication ########################### ######################## CAS authentication ###########################
if MITX_FEATURES.get('AUTH_USE_CAS'): if MITX_FEATURES.get('AUTH_USE_CAS'):
......
...@@ -28,9 +28,9 @@ MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True ...@@ -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_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard)
MITX_FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True MITX_FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True
MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True
MITX_FEATURES['ENABLE_INSTRUCTOR_BETA_DASHBOARD'] = True MITX_FEATURES['ENABLE_INSTRUCTOR_BETA_DASHBOARD'] = True
MITX_FEATURES['MULTIPLE_ENROLLMENT_ROLES'] = True
MITX_FEATURES['ENABLE_SHOPPING_CART'] = True MITX_FEATURES['ENABLE_SHOPPING_CART'] = True
FEEDBACK_SUBMISSION_EMAIL = "dummy@example.com" FEEDBACK_SUBMISSION_EMAIL = "dummy@example.com"
......
...@@ -155,6 +155,26 @@ OPENID_UPDATE_DETAILS_FROM_SREG = True ...@@ -155,6 +155,26 @@ OPENID_UPDATE_DETAILS_FROM_SREG = True
OPENID_USE_AS_ADMIN_LOGIN = False OPENID_USE_AS_ADMIN_LOGIN = False
OPENID_PROVIDER_TRUSTED_ROOTS = ['*'] 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 ######################################
CELERY_ALWAYS_EAGER = True CELERY_ALWAYS_EAGER = True
......
/*! Responsive Carousel - v0.1.0 - 2013-07-15
* https://github.com/filamentgroup/responsive-carousel
* Copyright (c) 2013 Filament Group, Inc.; Licensed MIT, GPL */
(function(e){var t="carousel",n="."+t,r="data-transition",i=t+"-transitioning",s=t+"-item",o=t+"-active",u=t+"-item-prev",a=t+"-item-next",f=t+"-in",l=t+"-out",c=t+"-nav",h=function(){var e="webkit Moz O Ms".split(" "),t=!1,n;while(e.length){n=e.shift()+"Transition";if(n in document.documentElement.style!==undefined&&n in document.documentElement.style!=0){t=!0;break}}return t}(),p={_create:function(){e(this).trigger("beforecreate."+t)[t]("_init")[t]("_addNextPrev").trigger("create."+t)},_init:function(){var n=e(this).attr(r);n||(h=!1),e(this).addClass(t+" "+(n?t+"-"+n:"")+" ").children().addClass(s).first().addClass(o),e(this)[t]("_addNextPrevClasses")},_addNextPrevClasses:function(){var t=e(this).find("."+s),n=t.filter("."+o),r=n.next("."+s),i=n.prev("."+s);r.length||(r=t.first().not("."+o)),i.length||(i=t.last().not("."+o)),t.removeClass(u+" "+a),i.addClass(u),r.addClass(a)},next:function(){e(this)[t]("goTo","+1")},prev:function(){e(this)[t]("goTo","-1")},goTo:function(n){var i=e(this),u=i.attr(r),a=" "+t+"-"+u+"-reverse";e(this).find("."+s).removeClass([l,f,a].join(" "));var c=e(this).find("."+o),p=c.index(),d=(p<0?0:p)+1,v=typeof n=="number"?n:d+parseFloat(n),m=e(this).find(".carousel-item").eq(v-1),g=typeof n=="string"&&!parseFloat(n)||v>d?"":a;m.length||(m=e(this).find("."+s)[g.length?"last":"first"]()),h?i[t]("_transitionStart",c,m,g):(m.addClass(o),i[t]("_transitionEnd",c,m,g)),i.trigger("goto."+t,m)},update:function(){return e(this).children().not("."+c).addClass(s),e(this).trigger("update."+t)},_transitionStart:function(n,r,i){var s=e(this);r.one(navigator.userAgent.indexOf("AppleWebKit")>-1?"webkitTransitionEnd":"transitionend otransitionend",function(){s[t]("_transitionEnd",n,r,i)}),e(this).addClass(i),n.addClass(l),r.addClass(f)},_transitionEnd:function(n,r,i){e(this).removeClass(i),n.removeClass(l+" "+o),r.removeClass(f).addClass(o),e(this)[t]("_addNextPrevClasses")},_bindEventListeners:function(){var n=e(this).bind("click",function(r){var i=e(r.target).closest("a[href='#next'],a[href='#prev']");i.length&&(n[t](i.is("[href='#next']")?"next":"prev"),r.preventDefault())});return this},_addNextPrev:function(){return e(this).append("<nav class='"+c+"'><a href='#prev' class='prev' aria-hidden='true' title='Previous'>Prev</a><a href='#next' class='next' aria-hidden='true' title='Next'>Next</a></nav>")[t]("_bindEventListeners")},destroy:function(){}};e.fn[t]=function(n,r,i,s){return this.each(function(){if(n&&typeof n=="string")return e.fn[t].prototype[n].call(this,r,i,s);if(e(this).data(t+"data"))return e(this);e(this).data(t+"active",!0),e.fn[t].prototype._create.call(this)})},e.extend(e.fn[t].prototype,p)})(jQuery),function(e){var t="carousel",n="."+t,r=t+"-no-transition",i=/iPhone|iPad|iPod/.test(navigator.platform)&&navigator.userAgent.indexOf("AppleWebKit")>-1,s={_dragBehavior:function(){var t=e(this),s,o={},u,a,f=function(t){var r=t.touches||t.originalEvent.touches,i=e(t.target).closest(n);t.type==="touchstart"&&(s={x:r[0].pageX,y:r[0].pageY}),r[0]&&r[0].pageX&&(o.touches=r,o.deltaX=r[0].pageX-s.x,o.deltaY=r[0].pageY-s.y,o.w=i.width(),o.h=i.height(),o.xPercent=o.deltaX/o.w,o.yPercent=o.deltaY/o.h,o.srcEvent=t)},l=function(t){f(t),o.touches.length===1&&e(t.target).closest(n).trigger("drag"+t.type.split("touch")[1],o)};e(this).bind("touchstart",function(t){e(this).addClass(r),l(t)}).bind("touchmove",function(e){f(e),l(e),i||(e.preventDefault(),window.scrollBy(0,-o.deltaY))}).bind("touchend",function(t){e(this).removeClass(r),l(t)})}};e.extend(e.fn[t].prototype,s),e(document).on("create."+t,n,function(){e(this)[t]("_dragBehavior")})}(jQuery),function(e){var t="carousel",n="."+t,r=t+"-active",i=t+"-item",s=function(e){return Math.abs(e)>4},o=function(e,n){var r=e.find("."+t+"-active"),s=r.prevAll().length+1,o=n<0,u=s+(o?1:-1),a=e.find("."+i).eq(u-1);return a.length||(a=e.find("."+i)[o?"first":"last"]()),[r,a]};e(document).on("dragmove",n,function(t,n){if(!s(n.deltaX))return;var r=o(e(this),n.deltaX);r[0].css("left",n.deltaX+"px"),r[1].css("left",n.deltaX<0?n.w+n.deltaX+"px":-n.w+n.deltaX+"px")}).on("dragend",n,function(n,i){if(!s(i.deltaX))return;var u=o(e(this),i.deltaX),a=Math.abs(i.deltaX)>45;e(this).one(navigator.userAgent.indexOf("AppleWebKit")?"webkitTransitionEnd":"transitionEnd",function(){u[0].add(u[1]).css("left",""),e(this).trigger("goto."+t,u[1])}),a?(u[0].removeClass(r).css("left",i.deltaX>0?i.w+"px":-i.w+"px"),u[1].addClass(r).css("left",0)):(u[0].css("left",0),u[1].css("left",i.deltaX>0?-i.w+"px":i.w+"px"))})}(jQuery),function(e,t){var n="carousel",r="."+n+"[data-paginate]",i=n+"-pagination",s=n+"-active-page",o={_createPagination:function(){var t=e(this).find("."+n+"-nav"),r=e(this).find("."+n+"-item"),s=e("<ol class='"+i+"'></ol>"),o,u,a;t.find("."+i).remove(),r.each(function(t){o=t+1,u=e(this).attr("data-thumb"),a=o,u&&(a="<img src='"+u+"' alt=''>"),s.append("<li><a href='#"+o+"' title='Go to slide "+o+"'>"+a+"</a>")}),u&&s.addClass(n+"-nav-thumbs"),t.addClass(n+"-nav-paginated").find("a").first().after(s)},_bindPaginationEvents:function(){e(this).bind("click",function(t){var r=e(t.target);t.target.nodeName==="IMG"&&(r=r.parent()),r=r.closest("a");var s=r.attr("href");r.closest("."+i).length&&s&&(e(this)[n]("goTo",parseFloat(s.split("#")[1])),t.preventDefault())}).bind("goto."+n,function(t,n){var r=n?e(n).index():0;e(this).find("ol."+i+" li").removeClass(s).eq(r).addClass(s)}).trigger("goto."+n)}};e.extend(e.fn[n].prototype,o),e(document).on("create."+n,r,function(){e(this)[n]("_createPagination")[n]("_bindPaginationEvents")}).on("update."+n,r,function(){e(this)[n]("_createPagination")})}(jQuery),function(e){e(function(){e(".carousel").carousel()})}(jQuery);
/*
* 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));
/**
* 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 "";
}
}
}
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 @@ ...@@ -130,7 +130,13 @@
// ==================== // ====================
// edx.org marketing site - registration iframe band-aid (poor form enough to isolate out) // 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 { .view-partial-mktgregister {
background: transparent !important;
// dimensions needed for course about page on marketing site // dimensions needed for course about page on marketing site
.wrapper-view { .wrapper-view {
...@@ -169,6 +175,10 @@ ...@@ -169,6 +175,10 @@
&:hover .track { &:hover .track {
opacity: 1.0; opacity: 1.0;
} }
&.has-option-verified {
padding-top: 12px !important;
}
} }
// already registered but course not started or registration is closed // already registered but course not started or registration is closed
......
...@@ -9,7 +9,8 @@ ...@@ -9,7 +9,8 @@
@import 'base/reset'; @import 'base/reset';
@import 'vendor/font-awesome'; @import 'vendor/font-awesome';
@import 'vendor/responsive-carousel/responsive-carousel';
@import 'vendor/responsive-carousel/responsive-carousel.slide';
// BASE *default edX offerings* // BASE *default edX offerings*
// ==================== // ====================
...@@ -36,12 +37,19 @@ ...@@ -36,12 +37,19 @@
// base - assets // base - assets
@import 'base/font_face'; @import 'base/font_face';
@import 'base/extends'; @import 'base/extends';
@import 'base/animations'; @import 'base/animations';
// base - starter // base - starter
@import 'base/base'; @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/forms';
@import 'shared/footer'; @import 'shared/footer';
@import 'shared/header'; @import 'shared/header';
...@@ -67,7 +75,7 @@ ...@@ -67,7 +75,7 @@
@import 'multicourse/help'; @import 'multicourse/help';
@import 'multicourse/edge'; @import 'multicourse/edge';
// applications // applications
@import 'discussion'; @import 'discussion';
@import 'news'; @import 'news';
......
// lms - utilities - mixins and extends
// ====================
// mixins - font sizing // mixins - font sizing
@mixin font-size($sizeValue: 16){ @mixin font-size($sizeValue: 16){
font-size: $sizeValue + px; font-size: $sizeValue + px;
font-size: ($sizeValue/10) + rem; // font-size: ($sizeValue/10) + rem;
} }
// mixins - line height // mixins - line height
@mixin line-height($fontSize: auto){ @mixin line-height($fontSize: auto){
line-height: ($fontSize*1.48) + px; line-height: ($fontSize*1.48) + px;
line-height: (($fontSize/10)*1.48) + rem; // line-height: (($fontSize/10)*1.48) + rem;
} }
// image-replacement hidden text // image-replacement hidden text
...@@ -38,21 +41,38 @@ ...@@ -38,21 +41,38 @@
@return #{$pxval / $base}em; @return #{$pxval / $base}em;
} }
// Line-height // line-height
@function lh($amount: 1) { @function lh($amount: 1) {
@return $body-line-height * $amount; @return $body-line-height * $amount;
} }
// ====================
//----------------- // theme mixin styles
// Theme Mixin Styles
//-----------------
@mixin login_register_h1_style {} @mixin login_register_h1_style {}
@mixin footer_references_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 // extends -hidden elems - screenreaders
.text-sr { .text-sr {
border: 0; border: 0;
...@@ -64,3 +84,39 @@ ...@@ -64,3 +84,39 @@
position: absolute; position: absolute;
width: 1px; 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;
}
// base
$baseline: 20px; $baseline: 20px;
// ====================
// LAYOUT: grid
$gw-column: 80px; $gw-column: 80px;
$gw-gutter: 20px; $gw-gutter: 20px;
$fg-column: $gw-column; $fg-column: $gw-column;
$fg-gutter: $gw-gutter; $fg-gutter: $gw-gutter;
$fg-max-columns: 12; $fg-max-columns: 12;
$fg-max-width: 1400px; $fg-max-width: 1400px;
$fg-min-width: 810px; $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; $monospace: Monaco, 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
$body-font-family: $sans-serif; $body-font-family: $sans-serif;
$serif: $georgia; $serif: $georgia;
// ====================
// MISC: base fonts/colors
$body-font-size: em(14); $body-font-size: em(14);
$body-line-height: golden-ratio(.875em, 1); $body-line-height: golden-ratio(.875em, 1);
$base-font-color: rgb(60,60,60); $base-font-color: rgb(60,60,60);
...@@ -22,8 +31,21 @@ $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); $lighter-base-font-color: rgb(100,100,100);
$very-light-text: #fff; $very-light-text: #fff;
// ====================
// COLORS: misc.
$white: rgb(255,255,255); $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: 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); $blue: rgb(29,157,217);
$pink: rgb(182,37,104); $pink: rgb(182,37,104);
$yellow: rgb(255, 252, 221); $yellow: rgb(255, 252, 221);
...@@ -35,9 +57,10 @@ $dark-gray: rgb(51, 51, 51); ...@@ -35,9 +57,10 @@ $dark-gray: rgb(51, 51, 51);
$border-color: rgb(200, 200, 200); $border-color: rgb(200, 200, 200);
$sidebar-color: rgb(246, 246, 246); $sidebar-color: rgb(246, 246, 246);
$outer-border-color: rgb(170, 170, 170); $outer-border-color: rgb(170, 170, 170);
$green: rgb(37, 184, 90); $green: rgb(37, 184, 90);
// old variables // COLORS: old variables
$light-gray: #ddd; $light-gray: #ddd;
$dark-gray: #333; $dark-gray: #333;
...@@ -60,24 +83,62 @@ $m-gray-d1: #7D7F83; ...@@ -60,24 +83,62 @@ $m-gray-d1: #7D7F83;
$m-gray-d2: #707276; $m-gray-d2: #707276;
$m-gray-d3: #646668; $m-gray-d3: #646668;
$m-gray-d4: #050505; $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: #1AA1DE;
$m-blue-l1: #2BACE6; $m-blue-l1: #2BACE6;
$m-blue-l2: #42B5E9; $m-blue-l2: #42B5E9;
$m-blue-l3: #59BEEC; $m-blue-l3: #59BEEC;
$m-blue-l4: tint($m-blue,90%);
$m-blue-l5: tint($m-blue,95%);
$m-blue-d1: #1790C7; $m-blue-d1: #1790C7;
$m-blue-d2: #1580B0; $m-blue-d2: #1580B0;
$m-blue-d3: #126F9A; $m-blue-d3: #126F9A;
$m-blue-d4: #0A4A67; $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: #B52A67;
$m-pink-l1: #CA2F73; $m-pink-l1: #CA2F73;
$m-pink-l2: #D33F80; $m-pink-l2: #D33F80;
$m-pink-l3: #D7548E; $m-pink-l3: #D7548E;
$m-pink-l4: tint($m-pink,75%);
$m-pink-l5: tint($m-pink,85%);
$m-pink-d1: #A0255B; $m-pink-d1: #A0255B;
$m-pink-d2: #8C204F; $m-pink-d2: #8C204F;
$m-pink-d3: #771C44; $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); $m-base-font-size: em(15);
$base-font-color: rgb(60,60,60); $base-font-color: rgb(60,60,60);
...@@ -98,57 +159,65 @@ $courseware-footer-border: none; ...@@ -98,57 +159,65 @@ $courseware-footer-border: none;
$courseware-footer-shadow: none; $courseware-footer-shadow: none;
$courseware-footer-margin: 0px; $courseware-footer-margin: 0px;
// ====================
// 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 // ====================
// 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-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-color: transparent;
$button-bg-hover-color: #fff; $button-bg-hover-color: #fff;
// actions - primary // ACTIONS: primary
$action-primary-bg: $m-blue-d3; $action-primary-bg: $m-blue-d3;
$action-primary-fg: $white; $action-primary-fg: $white;
$action-primary-shadow: $m-blue-d4; $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-bg: $m-blue-d1;
$action-primary-focused-fg: $white; $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-bg: $m-blue;
$action-primary-active-fg: $m-blue-d3; $action-primary-active-fg: $m-blue-d3;
$action-primary-active-shadow: $m-blue-d2; $action-primary-active-shadow: $m-blue-d2;
$action-primary-active-focused-fg: $m-blue-d4; $action-primary-active-focused-fg: $m-blue-d4;
$action-primary-active-focused-shadow: $m-blue-d3; $action-primary-active-focused-shadow: $m-blue-d3;
// disabled // ACTIONS: disabled
$action-primary-disabled-bg: $m-gray-d3; $action-primary-disabled-bg: $m-gray-d3;
$action-prmary-disabled-fg: $white; $action-prmary-disabled-fg: $white;
// ACTIONS: secondary
// actions - secondary
$action-secondary-bg: $m-pink; $action-secondary-bg: $m-pink;
$action-secondary-fg: $white; $action-secondary-fg: $white;
$action-secondary-shadow: $m-pink-d2; $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-bg: $m-pink-l3;
$action-secondary-focused-fg: $white; $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-bg: $m-pink-l2;
$action-secondary-active-fg: $m-pink-d1; $action-secondary-active-fg: $m-pink-d1;
$action-secondary-active-shadow: $m-pink-d1; $action-secondary-active-shadow: $m-pink-d1;
$action-secondary-active-focused-fg: $m-pink-d3; $action-secondary-active-focused-fg: $m-pink-d3;
$action-secondary-active-focused-shadow: $m-pink-d2; $action-secondary-active-focused-shadow: $m-pink-d2;
// disabled // ACTIONS: secondary - disabled
$action-secondary-disabled-bg: $m-gray-d3; $action-secondary-disabled-bg: $m-gray-d3;
$action-secondary-disabled-fg: $white; $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-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-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)); $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 ...@@ -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-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)); $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-image: linear-gradient(-90deg, rgb(255,255,255), rgb(245,245,245));
$dashboard-profile-header-color: transparent; $dashboard-profile-header-color: transparent;
$dashboard-profile-color: rgb(252,252,252); $dashboard-profile-color: rgb(252,252,252);
$dot-color: $light-gray; $dot-color: $light-gray;
// MISC: course assets
$content-wrapper-bg: $white; $content-wrapper-bg: $white;
$course-bg-color: #d6d6d6; $course-bg-color: #d6d6d6;
$course-bg-image: url(../images/bg-texture.png); $course-bg-image: url(../images/bg-texture.png);
$account-content-wrapper-bg: shade($body-bg, 2%); $account-content-wrapper-bg: shade($body-bg, 2%);
$course-profile-bg: rgb(245,245,245); $course-profile-bg: rgb(245,245,245);
$course-header-bg: rgba(255,255,255, 0.93); $course-header-bg: rgba(255,255,255, 0.93);
// MISC: borders
$border-color-1: rgb(190,190,190); $border-color-1: rgb(190,190,190);
$border-color-2: rgb(200,200,200); $border-color-2: rgb(200,200,200);
$border-color-3: rgb(100,100,100); $border-color-3: rgb(100,100,100);
$border-color-4: rgb(252,252,252); $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: $blue;
$link-color-d1: $m-blue-d2; $link-color-d1: $m-blue-d2;
$link-hover: $pink; $link-hover: $pink;
$site-status-color: $pink; $site-status-color: $pink;
$button-color: $blue; $button-color: $blue;
$button-archive-color: #eee; $button-archive-color: #eee;
// MISC: shadow, form, modal
$shadow-color: $blue; $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-top: rgba(255, 255, 255, .6);
$sidebar-chapter-bg-bottom: rgba(255, 255, 255, 0); $sidebar-chapter-bg-bottom: rgba(255, 255, 255, 0);
$sidebar-chapter-bg: #eee; $sidebar-chapter-bg: #eee;
$sidebar-active-image: linear-gradient(top, #e6e6e6, #d6d6d6); $sidebar-active-image: linear-gradient(top, #e6e6e6, #d6d6d6);
$form-bg-color: #fff; // TOP HEADER IMAGE MARGIN
$modal-bg-color: rgb(245,245,245);
//TOP HEADER IMAGE MARGIN
$header_image_margin: -69px; $header_image_margin: -69px;
//FOOTER MARGIN //FOOTER MARGIN
$footer_margin: ($baseline/4) 0 ($baseline*1.5) 0; $footer_margin: ($baseline/4) 0 ($baseline*1.5) 0;
//----------------- // ====================
// CSS BG Images
//----------------- // IMAGES: backgrounds
$homepage-bg-image: '../images/homepage-bg.jpg'; $homepage-bg-image: '../images/homepage-bg.jpg';
$login-banner-image: url(../images/bg-banner-login.png); $login-banner-image: url(../images/bg-banner-login.png);
$register-banner-image: url(../images/bg-banner-register.png); $register-banner-image: url(../images/bg-banner-register.png);
$video-thumb-url: '../images/courses/video-thumb.jpg'; $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;
// 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;
}
}
// 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;
}
// lms - views - user/student dashboard
// ====================
.dashboard { .dashboard {
@include clearfix; @include clearfix;
padding: 60px 0 0 0; padding: 60px 0 0 0;
...@@ -224,6 +227,7 @@ ...@@ -224,6 +227,7 @@
} }
} }
// course listings
.my-courses { .my-courses {
float: left; float: left;
margin: 0px; margin: 0px;
...@@ -268,21 +272,30 @@ ...@@ -268,21 +272,30 @@
} }
} }
.my-course { // UI: course list
clear: both; .listing-courses {
@include clearfix; @extend .ui-no-list;
margin-right: flex-gutter();
margin-bottom: 50px; .course-item {
padding-bottom: 50px; margin-bottom: ($baseline*2.5);
border-bottom: 1px solid $border-color-1; border-bottom: 4px solid $border-color-l4;
position: relative; padding-bottom: ($baseline*2.5);
width: flex-grid(12);
z-index: 20;
@include transition(all 0.15s linear 0s);
&:last-child { &:last-child {
margin-bottom: none; 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 { .cover {
@include box-sizing(border-box); @include box-sizing(border-box);
...@@ -402,6 +415,51 @@ ...@@ -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 { .message-status {
......
/*
* 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;
}
/*
* 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;
}
...@@ -47,7 +47,12 @@ ...@@ -47,7 +47,12 @@
$('#class_enroll_form').on('ajax:complete', function(event, xhr) { $('#class_enroll_form').on('ajax:complete', function(event, xhr) {
if(xhr.status == 200) { 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) { } else if (xhr.status == 403) {
location.href = "${reverse('register_user')}?course_id=${course.id}&enrollment_action=enroll"; location.href = "${reverse('register_user')}?course_id=${course.id}&enrollment_action=enroll";
} else { } else {
...@@ -95,7 +100,6 @@ ...@@ -95,7 +100,6 @@
%else: %else:
<a href="#" class="register">${_("Register for {course.display_number_with_default}").format(course=course) | h}</a> <a href="#" class="register">${_("Register for {course.display_number_with_default}").format(course=course) | h}</a>
<div id="register_error"></div> <div id="register_error"></div>
%endif %endif
</div> </div>
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
<%block name="title"><title>${_("About {course_id}").format(course_id=course_id)}</title></%block> <%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"> <%block name="headextra">
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
<%block name="title"><title>${_("About {course_number}").format(course_number=course.display_number_with_default) | h}</title></%block> <%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"> <%block name="headextra">
...@@ -27,7 +27,12 @@ ...@@ -27,7 +27,12 @@
$('#class_enroll_form').on('ajax:complete', function(event, xhr) { $('#class_enroll_form').on('ajax:complete', function(event, xhr) {
if(xhr.status == 200) { 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) { } else if (xhr.status == 403) {
window.top.location.href = "${reverse('register_user')}?course_id=${course.id}&enrollment_action=enroll"; window.top.location.href = "${reverse('register_user')}?course_id=${course.id}&enrollment_action=enroll";
} else { } else {
...@@ -52,7 +57,7 @@ ...@@ -52,7 +57,7 @@
<div class="action is-registered">${_("You Are Registered")}</div> <div class="action is-registered">${_("You Are Registered")}</div>
%endif %endif
%elif allow_registration: %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: %if len(course_modes) > 1:
<span class="track"> <span class="track">
and choose your student track and choose your student track
......
<%! from django.utils.translation import ugettext as _ %> <%! 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)} ${_("Your order number is: {order_number}").format(order_number=order.id)}
${_("Items in your order:")} ${_("The items in your order are:")}
${_("Quantity - Description - Price")} ${_("Quantity - Description - Price")}
%for order_item in order_items: %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 %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 @@ ...@@ -37,7 +37,7 @@
</nav> </nav>
<div class="colophon-about"> <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> <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> </div>
......
...@@ -50,7 +50,9 @@ ...@@ -50,7 +50,9 @@
next=u.split("next=")[1]; next=u.split("next=")[1];
if (next && !isExternal(next)) { if (next && !isExternal(next)) {
location.href=next; location.href=next;
} else { } else if(json.redirect_url){
location.href=json.redirect_url;
} else {
location.href="${reverse('dashboard')}"; location.href="${reverse('dashboard')}";
} }
} else { } else {
......
...@@ -16,7 +16,9 @@ ...@@ -16,7 +16,9 @@
</%def> </%def>
<!DOCTYPE html> <!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> <head>
<%block name="title"> <%block name="title">
% if stanford_theme_enabled(): % if stanford_theme_enabled():
...@@ -25,6 +27,9 @@ ...@@ -25,6 +27,9 @@
## "edX" should not be translated ## "edX" should not be translated
<title>edX</title> <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"> <script type="text/javascript">
/* immediately break out of an iframe if coming from the marketing website */ /* immediately break out of an iframe if coming from the marketing website */
(function(window) { (function(window) {
......
...@@ -53,7 +53,12 @@ ...@@ -53,7 +53,12 @@
$('#register-form').on('ajax:success', function(event, json, xhr) { $('#register-form').on('ajax:success', function(event, json, xhr) {
if(json.success) { if(json.success) {
location.href="${reverse('dashboard')}"; if(json.redirect_url){
location.href=json.redirect_url;
}
else {
location.href="${reverse('dashboard')}";
}
} else { } else {
toggleSubmitButton(true); toggleSubmitButton(true);
$('.status.message.submission-error').addClass('is-shown').focus(); $('.status.message.submission-error').addClass('is-shown').focus();
......
...@@ -3,8 +3,11 @@ ...@@ -3,8 +3,11 @@
<%! from django.conf import settings %> <%! from django.conf import settings %>
<%inherit file="../main.html" /> <%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: % if notification is not UNDEFINED:
<section class="notification"> <section class="notification">
...@@ -12,8 +15,15 @@ ...@@ -12,8 +15,15 @@
</section> </section>
% endif % endif
<section class="container cart-list"> <div class="container">
<p><h1>${_(settings.PLATFORM_NAME + " (" + settings.SITE_NAME + ")" + " Electronic Receipt")}</h1></p> <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>${_("Order #")}${order.id}</h2>
<h2>${_("Date:")} ${order.purchase_time.date().isoformat()}</h2> <h2>${_("Date:")} ${order.purchase_time.date().isoformat()}</h2>
<h2>${_("Items ordered:")}</h2> <h2>${_("Items ordered:")}</h2>
...@@ -57,4 +67,6 @@ ...@@ -57,4 +67,6 @@
${order.bill_to_country.upper()}<br /> ${order.bill_to_country.upper()}<br />
</p> </p>
</section> </section>
</div>
</%block>
<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>
<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>
<%! 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>
<%! 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>
<%! 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 -->
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%inherit file="../main.html" />
<%block name="content">
Final Verification!
</%block>
<%! 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>
<%! 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>
<%! 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 ...@@ -58,8 +58,16 @@ urlpatterns = ('', # nopep8
url(r'^heartbeat$', include('heartbeat.urls')), url(r'^heartbeat$', include('heartbeat.urls')),
url(r'^user_api/', include('user_api.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 = { js_info_dict = {
'domain': 'djangojs', 'domain': 'djangojs',
'packages': ('lms',), 'packages': ('lms',),
...@@ -338,6 +346,7 @@ if settings.COURSEWARE_ENABLED: ...@@ -338,6 +346,7 @@ if settings.COURSEWARE_ENABLED:
name='submission_history'), name='submission_history'),
) )
if settings.COURSEWARE_ENABLED and settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'): if settings.COURSEWARE_ENABLED and settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'):
urlpatterns += ( urlpatterns += (
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard$',
......
...@@ -18,6 +18,7 @@ django-followit==0.0.3 ...@@ -18,6 +18,7 @@ django-followit==0.0.3
django-keyedcache==1.4-6 django-keyedcache==1.4-6
django-kombu==0.9.4 django-kombu==0.9.4
django-mako==0.1.5pre django-mako==0.1.5pre
django-model-utils==1.4.0
django-masquerade==0.1.6 django-masquerade==0.1.6
django-mptt==0.5.5 django-mptt==0.5.5
django-openid-auth==0.4 django-openid-auth==0.4
...@@ -60,7 +61,6 @@ South==0.7.6 ...@@ -60,7 +61,6 @@ South==0.7.6
sympy==0.7.1 sympy==0.7.1
xmltodict==0.4.1 xmltodict==0.4.1
django-ratelimit-backend==0.6 django-ratelimit-backend==0.6
django-model-utils==1.4.0
# Used for debugging # Used for debugging
ipython==0.13.1 ipython==0.13.1
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment