Commit 18f69809 by Will Daly

Merge pull request #6247 from edx/will/update-verified-requirements

Separate verification / payment flow.
parents 49cb0486 93cc03a5
from django.conf.urls import patterns, url
from verify_student import views
from verify_student.views import PayAndVerifyView
from django.conf import settings
......@@ -82,3 +83,85 @@ urlpatterns = patterns(
name="verify_student_toggle_failed_banner_off"
),
)
if settings.FEATURES.get("SEPARATE_VERIFICATION_FROM_PAYMENT"):
urlpatterns += patterns(
'',
url(
r'^submit-photos/$',
views.submit_photos_for_verification,
name="verify_student_submit_photos"
),
# The user is starting the verification / payment process,
# most likely after enrolling in a course and selecting
# a "verified" track.
url(
r'^start-flow/{course}/$'.format(course=settings.COURSE_ID_PATTERN),
views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter
name="verify_student_start_flow",
kwargs={
'message': PayAndVerifyView.FIRST_TIME_VERIFY_MSG
}
),
# The user is enrolled in a non-paid mode and wants to upgrade.
# This is the same as the "start verification" flow,
# except with slight messaging changes.
url(
r'^upgrade/{course}/$'.format(course=settings.COURSE_ID_PATTERN),
views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter
name="verify_student_upgrade_and_verify",
kwargs={
'message': PayAndVerifyView.UPGRADE_MSG
}
),
# The user has paid and still needs to verify.
# Since the user has "just paid", we display *all* steps
# including payment. The user resumes the flow
# from the verification step.
# Note that if the user has already verified, this will redirect
# to the dashboard.
url(
r'^verify-now/{course}/$'.format(course=settings.COURSE_ID_PATTERN),
views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter
name="verify_student_verify_now",
kwargs={
'always_show_payment': True,
'current_step': PayAndVerifyView.FACE_PHOTO_STEP,
'message': PayAndVerifyView.VERIFY_NOW_MSG
}
),
# The user has paid and still needs to verify,
# but the user is NOT arriving directly from the paymen104ggt flow.
# This is equivalent to starting a new flow
# with the payment steps and requirements hidden
# (since the user already paid).
url(
r'^verify-later/{course}/$'.format(course=settings.COURSE_ID_PATTERN),
views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter
name="verify_student_verify_later",
kwargs={
'message': PayAndVerifyView.VERIFY_LATER_MSG
}
),
# The user is returning to the flow after paying.
# This usually occurs after a redirect from the shopping cart
# once the order has been fulfilled.
url(
r'^payment-confirmation/{course}/$'.format(course=settings.COURSE_ID_PATTERN),
views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter
name="verify_student_payment_confirmation",
kwargs={
'always_show_payment': True,
'current_step': PayAndVerifyView.PAYMENT_CONFIRMATION_STEP,
'message': PayAndVerifyView.PAYMENT_CONFIRMATION_MSG
}
),
)
......@@ -1010,12 +1010,12 @@ courseware_js = (
base_vendor_js = [
'js/vendor/jquery.min.js',
'js/vendor/jquery.cookie.js',
'js/vendor/underscore-min.js'
'js/vendor/underscore-min.js',
'js/vendor/require.js',
'js/RequireJS-namespace-undefine.js',
]
main_vendor_js = base_vendor_js + [
'js/vendor/require.js',
'js/RequireJS-namespace-undefine.js',
'js/vendor/json2.js',
'js/vendor/jquery-ui.min.js',
'js/vendor/jquery.qtip.min.js',
......@@ -1038,7 +1038,15 @@ instructor_dash_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/ins
student_account_js = [
'js/utils/rwd_header_footer.js',
'js/utils/edx.utils.validate.js',
'js/form.ext.js',
'js/my_courses_dropdown.js',
'js/toggle_login_modal.js',
'js/sticky_filter.js',
'js/query-params.js',
'js/src/utility.js',
'js/src/accessibility_tools.js',
'js/src/ie_shim.js',
'js/src/string_utils.js',
'js/student_account/enrollment.js',
'js/student_account/emailoptin.js',
'js/student_account/shoppingcart.js',
......@@ -1055,6 +1063,32 @@ student_account_js = [
student_profile_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/student_profile/**/*.js'))
verify_student_js = [
'js/form.ext.js',
'js/my_courses_dropdown.js',
'js/toggle_login_modal.js',
'js/sticky_filter.js',
'js/query-params.js',
'js/src/utility.js',
'js/src/accessibility_tools.js',
'js/src/ie_shim.js',
'js/src/string_utils.js',
'js/verify_student/models/verification_model.js',
'js/verify_student/views/error_view.js',
'js/verify_student/views/webcam_photo_view.js',
'js/verify_student/views/progress_view.js',
'js/verify_student/views/step_view.js',
'js/verify_student/views/intro_step_view.js',
'js/verify_student/views/make_payment_step_view.js',
'js/verify_student/views/payment_confirmation_step_view.js',
'js/verify_student/views/face_photo_step_view.js',
'js/verify_student/views/id_photo_step_view.js',
'js/verify_student/views/review_photos_step_view.js',
'js/verify_student/views/enrollment_confirmation_step_view.js',
'js/verify_student/views/pay_and_verify_view.js',
'js/verify_student/pay_and_verify.js',
]
PIPELINE_CSS = {
'style-vendor': {
'source_filenames': [
......@@ -1234,6 +1268,10 @@ PIPELINE_JS = {
'source_filenames': student_profile_js,
'output_filename': 'js/student_profile.js'
},
'verify_student': {
'source_filenames': verify_student_js,
'output_filename': 'js/verify_student.js'
}
}
PIPELINE_DISABLE_WRAPPER = True
......
......@@ -241,4 +241,4 @@ var edx = edx || {};
});
});
})(jQuery);
\ No newline at end of file
})(jQuery);
/**
* In-memory storage of verification photo data.
*
* This can be passed to multiple steps in the workflow
* to persist image data in-memory before it is submitted
* to the server.
*
*/
var edx = edx || {};
(function( $, _, Backbone ) {
'use strict';
edx.verify_student = edx.verify_student || {};
edx.verify_student.VerificationModel = Backbone.Model.extend({
defaults: {
fullName: null,
faceImage: "",
identificationImage: ""
},
sync: function( method, model ) {
var headers = { 'X-CSRFToken': $.cookie('csrftoken') },
data = {
face_image: model.get('faceImage'),
photo_id_image: model.get('identificationImage')
};
// Full name is an optional parameter; if not provided,
// it won't be changed.
if ( !_.isNull( model.get('fullName') ) ) {
data.full_name = model.get('fullName');
}
// Submit the request to the server,
// triggering events on success and error.
$.ajax({
url: '/verify_student/submit-photos/',
type: 'POST',
data: data,
headers: headers,
success: function() {
model.trigger( 'sync' );
},
error: function( error ) {
model.trigger( 'error', error );
}
});
}
});
})( jQuery, _, Backbone );
/**
* Entry point for the payment/verification flow.
* This loads the base view, which in turn loads
* subviews for each step in the flow.
*
* We pass some information to the base view
* using "data-" attributes on the parent div.
* See "pay_and_verify.html" for the exact attribute names.
*
*/
var edx = edx || {};
(function($) {
'use strict';
var errorView,
el = $('#pay-and-verify-container');
edx.verify_student = edx.verify_student || {};
// Initialize an error view for displaying top-level error messages.
errorView = new edx.verify_student.ErrorView({
el: $('#error-container')
});
// Initialize the base view, passing in information
// from the data attributes on the parent div.
//
// The data attributes capture information that only
// the server knows about, such as the course and course mode info,
// full URL paths to static underscore templates,
// and some messaging.
//
return new edx.verify_student.PayAndVerifyView({
errorModel: errorView.model,
displaySteps: el.data('display-steps'),
currentStep: el.data('current-step'),
stepInfo: {
'intro-step': {
introTitle: el.data('intro-title'),
introMsg: el.data('intro-msg'),
requirements: el.data('requirements')
},
'make-payment-step': {
courseKey: el.data('course-key'),
minPrice: el.data('course-mode-min-price'),
suggestedPrices: (el.data('course-mode-suggested-prices') || "").split(","),
currency: el.data('course-mode-currency'),
purchaseEndpoint: el.data('purchase-endpoint')
},
'payment-confirmation-step': {
courseName: el.data('course-name'),
courseStartDate: el.data('course-start-date'),
coursewareUrl: el.data('courseware-url')
},
'review-photos-step': {
fullName: el.data('full-name'),
platformName: el.data('platform-name')
}
}
}).render();
})(jQuery);
/**
* View for the "enrollment confirmation" step of
* the payment/verification flow.
*/
var edx = edx || {};
(function( $ ) {
'use strict';
edx.verify_student = edx.verify_student || {};
// Currently, this step does not need to install any event handlers,
// since the displayed information is static.
edx.verify_student.EnrollmentConfirmationStepView = edx.verify_student.StepView.extend({});
})( jQuery );
/**
* Display top-level errors in the payment/verification flow.
*/
var edx = edx || {};
(function ( $, _, Backbone ) {
'use strict';
edx.verify_student = edx.verify_student || {};
edx.verify_student.ErrorView = Backbone.View.extend({
initialize: function( obj ) {
var ErrorModel = Backbone.Model.extend({});
this.model = obj.model || new ErrorModel({
errorTitle: "",
errorMsg: "",
shown: false
});
this.listenToOnce( this.model, 'change', this.render );
},
render: function() {
var renderedHtml = _.template(
$( '#error-tpl' ).html(),
{
errorTitle: this.model.get( 'errorTitle' ),
errorMsg: this.model.get( 'errorMsg' )
}
);
$( this.el ).html( renderedHtml );
if ( this.model.get( 'shown' ) ) {
$( this.el ).show();
$( "html, body" ).animate({ scrollTop: 0 });
}
else {
$( this.el ).hide();
}
}
});
})( $, _, Backbone );
/**
* View for the "face photo" step in the payment/verification flow.
*/
var edx = edx || {};
(function( $ ) {
'use strict';
edx.verify_student = edx.verify_student || {};
edx.verify_student.FacePhotoStepView = edx.verify_student.StepView.extend({
postRender: function() {
new edx.verify_student.WebcamPhotoView({
el: $("#facecam"),
model: this.model,
modelAttribute: 'faceImage',
submitButton: '#next_step_button',
errorModel: this.errorModel
}).render();
$('#next_step_button').on( 'click', _.bind( this.nextStep, this ) );
},
});
})( jQuery );
/**
* View for the "id photo" step of the payment/verification flow.
*/
var edx = edx || {};
(function( $ ) {
'use strict';
edx.verify_student = edx.verify_student || {};
edx.verify_student.IDPhotoStepView = edx.verify_student.StepView.extend({
postRender: function() {
new edx.verify_student.WebcamPhotoView({
el: $("#idcam"),
model: this.model,
modelAttribute: 'identificationImage',
submitButton: '#next_step_button',
errorModel: this.errorModel
}).render();
$('#next_step_button').on( 'click', _.bind( this.nextStep, this ) );
},
});
})( jQuery );
/**
* View for the "intro step" of the payment/verification flow.
*/
var edx = edx || {};
(function( $ ) {
'use strict';
edx.verify_student = edx.verify_student || {};
// Currently, this view doesn't need to install any custom event handlers,
// since the button in the template reloads the page with a
// ?skip-intro=1 GET parameter. The reason for this is that we
// want to allow users to click "back" to see the requirements,
// and if they reload the page we want them to stay on the
// second step.
edx.verify_student.IntroStepView = edx.verify_student.StepView.extend({});
})( jQuery );
/**
* View for the "make payment" step of the payment/verification flow.
*/
var edx = edx || {};
(function( $, _, gettext ) {
'use strict';
edx.verify_student = edx.verify_student || {};
edx.verify_student.MakePaymentStepView = edx.verify_student.StepView.extend({
postRender: function() {
// Enable the payment button once an amount is chosen
$( "input[name='contribution']" ).on( 'click', _.bind( this.enablePaymentButton, this ) );
// Handle payment submission
$( "#pay_button" ).on( 'click', _.bind( this.createOrder, this ) );
},
enablePaymentButton: function() {
$("#pay_button").removeClass("is-disabled");
},
createOrder: function() {
var paymentAmount = this.getPaymentAmount(),
postData = {
'contribution': paymentAmount,
'course_id': this.stepData.courseKey,
};
// Disable the payment button to prevent multiple submissions
$("#pay_button").addClass("is-disabled");
// Create the order for the amount
$.ajax({
url: '/verify_student/create_order/',
type: 'POST',
headers: {
'X-CSRFToken': $.cookie('csrftoken')
},
data: postData,
context: this,
success: this.handleCreateOrderResponse,
error: this.handleCreateOrderError
});
},
handleCreateOrderResponse: function( paymentParams ) {
// At this point, the order has been created on the server,
// and we've received signed payment parameters.
// We need to dynamically construct a form using
// these parameters, then submit it to the payment processor.
// This will send the user to a hosted order page,
// where she can enter credit card information.
var form = $( "#payment-processor-form" );
$( "input", form ).remove();
form.attr( "action", this.stepData.purchaseEndpoint );
form.attr( "method", "POST" );
_.each( paymentParams, function( value, key ) {
$("<input>").attr({
type: "hidden",
name: key,
value: value
}).appendTo(form);
});
form.submit();
},
handleCreateOrderError: function( xhr ) {
if ( xhr.status === 400 ) {
this.errorModel.set({
errorTitle: gettext( 'Could not submit order' ),
errorMsg: xhr.responseText,
shown: true
});
} else {
this.errorModel.set({
errorTitle: gettext( 'Could not submit order' ),
errorMsg: gettext( 'An unexpected error occurred. Please try again' ),
shown: true
});
}
// Re-enable the button so the user can re-try
$( "#payment-processor-form" ).removeClass("is-disabled");
},
getPaymentAmount: function() {
var contributionInput = $("input[name='contribution']:checked", this.el);
if ( contributionInput.attr('id') === 'contribution-other' ) {
return $( "input[name='contribution-other-amt']", this.el ).val();
} else {
return contributionInput.val();
}
}
});
})( jQuery, _, gettext );
/**
* Base view for the payment/verification flow.
*
* This view is responsible for the "progress steps"
* at the top of the page, but it delegates
* to subviews to render individual steps.
*
*/
var edx = edx || {};
(function($, _, Backbone, gettext) {
'use strict';
edx.verify_student = edx.verify_student || {};
edx.verify_student.PayAndVerifyView = Backbone.View.extend({
el: '#pay-and-verify-container',
template: '#progress-tpl',
subviews: {},
VERIFICATION_VIEW_NAMES: [
'face-photo-step',
'id-photo-step',
'review-photos-step'
],
initialize: function( obj ) {
this.errorModel = obj.errorModel || {};
this.displaySteps = obj.displaySteps || [];
// Determine which step we're starting on
// Depending on how the user enters the flow,
// this could be anywhere in the sequence of steps.
this.currentStepIndex = _.indexOf(
_.pluck( this.displaySteps, 'name' ),
obj.currentStep
);
this.progressView = new edx.verify_student.ProgressView({
el: this.el,
displaySteps: this.displaySteps,
currentStepIndex: this.currentStepIndex
});
this.initializeStepViews( obj.stepInfo );
},
initializeStepViews: function( stepInfo ) {
var i,
stepName,
stepData,
subview,
subviewConfig,
nextStepTitle,
subviewConstructors,
verificationModel;
// We need to initialize this here, because
// outside of this method the subview classes
// might not yet have been loaded.
subviewConstructors = {
'intro-step': edx.verify_student.IntroStepView,
'make-payment-step': edx.verify_student.MakePaymentStepView,
'payment-confirmation-step': edx.verify_student.PaymentConfirmationStepView,
'face-photo-step': edx.verify_student.FacePhotoStepView,
'id-photo-step': edx.verify_student.IDPhotoStepView,
'review-photos-step': edx.verify_student.ReviewPhotosStepView,
'enrollment-confirmation-step': edx.verify_student.EnrollmentConfirmationStepView
};
// Create the verification model, which is shared
// among the different steps. This allows
// one step to save photos and another step
// to submit them.
verificationModel = new edx.verify_student.VerificationModel();
for ( i = 0; i < this.displaySteps.length; i++ ) {
stepName = this.displaySteps[i].name;
subview = null;
if ( i < this.displaySteps.length - 1) {
nextStepTitle = this.displaySteps[i + 1].title;
} else {
nextStepTitle = "";
}
if ( subviewConstructors.hasOwnProperty( stepName ) ) {
stepData = {};
// Add any info specific to this step
if ( stepInfo.hasOwnProperty( stepName ) ) {
_.extend( stepData, stepInfo[ stepName ] );
}
subviewConfig = {
errorModel: this.errorModel,
templateUrl: this.displaySteps[i].templateUrl,
nextStepNum: (i + 2), // Next index, starting from 1
nextStepTitle: nextStepTitle,
stepData: stepData
};
// For photo verification steps, set the shared photo model
if ( this.VERIFICATION_VIEW_NAMES.indexOf(stepName) >= 0 ) {
_.extend( subviewConfig, { model: verificationModel } );
}
// Create the subview instance
// Note that we are NOT yet rendering the view,
// so this doesn't trigger GET requests or modify
// the DOM.
this.subviews[stepName] = new subviewConstructors[stepName]( subviewConfig );
// Listen for events to change the current step
this.listenTo( this.subviews[stepName], 'next-step', this.nextStep );
this.listenTo( this.subviews[stepName], 'go-to-step', this.goToStep );
}
}
},
render: function() {
this.progressView.render();
this.renderCurrentStep();
return this;
},
renderCurrentStep: function() {
var stepName, stepView, stepEl;
// Get or create the step container
stepEl = $("#current-step-container");
if (!stepEl.length) {
stepEl = $('<div id="current-step-container"></div>').appendTo(this.el);
}
// Render the subview
// Note that this will trigger a GET request for the
// underscore template.
// When the view is rendered, it will overwrite the existing
// step in the DOM.
stepName = this.displaySteps[ this.currentStepIndex ].name;
stepView = this.subviews[ stepName ];
stepView.el = stepEl;
stepView.render();
},
nextStep: function() {
this.currentStepIndex = Math.min( this.currentStepIndex + 1, this.displaySteps.length - 1 );
this.render();
},
goToStep: function( stepName ) {
var stepIndex = _.indexOf(
_.pluck( this.displaySteps, 'name' ),
stepName
);
if ( stepIndex >= 0 ) {
this.currentStepIndex = stepIndex;
this.render();
}
},
});
})(jQuery, _, Backbone, gettext);
/**
* View for the "payment confirmation" step of the payment/verification flow.
*/
var edx = edx || {};
(function( $ ) {
'use strict';
edx.verify_student = edx.verify_student || {};
// The "Verify Later" button goes directly to the dashboard,
// The "Verify Now" button reloads this page with the "skip-first-step"
// flag set. This allows the user to navigate back to the confirmation
// if he/she wants to.
// For this reason, we don't need any custom click handlers here.
edx.verify_student.PaymentConfirmationStepView = edx.verify_student.StepView.extend({});
})( jQuery );
/**
* Show progress steps in the payment/verification flow.
*/
var edx = edx || {};
(function( $, _, Backbone, gettext ) {
'use strict';
edx.verify_student = edx.verify_student || {};
edx.verify_student.ProgressView = Backbone.View.extend({
template: '#progress-tpl',
initialize: function( obj ) {
this.displaySteps = obj.displaySteps || {};
this.currentStepIndex = obj.currentStepIndex || 0;
},
render: function() {
var renderedHtml, context;
context = {
steps: this.steps()
};
renderedHtml = _.template( $(this.template).html(), context );
$(this.el).html(renderedHtml);
},
steps: function() {
var i,
stepDescription,
steps = [];
for ( i = 0; i < this.displaySteps.length; i++ ) {
stepDescription = {
title: this.displaySteps[i].title,
isCurrent: (i === this.currentStepIndex ),
isComplete: (i < this.currentStepIndex )
};
steps.push(stepDescription);
}
return steps;
}
});
})( $, _, Backbone, gettext );
/**
* View for the "review photos" step of the payment/verification flow.
*/
var edx = edx || {};
(function( $, gettext ) {
'use strict';
edx.verify_student = edx.verify_student || {};
edx.verify_student.ReviewPhotosStepView = edx.verify_student.StepView.extend({
postRender: function() {
var model = this.model;
// Load the photos from the previous steps
$( "#face_image")[0].src = this.model.get('faceImage');
$( "#photo_id_image")[0].src = this.model.get('identificationImage');
// Prep the name change dropdown
$( '.expandable-area' ).slideUp();
$( '.is-expandable' ).addClass('is-ready');
$( '.is-expandable .title-expand' ).on( 'click', this.expandCallback );
// Disable the submit button until user confirmation
$( '#confirm_pics_good' ).on( 'click', this.toggleSubmitEnabled );
// Go back to the first photo step if we need to retake photos
$( '#retake_photos_button' ).on( 'click', _.bind( this.retakePhotos, this ) );
// When moving to the next step, submit photos for verification
$( '#next_step_button' ).on( 'click', _.bind( this.submitPhotos, this ) );
},
toggleSubmitEnabled: function() {
$( '#next_step_button' ).toggleClass( 'is-disabled' );
},
retakePhotos: function() {
this.goToStep( 'face-photo-step' );
},
submitPhotos: function() {
// Disable the submit button to prevent duplicate submissions
$( "#next_step_button" ).addClass( "is-disabled" );
// On success, move on to the next step
this.listenToOnce( this.model, 'sync', _.bind( this.nextStep, this ) );
// On failure, re-enable the submit button and display the error
this.listenToOnce( this.model, 'error', _.bind( this.handleSubmissionError, this ) );
// Submit
this.model.set( 'fullName', $( '#new-name' ).val() );
this.model.save();
},
handleSubmissionError: function( xhr ) {
// Re-enable the submit button to allow the user to retry
var isConfirmChecked = $( "#confirm_pics_good" ).prop('checked');
$( "#next_step_button" ).toggleClass( "is-disabled", !isConfirmChecked );
// Display the error
if ( xhr.status === 400 ) {
this.errorModel.set({
errorTitle: gettext( 'Could not submit photos' ),
errorMsg: xhr.responseText,
shown: true
});
}
else {
this.errorModel.set({
errorTitle: gettext( 'Could not submit photos' ),
errorMsg: gettext( 'An unexpected error occurred. Please try again later.' ),
shown: true
});
}
},
expandCallback: function(event) {
event.preventDefault();
$(this).next('.expandable-area' ).slideToggle();
var title = $( this ).parent();
title.toggleClass( 'is-expanded' );
title.attr( 'aria-expanded', !title.attr('aria-expanded') );
}
});
})( jQuery, gettext );
/**
* Base view for defining steps in the payment/verification flow.
*
* Each step view lazy-loads its underscore template.
* This reduces the size of the initial page, since we don't
* need to include the DOM structure for each step
* in the initial load.
*
* Step subclasses are responsible for defining a template
* and installing custom event handlers (including buttons
* to move to the next step).
*
* The superclass is responsible for downloading the underscore
* template and rendering it, using context received from
* the server (in data attributes on the initial page load).
*
*/
var edx = edx || {};
(function( $, _, _s, Backbone, gettext ) {
'use strict';
edx.verify_student = edx.verify_student || {};
edx.verify_student.StepView = Backbone.View.extend({
initialize: function( obj ) {
_.extend( this, obj );
/* Mix non-conflicting functions from underscore.string
* (all but include, contains, and reverse) into the
* Underscore namespace
*/
_.mixin( _s.exports() );
},
render: function() {
if ( !this.renderedHtml && this.templateUrl) {
$.ajax({
url: this.templateUrl,
type: 'GET',
context: this,
success: this.handleResponse,
error: this.handleError
});
} else {
$( this.el ).html( this.renderedHtml );
this.postRender();
}
},
handleResponse: function( data ) {
var context = {
nextStepNum: this.nextStepNum,
nextStepTitle: this.nextStepTitle
};
// Include step-specific information
_.extend( context, this.stepData );
this.renderedHtml = _.template( data, context );
$( this.el ).html( this.renderedHtml );
this.postRender();
},
handleError: function() {
this.errorModel.set({
errorTitle: gettext("Error"),
errorMsg: gettext("An unexpected error occurred. Please reload the page to try again."),
shown: true
});
},
postRender: function() {
// Sub-classes can override this method
// to install custom event handlers.
},
nextStep: function() {
this.trigger('next-step');
},
goToStep: function( stepName ) {
this.trigger( 'go-to-step', stepName );
}
});
})( jQuery, _, _.str, Backbone, gettext );
<div id="error" 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"><%- errorTitle %></h3>
<div class="copy">
<p><%- errorMsg %></p>
</div>
</div>
</div>
</div>
<div id="wrapper-facephoto" class="wrapper-view block-photo">
<div class="facephoto view">
<h3 class="title"><%- gettext( "Take Your Photo" ) %></h3>
<div class="instruction">
<p><%- gettext( "Use your webcam to take a picture of your face so we can match it with the picture on your ID." ) %></p>
</div>
<div class="wrapper-task">
<div id="facecam" class="task cam"></div>
<div class="wrapper-help">
<div class="help help-task photo-tips facetips">
<h4 class="title"><%- gettext( "Tips on taking a successful photo" ) %></h4>
<div class="copy">
<ul class="list-help">
<li class="help-item"><%- gettext( "Make sure your face is well-lit" ) %></li>
<li class="help-item"><%- gettext( "Be sure your entire face is inside the frame" ) %></li>
<li class="help-item"><%- gettext( "Can we match the photo you took with the one on your ID?" ) %></li>
<li class="help-item"><%- gettext( "Once in position, use the camera button" ) %> <span class="example">(<i class="icon-camera"></i>)</span> <%- gettext( "to capture your picture" ) %></li>
<li class="help-item"><%- gettext( "Use the checkmark button" ) %> <span class="example">(<i class="icon-ok"></i>)</span> <%- gettext( "once you are happy with the photo" ) %></li>
</ul>
</div>
</div>
<div class="help help-faq facefaq">
<h4 class="sr title"><%- gettext( "Common Questions" ) %></h4>
<div class="copy">
<dl class="list-faq">
<dt class="faq-question"><%- gettext( "Why do you need my photo?" ) %></dt>
<dd class="faq-answer"><%- gettext( "As part of the verification process, we need your photo to confirm that you are you." ) %></dd>
<dt class="faq-question"><%- gettext( "What do you do with this picture?" ) %></dt>
<dd class="faq-answer"><%- gettext( "We only use it to verify your identity. It is not displayed anywhere." ) %></dd>
</dl>
</div>
</div>
</div>
</div>
<% if ( nextStepTitle ) { %>
<nav class="nav-wizard" id="face_next_button_nav">
<span class="help help-inline">
<%- _.sprintf( gettext( "Once you verify your photo looks good, you can move on to step %s." ), nextStepNum ) %>
</span>
<ol class="wizard-steps">
<li class="wizard-step">
<a id="next_step_button" class="next action-primary is-disabled" aria-hidden="true" title="Next">
<%- _.sprintf( gettext( "Go to Step %s" ), nextStepNum ) %>: <%- nextStepTitle %>
</a>
</li>
</ol>
</nav>
<% } %>
</div>
</div>
<div id="wrapper-idphoto" class="wrapper-view block-photo">
<div class="idphoto view">
<h3 class="title"><%- gettext( "Show Us Your ID" ) %></h3>
<div class="instruction">
<p><%- gettext("Use your webcam to take a picture of your ID so we can match it with your photo and the name on your account.") %></p>
</div>
<div class="wrapper-task">
<div id="idcam" class="task cam"></div>
<div class="wrapper-help">
<div class="help help-task photo-tips idtips">
<h4 class="title"><%- gettext( "Tips on taking a successful photo" ) %></h4>
<div class="copy">
<ul class="list-help">
<li class="help-item"><%- gettext( "Make sure your ID is well-lit" ) %></li>
<li class="help-item"><%- gettext( "Check that there isn't any glare" ) %></li>
<li class="help-item"><%- gettext( "Ensure that you can see your photo and read your name" ) %></li>
<li class="help-item"><%- gettext( "Try to keep your fingers at the edge to avoid covering important information" ) %></li>
<li class="help-item"><%- gettext( "Acceptable IDs include drivers licenses, passports, or other goverment-issued IDs that include your name and photo" ) %></li>
<li class="help-item"><%- gettext( "Once in position, use the camera button ") %> <span class="example">(<i class="icon-camera"></i>)</span> <%- gettext( "to capture your ID" ) %></li>
<li class="help-item"><%- gettext( "Use the checkmark button" ) %> <span class="example">(<i class="icon-ok"></i>)</span> <%- gettext( "once you are happy with the photo" ) %></li>
</ul>
</div>
</div>
<div class="help help-faq facefaq">
<h4 class="sr title"><%- gettext( "Common Questions" ) %></h4>
<div class="copy">
<dl class="list-faq">
<dt class="faq-question"><%- gettext( "Why do you need a photo of my ID?" ) %></dt>
<dd class="faq-answer"><%- gettext( "We need to match your ID with your photo and name to confirm that you are you." ) %></dd>
<dt class="faq-question"><%- gettext( "What do you do with this picture?" ) %></dt>
<dd class="faq-answer"><%- gettext( "We encrypt it and send it to our secure authorization service for review. We use the highest levels of security and do not save the photo or information anywhere once the match has been completed." ) %></dd>
</dl>
</div>
</div>
</div>
</div>
<% if ( nextStepTitle ) { %>
<nav class="nav-wizard" id="face_next_button_nav">
<span class="help help-inline">
<%- _.sprintf( gettext( "Once you verify your photo looks good, you can move on to step %s." ), nextStepNum ) %>
</span>
<ol class="wizard-steps">
<li class="wizard-step">
<a id="next_step_button" class="next action-primary is-disabled" aria-hidden="true" title="Next">
<%- _.sprintf( gettext( "Go to Step %s" ), nextStepNum ) %>: <%- nextStepTitle %>
</a>
</li>
</ol>
</nav>
<% } %>
</div>
</div>
<div class="wrapper-content-main">
<article class="content-main">
<h3 class="title"><%- introTitle %></h3>
<div class="instruction"><p><%- introMsg %></p></div>
<ul class="list-reqs">
<% if ( requirements['photo-id-required'] ) { %>
<li class="req req-1 req-id">
<h4 class="title"><%- gettext( "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"><%- gettext( "A photo identification document" ) %></span>
<span class="copy-sub"><%- gettext( "A driver's license, passport, or other government or school-issued ID with your name and picture on it." ) %></span>
</p>
</div>
</li>
<% } %>
<% if ( requirements['webcam-required']) { %>
<li class="req req-2 req-webcam">
<h4 class="title"><%- gettext( "Webcam" ) %></h4>
<div class="placeholder-art">
<i class="icon-facetime-video"></i>
</div>
<div class="copy">
<p>
<span class="copy-super"><%- gettext( "A webcam and a modern browser" ) %></span>
<span class="copy-sub"><strong>
<a rel="external" href="https://www.mozilla.org/en-US/firefox/new/">Firefox</a>,
<a rel="external" href="https://www.google.com/intl/en/chrome/browser/">Chrome</a>,
<a rel="external" href="http://www.apple.com/safari/">Safari</a>,
<a rel="external" href="http://windows.microsoft.com/en-us/internet-explorer/download-ie"><%- gettext("Internet Explorer 9 or later" ) %></a></strong>.
<%- gettext( "Please make sure your browser is updated to the most recent version possible." ) %>
</span>
</p>
</div>
</li>
<% } %>
<% if ( requirements['credit-card-required'] ) { %>
<li class="req req-3 req-payment">
<h4 class="title"><%- gettext( "Credit or Debit Card" ) %></h4>
<div class="placeholder-art">
<i class="icon-credit-card"></i>
</div>
<div class="copy">
<p>
<span class="copy-super"><%- gettext( "A major credit or debit card" ) %></span>
<span class="copy-sub"><%- gettext( "Visa, MasterCard, American Express, Discover, Diners Club, or JCB with the Discover logo." ) %></span>
</p>
</div>
</li>
<% } %>
</ul>
<% if ( nextStepTitle ) { %>
<nav class="nav-wizard is-ready">
<ol class="wizard-steps">
<li class="wizard-step">
<a class="next action-primary" id="next_step_button" href="?skip-first-step=1">
<%- _.sprintf( gettext( "Go to Step %s" ), nextStepNum ) %>: <%- nextStepTitle %>
</a>
</li>
</ol>
</nav>
<% } %>
</article>
</div>
<div id="wrapper-review" class="wrapper-view">
<div class="review view">
<h3 class="title"><%- gettext( "Make Payment" ) %></h3>
<div class="instruction">
<p><%- gettext( "Make payment. TODO: actual copy here." ) %></p>
</div>
<div class="wrapper-task">
<ol class="review-tasks">
<% if ( suggestedPrices.length > 0 ) { %>
<li class="review-task review-task-contribution">
<h4 class="title"><%- gettext( "Enter Your Contribution Level" ) %></h4>
<div class="copy">
<p><%- _.sprintf(
gettext( "Please confirm your contribution for this course (min. $ %(minPrice)s %(currency)s)" ),
{ minPrice: minPrice, currency: currency }
) %>
</p>
</div>
<ul class="list-fields contribution-options">
<% for ( var i = 0; i < suggestedPrices.length; i++ ) {
price = suggestedPrices[i];
%>
<li class="field contribution-option">
<input type="radio" name="contribution" value="<%- price %>" id="contribution-<%- price %>" />
<label for="contribution-<%- price %>">
<span class="deco-denomination">$</span>
<span class="label-value"><%- price %></span>
<span class="denomination-name"><%- currency %></span>
</label>
</li>
<% } %>
<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="" />
<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=""/>
<span class="denomination-name"><%- currency %></span>
</div>
</li>
</ul>
</li>
</ul>
</li>
<% } else {%>
<li class="review-task review-task-contribution">
<h4 class="title"><%- gettext( "Your Course Total" ) %></h4>
<div class="copy">
<p><%- gettext( "To complete your registration, you will need to pay:" ) %></p>
</div>
<ul class="list-fields contribution-options">
<li class="field contribution-option">
<span class="deco-denomination">$</span>
<span class="label-value"><%- minPrice %></span>
<span class="denomination-name"><%- currency %></span>
</li>
</ul>
</li>
<% } %>
</ol>
</div>
</div>
<nav class="nav-wizard is-ready">
<ol class="wizard-steps">
<li class="wizard-step">
<a class="next action-primary is-disabled" id="pay_button">
<%- gettext( "Go to payment" ) %>
</a>
</li>
</ol>
</nav>
<form id="payment-processor-form"></form>
</div>
<%!
import json
from django.utils.translation import ugettext as _
from verify_student.views import PayAndVerifyView
%>
<%namespace name='static' file='../static_content.html'/>
<%inherit file="../main.html" />
<%block name="bodyclass">register verification-process step-requirements</%block>
<%block name="pagetitle">${messages.page_title}</%block>
<%block name="header_extras">
% for template_name in ["progress", "webcam_photo", "error"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="verify_student/${template_name}.underscore" />
</script>
% endfor
</%block>
<%block name="js_extra">
<%static:js group='rwd_header_footer'/>
<script src="${static.url('js/vendor/underscore-min.js')}"></script>
<script src="${static.url('js/vendor/underscore.string.min.js')}"></script>
<script src="${static.url('js/vendor/backbone-min.js')}"></script>
<%static:js group='verify_student'/>
</%block>
<%block name="content">
## Top-level wrapper for errors
## JavaScript views may append to this wrapper
<div id="error-container" style="display: none;"></div>
<div class="container">
<section class="wrapper carousel">
## Verification status header
<header class="page-header">
<h2 class="title">
<span class="wrapper-sts">
<span class="sts-label">${messages.top_level_msg}</span>
</span>
<span class="sts-track ${"professional-ed" if course_mode.slug == "professional" else ""}">
<span class="sts-track-value"><span class="context">${messages.status_msg}</span>: ${course_mode.name}<span>
</span>
</h2>
</header>
## Payment / Verification flow
## Most of these data attributes are used to dynamically render
## the steps, but some are just useful for A/B test setup.
<div
id="pay-and-verify-container"
data-full-name='${user_full_name}'
data-platform-name='${platform_name}'
data-course-key='${course_key}'
data-course-name='${course.display_name}'
data-course-start-date='${course.start_datetime_text()}'
data-courseware-url='${courseware_url}'
data-course-mode-name='${course_mode.name}'
data-course-mode-slug='${course_mode.slug}'
data-course-mode-min-price='${course_mode.min_price}'
data-course-mode-suggested-prices='${course_mode.suggested_prices}'
data-course-mode-currency='${course_mode.currency}'
data-purchase-endpoint='${purchase_endpoint}'
data-display-steps='${json.dumps(display_steps)}'
data-current-step='${current_step}'
data-requirements='${json.dumps(requirements)}'
data-msg-key='${message_key}'
data-intro-title='${messages.intro_title}'
data-intro-msg='${messages.intro_msg}'
></div>
## Support
<div class="wrapper-content-supplementary">
<aside class="content-supplementary">
<ul class="list-help">
<li class="help-item help-item-questions">
<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>
% if PayAndVerifyView.WEBCAM_REQ in requirements:
<li class="help-item help-item-technical">
<h3 class="title">${_("Technical Requirements")}</h3>
<div class="copy">
<p>${_("Please make sure your browser is updated to the {a_start}most recent version possible{a_end}. Also, please make sure your <strong>webcam is plugged in, turned on, and allowed to function in your web browser (commonly adjustable in your browser settings).</strong>").format(a_start='<strong><a rel="external" href="http://browsehappy.com/">', a_end="</a></strong>")}</p>
</div>
</li>
% endif
</ul>
</aside>
</div>
</section>
</div>
</%block>
<div class="wrapper-content-main">
<article class="content-main">
<h3 class="title"><%- gettext( "Congratulations! You are now enrolled in the verified track." ) %></h3>
<div class="instruction">
<p><%- gettext( "You are now enrolled as a verified student! Your enrollment details are below.") %></p>
</div>
<ul class="list-info">
<li class="info-item course-info">
<h4 class="title">
<%- gettext( "You are enrolled in " ) %> :
</h4>
<div class="wrapper-report">
<table class="report report-course">
<caption class="sr"><%- gettext( "A list of courses you have just enrolled in as a verified student" ) %></caption>
<thead>
<tr>
<th scope="col" ><%- gettext( "Course" ) %></th>
<th scope="col" ><%- gettext( "Status" ) %></th>
<th scope="col" ><span class="sr"><%- gettext( "Options" ) %></span></th>
</tr>
</thead>
<tbody>
<tr>
<td><%- courseName %></td>
<td>
<%- _.sprintf( gettext( "Starts: %(start)s" ), { start: courseStartDate } ) %>
</td>
<td class="options">
<% if ( coursewareUrl ) { %>
<a class="action action-course" href="<%- coursewareUrl %>"><%- gettext( "Go to Course" ) %></a>
<% } %>
</td>
</tr>
</tbody>
<tfoot>
<tr class="course-actions">
<td colspan="3">
<a class="action action-dashboard" href="/dashboard"><%- gettext("Go to your dashboard") %></a>
</td>
</tr>
</tfoot>
</table>
</div>
</li>
</ul>
<% if ( nextStepTitle ) { %>
<nav class="nav-wizard is-ready">
<ol class="wizard-steps">
<li class="wizard-step">
<a class="next action-primary" id="verify_now_button" href="?skip-first-step=1">
<%- gettext( "Verify Now" ) %>
</a>
</li>
<li class="wizard-step">
<a class="next action-secondary" id="verify_later_button" href="/dashboard">
<%- gettext( "Verify Later" ) %>
</a>
</li>
</ol>
</nav>
<% } %>
</article>
</div>
<div class="wrapper-progress">
<section class="progress">
<h3 class="sr title"><%- gettext("Your Progress") %></h3>
<ol class="progress-steps">
<% for ( var stepNum = 0; stepNum < steps.length; stepNum++ ) { %>
<li
class="progress-step
<% if ( steps[stepNum].isCurrent ) { %> is-current <% } %>
<% if ( steps[stepNum].isComplete ) { %> is-completed <% } %>"
id="progress-step-<%- stepNum + 1 %>"
>
<span class="wrapper-step-number"><span class="step-number"><%- stepNum + 1 %></span></span>
<span class="step-name"><span class="sr"><%- gettext("Current Step") %>: </span><%- steps[stepNum].title %></span>
</li>
<% } %>
<span class="progress-sts">
<span class="progress-sts-value"></span>
</span>
</section>
</div>
<div id="wrapper-review" class="wrapper-view">
<div class="review view">
<h3 class="title"><%- gettext( "Verify Your Submission" ) %></h3>
<div class="instruction">
<p><%- gettext( "Make sure we can verify your identity with the photos and information below." ) %></p>
</div>
<div class="wrapper-task">
<ol class="review-tasks">
<li class="review-task review-task-photos">
<h4 class="title"><%- gettext( "Review the Photos You've Taken" ) %></h4>
<div class="copy">
<p><%- gettext( "Please review the photos and verify that they meet the requirements listed below." ) %></p>
</div>
<ol class="wrapper-photos">
<li class="wrapper-photo">
<div class="placeholder-photo">
<img id="face_image" src=""/>
</div>
<div class="help-tips">
<h5 class="title"><%- gettext( "The photo above needs to meet the following requirements:" ) %></h5>
<ul class="list-help list-tips copy">
<li class="tip"><%- gettext( "Be well lit" ) %></li>
<li class="tip"><%- gettext( "Show your whole face" ) %></li>
<li class="tip"><%- gettext( "The photo on your ID must match the photo of your face" ) %></li>
</ul>
</div>
</li>
<li class="wrapper-photo">
<div class="placeholder-photo">
<img id="photo_id_image" src=""/>
</div>
<div class="help-tips">
<h5 class="title"><%- gettext( "The photo above needs to meet the following requirements:" ) %></h5>
<ul class="list-help list-tips copy">
<li class="tip"><%- gettext( "Be readable (not too far away, no glare)" ) %></li>
<li class="tip"><%- gettext( "The photo on your ID must match the photo of your face" ) %></li>
<li class="tip"><%- gettext( "The name on your ID must match the name on your account below" ) %></li>
</ul>
</div>
</li>
</ol>
<div class="msg msg-retake msg-followup">
<div class="copy">
<p><%- gettext( "Photos don't meet the requirements?" ) %></p>
</div>
<ul class="list-actions">
<li class="action action-retakephotos">
<a id="retake_photos_button" class="retake-photos">
<%- gettext( "Retake Your Photos" ) %>
</a>
</li>
</ul>
</div>
</li>
<li class="review-task review-task-name">
<h4 class="title"><%- gettext( "Check Your Name" ) %></h4>
<div class="copy">
<p><%- _.sprintf( gettext( "Make sure your full name on your %(platformName)s account (%(fullName)s) matches your ID. We will also use this as the name on your certificate." ), { platformName: platformName, fullName: fullName } ) %></p>
</div>
<div class="msg msg-followup">
<div class="help-tip is-expandable">
<h5 class="title title-expand" aria-expanded="false" role="link">
<i class="icon-caret-down expandable-icon"></i>
<%- gettext( "What if the name on my account doesn't match the name on my ID?" ) %>
</h5>
<div class="copy expandable-area">
<p><%- gettext( "You should change the name on your account to match." ) %></p>
<input type="text" name="new-name" id="new-name" placeholder="New name">
</div>
</div>
</div>
</ol>
</div>
<nav class="nav-wizard">
<div class="prompt-verify">
<h3 class="title"><%- gettext( "Before proceeding, please confirm that your details match" ) %></h3>
<p class="copy">
<%- _.sprintf( gettext( "Once you verify your details match the requirements, you can move on to step %(stepNum)s, %(stepTitle)s." ), { stepNum: nextStepNum, stepTitle: nextStepTitle } ) %>
</p>
<ul class="list-actions">
<li class="action action-verify">
<input type="checkbox" name="match" id="confirm_pics_good" />
<label for="confirm_pics_good"><%- gettext( "Yes! My details all match." ) %></label>
</li>
</ul>
</div>
<ol class="wizard-steps">
<li class="wizard-step step-proceed">
<a id="next_step_button" class="next action-primary is-disabled" aria-hidden="true" title="Next">
<%- _.sprintf( gettext( "Go to Step %s" ), nextStepNum ) %>: <%- nextStepTitle %>
</a>
</li>
</ol>
</nav>
</div>
</div>
<div class="placeholder-cam" id="camera">
<div class="placeholder-art">
<p class="copy"><%- gettext( "Don't see your picture? Make sure to allow your browser to use your camera when it asks for permission." ) %></p>
</div>
<video id="photo_id_video" autoplay></video><br/>
<canvas id="photo_id_canvas" style="display:none;" width="640" height="480"></canvas>
</div>
<div class="controls photo-controls">
<ul class="list-controls">
<li class="control control-redo" id="webcam_reset_button" style="display: none;">
<a class="action action-redo">
<i class="icon-undo"></i> <span class="sr"><%- gettext( "Retake" ) %></span>
</a>
</li>
<li class="control control-do" id="webcam_capture_button">
<a class="action action-do">
<i class="icon-camera"></i> <span class="sr"><%- gettext( "Take photo" ) %></span>
</a>
</li>
<li class="control control-approve" id="webcam_approve_button" style="display: none;">
<a class="action action-approve">
<i class="icon-ok"></i> <span class="sr"><%- gettext( "Looks good" ) %></span>
</a>
</li>
</ul>
</div>
......@@ -51,6 +51,7 @@ class ProfileInternalError(Exception):
FULL_NAME_MAX_LENGTH = 255
FULL_NAME_MIN_LENGTH = 2
@intercept_errors(ProfileInternalError, ignore_errors=[ProfileRequestError])
......@@ -113,7 +114,7 @@ def update_profile(username, full_name=None):
if full_name is not None:
name_length = len(full_name)
if name_length > FULL_NAME_MAX_LENGTH or name_length == 0:
if name_length > FULL_NAME_MAX_LENGTH or name_length < FULL_NAME_MIN_LENGTH:
raise ProfileInvalidField("full_name", full_name)
else:
profile.update_name(full_name)
......
......@@ -47,7 +47,7 @@ class ProfileApiTest(TestCase):
self.assertEqual(profile['full_name'], u'ȻħȺɍłɇs')
@raises(profile_api.ProfileInvalidField)
@ddt.data('', 'a' * profile_api.FULL_NAME_MAX_LENGTH + 'a')
@ddt.data('', 'a', 'a' * profile_api.FULL_NAME_MAX_LENGTH + 'a')
def test_update_full_name_invalid(self, invalid_name):
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
profile_api.update_profile(self.USERNAME, full_name=invalid_name)
......
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