Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
0dd9ad8c
Commit
0dd9ad8c
authored
Dec 17, 2014
by
Will Daly
Committed by
Zia Fazal
Apr 07, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add Jasmine tests for the payment/verification flow.
parent
8e6f4654
Hide whitespace changes
Inline
Side-by-side
Showing
27 changed files
with
1123 additions
and
124 deletions
+1123
-124
common/static/js/spec_helpers/ajax_helpers.js
+18
-1
lms/djangoapps/verify_student/views.py
+1
-1
lms/static/js/spec/main.js
+115
-2
lms/static/js/spec/photocapture_spec.js
+1
-2
lms/static/js/spec/verify_student/make_payment_step_view_spec.js
+249
-0
lms/static/js/spec/verify_student/pay_and_verify_view_spec.js
+188
-0
lms/static/js/spec/verify_student/review_photos_step_view_spec.js
+149
-0
lms/static/js/spec/verify_student/webcam_photo_view_spec.js
+212
-0
lms/static/js/verify_student/pay_and_verify.js
+6
-3
lms/static/js/verify_student/views/enrollment_confirmation_step_view.js
+8
-0
lms/static/js/verify_student/views/intro_step_view.js
+8
-0
lms/static/js/verify_student/views/make_payment_step_view.js
+51
-20
lms/static/js/verify_student/views/pay_and_verify_view.js
+2
-4
lms/static/js/verify_student/views/review_photos_step_view.js
+22
-17
lms/static/js/verify_student/views/step_view.js
+26
-14
lms/static/js/verify_student/views/webcam_photo_view.js
+44
-40
lms/static/js_test.yml
+1
-0
lms/templates/verify_student/enrollment_confirmation_step.underscore
+1
-1
lms/templates/verify_student/face_photo_step.underscore
+1
-1
lms/templates/verify_student/id_photo_step.underscore
+1
-1
lms/templates/verify_student/intro_step.underscore
+4
-4
lms/templates/verify_student/make_payment_step.underscore
+9
-7
lms/templates/verify_student/pay_and_verify.html
+1
-1
lms/templates/verify_student/payment_confirmation_step.underscore
+1
-1
lms/templates/verify_student/progress.underscore
+1
-1
lms/templates/verify_student/review_photos_step.underscore
+1
-1
lms/templates/verify_student/webcam_photo.underscore
+2
-2
No files found.
common/static/js/spec_helpers/ajax_helpers.js
View file @
0dd9ad8c
define
([
'sinon'
,
'underscore'
],
function
(
sinon
,
_
)
{
define
([
'sinon'
,
'underscore'
],
function
(
sinon
,
_
)
{
var
fakeServer
,
fakeRequests
,
expectRequest
,
expectJsonRequest
,
var
fakeServer
,
fakeRequests
,
expectRequest
,
expectJsonRequest
,
respondWithJson
,
respondWithError
,
respondToDelete
;
respondWithJson
,
respondWithError
,
respond
WithTextError
,
respond
ToDelete
;
/* These utility methods are used by Jasmine tests to create a mock server or
/* 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
* get reference to mock requests. In either case, the cleanup (restore) is done with
...
@@ -93,6 +93,22 @@ define(['sinon', 'underscore'], function(sinon, _) {
...
@@ -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
)
{
respondToDelete
=
function
(
requests
,
requestIndex
)
{
if
(
_
.
isUndefined
(
requestIndex
))
{
if
(
_
.
isUndefined
(
requestIndex
))
{
requestIndex
=
requests
.
length
-
1
;
requestIndex
=
requests
.
length
-
1
;
...
@@ -108,6 +124,7 @@ define(['sinon', 'underscore'], function(sinon, _) {
...
@@ -108,6 +124,7 @@ define(['sinon', 'underscore'], function(sinon, _) {
'expectJsonRequest'
:
expectJsonRequest
,
'expectJsonRequest'
:
expectJsonRequest
,
'respondWithJson'
:
respondWithJson
,
'respondWithJson'
:
respondWithJson
,
'respondWithError'
:
respondWithError
,
'respondWithError'
:
respondWithError
,
'respondWithTextError'
:
respondWithTextError
,
'respondToDelete'
:
respondToDelete
'respondToDelete'
:
respondToDelete
};
};
});
});
lms/djangoapps/verify_student/views.py
View file @
0dd9ad8c
...
@@ -512,7 +512,7 @@ class PayAndVerifyView(View):
...
@@ -512,7 +512,7 @@ class PayAndVerifyView(View):
'disable_courseware_js'
:
True
,
'disable_courseware_js'
:
True
,
'display_steps'
:
display_steps
,
'display_steps'
:
display_steps
,
'contribution_amount'
:
contribution_amount
,
'contribution_amount'
:
contribution_amount
,
'is_active'
:
request
.
user
.
is_active
,
'is_active'
:
json
.
dumps
(
request
.
user
.
is_active
)
,
'messages'
:
self
.
_messages
(
'messages'
:
self
.
_messages
(
message
,
message
,
course
.
display_name
,
course
.
display_name
,
...
...
lms/static/js/spec/main.js
View file @
0dd9ad8c
...
@@ -384,7 +384,116 @@
...
@@ -384,7 +384,116 @@
'js/student_account/enrollment'
,
'js/student_account/enrollment'
,
'js/student_account/shoppingcart'
,
'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 @@
...
@@ -406,7 +515,11 @@
'lms/include/js/spec/student_account/enrollment_spec.js'
,
'lms/include/js/spec/student_account/enrollment_spec.js'
,
'lms/include/js/spec/student_account/emailoptin_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_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
);
}).
call
(
this
,
requirejs
,
define
);
lms/static/js/spec/photocapture_spec.js
View file @
0dd9ad8c
...
@@ -52,4 +52,4 @@ define(['backbone', 'jquery', 'js/verify_student/photocapture'],
...
@@ -52,4 +52,4 @@ define(['backbone', 'jquery', 'js/verify_student/photocapture'],
});
});
});
});
});
});
\ No newline at end of file
lms/static/js/spec/verify_student/make_payment_step_view_spec.js
0 → 100644
View file @
0dd9ad8c
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
);
});
});
}
);
lms/static/js/spec/verify_student/pay_and_verify_view_spec.js
0 → 100644
View file @
0dd9ad8c
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
);
});
});
}
);
lms/static/js/spec/verify_student/review_photos_step_view_spec.js
0 → 100644
View file @
0dd9ad8c
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
);
});
});
}
);
lms/static/js/spec/verify_student/webcam_photo_view_spec.js
0 → 100644
View file @
0dd9ad8c
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
);
});
});
}
);
lms/static/js/verify_student/pay_and_verify.js
View file @
0dd9ad8c
...
@@ -10,7 +10,7 @@
...
@@ -10,7 +10,7 @@
*/
*/
var
edx
=
edx
||
{};
var
edx
=
edx
||
{};
(
function
(
$
)
{
(
function
(
$
,
_
)
{
'use strict'
;
'use strict'
;
var
errorView
,
var
errorView
,
el
=
$
(
'#pay-and-verify-container'
);
el
=
$
(
'#pay-and-verify-container'
);
...
@@ -47,8 +47,11 @@ var edx = edx || {};
...
@@ -47,8 +47,11 @@ var edx = edx || {};
requirements
:
el
.
data
(
'requirements'
),
requirements
:
el
.
data
(
'requirements'
),
courseKey
:
el
.
data
(
'course-key'
),
courseKey
:
el
.
data
(
'course-key'
),
minPrice
:
el
.
data
(
'course-mode-min-price'
),
minPrice
:
el
.
data
(
'course-mode-min-price'
),
suggestedPrices
:
(
el
.
data
(
'course-mode-suggested-prices'
)
||
""
).
split
(
","
),
contributionAmount
:
el
.
data
(
'contribution-amount'
),
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'
),
currency
:
el
.
data
(
'course-mode-currency'
),
purchaseEndpoint
:
el
.
data
(
'purchase-endpoint'
)
purchaseEndpoint
:
el
.
data
(
'purchase-endpoint'
)
},
},
...
@@ -68,4 +71,4 @@ var edx = edx || {};
...
@@ -68,4 +71,4 @@ var edx = edx || {};
}
}
}
}
}).
render
();
}).
render
();
})(
jQuery
);
})(
jQuery
,
_
);
lms/static/js/verify_student/views/enrollment_confirmation_step_view.js
View file @
0dd9ad8c
...
@@ -15,6 +15,14 @@ var edx = edx || {};
...
@@ -15,6 +15,14 @@ var edx = edx || {};
postRender
:
function
()
{
postRender
:
function
()
{
// Track a virtual pageview, for easy funnel reconstruction.
// Track a virtual pageview, for easy funnel reconstruction.
window
.
analytics
.
page
(
'verification'
,
this
.
templateName
);
window
.
analytics
.
page
(
'verification'
,
this
.
templateName
);
},
defaultContext
:
function
()
{
return
{
courseName
:
''
,
courseStartDate
:
''
,
coursewareUrl
:
''
};
}
}
});
});
...
...
lms/static/js/verify_student/views/intro_step_view.js
View file @
0dd9ad8c
...
@@ -10,6 +10,14 @@ var edx = edx || {};
...
@@ -10,6 +10,14 @@ var edx = edx || {};
edx
.
verify_student
.
IntroStepView
=
edx
.
verify_student
.
StepView
.
extend
({
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,
// Currently, this view doesn't need to install any custom event handlers,
// since the button in the template reloads the page with a
// since the button in the template reloads the page with a
// ?skip-intro=1 GET parameter. The reason for this is that we
// ?skip-intro=1 GET parameter. The reason for this is that we
...
...
lms/static/js/verify_student/views/make_payment_step_view.js
View file @
0dd9ad8c
...
@@ -10,6 +10,15 @@ var edx = edx || {};
...
@@ -10,6 +10,15 @@ var edx = edx || {};
edx
.
verify_student
.
MakePaymentStepView
=
edx
.
verify_student
.
StepView
.
extend
({
edx
.
verify_student
.
MakePaymentStepView
=
edx
.
verify_student
.
StepView
.
extend
({
defaultContext
:
function
()
{
return
{
isActive
:
true
,
suggestedPrices
:
[],
minPrice
:
0
,
currency
:
"usd"
};
},
postRender
:
function
()
{
postRender
:
function
()
{
// Render requirements
// Render requirements
new
edx
.
verify_student
.
RequirementsView
({
new
edx
.
verify_student
.
RequirementsView
({
...
@@ -26,8 +35,14 @@ var edx = edx || {};
...
@@ -26,8 +35,14 @@ var edx = edx || {};
this
.
selectPaymentAmount
(
this
.
stepData
.
contributionAmount
);
this
.
selectPaymentAmount
(
this
.
stepData
.
contributionAmount
);
}
}
// Enable the payment button once an amount is chosen
if
(
this
.
templateContext
().
suggestedPrices
.
length
>
0
)
{
$
(
"input[name='contribution']"
).
on
(
'click'
,
_
.
bind
(
this
.
enablePaymentButton
,
this
)
);
// 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
// Handle payment submission
$
(
"#pay_button"
).
on
(
'click'
,
_
.
bind
(
this
.
createOrder
,
this
)
);
$
(
"#pay_button"
).
on
(
'click'
,
_
.
bind
(
this
.
createOrder
,
this
)
);
...
@@ -89,41 +104,49 @@ var edx = edx || {};
...
@@ -89,41 +104,49 @@ var edx = edx || {};
// this page. A virtual pageview can be used to do this.
// this page. A virtual pageview can be used to do this.
window
.
analytics
.
page
(
'payment'
,
'payment_processor_step'
);
window
.
analytics
.
page
(
'payment'
,
'payment_processor_step'
);
form
.
submit
(
);
this
.
submitForm
(
form
);
},
},
handleCreateOrderError
:
function
(
xhr
)
{
handleCreateOrderError
:
function
(
xhr
)
{
var
errorMsg
=
gettext
(
'An unexpected error occurred. Please try again.'
);
if
(
xhr
.
status
===
400
)
{
if
(
xhr
.
status
===
400
)
{
this
.
errorModel
.
set
({
errorMsg
=
xhr
.
responseText
;
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
});
}
}
this
.
errorModel
.
set
({
errorTitle
:
gettext
(
'Could not submit order'
),
errorMsg
:
errorMsg
,
shown
:
true
});
// Re-enable the button so the user can re-try
// Re-enable the button so the user can re-try
$
(
"#pay
ment-processor-form
"
).
removeClass
(
"is-disabled"
);
$
(
"#pay
_button
"
).
removeClass
(
"is-disabled"
);
},
},
getPaymentAmount
:
function
()
{
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'
)
{
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
{
}
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
)
{
selectPaymentAmount
:
function
(
amount
)
{
var
amountFloat
=
parseFloat
(
amount
),
var
amountFloat
=
parseFloat
(
amount
),
foundPrice
;
foundPrice
,
sel
;
// Check if we have a suggested price that matches the amount
// Check if we have a suggested price that matches the amount
foundPrice
=
_
.
find
(
foundPrice
=
_
.
find
(
...
@@ -135,7 +158,8 @@ var edx = edx || {};
...
@@ -135,7 +158,8 @@ var edx = edx || {};
// If we've found an option for the price, select it.
// If we've found an option for the price, select it.
if
(
foundPrice
)
{
if
(
foundPrice
)
{
$
(
'#contribution-'
+
foundPrice
,
this
.
el
).
prop
(
'checked'
,
true
);
sel
=
_
.
sprintf
(
'input[name="contribution"][value="%s"]'
,
foundPrice
);
$
(
sel
).
prop
(
'checked'
,
true
);
}
else
{
}
else
{
// Otherwise, enter the value into the text box
// Otherwise, enter the value into the text box
$
(
'#contribution-other-amt'
,
this
.
el
).
val
(
amount
);
$
(
'#contribution-other-amt'
,
this
.
el
).
val
(
amount
);
...
@@ -144,6 +168,13 @@ var edx = edx || {};
...
@@ -144,6 +168,13 @@ var edx = edx || {};
// In either case, enable the payment button
// In either case, enable the payment button
this
.
enablePaymentButton
();
this
.
enablePaymentButton
();
return
amount
;
},
// Stubbed out in tests
submitForm
:
function
(
form
)
{
form
.
submit
();
}
}
});
});
...
...
lms/static/js/verify_student/views/pay_and_verify_view.js
View file @
0dd9ad8c
...
@@ -16,8 +16,6 @@ var edx = edx || {};
...
@@ -16,8 +16,6 @@ var edx = edx || {};
edx
.
verify_student
.
PayAndVerifyView
=
Backbone
.
View
.
extend
({
edx
.
verify_student
.
PayAndVerifyView
=
Backbone
.
View
.
extend
({
el
:
'#pay-and-verify-container'
,
el
:
'#pay-and-verify-container'
,
template
:
'#progress-tpl'
,
subviews
:
{},
subviews
:
{},
VERIFICATION_VIEW_NAMES
:
[
VERIFICATION_VIEW_NAMES
:
[
...
@@ -27,7 +25,7 @@ var edx = edx || {};
...
@@ -27,7 +25,7 @@ var edx = edx || {};
],
],
initialize
:
function
(
obj
)
{
initialize
:
function
(
obj
)
{
this
.
errorModel
=
obj
.
errorModel
||
{}
;
this
.
errorModel
=
obj
.
errorModel
||
null
;
this
.
displaySteps
=
obj
.
displaySteps
||
[];
this
.
displaySteps
=
obj
.
displaySteps
||
[];
this
.
progressView
=
new
edx
.
verify_student
.
ProgressView
({
this
.
progressView
=
new
edx
.
verify_student
.
ProgressView
({
...
@@ -43,7 +41,7 @@ var edx = edx || {};
...
@@ -43,7 +41,7 @@ var edx = edx || {};
)
)
});
});
this
.
initializeStepViews
(
obj
.
stepInfo
);
this
.
initializeStepViews
(
obj
.
stepInfo
||
{}
);
},
},
initializeStepViews
:
function
(
stepInfo
)
{
initializeStepViews
:
function
(
stepInfo
)
{
...
...
lms/static/js/verify_student/views/review_photos_step_view.js
View file @
0dd9ad8c
...
@@ -10,9 +10,14 @@ var edx = edx || {};
...
@@ -10,9 +10,14 @@ var edx = edx || {};
edx
.
verify_student
.
ReviewPhotosStepView
=
edx
.
verify_student
.
StepView
.
extend
({
edx
.
verify_student
.
ReviewPhotosStepView
=
edx
.
verify_student
.
StepView
.
extend
({
postRender
:
function
()
{
defaultContext
:
function
()
{
var
model
=
this
.
model
;
return
{
platformName
:
""
,
fullName
:
""
,
};
},
postRender
:
function
()
{
// Load the photos from the previous steps
// Load the photos from the previous steps
$
(
'#face_image'
)[
0
].
src
=
this
.
model
.
get
(
'faceImage'
);
$
(
'#face_image'
)[
0
].
src
=
this
.
model
.
get
(
'faceImage'
);
$
(
'#photo_id_image'
)[
0
].
src
=
this
.
model
.
get
(
'identificationImage'
);
$
(
'#photo_id_image'
)[
0
].
src
=
this
.
model
.
get
(
'identificationImage'
);
...
@@ -49,6 +54,8 @@ var edx = edx || {};
...
@@ -49,6 +54,8 @@ var edx = edx || {};
},
},
submitPhotos
:
function
()
{
submitPhotos
:
function
()
{
var
fullName
=
$
(
'#new-name'
).
val
();
// Disable the submit button to prevent duplicate submissions
// Disable the submit button to prevent duplicate submissions
$
(
'#next_step_button'
).
addClass
(
'is-disabled'
);
$
(
'#next_step_button'
).
addClass
(
'is-disabled'
);
...
@@ -59,30 +66,28 @@ var edx = edx || {};
...
@@ -59,30 +66,28 @@ var edx = edx || {};
this
.
listenToOnce
(
this
.
model
,
'error'
,
_
.
bind
(
this
.
handleSubmissionError
,
this
)
);
this
.
listenToOnce
(
this
.
model
,
'error'
,
_
.
bind
(
this
.
handleSubmissionError
,
this
)
);
// Submit
// Submit
this
.
model
.
set
(
'fullName'
,
$
(
'#new-name'
).
val
()
);
if
(
fullName
)
{
this
.
model
.
set
(
'fullName'
,
fullName
);
}
this
.
model
.
save
();
this
.
model
.
save
();
},
},
handleSubmissionError
:
function
(
xhr
)
{
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
// 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
);
$
(
'#next_step_button'
).
toggleClass
(
'is-disabled'
,
!
isConfirmChecked
);
// Display the error
if
(
xhr
.
status
===
400
)
{
if
(
xhr
.
status
===
400
)
{
this
.
errorModel
.
set
({
errorMsg
=
xhr
.
responseText
;
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
});
}
}
this
.
errorModel
.
set
({
errorTitle
:
gettext
(
'Could not submit photos'
),
errorMsg
:
errorMsg
,
shown
:
true
});
},
},
expandCallback
:
function
(
event
)
{
expandCallback
:
function
(
event
)
{
...
...
lms/static/js/verify_student/views/step_view.js
View file @
0dd9ad8c
...
@@ -26,19 +26,11 @@
...
@@ -26,19 +26,11 @@
},
},
render
:
function
()
{
render
:
function
()
{
var
templateHtml
=
$
(
"#"
+
this
.
templateName
+
"-tpl"
).
html
(),
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
);
// Allow subclasses to add additional information
// Allow subclasses to add additional information
// to the template context, perhaps asynchronously.
// to the template context, perhaps asynchronously.
this
.
updateContext
(
t
emplateContext
).
done
(
this
.
updateContext
(
t
his
.
templateContext
()
).
done
(
function
(
templateContext
)
{
function
(
templateContext
)
{
// Render the template into the DOM
// Render the template into the DOM
$
(
this
.
el
).
html
(
_
.
template
(
templateHtml
,
templateContext
)
);
$
(
this
.
el
).
html
(
_
.
template
(
templateHtml
,
templateContext
)
);
...
@@ -47,6 +39,8 @@
...
@@ -47,6 +39,8 @@
this
.
postRender
();
this
.
postRender
();
}
}
).
fail
(
_
.
bind
(
this
.
handleError
,
this
)
);
).
fail
(
_
.
bind
(
this
.
handleError
,
this
)
);
return
this
;
},
},
handleResponse
:
function
(
data
)
{
handleResponse
:
function
(
data
)
{
...
@@ -58,10 +52,8 @@
...
@@ -58,10 +52,8 @@
// Include step-specific information
// Include step-specific information
_
.
extend
(
context
,
this
.
stepData
);
_
.
extend
(
context
,
this
.
stepData
);
this
.
renderedHtml
=
_
.
template
(
data
,
context
);
// Track a virtual pageview, for easy funnel reconstruction.
$
(
this
.
el
).
html
(
this
.
renderedHtml
);
window
.
analytics
.
page
(
'verification'
,
this
.
templateName
);
this
.
postRender
();
},
},
handleError
:
function
(
errorTitle
,
errorMsg
)
{
handleError
:
function
(
errorTitle
,
errorMsg
)
{
...
@@ -72,6 +64,26 @@
...
@@ -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
* Subclasses can override this to add information to
* the template context. This returns an asynchronous
* the template context. This returns an asynchronous
...
...
lms/static/js/verify_student/views/webcam_photo_view.js
View file @
0dd9ad8c
...
@@ -13,9 +13,10 @@
...
@@ -13,9 +13,10 @@
template
:
"#webcam_photo-tpl"
,
template
:
"#webcam_photo-tpl"
,
videoCaptureBackend
:
{
backends
:
[
{
name
:
"html5"
,
html5
:
{
initialize
:
function
(
obj
)
{
initialize
:
function
(
obj
)
{
this
.
URL
=
(
window
.
URL
||
window
.
webkitURL
);
this
.
URL
=
(
window
.
URL
||
window
.
webkitURL
);
this
.
video
=
obj
.
video
||
""
;
this
.
video
=
obj
.
video
||
""
;
...
@@ -90,7 +91,9 @@
...
@@ -90,7 +91,9 @@
}
}
},
},
flash
:
{
{
name
:
"flash"
,
initialize
:
function
(
obj
)
{
initialize
:
function
(
obj
)
{
this
.
wrapper
=
obj
.
wrapper
||
""
;
this
.
wrapper
=
obj
.
wrapper
||
""
;
this
.
imageData
=
""
;
this
.
imageData
=
""
;
...
@@ -193,15 +196,18 @@
...
@@ -193,15 +196,18 @@
// so we don't need to keep checking.
// so we don't need to keep checking.
}
}
}
}
},
],
videoBackendPriority
:
[
'html5'
,
'flash'
],
initialize
:
function
(
obj
)
{
initialize
:
function
(
obj
)
{
this
.
submitButton
=
obj
.
submitButton
||
""
;
this
.
submitButton
=
obj
.
submitButton
||
""
;
this
.
modelAttribute
=
obj
.
modelAttribute
||
""
;
this
.
modelAttribute
=
obj
.
modelAttribute
||
""
;
this
.
errorModel
=
obj
.
errorModel
||
{};
this
.
errorModel
=
obj
.
errorModel
||
null
;
this
.
backend
=
this
.
chooseVideoCaptureBackend
();
this
.
backend
=
_
.
find
(
obj
.
backends
||
this
.
backends
,
function
(
backend
)
{
return
backend
.
isSupported
();
}
);
if
(
!
this
.
backend
)
{
if
(
!
this
.
backend
)
{
this
.
handleError
(
this
.
handleError
(
...
@@ -232,15 +238,20 @@
...
@@ -232,15 +238,20 @@
// Initialize the video capture backend
// Initialize the video capture backend
// We need to do this after rendering the template
// We need to do this after rendering the template
// so that the backend has the opportunity to modify the DOM.
// so that the backend has the opportunity to modify the DOM.
this
.
backend
.
initialize
({
if
(
this
.
backend
)
{
wrapper
:
"#camera"
,
this
.
backend
.
initialize
({
video
:
'#photo_id_video'
,
wrapper
:
"#camera"
,
canvas
:
'#photo_id_canvas'
video
:
'#photo_id_video'
,
});
canvas
:
'#photo_id_canvas'
});
// Install event handlers
$
(
"#webcam_reset_button"
,
this
.
el
).
on
(
'click'
,
_
.
bind
(
this
.
reset
,
this
)
);
// Install event handlers
$
(
"#webcam_capture_button"
,
this
.
el
).
on
(
'click'
,
_
.
bind
(
this
.
capture
,
this
)
);
$
(
"#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
;
return
this
;
},
},
...
@@ -252,9 +263,12 @@
...
@@ -252,9 +263,12 @@
// Reset the video capture
// Reset the video capture
this
.
backend
.
reset
();
this
.
backend
.
reset
();
// Reset data on the model
this
.
model
.
set
(
this
.
modelAttribute
,
""
);
// Go back to the initial button state
// Go back to the initial button state
$
(
"#webcam_reset_button"
,
this
.
el
).
hide
(
);
$
(
"#webcam_reset_button"
,
this
.
el
).
addClass
(
'is-hidden'
);
$
(
"#webcam_capture_button"
,
this
.
el
).
show
(
);
$
(
"#webcam_capture_button"
,
this
.
el
).
removeClass
(
'is-hidden'
);
},
},
capture
:
function
()
{
capture
:
function
()
{
...
@@ -267,8 +281,8 @@
...
@@ -267,8 +281,8 @@
this
.
trigger
(
'imageCaptured'
);
this
.
trigger
(
'imageCaptured'
);
// Hide the capture button, and show the reset button
// Hide the capture button, and show the reset button
$
(
"#webcam_capture_button"
,
this
.
el
).
hide
(
);
$
(
"#webcam_capture_button"
,
this
.
el
).
addClass
(
'is-hidden'
);
$
(
"#webcam_reset_button"
,
this
.
el
).
show
(
);
$
(
"#webcam_reset_button"
,
this
.
el
).
removeClass
(
'is-hidden'
);
// Save the data to the model
// Save the data to the model
this
.
model
.
set
(
this
.
modelAttribute
,
this
.
backend
.
getImageData
()
);
this
.
model
.
set
(
this
.
modelAttribute
,
this
.
backend
.
getImageData
()
);
...
@@ -278,29 +292,19 @@
...
@@ -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
)
{
handleError
:
function
(
errorTitle
,
errorMsg
)
{
// Hide the buttons
// Hide the buttons
$
(
"#webcam_capture_button"
,
this
.
el
).
hide
(
);
$
(
"#webcam_capture_button"
,
this
.
el
).
addClass
(
'is-hidden'
);
$
(
"#webcam_reset_button"
,
this
.
el
).
hide
(
);
$
(
"#webcam_reset_button"
,
this
.
el
).
addClass
(
'is-hidden'
);
// Show the error message
// Show the error message
this
.
errorModel
.
set
({
if
(
this
.
errorModel
)
{
errorTitle
:
errorTitle
,
this
.
errorModel
.
set
({
errorMsg
:
errorMsg
,
errorTitle
:
errorTitle
,
shown
:
true
errorMsg
:
errorMsg
,
});
shown
:
true
});
}
}
}
});
});
...
...
lms/static/js_test.yml
View file @
0dd9ad8c
...
@@ -79,6 +79,7 @@ fixture_paths:
...
@@ -79,6 +79,7 @@ fixture_paths:
-
templates/dashboard
-
templates/dashboard
-
templates/student_account
-
templates/student_account
-
templates/student_profile
-
templates/student_profile
-
templates/verify_student
-
templates/file-upload.underscore
-
templates/file-upload.underscore
requirejs
:
requirejs
:
...
...
lms/templates/verify_student/enrollment_confirmation_step.underscore
View file @
0dd9ad8c
<div class="wrapper-content-main">
<div class="wrapper-content-main
enrollment-confirmation-step
">
<article class="content-main">
<article class="content-main">
<h3 class="title"><%- gettext( "Congratulations! You are now enrolled in the verified track." ) %></h3>
<h3 class="title"><%- gettext( "Congratulations! You are now enrolled in the verified track." ) %></h3>
<div class="instruction">
<div class="instruction">
...
...
lms/templates/verify_student/face_photo_step.underscore
View file @
0dd9ad8c
<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">
<div class="facephoto view">
<h3 class="title"><%- gettext( "Take Your Photo" ) %></h3>
<h3 class="title"><%- gettext( "Take Your Photo" ) %></h3>
<div class="instruction">
<div class="instruction">
...
...
lms/templates/verify_student/id_photo_step.underscore
View file @
0dd9ad8c
<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">
<div class="idphoto view">
<h3 class="title"><%- gettext( "Show Us Your ID" ) %></h3>
<h3 class="title"><%- gettext( "Show Us Your ID" ) %></h3>
<div class="instruction">
<div class="instruction">
...
...
lms/templates/verify_student/intro_step.underscore
View file @
0dd9ad8c
<div class="wrapper-content-main">
<div class="wrapper-content-main
intro-step
">
<article class="content-main">
<article class="content-main">
<h3 class="title"><%- introTitle %></h3>
<h3 class="title"><%- introTitle %></h3>
<div class="instruction"><p><%- introMsg %></p></div>
<div class="instruction"><p><%- introMsg %></p></div>
...
@@ -9,11 +9,11 @@
...
@@ -9,11 +9,11 @@
<nav class="nav-wizard is-ready">
<nav class="nav-wizard is-ready">
<ol class="wizard-steps">
<ol class="wizard-steps">
<li class="wizard-step">
<li class="wizard-step">
<a class="next action-primary" <% if (
isActive == "False"
) { %>disabled="true"<% } %> id="next_step_button" href="?skip-first-step=1">
<a class="next action-primary" <% if (
!isActive
) { %>disabled="true"<% } %> id="next_step_button" href="?skip-first-step=1">
<% if (
isActive == "False"
) { %>
<% if (
!isActive
) { %>
<%- gettext( "Activate Your Account" ) %>
<%- gettext( "Activate Your Account" ) %>
<% } else { %>
<% } else { %>
<%- _.sprintf( gettext( "Go to Step %
s" ), nextStepNum
) %>: <%- nextStepTitle %>
<%- _.sprintf( gettext( "Go to Step %
(stepNumber)s" ), { stepNumber: nextStepNum }
) %>: <%- nextStepTitle %>
<% } %>
<% } %>
</a>
</a>
</li>
</li>
...
...
lms/templates/verify_student/make_payment_step.underscore
View file @
0dd9ad8c
<div id="wrapper-review" class="wrapper-view">
<div id="wrapper-review" class="wrapper-view
make-payment-step
">
<div class="review view">
<div class="review view">
<h3 class="title"><%- gettext( "Make Payment" ) %></h3>
<h3 class="title"><%- gettext( "Make Payment" ) %></h3>
<div class="instruction">
<div class="instruction">
...
@@ -7,7 +7,7 @@
...
@@ -7,7 +7,7 @@
<div class="requirements-container"></div>
<div class="requirements-container"></div>
<% if ( isActive
== "True"
) { %>
<% if ( isActive ) { %>
<div class="wrapper-task">
<div class="wrapper-task">
<ol class="review-tasks">
<ol class="review-tasks">
<% if ( suggestedPrices.length > 0 ) { %>
<% if ( suggestedPrices.length > 0 ) { %>
...
@@ -79,13 +79,15 @@
...
@@ -79,13 +79,15 @@
<nav class="nav-wizard is-ready">
<nav class="nav-wizard is-ready">
<ol class="wizard-steps">
<ol class="wizard-steps">
<li class="wizard-step">
<li class="wizard-step">
<% if ( isActive ) { %>
<a class="next action-primary is-disabled" id="pay_button">
<a class="next action-primary is-disabled" id="pay_button">
<%
if ( isActive == "False" ) {
%>
<%
- gettext( "Go to payment" )
%>
<%- gettext( "Activate Your Account" ) %
>
</a
>
<% } else { %>
<% } else { %>
<%- gettext( "Go to payment" ) %
>
<a class="next action-primary is-disabled" id="activate_button"
>
<%
}
%>
<%
- gettext( "Activate Your Account" )
%>
</a>
</a>
<% } %>
</li>
</li>
</ol>
</ol>
</nav>
</nav>
...
...
lms/templates/verify_student/pay_and_verify.html
View file @
0dd9ad8c
...
@@ -71,7 +71,7 @@ from verify_student.views import PayAndVerifyView
...
@@ -71,7 +71,7 @@ from verify_student.views import PayAndVerifyView
data-current-step=
'${current_step}'
data-current-step=
'${current_step}'
data-requirements=
'${json.dumps(requirements)}'
data-requirements=
'${json.dumps(requirements)}'
data-msg-key=
'${message_key}'
data-msg-key=
'${message_key}'
data-is-active=
"${is_active}"
data-is-active=
'${is_active}'
data-intro-title=
'${messages.intro_title}'
data-intro-title=
'${messages.intro_title}'
data-intro-msg=
'${messages.intro_msg}'
data-intro-msg=
'${messages.intro_msg}'
></div>
></div>
...
...
lms/templates/verify_student/payment_confirmation_step.underscore
View file @
0dd9ad8c
<div class="wrapper-content-main">
<div class="wrapper-content-main
payment-confirmation-step
">
<article class="content-main">
<article class="content-main">
<h3 class="title"><%- gettext( "Congratulations! You are now enrolled in the verified track." ) %></h3>
<h3 class="title"><%- gettext( "Congratulations! You are now enrolled in the verified track." ) %></h3>
<div class="instruction">
<div class="instruction">
...
...
lms/templates/verify_student/progress.underscore
View file @
0dd9ad8c
<div class="wrapper-progress">
<div class="wrapper-progress
progress
">
<section class="progress">
<section class="progress">
<h3 class="sr title"><%- gettext("Your Progress") %></h3>
<h3 class="sr title"><%- gettext("Your Progress") %></h3>
...
...
lms/templates/verify_student/review_photos_step.underscore
View file @
0dd9ad8c
<div id="wrapper-review" class="wrapper-view">
<div id="wrapper-review" class="wrapper-view
review-photos-step
">
<div class="review view">
<div class="review view">
<h3 class="title"><%- gettext( "Verify Your Submission" ) %></h3>
<h3 class="title"><%- gettext( "Verify Your Submission" ) %></h3>
<div class="instruction">
<div class="instruction">
...
...
lms/templates/verify_student/webcam_photo.underscore
View file @
0dd9ad8c
...
@@ -9,13 +9,13 @@
...
@@ -9,13 +9,13 @@
<div class="controls photo-controls">
<div class="controls photo-controls">
<ul class="list-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">
<a class="action action-redo">
<i class="icon-undo"></i> <span class="sr"><%- gettext( "Retake" ) %></span>
<i class="icon-undo"></i> <span class="sr"><%- gettext( "Retake" ) %></span>
</a>
</a>
</li>
</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">
<a class="action action-do">
<i class="icon-camera"></i> <span class="sr"><%- gettext( "Take photo" ) %></span>
<i class="icon-camera"></i> <span class="sr"><%- gettext( "Take photo" ) %></span>
</a>
</a>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment