Add Jasmine tests for the payment/verification flow.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
define(['sinon', 'underscore'], function(sinon, _) {
|
||||
var fakeServer, fakeRequests, expectRequest, expectJsonRequest,
|
||||
respondWithJson, respondWithError, respondToDelete;
|
||||
respondWithJson, respondWithError, respondWithTextError, respondToDelete;
|
||||
|
||||
/* These utility methods are used by Jasmine tests to create a mock server or
|
||||
* get reference to mock requests. In either case, the cleanup (restore) is done with
|
||||
@@ -93,6 +93,22 @@ define(['sinon', 'underscore'], function(sinon, _) {
|
||||
);
|
||||
};
|
||||
|
||||
respondWithTextError = function(requests, statusCode, textResponse, requestIndex) {
|
||||
if (_.isUndefined(requestIndex)) {
|
||||
requestIndex = requests.length - 1;
|
||||
}
|
||||
if (_.isUndefined(statusCode)) {
|
||||
statusCode = 500;
|
||||
}
|
||||
if (_.isUndefined(textResponse)) {
|
||||
textResponse = "";
|
||||
}
|
||||
requests[requestIndex].respond(statusCode,
|
||||
{ 'Content-Type': 'text/plain' },
|
||||
textResponse
|
||||
);
|
||||
};
|
||||
|
||||
respondToDelete = function(requests, requestIndex) {
|
||||
if (_.isUndefined(requestIndex)) {
|
||||
requestIndex = requests.length - 1;
|
||||
@@ -108,6 +124,7 @@ define(['sinon', 'underscore'], function(sinon, _) {
|
||||
'expectJsonRequest': expectJsonRequest,
|
||||
'respondWithJson': respondWithJson,
|
||||
'respondWithError': respondWithError,
|
||||
'respondWithTextError': respondWithTextError,
|
||||
'respondToDelete': respondToDelete
|
||||
};
|
||||
});
|
||||
|
||||
@@ -512,7 +512,7 @@ class PayAndVerifyView(View):
|
||||
'disable_courseware_js': True,
|
||||
'display_steps': display_steps,
|
||||
'contribution_amount': contribution_amount,
|
||||
'is_active': request.user.is_active,
|
||||
'is_active': json.dumps(request.user.is_active),
|
||||
'messages': self._messages(
|
||||
message,
|
||||
course.display_name,
|
||||
|
||||
@@ -384,7 +384,116 @@
|
||||
'js/student_account/enrollment',
|
||||
'js/student_account/shoppingcart',
|
||||
]
|
||||
}
|
||||
},
|
||||
'js/verify_student/models/verification_model': {
|
||||
exports: 'edx.verify_student.VerificationModel',
|
||||
deps: [ 'jquery', 'underscore', 'backbone', 'jquery.cookie' ]
|
||||
},
|
||||
'js/verify_student/views/error_view': {
|
||||
exports: 'edx.verify_student.ErrorView',
|
||||
deps: [ 'jquery', 'underscore', 'backbone' ]
|
||||
},
|
||||
'js/verify_student/views/webcam_photo_view': {
|
||||
exports: 'edx.verify_student.WebcamPhotoView',
|
||||
deps: [ 'jquery', 'underscore', 'backbone', 'gettext' ]
|
||||
},
|
||||
'js/verify_student/views/progress_view': {
|
||||
exports: 'edx.verify_student.ProgressView',
|
||||
deps: [ 'jquery', 'underscore', 'backbone', 'gettext' ]
|
||||
},
|
||||
'js/verify_student/views/requirements_view': {
|
||||
exports: 'edx.verify_student.RequirementsView',
|
||||
deps: [ 'jquery', 'backbone', 'underscore', 'gettext' ]
|
||||
},
|
||||
'js/verify_student/views/step_view': {
|
||||
exports: 'edx.verify_student.StepView',
|
||||
deps: [ 'jquery', 'underscore', 'underscore.string', 'backbone', 'gettext' ]
|
||||
},
|
||||
'js/verify_student/views/intro_step_view': {
|
||||
exports: 'edx.verify_student.IntroStepView',
|
||||
deps: [
|
||||
'jquery',
|
||||
'js/verify_student/views/step_view',
|
||||
'js/verify_student/views/requirements_view'
|
||||
]
|
||||
},
|
||||
'js/verify_student/views/make_payment_step_view': {
|
||||
exports: 'edx.verify_student.MakePaymentStepView',
|
||||
deps: [
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'jquery.cookie',
|
||||
'jquery.url',
|
||||
'js/verify_student/views/step_view',
|
||||
'js/verify_student/views/requirements_view'
|
||||
]
|
||||
},
|
||||
'js/verify_student/views/payment_confirmation_step_view': {
|
||||
exports: 'edx.verify_student.PaymentConfirmationStepView',
|
||||
deps: [
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'js/verify_student/views/step_view',
|
||||
'js/verify_student/views/requirements_view'
|
||||
]
|
||||
},
|
||||
'js/verify_student/views/face_photo_step_view': {
|
||||
exports: 'edx.verify_student.FacePhotoStepView',
|
||||
deps: [
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'js/verify_student/views/step_view',
|
||||
'js/verify_student/views/webcam_photo_view'
|
||||
]
|
||||
},
|
||||
'js/verify_student/views/id_photo_step_view': {
|
||||
exports: 'edx.verify_student.IDPhotoStepView',
|
||||
deps: [
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'js/verify_student/views/step_view',
|
||||
'js/verify_student/views/webcam_photo_view'
|
||||
]
|
||||
},
|
||||
'js/verify_student/views/review_photos_step_view': {
|
||||
exports: 'edx.verify_student.ReviewPhotosStepView',
|
||||
deps: [
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'js/verify_student/views/step_view',
|
||||
'js/verify_student/views/webcam_photo_view'
|
||||
]
|
||||
},
|
||||
'js/verify_student/views/enrollment_confirmation_step_view': {
|
||||
exports: 'edx.verify_student.EnrollmentConfirmationStepView',
|
||||
deps: [
|
||||
'jquery',
|
||||
'js/verify_student/views/step_view',
|
||||
]
|
||||
},
|
||||
'js/verify_student/views/pay_and_verify_view': {
|
||||
exports: 'edx.verify_student.PayAndVerifyView',
|
||||
deps: [
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'gettext',
|
||||
'js/verify_student/models/verification_model',
|
||||
'js/verify_student/views/progress_view',
|
||||
'js/verify_student/views/intro_step_view',
|
||||
'js/verify_student/views/make_payment_step_view',
|
||||
'js/verify_student/views/payment_confirmation_step_view',
|
||||
'js/verify_student/views/face_photo_step_view',
|
||||
'js/verify_student/views/id_photo_step_view',
|
||||
'js/verify_student/views/review_photos_step_view',
|
||||
'js/verify_student/views/enrollment_confirmation_step_view'
|
||||
]
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -406,7 +515,11 @@
|
||||
'lms/include/js/spec/student_account/enrollment_spec.js',
|
||||
'lms/include/js/spec/student_account/emailoptin_spec.js',
|
||||
'lms/include/js/spec/student_account/shoppingcart_spec.js',
|
||||
'lms/include/js/spec/student_profile/profile_spec.js'
|
||||
'lms/include/js/spec/student_profile/profile_spec.js',
|
||||
'lms/include/js/spec/verify_student/pay_and_verify_view_spec.js',
|
||||
'lms/include/js/spec/verify_student/webcam_photo_view_spec.js',
|
||||
'lms/include/js/spec/verify_student/review_photos_step_view_spec.js',
|
||||
'lms/include/js/spec/verify_student/make_payment_step_view_spec.js'
|
||||
]);
|
||||
|
||||
}).call(this, requirejs, define);
|
||||
|
||||
@@ -52,4 +52,4 @@ define(['backbone', 'jquery', 'js/verify_student/photocapture'],
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
249
lms/static/js/spec/verify_student/make_payment_step_view_spec.js
Normal file
249
lms/static/js/spec/verify_student/make_payment_step_view_spec.js
Normal file
@@ -0,0 +1,249 @@
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'js/common_helpers/ajax_helpers',
|
||||
'js/common_helpers/template_helpers',
|
||||
'js/verify_student/views/make_payment_step_view'
|
||||
],
|
||||
function( $, _, Backbone, AjaxHelpers, TemplateHelpers, MakePaymentStepView ) {
|
||||
'use strict';
|
||||
|
||||
describe( 'edx.verify_student.MakePaymentStepView', function() {
|
||||
|
||||
var PAYMENT_URL = "/pay";
|
||||
|
||||
var PAYMENT_PARAMS = {
|
||||
orderId: "test-order",
|
||||
signature: "abcd1234"
|
||||
};
|
||||
|
||||
var STEP_DATA = {
|
||||
minPrice: "12",
|
||||
suggestedPrices: ["34.56", "78.90"],
|
||||
currency: "usd",
|
||||
purchaseEndpoint: PAYMENT_URL,
|
||||
courseKey: "edx/test/test"
|
||||
};
|
||||
|
||||
var SERVER_ERROR_MSG = "An error occurred!";
|
||||
|
||||
var createView = function( stepDataOverrides ) {
|
||||
var view = new MakePaymentStepView({
|
||||
el: $( '#current-step-container' ),
|
||||
templateName: 'make_payment_step',
|
||||
stepData: _.extend( _.clone( STEP_DATA ), stepDataOverrides ),
|
||||
errorModel: new ( Backbone.Model.extend({}) )()
|
||||
}).render();
|
||||
|
||||
// Stub the payment form submission
|
||||
spyOn( view, 'submitForm' ).andCallFake( function() {} );
|
||||
return view;
|
||||
};
|
||||
|
||||
var expectPriceOptions = function( prices ) {
|
||||
var sel;
|
||||
_.each( prices, function( price ) {
|
||||
sel = _.sprintf( 'input[name="contribution"][value="%s"]', price );
|
||||
expect( $( sel ).length > 0 ).toBe( true );
|
||||
});
|
||||
};
|
||||
|
||||
var expectPriceSelected = function( price ) {
|
||||
var sel = $( _.sprintf( 'input[name="contribution"][value="%s"]', price ) );
|
||||
|
||||
// If the option is available, it should be selected
|
||||
if ( sel.length > 0 ) {
|
||||
expect( sel.prop( 'checked' ) ).toBe( true );
|
||||
} else {
|
||||
// Otherwise, the text box amount should be filled in
|
||||
expect( $( '#contribution-other' ).prop( 'checked' ) ).toBe( true );
|
||||
expect( $( '#contribution-other-amt' ).val() ).toEqual( price );
|
||||
}
|
||||
};
|
||||
|
||||
var choosePriceOption = function( price ) {
|
||||
var sel = _.sprintf( 'input[name="contribution"][value="%s"]', price );
|
||||
$( sel ).trigger( 'click' );
|
||||
};
|
||||
|
||||
var enterPrice = function( price ) {
|
||||
$( '#contribution-other' ).trigger( 'click' );
|
||||
$( '#contribution-other-amt' ).val( price );
|
||||
};
|
||||
|
||||
var expectSinglePriceDisplayed = function( price ) {
|
||||
var displayedPrice = $( '.contribution-option .label-value' ).text();
|
||||
expect( displayedPrice ).toEqual( price );
|
||||
};
|
||||
|
||||
var expectPaymentButtonEnabled = function( isEnabled ) {
|
||||
var isDisabled = $( '#pay_button' ).hasClass('is-disabled');
|
||||
expect( !isDisabled ).toEqual( isEnabled );
|
||||
};
|
||||
|
||||
var expectPaymentDisabledBecauseInactive = function() {
|
||||
var payButton = $( '#pay_button'),
|
||||
activateButton = $( '#activate_button' );
|
||||
|
||||
// Payment button should be hidden
|
||||
expect( payButton.length ).toEqual(0);
|
||||
|
||||
// Activate button should be displayed and disabled
|
||||
expect( activateButton.length ).toEqual(1);
|
||||
expect( activateButton.hasClass( 'is-disabled' ) ).toBe( true );
|
||||
};
|
||||
|
||||
var goToPayment = function( requests, kwargs ) {
|
||||
var params = {
|
||||
contribution: kwargs.amount || "",
|
||||
course_id: kwargs.courseId || ""
|
||||
};
|
||||
|
||||
// Click the "go to payment" button
|
||||
$( '#pay_button' ).click();
|
||||
|
||||
// Verify that the request was made to the server
|
||||
AjaxHelpers.expectRequest(
|
||||
requests, "POST", "/verify_student/create_order/",
|
||||
$.param( params )
|
||||
);
|
||||
|
||||
// Simulate the server response
|
||||
if ( kwargs.succeeds ) {
|
||||
AjaxHelpers.respondWithJson( requests, PAYMENT_PARAMS );
|
||||
} else {
|
||||
AjaxHelpers.respondWithTextError( requests, 400, SERVER_ERROR_MSG );
|
||||
}
|
||||
};
|
||||
|
||||
var expectPaymentSubmitted = function( view, params ) {
|
||||
var form;
|
||||
|
||||
expect(view.submitForm).toHaveBeenCalled();
|
||||
form = view.submitForm.mostRecentCall.args[0];
|
||||
|
||||
expect(form.serialize()).toEqual($.param(params));
|
||||
expect(form.attr('method')).toEqual("POST");
|
||||
expect(form.attr('action')).toEqual(PAYMENT_URL);
|
||||
};
|
||||
|
||||
var expectErrorDisplayed = function( errorTitle ) {
|
||||
var actualTitle = $( '#error h3.title' ).text();
|
||||
expect( actualTitle ).toEqual( errorTitle );
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'trackLink']);
|
||||
|
||||
setFixtures( '<div id="current-step-container"></div>' );
|
||||
TemplateHelpers.installTemplate( 'templates/verify_student/make_payment_step' );
|
||||
TemplateHelpers.installTemplate( 'templates/verify_student/requirements' );
|
||||
});
|
||||
|
||||
it( 'allows users to choose a suggested price', function() {
|
||||
var view = createView({}),
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
expectPriceOptions( STEP_DATA.suggestedPrices );
|
||||
expectPaymentButtonEnabled( false );
|
||||
|
||||
choosePriceOption( STEP_DATA.suggestedPrices[1] );
|
||||
expectPaymentButtonEnabled( true );
|
||||
|
||||
goToPayment( requests, {
|
||||
amount: STEP_DATA.suggestedPrices[1],
|
||||
courseId: STEP_DATA.courseKey,
|
||||
succeeds: true
|
||||
});
|
||||
expectPaymentSubmitted( view, PAYMENT_PARAMS );
|
||||
});
|
||||
|
||||
it( 'allows users to pay the minimum price if no suggested prices are given', function() {
|
||||
var view = createView({ suggestedPrices: [] }),
|
||||
requests = AjaxHelpers.requests( this );
|
||||
|
||||
expectSinglePriceDisplayed( STEP_DATA.minPrice );
|
||||
expectPaymentButtonEnabled( true );
|
||||
|
||||
goToPayment( requests, {
|
||||
amount: STEP_DATA.minPrice,
|
||||
courseId: STEP_DATA.courseKey,
|
||||
succeeds: true
|
||||
});
|
||||
expectPaymentSubmitted( view, PAYMENT_PARAMS );
|
||||
});
|
||||
|
||||
it( 'allows the user to enter a contribution amount', function() {
|
||||
var view = createView({}),
|
||||
requests = AjaxHelpers.requests( this );
|
||||
|
||||
enterPrice( "67.89" );
|
||||
expectPaymentButtonEnabled( true );
|
||||
goToPayment( requests, {
|
||||
amount: "67.89",
|
||||
courseId: STEP_DATA.courseKey,
|
||||
succeeds: true
|
||||
});
|
||||
expectPaymentSubmitted( view, PAYMENT_PARAMS );
|
||||
});
|
||||
|
||||
it( 'selects in the contribution amount if provided', function() {
|
||||
// Pre-select one of the suggested prices
|
||||
createView({
|
||||
contributionAmount: STEP_DATA.suggestedPrices[1]
|
||||
});
|
||||
|
||||
// Expect that the price is selected
|
||||
expectPriceSelected( STEP_DATA.suggestedPrices[1]);
|
||||
});
|
||||
|
||||
it( 'fills in the contribution amount if provided', function() {
|
||||
// Pre-select a price NOT in the suggestions
|
||||
createView({
|
||||
contributionAmount: '99.99'
|
||||
});
|
||||
|
||||
// Expect that the price is filled in
|
||||
expectPriceSelected( '99.99' );
|
||||
});
|
||||
|
||||
it( 'ignores the contribution pre-selected if no suggested prices are given', function() {
|
||||
// No suggested prices, but a contribution is set
|
||||
createView({
|
||||
suggestedPrices: [],
|
||||
contributionAmount: '99.99'
|
||||
});
|
||||
|
||||
// Expect that the single price is displayed
|
||||
expectSinglePriceDisplayed( STEP_DATA.minPrice );
|
||||
});
|
||||
|
||||
it( 'disables payment for inactive users', function() {
|
||||
createView({ isActive: false });
|
||||
expectPaymentDisabledBecauseInactive();
|
||||
});
|
||||
|
||||
it( 'displays an error if the order could not be created', function() {
|
||||
var requests = AjaxHelpers.requests( this ),
|
||||
view = createView({});
|
||||
|
||||
choosePriceOption( STEP_DATA.suggestedPrices[0] );
|
||||
goToPayment( requests, {
|
||||
amount: STEP_DATA.suggestedPrices[0],
|
||||
courseId: STEP_DATA.courseKey,
|
||||
succeeds: false
|
||||
});
|
||||
|
||||
// Expect that an error is displayed
|
||||
expect( view.errorModel.get('shown') ).toBe( true );
|
||||
expect( view.errorModel.get('errorTitle') ).toEqual( 'Could not submit order' );
|
||||
expect( view.errorModel.get('errorMsg') ).toEqual( SERVER_ERROR_MSG );
|
||||
|
||||
// Expect that the payment button is re-enabled
|
||||
expectPaymentButtonEnabled( true );
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
188
lms/static/js/spec/verify_student/pay_and_verify_view_spec.js
Normal file
188
lms/static/js/spec/verify_student/pay_and_verify_view_spec.js
Normal file
@@ -0,0 +1,188 @@
|
||||
define(['jquery', 'js/common_helpers/template_helpers', 'js/verify_student/views/pay_and_verify_view'],
|
||||
function( $, TemplateHelpers, PayAndVerifyView ) {
|
||||
'use strict';
|
||||
|
||||
describe( 'edx.verify_student.PayAndVerifyView', function() {
|
||||
|
||||
var TEMPLATES = [
|
||||
'enrollment_confirmation_step',
|
||||
'error',
|
||||
'face_photo_step',
|
||||
'id_photo_step',
|
||||
'intro_step',
|
||||
'make_payment_step',
|
||||
'payment_confirmation_step',
|
||||
'progress',
|
||||
'requirements',
|
||||
'review_photos_step',
|
||||
'webcam_photo'
|
||||
];
|
||||
|
||||
var INTRO_STEP = {
|
||||
templateName: "intro_step",
|
||||
name: "intro-step",
|
||||
title: "Intro"
|
||||
};
|
||||
|
||||
var DISPLAY_STEPS_FOR_PAYMENT = [
|
||||
{
|
||||
templateName: "make_payment_step",
|
||||
name: "make-payment-step",
|
||||
title: "Make Payment"
|
||||
},
|
||||
{
|
||||
templateName: "payment_confirmation_step",
|
||||
name: "payment-confirmation-step",
|
||||
title: "Payment Confirmation"
|
||||
}
|
||||
];
|
||||
|
||||
var DISPLAY_STEPS_FOR_VERIFICATION = [
|
||||
{
|
||||
templateName: "face_photo_step",
|
||||
name: "face-photo-step",
|
||||
title: "Take Face Photo"
|
||||
},
|
||||
{
|
||||
templateName: "id_photo_step",
|
||||
name: "id-photo-step",
|
||||
title: "ID Photo"
|
||||
},
|
||||
{
|
||||
templateName: "review_photos_step",
|
||||
name: "review-photos-step",
|
||||
title: "Review Photos"
|
||||
},
|
||||
{
|
||||
templateName: "enrollment_confirmation_step",
|
||||
name: "enrollment-confirmation-step",
|
||||
title: "Enrollment Confirmation"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
var createView = function( displaySteps, currentStep ) {
|
||||
return new PayAndVerifyView({
|
||||
displaySteps: displaySteps,
|
||||
currentStep: currentStep
|
||||
}).render();
|
||||
};
|
||||
|
||||
var expectStepRendered = function( stepName, stepNum, numSteps ) {
|
||||
var i, j, sel;
|
||||
|
||||
// Expect that the step container div rendered
|
||||
expect( $( '.' + stepName ).length > 0 ).toBe( true );
|
||||
|
||||
// Expect that the progress indicator shows the correct step
|
||||
expect( $( '#progress-step-' + stepNum ).hasClass( 'is-current' ) ).toBe( true );
|
||||
|
||||
// Expect that all steps before this step are completed
|
||||
for ( i = 1; i < stepNum; i++ ) {
|
||||
sel = $( '#progress-step-' + i );
|
||||
expect( sel.hasClass('is-completed') ).toBe( true );
|
||||
expect( sel.hasClass('is-current') ).toBe( false );
|
||||
}
|
||||
|
||||
// Expect that all steps after this step are neither completed nor current
|
||||
for ( j = stepNum + 1; j <= numSteps; j++ ) {
|
||||
sel = $( '#progress-step-' + j );
|
||||
expect( sel.hasClass('is-completed') ).toBe( false );
|
||||
expect( sel.hasClass('is-current') ).toBe( false );
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'trackLink']);
|
||||
|
||||
setFixtures('<div id="pay-and-verify-container"></div>');
|
||||
$.each( TEMPLATES, function( index, templateName ) {
|
||||
TemplateHelpers.installTemplate('templates/verify_student/' + templateName );
|
||||
});
|
||||
});
|
||||
|
||||
it( 'renders payment and verification steps', function() {
|
||||
// Create the view, starting on the first step
|
||||
var view = createView(
|
||||
DISPLAY_STEPS_FOR_PAYMENT.concat(DISPLAY_STEPS_FOR_VERIFICATION),
|
||||
'make-payment-step'
|
||||
);
|
||||
|
||||
// Verify that the first step rendered
|
||||
expectStepRendered('make-payment-step', 1, 6);
|
||||
|
||||
// Iterate through the steps, ensuring that each is rendered
|
||||
view.nextStep();
|
||||
expectStepRendered('payment-confirmation-step', 2, 6);
|
||||
|
||||
view.nextStep();
|
||||
expectStepRendered('face-photo-step', 3, 6);
|
||||
|
||||
view.nextStep();
|
||||
expectStepRendered('id-photo-step', 4, 6);
|
||||
|
||||
view.nextStep();
|
||||
expectStepRendered('review-photos-step', 5, 6);
|
||||
|
||||
view.nextStep();
|
||||
expectStepRendered('enrollment-confirmation-step', 6, 6);
|
||||
|
||||
// Going past the last step stays on the last step
|
||||
view.nextStep();
|
||||
expectStepRendered('enrollment-confirmation-step', 6, 6);
|
||||
});
|
||||
|
||||
it( 'renders intro and verification steps', function() {
|
||||
var view = createView(
|
||||
[INTRO_STEP].concat(DISPLAY_STEPS_FOR_VERIFICATION),
|
||||
'intro-step'
|
||||
);
|
||||
|
||||
// Verify that the first step rendered
|
||||
expectStepRendered('intro-step', 1, 5);
|
||||
|
||||
// Iterate through the steps, ensuring that each is rendered
|
||||
view.nextStep();
|
||||
expectStepRendered('face-photo-step', 2, 5);
|
||||
|
||||
view.nextStep();
|
||||
expectStepRendered('id-photo-step', 3, 5);
|
||||
|
||||
view.nextStep();
|
||||
expectStepRendered('review-photos-step', 4, 5);
|
||||
|
||||
view.nextStep();
|
||||
expectStepRendered('enrollment-confirmation-step', 5, 5);
|
||||
});
|
||||
|
||||
it( 'starts from a later step', function() {
|
||||
// Start from the payment confirmation step
|
||||
var view = createView(
|
||||
DISPLAY_STEPS_FOR_PAYMENT.concat(DISPLAY_STEPS_FOR_VERIFICATION),
|
||||
'payment-confirmation-step'
|
||||
);
|
||||
|
||||
// Verify that we start on the right step
|
||||
expectStepRendered('payment-confirmation-step', 2, 6);
|
||||
|
||||
// Try moving to the next step
|
||||
view.nextStep();
|
||||
expectStepRendered('face-photo-step', 3, 6);
|
||||
|
||||
});
|
||||
|
||||
it( 'jumps to a particular step', function() {
|
||||
// Start on the review photos step
|
||||
var view = createView(
|
||||
DISPLAY_STEPS_FOR_VERIFICATION,
|
||||
'review-photos-step'
|
||||
);
|
||||
|
||||
// Jump back to the face photo step
|
||||
view.goToStep('face-photo-step');
|
||||
expectStepRendered('face-photo-step', 1, 4);
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,149 @@
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'js/common_helpers/ajax_helpers',
|
||||
'js/common_helpers/template_helpers',
|
||||
'js/verify_student/views/review_photos_step_view',
|
||||
'js/verify_student/models/verification_model'
|
||||
],
|
||||
function( $, _, Backbone, AjaxHelpers, TemplateHelpers, ReviewPhotosStepView, VerificationModel ) {
|
||||
'use strict';
|
||||
|
||||
describe( 'edx.verify_student.ReviewPhotosStepView', function() {
|
||||
|
||||
var STEP_DATA = {},
|
||||
FULL_NAME = "Test User",
|
||||
FACE_IMAGE = "abcd1234",
|
||||
PHOTO_ID_IMAGE = "efgh56789",
|
||||
SERVER_ERROR_MSG = "An error occurred!";
|
||||
|
||||
var createView = function() {
|
||||
return new ReviewPhotosStepView({
|
||||
el: $( '#current-step-container' ),
|
||||
templateName: 'review_photos_step',
|
||||
stepData: STEP_DATA,
|
||||
model: new VerificationModel({
|
||||
faceImage: FACE_IMAGE,
|
||||
identificationImage: PHOTO_ID_IMAGE
|
||||
}),
|
||||
errorModel: new ( Backbone.Model.extend({}) )()
|
||||
}).render();
|
||||
};
|
||||
|
||||
var confirmPhotos = function( isConfirmed ) {
|
||||
$('#confirm_pics_good').trigger( 'click' );
|
||||
};
|
||||
|
||||
var submitPhotos = function( requests, expectedParams, succeeds ) {
|
||||
// Submit the photos
|
||||
$( '#next_step_button' ).click();
|
||||
|
||||
// Expect a request to the server
|
||||
AjaxHelpers.expectRequest(
|
||||
requests, "POST", "/verify_student/submit-photos/",
|
||||
$.param( expectedParams )
|
||||
);
|
||||
|
||||
// Simulate the server response
|
||||
if ( succeeds ) {
|
||||
AjaxHelpers.respondWithJson( requests );
|
||||
} else {
|
||||
AjaxHelpers.respondWithTextError( requests, 400, SERVER_ERROR_MSG );
|
||||
}
|
||||
};
|
||||
|
||||
var setFullName = function( fullName ) {
|
||||
$('#new-name').val( fullName );
|
||||
};
|
||||
|
||||
var expectSubmitEnabled = function( isEnabled ) {
|
||||
var isDisabled = $('#next_step_button').hasClass('is-disabled');
|
||||
expect( !isDisabled ).toBe( isEnabled );
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'trackLink']);
|
||||
|
||||
setFixtures( '<div id="current-step-container"></div>' );
|
||||
TemplateHelpers.installTemplate( 'templates/verify_student/review_photos_step' );
|
||||
});
|
||||
|
||||
it( 'requires the user to confirm before submitting photos', function() {
|
||||
createView();
|
||||
|
||||
// Initially disabled
|
||||
expectSubmitEnabled( false );
|
||||
|
||||
// Confirm the photos, enabling submission
|
||||
confirmPhotos( true );
|
||||
expectSubmitEnabled( true );
|
||||
|
||||
// Unconfirm the photos, disabling submission
|
||||
confirmPhotos( false );
|
||||
expectSubmitEnabled( false );
|
||||
});
|
||||
|
||||
it( 'allows the user to change her full name', function() {
|
||||
var requests = AjaxHelpers.requests( this );
|
||||
|
||||
createView();
|
||||
setFullName( FULL_NAME );
|
||||
confirmPhotos( true );
|
||||
submitPhotos(
|
||||
requests,
|
||||
{
|
||||
face_image: FACE_IMAGE,
|
||||
photo_id_image: PHOTO_ID_IMAGE,
|
||||
full_name: FULL_NAME
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it( 'submits photos for verification', function() {
|
||||
var requests = AjaxHelpers.requests( this );
|
||||
|
||||
createView();
|
||||
confirmPhotos( true );
|
||||
submitPhotos(
|
||||
requests,
|
||||
{
|
||||
face_image: FACE_IMAGE,
|
||||
photo_id_image: PHOTO_ID_IMAGE
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// Expect that submission is disabled to prevent
|
||||
// duplicate submission.
|
||||
expectSubmitEnabled( false );
|
||||
});
|
||||
|
||||
it( 'displays an error if photo submission fails', function() {
|
||||
var view = createView(),
|
||||
requests = AjaxHelpers.requests( this );
|
||||
|
||||
confirmPhotos( true );
|
||||
submitPhotos(
|
||||
requests,
|
||||
{
|
||||
face_image: FACE_IMAGE,
|
||||
photo_id_image: PHOTO_ID_IMAGE
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// Expect the submit button is re-enabled to allow
|
||||
// the user to retry.
|
||||
expectSubmitEnabled( true );
|
||||
|
||||
// Expect that an error message is displayed
|
||||
expect( view.errorModel.get('shown') ).toBe( true );
|
||||
expect( view.errorModel.get('errorTitle') ).toEqual( 'Could not submit photos' );
|
||||
expect( view.errorModel.get('errorMsg') ).toEqual( SERVER_ERROR_MSG );
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
212
lms/static/js/spec/verify_student/webcam_photo_view_spec.js
Normal file
212
lms/static/js/spec/verify_student/webcam_photo_view_spec.js
Normal file
@@ -0,0 +1,212 @@
|
||||
define([
|
||||
'jquery',
|
||||
'backbone',
|
||||
'js/common_helpers/template_helpers',
|
||||
'js/common_helpers/ajax_helpers',
|
||||
'js/verify_student/views/webcam_photo_view',
|
||||
'js/verify_student/models/verification_model'
|
||||
],
|
||||
function( $, Backbone, TemplateHelpers, AjaxHelpers, WebcamPhotoView, VerificationModel ) {
|
||||
'use strict';
|
||||
|
||||
describe( 'edx.verify_student.WebcamPhotoView', function() {
|
||||
|
||||
var IMAGE_DATA = "abcd1234",
|
||||
VIDEO_ERROR_TITLE = "video capture error",
|
||||
VIDEO_ERROR_MSG = "video error msg";
|
||||
|
||||
/**
|
||||
* For the purposes of these tests, we stub out the backend
|
||||
* video capture implementation.
|
||||
* This allows us to easily test the application logic
|
||||
* without needing to handle the subtleties of video capture
|
||||
* (especially cross-browser).
|
||||
* However, this means that the test suite does NOT adequately
|
||||
* cover the HTML5 / Flash webcam integration. We will need
|
||||
* cross-browser manual testing to verify that this works correctly.
|
||||
*/
|
||||
var StubBackend = function( name, isSupported, snapshotSuccess ) {
|
||||
|
||||
if ( _.isUndefined( isSupported ) ) {
|
||||
isSupported = true;
|
||||
}
|
||||
|
||||
if ( _.isUndefined( snapshotSuccess ) ) {
|
||||
snapshotSuccess = true;
|
||||
}
|
||||
|
||||
return {
|
||||
name: name,
|
||||
initialize: function() {},
|
||||
isSupported: function() { return isSupported; },
|
||||
snapshot: function() { return snapshotSuccess; },
|
||||
getImageData: function() { return IMAGE_DATA; },
|
||||
reset: function() {}
|
||||
};
|
||||
};
|
||||
|
||||
var createView = function( backends ) {
|
||||
return new WebcamPhotoView({
|
||||
el: $( '#current-step-container' ),
|
||||
model: new VerificationModel({}),
|
||||
modelAttribute: 'faceImage',
|
||||
errorModel: new ( Backbone.Model.extend({}) )(),
|
||||
submitButton: $( '#submit_button' ),
|
||||
backends: backends
|
||||
}).render();
|
||||
};
|
||||
|
||||
var takeSnapshot = function() {
|
||||
$( '#webcam_capture_button' ).click();
|
||||
};
|
||||
|
||||
var resetWebcam = function() {
|
||||
$( '#webcam_reset_button' ).click();
|
||||
};
|
||||
|
||||
var expectButtonShown = function( obj ) {
|
||||
var resetButton = $( '#webcam_reset_button' ),
|
||||
captureButton = $( '#webcam_capture_button' );
|
||||
|
||||
expect( captureButton.hasClass( 'is-hidden') ).toBe( !obj.snapshot );
|
||||
expect( resetButton.hasClass( 'is-hidden') ).toBe( !obj.reset );
|
||||
};
|
||||
|
||||
var expectSubmitEnabled = function( isEnabled ) {
|
||||
var isDisabled = $( '#submit_button' ).hasClass( 'is-disabled' );
|
||||
expect( !isDisabled ).toEqual( isEnabled );
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'trackLink']);
|
||||
|
||||
setFixtures(
|
||||
'<div id="current-step-container"></div>' +
|
||||
'<input type="button" id="submit_button" class="is-disabled"></input>'
|
||||
);
|
||||
TemplateHelpers.installTemplate( 'templates/verify_student/webcam_photo' );
|
||||
});
|
||||
|
||||
it( 'takes a snapshot', function() {
|
||||
var view = createView( [ StubBackend( "html5" ) ] );
|
||||
|
||||
// Spy on the backend
|
||||
spyOn( view.backend, 'snapshot' ).andCallThrough();
|
||||
|
||||
// Initially, only the snapshot button is shown
|
||||
expectButtonShown({
|
||||
snapshot: true,
|
||||
reset: false
|
||||
});
|
||||
|
||||
expectSubmitEnabled( false );
|
||||
|
||||
// Take the snapshot
|
||||
takeSnapshot();
|
||||
|
||||
// Expect that the backend was used to take the snapshot
|
||||
expect( view.backend.snapshot ).toHaveBeenCalled();
|
||||
|
||||
// Expect that buttons were updated
|
||||
expectButtonShown({
|
||||
snapshot: false,
|
||||
reset: true
|
||||
});
|
||||
expectSubmitEnabled( true );
|
||||
|
||||
// Expect that the image data was saved to the model
|
||||
expect( view.model.get( 'faceImage' ) ).toEqual( IMAGE_DATA );
|
||||
});
|
||||
|
||||
it( 'resets the camera', function() {
|
||||
var view = createView( [ StubBackend( "html5" ) ]);
|
||||
|
||||
// Spy on the backend
|
||||
spyOn( view.backend, 'reset' ).andCallThrough();
|
||||
|
||||
// Take the snapshot, then reset
|
||||
takeSnapshot();
|
||||
resetWebcam();
|
||||
|
||||
// Expect that the backend was reset
|
||||
expect( view.backend.reset ).toHaveBeenCalled();
|
||||
|
||||
// Expect that we're back to the initial button shown state
|
||||
expectButtonShown({
|
||||
snapshot: true,
|
||||
reset: false
|
||||
});
|
||||
expectSubmitEnabled( false );
|
||||
|
||||
// Expect that the image data is wiped from the model
|
||||
expect( view.model.get( 'faceImage' ) ).toEqual( "" );
|
||||
});
|
||||
|
||||
it( 'falls back to a second video capture backend', function() {
|
||||
var backends = [ StubBackend( "html5", false ), StubBackend( "flash", true ) ],
|
||||
view = createView( backends );
|
||||
|
||||
// Expect that the second backend is chosen
|
||||
expect( view.backend.name ).toEqual( backends[1].name );
|
||||
});
|
||||
|
||||
it( 'displays an error if no video backend is supported', function() {
|
||||
var backends = [ StubBackend( "html5", false ), StubBackend( "flash", false ) ],
|
||||
view = createView( backends );
|
||||
|
||||
// Expect an error
|
||||
expect( view.errorModel.get( 'errorTitle' ) ).toEqual( 'No Flash Detected' );
|
||||
expect( view.errorModel.get( 'errorMsg' ) ).toContain( 'Get Flash' );
|
||||
expect( view.errorModel.get( 'shown' ) ).toBe( true );
|
||||
|
||||
// Expect that submission is disabled
|
||||
expectSubmitEnabled( false );
|
||||
});
|
||||
|
||||
it( 'displays an error if the snapshot fails', function() {
|
||||
var backends = [ StubBackend( "html5", true, false ) ],
|
||||
view = createView( backends );
|
||||
|
||||
// Take a snapshot
|
||||
takeSnapshot();
|
||||
|
||||
// Do NOT expect an error displayed
|
||||
expect( view.errorModel.get( 'shown' ) ).not.toBe( true );
|
||||
|
||||
// Expect that the capture button is still enabled
|
||||
// so the user can retry.
|
||||
expectButtonShown({
|
||||
snapshot: true,
|
||||
reset: false
|
||||
});
|
||||
|
||||
// Expect that submit is NOT enabled, since the user didn't
|
||||
// successfully take a snapshot.
|
||||
expectSubmitEnabled( false );
|
||||
});
|
||||
|
||||
it( 'displays an error triggered by the backend', function() {
|
||||
var view = createView( [ StubBackend( "html5") ] );
|
||||
|
||||
// Simulate an error triggered by the backend
|
||||
// This could occur at any point, including
|
||||
// while the video capture is being set up.
|
||||
view.backend.trigger( 'error', VIDEO_ERROR_TITLE, VIDEO_ERROR_MSG );
|
||||
|
||||
// Verify that the error is displayed
|
||||
expect( view.errorModel.get( 'errorTitle' ) ).toEqual( VIDEO_ERROR_TITLE );
|
||||
expect( view.errorModel.get( 'errorMsg' ) ).toEqual( VIDEO_ERROR_MSG );
|
||||
expect( view.errorModel.get( 'shown' ) ).toBe( true );
|
||||
|
||||
// Expect that buttons are hidden
|
||||
expectButtonShown({
|
||||
snapshot: false,
|
||||
reset: false
|
||||
});
|
||||
expectSubmitEnabled( false );
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
var edx = edx || {};
|
||||
|
||||
(function($) {
|
||||
(function( $, _ ) {
|
||||
'use strict';
|
||||
var errorView,
|
||||
el = $('#pay-and-verify-container');
|
||||
@@ -47,8 +47,11 @@ var edx = edx || {};
|
||||
requirements: el.data('requirements'),
|
||||
courseKey: el.data('course-key'),
|
||||
minPrice: el.data('course-mode-min-price'),
|
||||
suggestedPrices: (el.data('course-mode-suggested-prices') || "").split(","),
|
||||
contributionAmount: el.data('contribution-amount'),
|
||||
suggestedPrices: _.filter(
|
||||
(el.data('course-mode-suggested-prices') || "").split(","),
|
||||
function( price ) { return Boolean( price ); }
|
||||
),
|
||||
currency: el.data('course-mode-currency'),
|
||||
purchaseEndpoint: el.data('purchase-endpoint')
|
||||
},
|
||||
@@ -68,4 +71,4 @@ var edx = edx || {};
|
||||
}
|
||||
}
|
||||
}).render();
|
||||
})(jQuery);
|
||||
})( jQuery, _ );
|
||||
|
||||
@@ -15,6 +15,14 @@ var edx = edx || {};
|
||||
postRender: function() {
|
||||
// Track a virtual pageview, for easy funnel reconstruction.
|
||||
window.analytics.page( 'verification', this.templateName );
|
||||
},
|
||||
|
||||
defaultContext: function() {
|
||||
return {
|
||||
courseName: '',
|
||||
courseStartDate: '',
|
||||
coursewareUrl: ''
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,14 @@ var edx = edx || {};
|
||||
|
||||
edx.verify_student.IntroStepView = edx.verify_student.StepView.extend({
|
||||
|
||||
defaultContext: function() {
|
||||
return {
|
||||
introTitle: '',
|
||||
introMsg: '',
|
||||
isActive: false
|
||||
};
|
||||
},
|
||||
|
||||
// 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
|
||||
|
||||
@@ -10,6 +10,15 @@ var edx = edx || {};
|
||||
|
||||
edx.verify_student.MakePaymentStepView = edx.verify_student.StepView.extend({
|
||||
|
||||
defaultContext: function() {
|
||||
return {
|
||||
isActive: true,
|
||||
suggestedPrices: [],
|
||||
minPrice: 0,
|
||||
currency: "usd"
|
||||
};
|
||||
},
|
||||
|
||||
postRender: function() {
|
||||
// Render requirements
|
||||
new edx.verify_student.RequirementsView({
|
||||
@@ -26,8 +35,14 @@ var edx = edx || {};
|
||||
this.selectPaymentAmount( this.stepData.contributionAmount );
|
||||
}
|
||||
|
||||
// Enable the payment button once an amount is chosen
|
||||
$( "input[name='contribution']" ).on( 'click', _.bind( this.enablePaymentButton, this ) );
|
||||
if ( this.templateContext().suggestedPrices.length > 0 ) {
|
||||
// Enable the payment button once an amount is chosen
|
||||
$( "input[name='contribution']" ).on( 'click', _.bind( this.enablePaymentButton, this ) );
|
||||
} else {
|
||||
// If there is only one payment option, then the user isn't shown
|
||||
// radio buttons, so we need to enable the radio button.
|
||||
this.enablePaymentButton();
|
||||
}
|
||||
|
||||
// Handle payment submission
|
||||
$( "#pay_button" ).on( 'click', _.bind( this.createOrder, this ) );
|
||||
@@ -89,41 +104,49 @@ var edx = edx || {};
|
||||
// this page. A virtual pageview can be used to do this.
|
||||
window.analytics.page( 'payment', 'payment_processor_step' );
|
||||
|
||||
form.submit();
|
||||
this.submitForm( form );
|
||||
},
|
||||
|
||||
handleCreateOrderError: function( xhr ) {
|
||||
var errorMsg = gettext( 'An unexpected error occurred. Please try again.' );
|
||||
|
||||
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
|
||||
});
|
||||
errorMsg = xhr.responseText;
|
||||
}
|
||||
|
||||
this.errorModel.set({
|
||||
errorTitle: gettext( 'Could not submit order' ),
|
||||
errorMsg: errorMsg,
|
||||
shown: true
|
||||
});
|
||||
|
||||
// Re-enable the button so the user can re-try
|
||||
$( "#payment-processor-form" ).removeClass("is-disabled");
|
||||
$( "#pay_button" ).removeClass("is-disabled");
|
||||
},
|
||||
|
||||
getPaymentAmount: function() {
|
||||
var contributionInput = $("input[name='contribution']:checked", this.el);
|
||||
var contributionInput = $("input[name='contribution']:checked", this.el),
|
||||
amount = null;
|
||||
|
||||
if ( contributionInput.attr('id') === 'contribution-other' ) {
|
||||
return $( "input[name='contribution-other-amt']", this.el ).val();
|
||||
amount = $( "input[name='contribution-other-amt']", this.el ).val();
|
||||
} else {
|
||||
return contributionInput.val();
|
||||
amount = contributionInput.val();
|
||||
}
|
||||
|
||||
// If no suggested prices are available, then the user does not
|
||||
// get the option to select a price. Default to the minimum.
|
||||
if ( !amount ) {
|
||||
amount = this.templateContext().minPrice;
|
||||
}
|
||||
|
||||
return amount;
|
||||
},
|
||||
|
||||
selectPaymentAmount: function( amount ) {
|
||||
var amountFloat = parseFloat( amount ),
|
||||
foundPrice;
|
||||
foundPrice,
|
||||
sel;
|
||||
|
||||
// Check if we have a suggested price that matches the amount
|
||||
foundPrice = _.find(
|
||||
@@ -135,7 +158,8 @@ var edx = edx || {};
|
||||
|
||||
// If we've found an option for the price, select it.
|
||||
if ( foundPrice ) {
|
||||
$( '#contribution-' + foundPrice, this.el ).prop( 'checked', true );
|
||||
sel = _.sprintf( 'input[name="contribution"][value="%s"]', foundPrice );
|
||||
$( sel ).prop( 'checked', true );
|
||||
} else {
|
||||
// Otherwise, enter the value into the text box
|
||||
$( '#contribution-other-amt', this.el ).val( amount );
|
||||
@@ -144,6 +168,13 @@ var edx = edx || {};
|
||||
|
||||
// In either case, enable the payment button
|
||||
this.enablePaymentButton();
|
||||
|
||||
return amount;
|
||||
},
|
||||
|
||||
// Stubbed out in tests
|
||||
submitForm: function( form ) {
|
||||
form.submit();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -16,8 +16,6 @@ var edx = edx || {};
|
||||
edx.verify_student.PayAndVerifyView = Backbone.View.extend({
|
||||
el: '#pay-and-verify-container',
|
||||
|
||||
template: '#progress-tpl',
|
||||
|
||||
subviews: {},
|
||||
|
||||
VERIFICATION_VIEW_NAMES: [
|
||||
@@ -27,7 +25,7 @@ var edx = edx || {};
|
||||
],
|
||||
|
||||
initialize: function( obj ) {
|
||||
this.errorModel = obj.errorModel || {};
|
||||
this.errorModel = obj.errorModel || null;
|
||||
this.displaySteps = obj.displaySteps || [];
|
||||
|
||||
this.progressView = new edx.verify_student.ProgressView({
|
||||
@@ -43,7 +41,7 @@ var edx = edx || {};
|
||||
)
|
||||
});
|
||||
|
||||
this.initializeStepViews( obj.stepInfo );
|
||||
this.initializeStepViews( obj.stepInfo || {} );
|
||||
},
|
||||
|
||||
initializeStepViews: function( stepInfo ) {
|
||||
|
||||
@@ -10,9 +10,14 @@ var edx = edx || {};
|
||||
|
||||
edx.verify_student.ReviewPhotosStepView = edx.verify_student.StepView.extend({
|
||||
|
||||
postRender: function() {
|
||||
var model = this.model;
|
||||
defaultContext: function() {
|
||||
return {
|
||||
platformName: "",
|
||||
fullName: "",
|
||||
};
|
||||
},
|
||||
|
||||
postRender: function() {
|
||||
// Load the photos from the previous steps
|
||||
$( '#face_image' )[0].src = this.model.get('faceImage');
|
||||
$( '#photo_id_image' )[0].src = this.model.get('identificationImage');
|
||||
@@ -49,6 +54,8 @@ var edx = edx || {};
|
||||
},
|
||||
|
||||
submitPhotos: function() {
|
||||
var fullName = $( '#new-name' ).val();
|
||||
|
||||
// Disable the submit button to prevent duplicate submissions
|
||||
$( '#next_step_button' ).addClass( 'is-disabled' );
|
||||
|
||||
@@ -59,30 +66,28 @@ var edx = edx || {};
|
||||
this.listenToOnce( this.model, 'error', _.bind( this.handleSubmissionError, this ) );
|
||||
|
||||
// Submit
|
||||
this.model.set( 'fullName', $( '#new-name' ).val() );
|
||||
if ( fullName ) {
|
||||
this.model.set( 'fullName', fullName );
|
||||
}
|
||||
this.model.save();
|
||||
},
|
||||
|
||||
handleSubmissionError: function( xhr ) {
|
||||
var isConfirmChecked = $( "#confirm_pics_good" ).prop('checked'),
|
||||
errorMsg = gettext( 'An unexpected error occurred. Please try again later.' );
|
||||
|
||||
// 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
|
||||
});
|
||||
errorMsg = xhr.responseText;
|
||||
}
|
||||
|
||||
this.errorModel.set({
|
||||
errorTitle: gettext( 'Could not submit photos' ),
|
||||
errorMsg: errorMsg,
|
||||
shown: true
|
||||
});
|
||||
},
|
||||
|
||||
expandCallback: function( event ) {
|
||||
|
||||
@@ -26,19 +26,11 @@
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var templateHtml = $( "#" + this.templateName + "-tpl" ).html(),
|
||||
templateContext = {
|
||||
nextStepNum: this.nextStepNum,
|
||||
nextStepTitle: this.nextStepTitle
|
||||
};
|
||||
|
||||
// Include step-specific information from the server
|
||||
// (passed in from data- attributes to the parent view)
|
||||
_.extend( templateContext, this.stepData );
|
||||
var templateHtml = $( "#" + this.templateName + "-tpl" ).html();
|
||||
|
||||
// Allow subclasses to add additional information
|
||||
// to the template context, perhaps asynchronously.
|
||||
this.updateContext( templateContext ).done(
|
||||
this.updateContext( this.templateContext() ).done(
|
||||
function( templateContext ) {
|
||||
// Render the template into the DOM
|
||||
$( this.el ).html( _.template( templateHtml, templateContext ) );
|
||||
@@ -47,6 +39,8 @@
|
||||
this.postRender();
|
||||
}
|
||||
).fail( _.bind( this.handleError, this ) );
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
handleResponse: function( data ) {
|
||||
@@ -58,10 +52,8 @@
|
||||
// Include step-specific information
|
||||
_.extend( context, this.stepData );
|
||||
|
||||
this.renderedHtml = _.template( data, context );
|
||||
$( this.el ).html( this.renderedHtml );
|
||||
|
||||
this.postRender();
|
||||
// Track a virtual pageview, for easy funnel reconstruction.
|
||||
window.analytics.page( 'verification', this.templateName );
|
||||
},
|
||||
|
||||
handleError: function( errorTitle, errorMsg ) {
|
||||
@@ -72,6 +64,26 @@
|
||||
});
|
||||
},
|
||||
|
||||
templateContext: function() {
|
||||
var context = {
|
||||
nextStepNum: this.nextStepNum,
|
||||
nextStepTitle: this.nextStepTitle
|
||||
};
|
||||
return _.extend( context, this.defaultContext(), this.stepData );
|
||||
},
|
||||
|
||||
/**
|
||||
* Provide default values for the template context.
|
||||
* Subclasses can use this to fill in values that
|
||||
* the underscore templates expect to be defined.
|
||||
* This is especially useful for testing, so that the
|
||||
* tests can pass in only the values relevant
|
||||
* to the test.
|
||||
*/
|
||||
defaultContext: function() {
|
||||
return {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Subclasses can override this to add information to
|
||||
* the template context. This returns an asynchronous
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
|
||||
template: "#webcam_photo-tpl",
|
||||
|
||||
videoCaptureBackend: {
|
||||
backends: [
|
||||
{
|
||||
name: "html5",
|
||||
|
||||
html5: {
|
||||
initialize: function( obj ) {
|
||||
this.URL = (window.URL || window.webkitURL);
|
||||
this.video = obj.video || "";
|
||||
@@ -90,7 +91,9 @@
|
||||
}
|
||||
},
|
||||
|
||||
flash: {
|
||||
{
|
||||
name: "flash",
|
||||
|
||||
initialize: function( obj ) {
|
||||
this.wrapper = obj.wrapper || "";
|
||||
this.imageData = "";
|
||||
@@ -193,15 +196,18 @@
|
||||
// so we don't need to keep checking.
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
videoBackendPriority: ['html5', 'flash'],
|
||||
],
|
||||
|
||||
initialize: function( obj ) {
|
||||
this.submitButton = obj.submitButton || "";
|
||||
this.modelAttribute = obj.modelAttribute || "";
|
||||
this.errorModel = obj.errorModel || {};
|
||||
this.backend = this.chooseVideoCaptureBackend();
|
||||
this.errorModel = obj.errorModel || null;
|
||||
this.backend = _.find(
|
||||
obj.backends || this.backends,
|
||||
function( backend ) {
|
||||
return backend.isSupported();
|
||||
}
|
||||
);
|
||||
|
||||
if ( !this.backend ) {
|
||||
this.handleError(
|
||||
@@ -232,15 +238,20 @@
|
||||
// Initialize the video capture backend
|
||||
// We need to do this after rendering the template
|
||||
// so that the backend has the opportunity to modify the DOM.
|
||||
this.backend.initialize({
|
||||
wrapper: "#camera",
|
||||
video: '#photo_id_video',
|
||||
canvas: '#photo_id_canvas'
|
||||
});
|
||||
if ( this.backend ) {
|
||||
this.backend.initialize({
|
||||
wrapper: "#camera",
|
||||
video: '#photo_id_video',
|
||||
canvas: '#photo_id_canvas'
|
||||
});
|
||||
|
||||
// Install event handlers
|
||||
$( "#webcam_reset_button", this.el ).on( 'click', _.bind( this.reset, this ) );
|
||||
$( "#webcam_capture_button", this.el ).on( 'click', _.bind( this.capture, this ) );
|
||||
// Install event handlers
|
||||
$( "#webcam_reset_button", this.el ).on( 'click', _.bind( this.reset, this ) );
|
||||
$( "#webcam_capture_button", this.el ).on( 'click', _.bind( this.capture, this ) );
|
||||
|
||||
// Show the capture button
|
||||
$( "#webcam_capture_button", this.el ).removeClass('is-hidden');
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
@@ -252,9 +263,12 @@
|
||||
// Reset the video capture
|
||||
this.backend.reset();
|
||||
|
||||
// Reset data on the model
|
||||
this.model.set( this.modelAttribute, "" );
|
||||
|
||||
// Go back to the initial button state
|
||||
$( "#webcam_reset_button", this.el ).hide();
|
||||
$( "#webcam_capture_button", this.el ).show();
|
||||
$( "#webcam_reset_button", this.el ).addClass('is-hidden');
|
||||
$( "#webcam_capture_button", this.el ).removeClass('is-hidden');
|
||||
},
|
||||
|
||||
capture: function() {
|
||||
@@ -267,8 +281,8 @@
|
||||
this.trigger( 'imageCaptured' );
|
||||
|
||||
// Hide the capture button, and show the reset button
|
||||
$( "#webcam_capture_button", this.el ).hide();
|
||||
$( "#webcam_reset_button", this.el ).show();
|
||||
$( "#webcam_capture_button", this.el ).addClass('is-hidden');
|
||||
$( "#webcam_reset_button", this.el ).removeClass('is-hidden');
|
||||
|
||||
// Save the data to the model
|
||||
this.model.set( this.modelAttribute, this.backend.getImageData() );
|
||||
@@ -278,29 +292,19 @@
|
||||
}
|
||||
},
|
||||
|
||||
chooseVideoCaptureBackend: function() {
|
||||
var i, backendName, backend;
|
||||
|
||||
for ( i = 0; i < this.videoBackendPriority.length; i++ ) {
|
||||
backendName = this.videoBackendPriority[i];
|
||||
backend = this.videoCaptureBackend[backendName];
|
||||
if ( backend.isSupported() ) {
|
||||
return backend;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleError: function( errorTitle, errorMsg ) {
|
||||
// Hide the buttons
|
||||
$( "#webcam_capture_button", this.el ).hide();
|
||||
$( "#webcam_reset_button", this.el ).hide();
|
||||
$( "#webcam_capture_button", this.el ).addClass('is-hidden');
|
||||
$( "#webcam_reset_button", this.el ).addClass('is-hidden');
|
||||
|
||||
// Show the error message
|
||||
this.errorModel.set({
|
||||
errorTitle: errorTitle,
|
||||
errorMsg: errorMsg,
|
||||
shown: true
|
||||
});
|
||||
if ( this.errorModel ) {
|
||||
this.errorModel.set({
|
||||
errorTitle: errorTitle,
|
||||
errorMsg: errorMsg,
|
||||
shown: true
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ fixture_paths:
|
||||
- templates/dashboard
|
||||
- templates/student_account
|
||||
- templates/student_profile
|
||||
- templates/verify_student
|
||||
- templates/file-upload.underscore
|
||||
|
||||
requirejs:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="wrapper-content-main">
|
||||
<div class="wrapper-content-main enrollment-confirmation-step">
|
||||
<article class="content-main">
|
||||
<h3 class="title"><%- gettext( "Congratulations! You are now enrolled in the verified track." ) %></h3>
|
||||
<div class="instruction">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="wrapper-facephoto" class="wrapper-view block-photo">
|
||||
<div id="wrapper-facephoto" class="wrapper-view block-photo face-photo-step">
|
||||
<div class="facephoto view">
|
||||
<h3 class="title"><%- gettext( "Take Your Photo" ) %></h3>
|
||||
<div class="instruction">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="wrapper-idphoto" class="wrapper-view block-photo">
|
||||
<div id="wrapper-idphoto" class="wrapper-view block-photo id-photo-step">
|
||||
<div class="idphoto view">
|
||||
<h3 class="title"><%- gettext( "Show Us Your ID" ) %></h3>
|
||||
<div class="instruction">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="wrapper-content-main">
|
||||
<div class="wrapper-content-main intro-step">
|
||||
<article class="content-main">
|
||||
<h3 class="title"><%- introTitle %></h3>
|
||||
<div class="instruction"><p><%- introMsg %></p></div>
|
||||
@@ -9,11 +9,11 @@
|
||||
<nav class="nav-wizard is-ready">
|
||||
<ol class="wizard-steps">
|
||||
<li class="wizard-step">
|
||||
<a class="next action-primary" <% if (isActive == "False") { %>disabled="true"<% } %> id="next_step_button" href="?skip-first-step=1">
|
||||
<% if ( isActive == "False" ) { %>
|
||||
<a class="next action-primary" <% if ( !isActive ) { %>disabled="true"<% } %> id="next_step_button" href="?skip-first-step=1">
|
||||
<% if ( !isActive ) { %>
|
||||
<%- gettext( "Activate Your Account" ) %>
|
||||
<% } else { %>
|
||||
<%- _.sprintf( gettext( "Go to Step %s" ), nextStepNum ) %>: <%- nextStepTitle %>
|
||||
<%- _.sprintf( gettext( "Go to Step %(stepNumber)s" ), { stepNumber: nextStepNum } ) %>: <%- nextStepTitle %>
|
||||
<% } %>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="wrapper-review" class="wrapper-view">
|
||||
<div id="wrapper-review" class="wrapper-view make-payment-step">
|
||||
<div class="review view">
|
||||
<h3 class="title"><%- gettext( "Make Payment" ) %></h3>
|
||||
<div class="instruction">
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<div class="requirements-container"></div>
|
||||
|
||||
<% if ( isActive == "True" ) { %>
|
||||
<% if ( isActive ) { %>
|
||||
<div class="wrapper-task">
|
||||
<ol class="review-tasks">
|
||||
<% if ( suggestedPrices.length > 0 ) { %>
|
||||
@@ -79,13 +79,15 @@
|
||||
<nav class="nav-wizard is-ready">
|
||||
<ol class="wizard-steps">
|
||||
<li class="wizard-step">
|
||||
<% if ( isActive ) { %>
|
||||
<a class="next action-primary is-disabled" id="pay_button">
|
||||
<% if ( isActive == "False" ) { %>
|
||||
<%- gettext( "Activate Your Account" ) %>
|
||||
<% } else { %>
|
||||
<%- gettext( "Go to payment" ) %>
|
||||
<% } %>
|
||||
<%- gettext( "Go to payment" ) %>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<a class="next action-primary is-disabled" id="activate_button">
|
||||
<%- gettext( "Activate Your Account" ) %>
|
||||
</a>
|
||||
<% } %>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
@@ -71,7 +71,7 @@ from verify_student.views import PayAndVerifyView
|
||||
data-current-step='${current_step}'
|
||||
data-requirements='${json.dumps(requirements)}'
|
||||
data-msg-key='${message_key}'
|
||||
data-is-active="${is_active}"
|
||||
data-is-active='${is_active}'
|
||||
data-intro-title='${messages.intro_title}'
|
||||
data-intro-msg='${messages.intro_msg}'
|
||||
></div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="wrapper-content-main">
|
||||
<div class="wrapper-content-main payment-confirmation-step">
|
||||
<article class="content-main">
|
||||
<h3 class="title"><%- gettext( "Congratulations! You are now enrolled in the verified track." ) %></h3>
|
||||
<div class="instruction">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="wrapper-progress">
|
||||
<div class="wrapper-progress progress">
|
||||
<section class="progress">
|
||||
<h3 class="sr title"><%- gettext("Your Progress") %></h3>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="wrapper-review" class="wrapper-view">
|
||||
<div id="wrapper-review" class="wrapper-view review-photos-step">
|
||||
<div class="review view">
|
||||
<h3 class="title"><%- gettext( "Verify Your Submission" ) %></h3>
|
||||
<div class="instruction">
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
|
||||
<div class="controls photo-controls">
|
||||
<ul class="list-controls">
|
||||
<li class="control control-retake" id="webcam_reset_button" style="display: none;">
|
||||
<li class="control control-retake is-hidden" id="webcam_reset_button">
|
||||
<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">
|
||||
<li class="control control-do is-hidden" id="webcam_capture_button">
|
||||
<a class="action action-do">
|
||||
<i class="icon-camera"></i> <span class="sr"><%- gettext( "Take photo" ) %></span>
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user