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
efde11d5
Commit
efde11d5
authored
Apr 21, 2015
by
jsa
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Use ecommerce api v2.
XCOM-213 XCOM-214
parent
960ec06f
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
517 additions
and
331 deletions
+517
-331
common/static/js/spec_helpers/ajax_helpers.js
+17
-2
common/test/acceptance/pages/lms/pay_and_verify.py
+1
-1
lms/djangoapps/commerce/api.py
+43
-12
lms/djangoapps/commerce/constants.py
+0
-5
lms/djangoapps/commerce/tests/__init__.py
+31
-23
lms/djangoapps/commerce/tests/test_api.py
+19
-15
lms/djangoapps/commerce/tests/test_views.py
+50
-26
lms/djangoapps/commerce/urls.py
+4
-2
lms/djangoapps/commerce/views.py
+29
-26
lms/djangoapps/shoppingcart/tests/test_views.py
+21
-18
lms/djangoapps/verify_student/tests/test_integration.py
+1
-1
lms/djangoapps/verify_student/tests/test_views.py
+116
-120
lms/djangoapps/verify_student/views.py
+65
-36
lms/static/js/spec/photocapture_spec.js
+3
-3
lms/static/js/spec/student_account/enrollment_spec.js
+1
-1
lms/static/js/spec/verify_student/make_payment_step_view_spec.js
+18
-15
lms/static/js/student_account/enrollment.js
+2
-2
lms/static/js/verify_student/pay_and_verify.js
+1
-1
lms/static/js/verify_student/photocapture.js
+4
-4
lms/static/js/verify_student/views/make_payment_step_view.js
+62
-13
lms/static/sass/_developer.scss
+19
-0
lms/templates/verify_student/make_payment_step.underscore
+9
-4
lms/templates/verify_student/pay_and_verify.html
+1
-1
No files found.
common/static/js/spec_helpers/ajax_helpers.js
View file @
efde11d5
define
([
'sinon'
,
'underscore'
],
function
(
sinon
,
_
)
{
define
([
'sinon'
,
'underscore'
],
function
(
sinon
,
_
)
{
var
fakeServer
,
fakeRequests
,
expectRequest
,
expectJsonRequest
,
var
fakeServer
,
fakeRequests
,
expectRequest
,
expectJsonRequest
,
expectPostRequest
,
respondWithJson
,
respondWithError
,
respondWithTextError
,
respon
se
WithNoContent
;
respondWithJson
,
respondWithError
,
respondWithTextError
,
respon
d
WithNoContent
;
/* 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
...
@@ -68,6 +68,20 @@ define(['sinon', 'underscore'], function(sinon, _) {
...
@@ -68,6 +68,20 @@ define(['sinon', 'underscore'], function(sinon, _) {
expect
(
JSON
.
parse
(
request
.
requestBody
)).
toEqual
(
jsonRequest
);
expect
(
JSON
.
parse
(
request
.
requestBody
)).
toEqual
(
jsonRequest
);
};
};
/**
* Intended for use with POST requests using application/x-www-form-urlencoded.
*/
expectPostRequest
=
function
(
requests
,
url
,
body
,
requestIndex
)
{
var
request
;
if
(
_
.
isUndefined
(
requestIndex
))
{
requestIndex
=
requests
.
length
-
1
;
}
request
=
requests
[
requestIndex
];
expect
(
request
.
url
).
toEqual
(
url
);
expect
(
request
.
method
).
toEqual
(
"POST"
);
expect
(
_
.
difference
(
request
.
requestBody
.
split
(
'&'
),
body
.
split
(
'&'
))).
toEqual
([]);
};
respondWithJson
=
function
(
requests
,
jsonResponse
,
requestIndex
)
{
respondWithJson
=
function
(
requests
,
jsonResponse
,
requestIndex
)
{
if
(
_
.
isUndefined
(
requestIndex
))
{
if
(
_
.
isUndefined
(
requestIndex
))
{
requestIndex
=
requests
.
length
-
1
;
requestIndex
=
requests
.
length
-
1
;
...
@@ -122,6 +136,7 @@ define(['sinon', 'underscore'], function(sinon, _) {
...
@@ -122,6 +136,7 @@ define(['sinon', 'underscore'], function(sinon, _) {
'requests'
:
fakeRequests
,
'requests'
:
fakeRequests
,
'expectRequest'
:
expectRequest
,
'expectRequest'
:
expectRequest
,
'expectJsonRequest'
:
expectJsonRequest
,
'expectJsonRequest'
:
expectJsonRequest
,
'expectPostRequest'
:
expectPostRequest
,
'respondWithJson'
:
respondWithJson
,
'respondWithJson'
:
respondWithJson
,
'respondWithError'
:
respondWithError
,
'respondWithError'
:
respondWithError
,
'respondWithTextError'
:
respondWithTextError
,
'respondWithTextError'
:
respondWithTextError
,
...
...
common/test/acceptance/pages/lms/pay_and_verify.py
View file @
efde11d5
...
@@ -76,7 +76,7 @@ class PaymentAndVerificationFlow(PageObject):
...
@@ -76,7 +76,7 @@ class PaymentAndVerificationFlow(PageObject):
def
proceed_to_payment
(
self
):
def
proceed_to_payment
(
self
):
"""Interact with the payment button."""
"""Interact with the payment button."""
self
.
q
(
css
=
"
#pay_
button"
)
.
click
()
self
.
q
(
css
=
"
.payment-
button"
)
.
click
()
FakePaymentPage
(
self
.
browser
,
self
.
_course_id
)
.
wait_for_page
()
FakePaymentPage
(
self
.
browser
,
self
.
_course_id
)
.
wait_for_page
()
...
...
lms/djangoapps/commerce/api.py
View file @
efde11d5
...
@@ -60,26 +60,57 @@ class EcommerceAPI(object):
...
@@ -60,26 +60,57 @@ class EcommerceAPI(object):
}
}
url
=
'{base_url}/orders/{order_number}/'
.
format
(
base_url
=
self
.
url
,
order_number
=
order_number
)
url
=
'{base_url}/orders/{order_number}/'
.
format
(
base_url
=
self
.
url
,
order_number
=
order_number
)
return
requests
.
get
(
url
,
headers
=
headers
,
timeout
=
self
.
timeout
)
return
requests
.
get
(
url
,
headers
=
headers
,
timeout
=
self
.
timeout
)
return
self
.
_call_ecommerce_service
(
get
)
def
create_order
(
self
,
user
,
sku
):
data
=
self
.
_call_ecommerce_service
(
get
)
return
data
[
'number'
],
data
[
'status'
],
data
def
get_processors
(
self
,
user
):
"""
"""
Create a new order
.
Retrieve the list of available payment processors
.
Arguments
Returns a list of strings.
user -- User for which the order should be created.
"""
sku -- SKU of the course seat being ordered.
def
get
():
"""Internal service call to retrieve the processor list. """
headers
=
{
'Content-Type'
:
'application/json'
,
'Authorization'
:
'JWT {}'
.
format
(
self
.
_get_jwt
(
user
))
}
url
=
'{base_url}/payment/processors/'
.
format
(
base_url
=
self
.
url
)
return
requests
.
get
(
url
,
headers
=
headers
,
timeout
=
self
.
timeout
)
Returns a tuple with the order number, order status, API response data.
return
self
.
_call_ecommerce_service
(
get
)
def
create_basket
(
self
,
user
,
sku
,
payment_processor
=
None
):
"""Create a new basket and immediately trigger checkout.
Note that while the API supports deferring checkout to a separate step,
as well as adding multiple products to the basket, this client does not
currently need that capability, so that case is not supported.
Args:
user: the django.auth.User for which the basket should be created.
sku: a string containing the SKU of the course seat being ordered.
payment_processor: (optional) the name of the payment processor to
use for checkout.
Returns:
A dictionary containing {id, order, payment_data}.
Raises:
TimeoutError: the request to the API server timed out.
InvalidResponseError: the API server response was not understood.
"""
"""
def
create
():
def
create
():
"""Internal service call to create a
n order
. """
"""Internal service call to create a
basket
. """
headers
=
{
headers
=
{
'Content-Type'
:
'application/json'
,
'Content-Type'
:
'application/json'
,
'Authorization'
:
'JWT {}'
.
format
(
self
.
_get_jwt
(
user
))
'Authorization'
:
'JWT {}'
.
format
(
self
.
_get_jwt
(
user
))
}
}
url
=
'{}/orders/'
.
format
(
self
.
url
)
url
=
'{}/baskets/'
.
format
(
self
.
url
)
return
requests
.
post
(
url
,
data
=
json
.
dumps
({
'sku'
:
sku
}),
headers
=
headers
,
timeout
=
self
.
timeout
)
data
=
{
'products'
:
[{
'sku'
:
sku
}],
'checkout'
:
True
,
'payment_processor_name'
:
payment_processor
}
return
requests
.
post
(
url
,
data
=
json
.
dumps
(
data
),
headers
=
headers
,
timeout
=
self
.
timeout
)
return
self
.
_call_ecommerce_service
(
create
)
return
self
.
_call_ecommerce_service
(
create
)
@staticmethod
@staticmethod
...
@@ -92,7 +123,7 @@ class EcommerceAPI(object):
...
@@ -92,7 +123,7 @@ class EcommerceAPI(object):
Arguments
Arguments
call -- A callable function that makes a request to the E-Commerce Service.
call -- A callable function that makes a request to the E-Commerce Service.
Returns a
tuple with the order number, order status,
API response data.
Returns a
dict of JSON-decoded
API response data.
"""
"""
try
:
try
:
response
=
call
()
response
=
call
()
...
@@ -109,7 +140,7 @@ class EcommerceAPI(object):
...
@@ -109,7 +140,7 @@ class EcommerceAPI(object):
status_code
=
response
.
status_code
status_code
=
response
.
status_code
if
status_code
==
HTTP_200_OK
:
if
status_code
==
HTTP_200_OK
:
return
data
[
'number'
],
data
[
'status'
],
data
return
data
else
:
else
:
msg
=
u'Response from E-Commerce API was invalid: (
%(status)
d) -
%(msg)
s'
msg
=
u'Response from E-Commerce API was invalid: (
%(status)
d) -
%(msg)
s'
msg_kwargs
=
{
msg_kwargs
=
{
...
...
lms/djangoapps/commerce/constants.py
View file @
efde11d5
...
@@ -4,13 +4,8 @@
...
@@ -4,13 +4,8 @@
class
OrderStatus
(
object
):
class
OrderStatus
(
object
):
"""Constants representing all known order statuses. """
"""Constants representing all known order statuses. """
OPEN
=
'Open'
OPEN
=
'Open'
ORDER_CANCELLED
=
'Order Cancelled'
BEING_PROCESSED
=
'Being Processed'
PAYMENT_CANCELLED
=
'Payment Cancelled'
PAID
=
'Paid'
FULFILLMENT_ERROR
=
'Fulfillment Error'
FULFILLMENT_ERROR
=
'Fulfillment Error'
COMPLETE
=
'Complete'
COMPLETE
=
'Complete'
REFUNDED
=
'Refunded'
class
Messages
(
object
):
class
Messages
(
object
):
...
...
lms/djangoapps/commerce/tests/__init__.py
View file @
efde11d5
...
@@ -6,7 +6,6 @@ import jwt
...
@@ -6,7 +6,6 @@ import jwt
import
mock
import
mock
from
commerce.api
import
EcommerceAPI
from
commerce.api
import
EcommerceAPI
from
commerce.constants
import
OrderStatus
class
EcommerceApiTestMixin
(
object
):
class
EcommerceApiTestMixin
(
object
):
...
@@ -14,12 +13,19 @@ class EcommerceApiTestMixin(object):
...
@@ -14,12 +13,19 @@ class EcommerceApiTestMixin(object):
ECOMMERCE_API_URL
=
'http://example.com/api'
ECOMMERCE_API_URL
=
'http://example.com/api'
ECOMMERCE_API_SIGNING_KEY
=
'edx'
ECOMMERCE_API_SIGNING_KEY
=
'edx'
BASKET_ID
=
7
ORDER_NUMBER
=
'100004'
ORDER_NUMBER
=
'100004'
PROCESSOR
=
'test-processor'
PAYMENT_DATA
=
{
'payment_processor_name'
:
PROCESSOR
,
'payment_form_data'
:
{},
'payment_page_url'
:
'http://example.com/pay'
,
}
ORDER_DATA
=
{
'number'
:
ORDER_NUMBER
}
ECOMMERCE_API_SUCCESSFUL_BODY
=
{
ECOMMERCE_API_SUCCESSFUL_BODY
=
{
'status'
:
OrderStatus
.
COMPLETE
,
'id'
:
BASKET_ID
,
'number'
:
ORDER_NUMBER
,
'order'
:
{
'number'
:
ORDER_NUMBER
},
# never both None.
'payment_processor'
:
'cybersource'
,
'payment_data'
:
PAYMENT_DATA
,
'payment_parameters'
:
{
'orderNumber'
:
ORDER_NUMBER
}
}
}
ECOMMERCE_API_SUCCESSFUL_BODY_JSON
=
json
.
dumps
(
ECOMMERCE_API_SUCCESSFUL_BODY
)
# pylint: disable=invalid-name
ECOMMERCE_API_SUCCESSFUL_BODY_JSON
=
json
.
dumps
(
ECOMMERCE_API_SUCCESSFUL_BODY
)
# pylint: disable=invalid-name
...
@@ -28,14 +34,18 @@ class EcommerceApiTestMixin(object):
...
@@ -28,14 +34,18 @@ class EcommerceApiTestMixin(object):
expected_jwt
=
jwt
.
encode
({
'username'
:
user
.
username
,
'email'
:
user
.
email
},
key
)
expected_jwt
=
jwt
.
encode
({
'username'
:
user
.
username
,
'email'
:
user
.
email
},
key
)
self
.
assertEqual
(
request
.
headers
[
'Authorization'
],
'JWT {}'
.
format
(
expected_jwt
))
self
.
assertEqual
(
request
.
headers
[
'Authorization'
],
'JWT {}'
.
format
(
expected_jwt
))
def
assertValid
OrderRequest
(
self
,
request
,
user
,
jwt_signing_key
,
sku
):
def
assertValid
BasketRequest
(
self
,
request
,
user
,
jwt_signing_key
,
sku
,
processor
):
""" Verifies that an order request to the E-Commerce Service is valid. """
""" Verifies that an order request to the E-Commerce Service is valid. """
self
.
assertValidJWTAuthHeader
(
request
,
user
,
jwt_signing_key
)
self
.
assertValidJWTAuthHeader
(
request
,
user
,
jwt_signing_key
)
expected_body_data
=
{
self
.
assertEqual
(
request
.
body
,
'{{"sku": "{}"}}'
.
format
(
sku
))
'products'
:
[{
'sku'
:
sku
}],
'checkout'
:
True
,
'payment_processor_name'
:
processor
}
self
.
assertEqual
(
json
.
loads
(
request
.
body
),
expected_body_data
)
self
.
assertEqual
(
request
.
headers
[
'Content-Type'
],
'application/json'
)
self
.
assertEqual
(
request
.
headers
[
'Content-Type'
],
'application/json'
)
def
_mock_ecommerce_api
(
self
,
status
=
200
,
body
=
None
):
def
_mock_ecommerce_api
(
self
,
status
=
200
,
body
=
None
,
is_payment_required
=
False
):
"""
"""
Mock calls to the E-Commerce API.
Mock calls to the E-Commerce API.
...
@@ -43,27 +53,25 @@ class EcommerceApiTestMixin(object):
...
@@ -43,27 +53,25 @@ class EcommerceApiTestMixin(object):
"""
"""
self
.
assertTrue
(
httpretty
.
is_enabled
(),
'Test is missing @httpretty.activate decorator.'
)
self
.
assertTrue
(
httpretty
.
is_enabled
(),
'Test is missing @httpretty.activate decorator.'
)
url
=
self
.
ECOMMERCE_API_URL
+
'/orders/'
url
=
self
.
ECOMMERCE_API_URL
+
'/baskets/'
body
=
body
or
self
.
ECOMMERCE_API_SUCCESSFUL_BODY_JSON
if
body
is
None
:
response_data
=
{
'id'
:
self
.
BASKET_ID
,
'payment_data'
:
None
,
'order'
:
None
}
if
is_payment_required
:
response_data
[
'payment_data'
]
=
self
.
PAYMENT_DATA
else
:
response_data
[
'order'
]
=
{
'number'
:
self
.
ORDER_NUMBER
}
body
=
json
.
dumps
(
response_data
)
httpretty
.
register_uri
(
httpretty
.
POST
,
url
,
status
=
status
,
body
=
body
)
httpretty
.
register_uri
(
httpretty
.
POST
,
url
,
status
=
status
,
body
=
body
)
class
mock_create_
order
(
object
):
# pylint: disable=invalid-name
class
mock_create_
basket
(
object
):
# pylint: disable=invalid-name
""" Mocks calls to EcommerceAPI.create_
order
. """
""" Mocks calls to EcommerceAPI.create_
basket
. """
patch
=
None
patch
=
None
def
__init__
(
self
,
**
kwargs
):
def
__init__
(
self
,
**
kwargs
):
default_kwargs
=
{
default_kwargs
=
{
'return_value'
:
EcommerceApiTestMixin
.
ECOMMERCE_API_SUCCESSFUL_BODY
}
'return_value'
:
(
EcommerceApiTestMixin
.
ORDER_NUMBER
,
OrderStatus
.
COMPLETE
,
EcommerceApiTestMixin
.
ECOMMERCE_API_SUCCESSFUL_BODY
)
}
default_kwargs
.
update
(
kwargs
)
default_kwargs
.
update
(
kwargs
)
self
.
patch
=
mock
.
patch
.
object
(
EcommerceAPI
,
'create_basket'
,
mock
.
Mock
(
**
default_kwargs
))
self
.
patch
=
mock
.
patch
.
object
(
EcommerceAPI
,
'create_order'
,
mock
.
Mock
(
**
default_kwargs
))
def
__enter__
(
self
):
def
__enter__
(
self
):
self
.
patch
.
start
()
self
.
patch
.
start
()
...
...
lms/djangoapps/commerce/tests/test_api.py
View file @
efde11d5
...
@@ -10,7 +10,6 @@ import httpretty
...
@@ -10,7 +10,6 @@ import httpretty
from
requests
import
Timeout
from
requests
import
Timeout
from
commerce.api
import
EcommerceAPI
from
commerce.api
import
EcommerceAPI
from
commerce.constants
import
OrderStatus
from
commerce.exceptions
import
InvalidResponseError
,
TimeoutError
,
InvalidConfigurationError
from
commerce.exceptions
import
InvalidResponseError
,
TimeoutError
,
InvalidConfigurationError
from
commerce.tests
import
EcommerceApiTestMixin
from
commerce.tests
import
EcommerceApiTestMixin
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
...
@@ -26,7 +25,7 @@ class EcommerceAPITests(EcommerceApiTestMixin, TestCase):
...
@@ -26,7 +25,7 @@ class EcommerceAPITests(EcommerceApiTestMixin, TestCase):
def
setUp
(
self
):
def
setUp
(
self
):
super
(
EcommerceAPITests
,
self
)
.
setUp
()
super
(
EcommerceAPITests
,
self
)
.
setUp
()
self
.
url
=
reverse
(
'commerce:
order
s'
)
self
.
url
=
reverse
(
'commerce:
basket
s'
)
self
.
user
=
UserFactory
()
self
.
user
=
UserFactory
()
self
.
api
=
EcommerceAPI
()
self
.
api
=
EcommerceAPI
()
...
@@ -48,35 +47,40 @@ class EcommerceAPITests(EcommerceApiTestMixin, TestCase):
...
@@ -48,35 +47,40 @@ class EcommerceAPITests(EcommerceApiTestMixin, TestCase):
self
.
assertRaises
(
InvalidConfigurationError
,
EcommerceAPI
)
self
.
assertRaises
(
InvalidConfigurationError
,
EcommerceAPI
)
@httpretty.activate
@httpretty.activate
def
test_create_order
(
self
):
@data
(
True
,
False
)
def
test_create_basket
(
self
,
is_payment_required
):
""" Verify the method makes a call to the E-Commerce API with the correct headers and data. """
""" Verify the method makes a call to the E-Commerce API with the correct headers and data. """
self
.
_mock_ecommerce_api
()
self
.
_mock_ecommerce_api
(
is_payment_required
=
is_payment_required
)
number
,
status
,
body
=
self
.
api
.
create_order
(
self
.
user
,
self
.
SKU
)
response_data
=
self
.
api
.
create_basket
(
self
.
user
,
self
.
SKU
,
self
.
PROCESSOR
)
# Validate the request sent to the E-Commerce API endpoint.
# Validate the request sent to the E-Commerce API endpoint.
request
=
httpretty
.
last_request
()
request
=
httpretty
.
last_request
()
self
.
assertValid
OrderRequest
(
request
,
self
.
user
,
self
.
ECOMMERCE_API_SIGNING_KEY
,
self
.
SKU
)
self
.
assertValid
BasketRequest
(
request
,
self
.
user
,
self
.
ECOMMERCE_API_SIGNING_KEY
,
self
.
SKU
,
self
.
PROCESSOR
)
# Validate the data returned by the method
# Validate the data returned by the method
self
.
assertEqual
(
number
,
self
.
ORDER_NUMBER
)
self
.
assertEqual
(
response_data
[
'id'
],
self
.
BASKET_ID
)
self
.
assertEqual
(
status
,
OrderStatus
.
COMPLETE
)
if
is_payment_required
:
self
.
assertEqual
(
body
,
self
.
ECOMMERCE_API_SUCCESSFUL_BODY
)
self
.
assertEqual
(
response_data
[
'order'
],
None
)
self
.
assertEqual
(
response_data
[
'payment_data'
],
self
.
PAYMENT_DATA
)
else
:
self
.
assertEqual
(
response_data
[
'order'
],
{
"number"
:
self
.
ORDER_NUMBER
})
self
.
assertEqual
(
response_data
[
'payment_data'
],
None
)
@httpretty.activate
@httpretty.activate
@data
(
400
,
401
,
405
,
406
,
429
,
500
,
503
)
@data
(
400
,
401
,
405
,
406
,
429
,
500
,
503
)
def
test_create_
order
_with_invalid_http_status
(
self
,
status
):
def
test_create_
basket
_with_invalid_http_status
(
self
,
status
):
""" If the E-Commerce API returns a non-200 status, the method should raise an InvalidResponseError. """
""" If the E-Commerce API returns a non-200 status, the method should raise an InvalidResponseError. """
self
.
_mock_ecommerce_api
(
status
=
status
,
body
=
json
.
dumps
({
'user_message'
:
'FAIL!'
}))
self
.
_mock_ecommerce_api
(
status
=
status
,
body
=
json
.
dumps
({
'user_message'
:
'FAIL!'
}))
self
.
assertRaises
(
InvalidResponseError
,
self
.
api
.
create_
order
,
self
.
user
,
self
.
SKU
)
self
.
assertRaises
(
InvalidResponseError
,
self
.
api
.
create_
basket
,
self
.
user
,
self
.
SKU
,
self
.
PROCESSOR
)
@httpretty.activate
@httpretty.activate
def
test_create_
order
_with_invalid_json
(
self
):
def
test_create_
basket
_with_invalid_json
(
self
):
""" If the E-Commerce API returns un-parseable data, the method should raise an InvalidResponseError. """
""" If the E-Commerce API returns un-parseable data, the method should raise an InvalidResponseError. """
self
.
_mock_ecommerce_api
(
body
=
'TOTALLY NOT JSON!'
)
self
.
_mock_ecommerce_api
(
body
=
'TOTALLY NOT JSON!'
)
self
.
assertRaises
(
InvalidResponseError
,
self
.
api
.
create_
order
,
self
.
user
,
self
.
SKU
)
self
.
assertRaises
(
InvalidResponseError
,
self
.
api
.
create_
basket
,
self
.
user
,
self
.
SKU
,
self
.
PROCESSOR
)
@httpretty.activate
@httpretty.activate
def
test_create_
order
_with_timeout
(
self
):
def
test_create_
basket
_with_timeout
(
self
):
""" If the call to the E-Commerce API times out, the method should raise a TimeoutError. """
""" If the call to the E-Commerce API times out, the method should raise a TimeoutError. """
def
request_callback
(
_request
,
_uri
,
_headers
):
def
request_callback
(
_request
,
_uri
,
_headers
):
...
@@ -85,4 +89,4 @@ class EcommerceAPITests(EcommerceApiTestMixin, TestCase):
...
@@ -85,4 +89,4 @@ class EcommerceAPITests(EcommerceApiTestMixin, TestCase):
self
.
_mock_ecommerce_api
(
body
=
request_callback
)
self
.
_mock_ecommerce_api
(
body
=
request_callback
)
self
.
assertRaises
(
TimeoutError
,
self
.
api
.
create_
order
,
self
.
user
,
self
.
SKU
)
self
.
assertRaises
(
TimeoutError
,
self
.
api
.
create_
basket
,
self
.
user
,
self
.
SKU
,
self
.
PROCESSOR
)
lms/djangoapps/commerce/tests/test_views.py
View file @
efde11d5
...
@@ -9,7 +9,7 @@ from django.test.utils import override_settings
...
@@ -9,7 +9,7 @@ from django.test.utils import override_settings
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
commerce.constants
import
OrderStatus
,
Messages
from
commerce.constants
import
Messages
from
commerce.exceptions
import
TimeoutError
,
ApiError
from
commerce.exceptions
import
TimeoutError
,
ApiError
from
commerce.tests
import
EcommerceApiTestMixin
from
commerce.tests
import
EcommerceApiTestMixin
from
course_modes.models
import
CourseMode
from
course_modes.models
import
CourseMode
...
@@ -22,7 +22,7 @@ from student.tests.tests import EnrollmentEventTestMixin
...
@@ -22,7 +22,7 @@ from student.tests.tests import EnrollmentEventTestMixin
@ddt
@ddt
@override_settings
(
ECOMMERCE_API_URL
=
EcommerceApiTestMixin
.
ECOMMERCE_API_URL
,
@override_settings
(
ECOMMERCE_API_URL
=
EcommerceApiTestMixin
.
ECOMMERCE_API_URL
,
ECOMMERCE_API_SIGNING_KEY
=
EcommerceApiTestMixin
.
ECOMMERCE_API_SIGNING_KEY
)
ECOMMERCE_API_SIGNING_KEY
=
EcommerceApiTestMixin
.
ECOMMERCE_API_SIGNING_KEY
)
class
Order
sViewTests
(
EnrollmentEventTestMixin
,
EcommerceApiTestMixin
,
ModuleStoreTestCase
):
class
Basket
sViewTests
(
EnrollmentEventTestMixin
,
EcommerceApiTestMixin
,
ModuleStoreTestCase
):
"""
"""
Tests for the commerce orders view.
Tests for the commerce orders view.
"""
"""
...
@@ -48,6 +48,11 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
...
@@ -48,6 +48,11 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
actual
=
json
.
loads
(
response
.
content
)[
'detail'
]
actual
=
json
.
loads
(
response
.
content
)[
'detail'
]
self
.
assertEqual
(
actual
,
expected_msg
)
self
.
assertEqual
(
actual
,
expected_msg
)
def
assertResponsePaymentData
(
self
,
response
):
""" Asserts correctness of a JSON body containing payment information. """
actual_response
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
actual_response
,
self
.
PAYMENT_DATA
)
def
assertValidEcommerceInternalRequestErrorResponse
(
self
,
response
):
def
assertValidEcommerceInternalRequestErrorResponse
(
self
,
response
):
""" Asserts the response is a valid response sent when the E-Commerce API is unavailable. """
""" Asserts the response is a valid response sent when the E-Commerce API is unavailable. """
self
.
assertEqual
(
response
.
status_code
,
500
)
self
.
assertEqual
(
response
.
status_code
,
500
)
...
@@ -60,8 +65,8 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
...
@@ -60,8 +65,8 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
self
.
assert_no_events_were_emitted
()
self
.
assert_no_events_were_emitted
()
def
setUp
(
self
):
def
setUp
(
self
):
super
(
Order
sViewTests
,
self
)
.
setUp
()
super
(
Basket
sViewTests
,
self
)
.
setUp
()
self
.
url
=
reverse
(
'commerce:
order
s'
)
self
.
url
=
reverse
(
'commerce:
basket
s'
)
self
.
user
=
UserFactory
()
self
.
user
=
UserFactory
()
self
.
_login
()
self
.
_login
()
...
@@ -113,7 +118,7 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
...
@@ -113,7 +118,7 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
"""
"""
If the call to the E-Commerce API times out, the view should log an error and return an HTTP 503 status.
If the call to the E-Commerce API times out, the view should log an error and return an HTTP 503 status.
"""
"""
with
self
.
mock_create_
order
(
side_effect
=
TimeoutError
):
with
self
.
mock_create_
basket
(
side_effect
=
TimeoutError
):
response
=
self
.
_post_to_view
()
response
=
self
.
_post_to_view
()
self
.
assertValidEcommerceInternalRequestErrorResponse
(
response
)
self
.
assertValidEcommerceInternalRequestErrorResponse
(
response
)
...
@@ -123,22 +128,24 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
...
@@ -123,22 +128,24 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
"""
"""
If the E-Commerce API raises an error, the view should return an HTTP 503 status.
If the E-Commerce API raises an error, the view should return an HTTP 503 status.
"""
"""
with
self
.
mock_create_
order
(
side_effect
=
ApiError
):
with
self
.
mock_create_
basket
(
side_effect
=
ApiError
):
response
=
self
.
_post_to_view
()
response
=
self
.
_post_to_view
()
self
.
assertValidEcommerceInternalRequestErrorResponse
(
response
)
self
.
assertValidEcommerceInternalRequestErrorResponse
(
response
)
self
.
assertUserNotEnrolled
()
self
.
assertUserNotEnrolled
()
def
_test_successful_ecommerce_api_call
(
self
):
def
_test_successful_ecommerce_api_call
(
self
,
is_completed
=
True
):
"""
"""
Verifies that the view contacts the E-Commerce API with the correct data and headers.
Verifies that the view contacts the E-Commerce API with the correct data and headers.
"""
"""
with
self
.
mock_create_order
():
response
=
self
.
_post_to_view
()
response
=
self
.
_post_to_view
()
# Validate the response content
# Validate the response content
msg
=
Messages
.
ORDER_COMPLETED
.
format
(
order_number
=
self
.
ORDER_NUMBER
)
if
is_completed
:
self
.
assertResponseMessage
(
response
,
msg
)
msg
=
Messages
.
ORDER_COMPLETED
.
format
(
order_number
=
self
.
ORDER_NUMBER
)
self
.
assertResponseMessage
(
response
,
msg
)
else
:
self
.
assertResponsePaymentData
(
response
)
@data
(
True
,
False
)
@data
(
True
,
False
)
def
test_course_with_honor_seat_sku
(
self
,
user_is_active
):
def
test_course_with_honor_seat_sku
(
self
,
user_is_active
):
...
@@ -151,26 +158,30 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
...
@@ -151,26 +158,30 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
self
.
user
.
is_active
=
user_is_active
self
.
user
.
is_active
=
user_is_active
self
.
user
.
save
()
# pylint: disable=no-member
self
.
user
.
save
()
# pylint: disable=no-member
self
.
_test_successful_ecommerce_api_call
()
return_value
=
{
'id'
:
self
.
BASKET_ID
,
'payment_data'
:
None
,
'order'
:
{
'number'
:
self
.
ORDER_NUMBER
}}
with
self
.
mock_create_basket
(
return_value
=
return_value
):
self
.
_test_successful_ecommerce_api_call
()
def
test_order_not_complete
(
self
):
@data
(
True
,
False
)
with
self
.
mock_create_order
(
return_value
=
(
self
.
ORDER_NUMBER
,
def
test_course_with_paid_seat_sku
(
self
,
user_is_active
):
OrderStatus
.
OPEN
,
"""
self
.
ECOMMERCE_API_SUCCESSFUL_BODY
)):
If the course has a SKU, the view should return data that the client
response
=
self
.
_post_to_view
()
will use to redirect the user to an external payment processor.
self
.
assertEqual
(
response
.
status_code
,
202
)
"""
msg
=
Messages
.
ORDER_INCOMPLETE_ENROLLED
.
format
(
order_number
=
self
.
ORDER_NUMBER
)
# Set user's active flag
self
.
assertResponseMessage
(
response
,
msg
)
self
.
user
.
is_active
=
user_is_active
self
.
user
.
save
()
# pylint: disable=no-member
# TODO Eventually we should NOT be enrolling users directly from this view.
return_value
=
{
'id'
:
self
.
BASKET_ID
,
'payment_data'
:
self
.
PAYMENT_DATA
,
'order'
:
None
}
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course
.
id
))
with
self
.
mock_create_basket
(
return_value
=
return_value
):
self
.
_test_successful_ecommerce_api_call
(
False
)
def
_test_course_without_sku
(
self
):
def
_test_course_without_sku
(
self
):
"""
"""
Validates the view bypasses the E-Commerce API when the course has no CourseModes with SKUs.
Validates the view bypasses the E-Commerce API when the course has no CourseModes with SKUs.
"""
"""
# Place an order
# Place an order
with
self
.
mock_create_
order
()
as
api_mock
:
with
self
.
mock_create_
basket
()
as
api_mock
:
response
=
self
.
_post_to_view
()
response
=
self
.
_post_to_view
()
# Validate the response content
# Validate the response content
...
@@ -199,7 +210,7 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
...
@@ -199,7 +210,7 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
"""
"""
If the E-Commerce Service is not configured, the view should enroll the user.
If the E-Commerce Service is not configured, the view should enroll the user.
"""
"""
with
self
.
mock_create_
order
()
as
api_mock
:
with
self
.
mock_create_
basket
()
as
api_mock
:
response
=
self
.
_post_to_view
()
response
=
self
.
_post_to_view
()
# Validate the response
# Validate the response
...
@@ -219,7 +230,7 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
...
@@ -219,7 +230,7 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
CourseModeFactory
.
create
(
course_id
=
self
.
course
.
id
,
mode_slug
=
mode
,
mode_display_name
=
mode
,
CourseModeFactory
.
create
(
course_id
=
self
.
course
.
id
,
mode_slug
=
mode
,
mode_display_name
=
mode
,
sku
=
uuid4
()
.
hex
.
decode
(
'ascii'
))
sku
=
uuid4
()
.
hex
.
decode
(
'ascii'
))
with
self
.
mock_create_
order
()
as
api_mock
:
with
self
.
mock_create_
basket
()
as
api_mock
:
response
=
self
.
_post_to_view
()
response
=
self
.
_post_to_view
()
# The view should return an error status code
# The view should return an error status code
...
@@ -274,4 +285,17 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
...
@@ -274,4 +285,17 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course
.
id
))
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course
.
id
))
self
.
assertIsNotNone
(
get_enrollment
(
self
.
user
.
username
,
unicode
(
self
.
course
.
id
)))
self
.
assertIsNotNone
(
get_enrollment
(
self
.
user
.
username
,
unicode
(
self
.
course
.
id
)))
self
.
_test_successful_ecommerce_api_call
()
with
self
.
mock_create_basket
():
self
.
_test_successful_ecommerce_api_call
(
False
)
class
OrdersViewTests
(
BasketsViewTests
):
"""
Ensures that /orders/ points to and behaves like /baskets/, for backward
compatibility with stale js clients during updates.
(XCOM-214) remove after release.
"""
def
setUp
(
self
):
super
(
OrdersViewTests
,
self
)
.
setUp
()
self
.
url
=
reverse
(
'commerce:orders'
)
lms/djangoapps/commerce/urls.py
View file @
efde11d5
...
@@ -4,10 +4,12 @@ Defines the URL routes for this app.
...
@@ -4,10 +4,12 @@ Defines the URL routes for this app.
from
django.conf.urls
import
patterns
,
url
from
django.conf.urls
import
patterns
,
url
from
.views
import
Order
sView
,
checkout_cancel
from
.views
import
Basket
sView
,
checkout_cancel
urlpatterns
=
patterns
(
urlpatterns
=
patterns
(
''
,
''
,
url
(
r'^
orders/$'
,
OrdersView
.
as_view
(),
name
=
"order
s"
),
url
(
r'^
baskets/$'
,
BasketsView
.
as_view
(),
name
=
"basket
s"
),
url
(
r'^checkout/cancel/$'
,
checkout_cancel
,
name
=
"checkout_cancel"
),
url
(
r'^checkout/cancel/$'
,
checkout_cancel
,
name
=
"checkout_cancel"
),
# (XCOM-214) For backwards compatibility with js clients during intial release
url
(
r'^orders/$'
,
BasketsView
.
as_view
(),
name
=
"orders"
),
)
)
lms/djangoapps/commerce/views.py
View file @
efde11d5
...
@@ -3,31 +3,31 @@ import logging
...
@@ -3,31 +3,31 @@ import logging
from
django.conf
import
settings
from
django.conf
import
settings
from
django.views.decorators.cache
import
cache_page
from
django.views.decorators.cache
import
cache_page
from
opaque_keys
import
InvalidKeyError
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework.status
import
HTTP_406_NOT_ACCEPTABLE
,
HTTP_
202_ACCEPTED
,
HTTP_
409_CONFLICT
from
rest_framework.status
import
HTTP_406_NOT_ACCEPTABLE
,
HTTP_409_CONFLICT
from
rest_framework.views
import
APIView
from
rest_framework.views
import
APIView
from
commerce.api
import
EcommerceAPI
from
commerce.api
import
EcommerceAPI
from
commerce.constants
import
OrderStatus
,
Messages
from
commerce.constants
import
Messages
from
commerce.exceptions
import
ApiError
,
InvalidConfigurationError
from
commerce.exceptions
import
ApiError
,
InvalidConfigurationError
,
InvalidResponseError
from
commerce.http
import
DetailResponse
,
InternalRequestErrorResponse
from
commerce.http
import
DetailResponse
,
InternalRequestErrorResponse
from
course_modes.models
import
CourseMode
from
course_modes.models
import
CourseMode
from
courseware
import
courses
from
courseware
import
courses
from
edxmako.shortcuts
import
render_to_response
from
edxmako.shortcuts
import
render_to_response
from
enrollment.api
import
add_enrollment
from
enrollment.api
import
add_enrollment
from
microsite_configuration
import
microsite
from
microsite_configuration
import
microsite
from
student.models
import
CourseEnrollment
from
openedx.core.lib.api.authentication
import
SessionAuthenticationAllowInactiveUser
from
openedx.core.lib.api.authentication
import
SessionAuthenticationAllowInactiveUser
from
student.models
import
CourseEnrollment
from
util.json_request
import
JsonResponse
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
class
Order
sView
(
APIView
):
class
Basket
sView
(
APIView
):
""" Creates a
n order
with a course seat and enrolls users. """
""" Creates a
basket
with a course seat and enrolls users. """
# LMS utilizes User.user_is_active to indicate email verification, not whether an account is active. Sigh!
# LMS utilizes User.user_is_active to indicate email verification, not whether an account is active. Sigh!
authentication_classes
=
(
SessionAuthenticationAllowInactiveUser
,)
authentication_classes
=
(
SessionAuthenticationAllowInactiveUser
,)
...
@@ -63,7 +63,7 @@ class OrdersView(APIView):
...
@@ -63,7 +63,7 @@ class OrdersView(APIView):
def
post
(
self
,
request
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
def
post
(
self
,
request
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
"""
"""
Attempt to create the
order
and enroll the user.
Attempt to create the
basket
and enroll the user.
"""
"""
user
=
request
.
user
user
=
request
.
user
valid
,
course_key
,
error
=
self
.
_is_data_valid
(
request
)
valid
,
course_key
,
error
=
self
.
_is_data_valid
(
request
)
...
@@ -103,28 +103,31 @@ class OrdersView(APIView):
...
@@ -103,28 +103,31 @@ class OrdersView(APIView):
# Make the API call
# Make the API call
try
:
try
:
order_number
,
order_status
,
_body
=
api
.
create_order
(
user
,
honor_mode
.
sku
)
response_data
=
api
.
create_basket
(
if
order_status
==
OrderStatus
.
COMPLETE
:
user
,
msg
=
Messages
.
ORDER_COMPLETED
.
format
(
order_number
=
order_number
)
honor_mode
.
sku
,
payment_processor
=
"cybersource"
,
)
payment_data
=
response_data
[
"payment_data"
]
if
payment_data
is
not
None
:
# it is time to start the payment flow.
# NOTE this branch does not appear to be used at the moment.
return
JsonResponse
(
payment_data
)
elif
response_data
[
'order'
]:
# the order was completed immediately because there was no charge.
msg
=
Messages
.
ORDER_COMPLETED
.
format
(
order_number
=
response_data
[
'order'
][
'number'
])
log
.
debug
(
msg
)
log
.
debug
(
msg
)
return
DetailResponse
(
msg
)
return
DetailResponse
(
msg
)
else
:
else
:
#
TODO Before this functionality is fully rolled-out, this branch should be updated to NOT enroll the
#
Enroll in the honor mode directly as a failsafe.
#
user. Enrollments must be initiated by the E-Commerce API only
.
#
This MUST be removed when this code handles paid modes
.
self
.
_enroll
(
course_key
,
user
)
self
.
_enroll
(
course_key
,
user
)
msg
=
u'Order
%(order_number)
s was received with
%(status)
s status. Expected
%(complete_status)
s. '
\
msg
=
u'Unexpected response from basket endpoint.'
u'User
%(username)
s was enrolled in
%(course_id)
s by LMS.'
log
.
error
(
msg_kwargs
=
{
msg
+
u' Could not enroll user
%(username)
s in course
%(course_id)
s.'
,
'order_number'
:
order_number
,
{
'username'
:
user
.
id
,
'course_id'
:
course_id
},
'status'
:
order_status
,
)
'complete_status'
:
OrderStatus
.
COMPLETE
,
raise
InvalidResponseError
(
msg
)
'username'
:
user
.
username
,
'course_id'
:
course_id
,
}
log
.
error
(
msg
,
msg_kwargs
)
msg
=
Messages
.
ORDER_INCOMPLETE_ENROLLED
.
format
(
order_number
=
order_number
)
return
DetailResponse
(
msg
,
status
=
HTTP_202_ACCEPTED
)
except
ApiError
as
err
:
except
ApiError
as
err
:
# The API will handle logging of the error.
# The API will handle logging of the error.
return
InternalRequestErrorResponse
(
err
.
message
)
return
InternalRequestErrorResponse
(
err
.
message
)
...
...
lms/djangoapps/shoppingcart/tests/test_views.py
View file @
efde11d5
...
@@ -918,24 +918,27 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
...
@@ -918,24 +918,27 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
"""Mocks calls to EcommerceAPI.get_order. """
"""Mocks calls to EcommerceAPI.get_order. """
patch
=
None
patch
=
None
ORDER
=
copy
.
deepcopy
(
EcommerceApiTestMixin
.
ECOMMERCE_API_SUCCESSFUL_BODY
)
ORDER
=
{
ORDER
[
'total_excl_tax'
]
=
40.0
'status'
:
OrderStatus
.
COMPLETE
,
ORDER
[
'currency'
]
=
'USD'
'number'
:
EcommerceApiTestMixin
.
ORDER_NUMBER
,
ORDER
[
'sources'
]
=
[{
'transactions'
:
[
'total_excl_tax'
:
40.0
,
{
'date_created'
:
'2015-04-07 17:59:06.274587+00:00'
},
'currency'
:
'USD'
,
{
'date_created'
:
'2015-04-08 13:33:06.150000+00:00'
},
'sources'
:
[{
'transactions'
:
[
{
'date_created'
:
'2015-04-09 10:45:06.200000+00:00'
},
{
'date_created'
:
'2015-04-07 17:59:06.274587+00:00'
},
]}]
{
'date_created'
:
'2015-04-08 13:33:06.150000+00:00'
},
ORDER
[
'billing_address'
]
=
{
{
'date_created'
:
'2015-04-09 10:45:06.200000+00:00'
},
'first_name'
:
'Philip'
,
]}],
'last_name'
:
'Fry'
,
'billing_address'
:
{
'line1'
:
'Robot Arms Apts'
,
'first_name'
:
'Philip'
,
'line2'
:
'22 Robot Street'
,
'last_name'
:
'Fry'
,
'line4'
:
'New New York'
,
'line1'
:
'Robot Arms Apts'
,
'state'
:
'NY'
,
'line2'
:
'22 Robot Street'
,
'postcode'
:
'11201'
,
'line4'
:
'New New York'
,
'country'
:
{
'state'
:
'NY'
,
'display_name'
:
'United States'
,
'postcode'
:
'11201'
,
'country'
:
{
'display_name'
:
'United States'
,
},
},
},
}
}
...
...
lms/djangoapps/verify_student/tests/test_integration.py
View file @
efde11d5
...
@@ -58,4 +58,4 @@ class TestProfEdVerification(ModuleStoreTestCase):
...
@@ -58,4 +58,4 @@ class TestProfEdVerification(ModuleStoreTestCase):
# On the first page of the flow, verify that there's a button allowing the user
# On the first page of the flow, verify that there's a button allowing the user
# to proceed to the payment processor; this is the only action the user is allowed to take.
# to proceed to the payment processor; this is the only action the user is allowed to take.
self
.
assertContains
(
resp
,
'pay
_
button'
)
self
.
assertContains
(
resp
,
'pay
ment-
button'
)
lms/djangoapps/verify_student/tests/test_views.py
View file @
efde11d5
...
@@ -52,6 +52,8 @@ def mock_render_to_response(*args, **kwargs):
...
@@ -52,6 +52,8 @@ def mock_render_to_response(*args, **kwargs):
render_mock
=
Mock
(
side_effect
=
mock_render_to_response
)
render_mock
=
Mock
(
side_effect
=
mock_render_to_response
)
PAYMENT_DATA_KEYS
=
{
'payment_processor_name'
,
'payment_page_url'
,
'payment_form_data'
}
class
StartView
(
TestCase
):
class
StartView
(
TestCase
):
def
start_url
(
self
,
course_id
=
""
):
def
start_url
(
self
,
course_id
=
""
):
...
@@ -849,166 +851,159 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
...
@@ -849,166 +851,159 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
self
.
assertEqual
(
response_dict
[
'course_name'
],
mode_display_name
)
self
.
assertEqual
(
response_dict
[
'course_name'
],
mode_display_name
)
class
TestCreateOrder
(
EcommerceApiTestMixin
,
ModuleStoreTestCase
):
class
CheckoutTestMixin
(
object
):
"""
"""
Tests for the create order view.
Mixin implementing test methods that should behave identically regardless
of which backend is used (shoppingcart or ecommerce service). Subclasses
immediately follow for each backend, which inherit from TestCase and
define methods needed to customize test parameters, and patch the
appropriate checkout method.
Though the view endpoint under test is named 'create_order' for backward-
compatibility, the effect of using this endpoint is to choose a specific product
(i.e. course mode) and trigger immediate checkout.
"""
"""
def
setUp
(
self
):
def
setUp
(
self
):
""" Create a user and course. """
""" Create a user and course. """
super
(
TestCreateOrder
,
self
)
.
setUp
()
super
(
CheckoutTestMixin
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
(
username
=
"test"
,
password
=
"test"
)
self
.
user
=
UserFactory
.
create
(
username
=
"test"
,
password
=
"test"
)
self
.
course
=
CourseFactory
.
create
()
self
.
course
=
CourseFactory
.
create
()
for
mode
,
min_price
in
((
'audit'
,
0
),
(
'honor'
,
0
),
(
'verified'
,
100
)):
for
mode
,
min_price
in
((
'audit'
,
0
),
(
'honor'
,
0
),
(
'verified'
,
100
)):
# Set SKU to empty string to ensure view knows how to handle such values
CourseModeFactory
(
mode_slug
=
mode
,
course_id
=
self
.
course
.
id
,
min_price
=
min_price
,
sku
=
self
.
make_sku
())
CourseModeFactory
(
mode_slug
=
mode
,
course_id
=
self
.
course
.
id
,
min_price
=
min_price
,
sku
=
''
)
self
.
client
.
login
(
username
=
"test"
,
password
=
"test"
)
self
.
client
.
login
(
username
=
"test"
,
password
=
"test"
)
def
_post
(
self
,
data
):
def
_assert_checked_out
(
"""
self
,
POST to the view being tested and return the response.
post_params
,
patched_create_order
,
expected_course_key
,
expected_mode_slug
,
expected_status_code
=
200
):
"""
"""
url
=
reverse
(
'verify_student_create_order'
)
DRY helper.
return
self
.
client
.
post
(
url
,
data
)
def
test_create_order_already_verified
(
self
):
Ensures that checkout functions were invoked as
# Verify the student so we don't need to submit photos
expected during execution of the create_order endpoint.
self
.
_verify_student
()
"""
post_params
.
setdefault
(
'processor'
,
None
)
response
=
self
.
client
.
post
(
reverse
(
'verify_student_create_order'
),
post_params
)
self
.
assertEqual
(
response
.
status_code
,
expected_status_code
)
if
expected_status_code
==
200
:
# ensure we called checkout at all
self
.
assertTrue
(
patched_create_order
.
called
)
# ensure checkout args were correct
args
=
self
.
_get_checkout_args
(
patched_create_order
)
self
.
assertEqual
(
args
[
'user'
],
self
.
user
)
self
.
assertEqual
(
args
[
'course_key'
],
expected_course_key
)
self
.
assertEqual
(
args
[
'course_mode'
]
.
slug
,
expected_mode_slug
)
# ensure response data was correct
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
set
(
data
.
keys
()),
PAYMENT_DATA_KEYS
)
else
:
self
.
assertFalse
(
patched_create_order
.
called
)
def
test_create_order
(
self
,
patched_create_order
):
# Create an order
# Create an order
params
=
{
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'course_id'
:
unicode
(
self
.
course
.
id
),
'contribution'
:
100
'contribution'
:
100
,
}
}
response
=
self
.
_post
(
params
)
self
.
_assert_checked_out
(
params
,
patched_create_order
,
self
.
course
.
id
,
'verified'
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Verify that the information will be sent to the correct callback URL
# (configured by test settings)
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
[
'override_custom_receipt_page'
],
"http://testserver/shoppingcart/postpay_callback/"
)
# Verify that the course ID and transaction type are included in "merchant-defined data"
self
.
assertEqual
(
data
[
'merchant_defined_data1'
],
unicode
(
self
.
course
.
id
))
self
.
assertEqual
(
data
[
'merchant_defined_data2'
],
"verified"
)
def
test_create_order_already_verified_prof_ed
(
self
):
# Verify the student so we don't need to submit photos
self
.
_verify_student
()
def
test_create_order_prof_ed
(
self
,
patched_create_order
):
# Create a prof ed course
# Create a prof ed course
course
=
CourseFactory
.
create
()
course
=
CourseFactory
.
create
()
CourseModeFactory
(
mode_slug
=
"professional"
,
course_id
=
course
.
id
,
min_price
=
10
)
CourseModeFactory
(
mode_slug
=
"professional"
,
course_id
=
course
.
id
,
min_price
=
10
,
sku
=
self
.
make_sku
())
# Create an order for a prof ed course
# Create an order for a prof ed course
params
=
{
'course_id'
:
unicode
(
course
.
id
)}
params
=
{
'course_id'
:
unicode
(
course
.
id
)}
response
=
self
.
_post
(
params
)
self
.
_assert_checked_out
(
params
,
patched_create_order
,
course
.
id
,
'professional'
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Verify that the course ID and transaction type are included in "merchant-defined data"
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
[
'merchant_defined_data1'
],
unicode
(
course
.
id
))
self
.
assertEqual
(
data
[
'merchant_defined_data2'
],
"professional"
)
def
test_create_order_for_no_id_professional
(
self
):
def
test_create_order_no_id_professional
(
self
,
patched_create_order
):
# Create a no-id-professional ed course
# Create a no-id-professional ed course
course
=
CourseFactory
.
create
()
course
=
CourseFactory
.
create
()
CourseModeFactory
(
mode_slug
=
"no-id-professional"
,
course_id
=
course
.
id
,
min_price
=
10
)
CourseModeFactory
(
mode_slug
=
"no-id-professional"
,
course_id
=
course
.
id
,
min_price
=
10
,
sku
=
self
.
make_sku
())
# Create an order for a prof ed course
# Create an order for a prof ed course
params
=
{
'course_id'
:
unicode
(
course
.
id
)}
params
=
{
'course_id'
:
unicode
(
course
.
id
)}
response
=
self
.
_post
(
params
)
self
.
_assert_checked_out
(
params
,
patched_create_order
,
course
.
id
,
'no-id-professional'
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Verify that the course ID and transaction type are included in "merchant-defined data"
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
[
'merchant_defined_data1'
],
unicode
(
course
.
id
))
self
.
assertEqual
(
data
[
'merchant_defined_data2'
],
"no-id-professional"
)
def
test_create_order_for_multiple_paid_modes
(
self
):
def
test_create_order_for_multiple_paid_modes
(
self
,
patched_create_order
):
# Create a no-id-professional ed course
# Create a no-id-professional ed course
course
=
CourseFactory
.
create
()
course
=
CourseFactory
.
create
()
CourseModeFactory
(
mode_slug
=
"no-id-professional"
,
course_id
=
course
.
id
,
min_price
=
10
)
CourseModeFactory
(
mode_slug
=
"no-id-professional"
,
course_id
=
course
.
id
,
min_price
=
10
,
sku
=
self
.
make_sku
())
CourseModeFactory
(
mode_slug
=
"professional"
,
course_id
=
course
.
id
,
min_price
=
10
)
CourseModeFactory
(
mode_slug
=
"professional"
,
course_id
=
course
.
id
,
min_price
=
10
,
sku
=
self
.
make_sku
())
# Create an order for a prof ed course
# Create an order for a prof ed course
params
=
{
'course_id'
:
unicode
(
course
.
id
)}
params
=
{
'course_id'
:
unicode
(
course
.
id
)}
response
=
self
.
_post
(
params
)
# TODO jsa - is this the intended behavior?
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
_assert_checked_out
(
params
,
patched_create_order
,
course
.
id
,
'no-id-professional'
)
# Verify that the course ID and transaction type are included in "merchant-defined data"
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
[
'merchant_defined_data1'
],
unicode
(
course
.
id
))
self
.
assertEqual
(
data
[
'merchant_defined_data2'
],
"no-id-professional"
)
def
test_create_order_set_donation_amount
(
self
):
def
test_create_order_bad_donation_amount
(
self
,
patched_create_order
):
# Verify the student so we don't need to submit photos
# Create an order
self
.
_verify_student
()
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'contribution'
:
'99.9'
}
self
.
_assert_checked_out
(
params
,
patched_create_order
,
None
,
None
,
expected_status_code
=
400
)
def
test_create_order_good_donation_amount
(
self
,
patched_create_order
):
# Create an order
# Create an order
params
=
{
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'course_id'
:
unicode
(
self
.
course
.
id
),
'contribution'
:
'1
.23
'
'contribution'
:
'1
00.0
'
}
}
self
.
_post
(
params
)
self
.
_assert_checked_out
(
params
,
patched_create_order
,
self
.
course
.
id
,
'verified'
)
def
test_old_clients
(
self
,
patched_create_order
):
# ensure the response to a request from a stale js client is modified so as
# not to break behavior in the browser.
# (XCOM-214) remove after release.
expected_payment_data
=
EcommerceApiTestMixin
.
PAYMENT_DATA
.
copy
()
expected_payment_data
[
'payment_form_data'
]
.
update
({
'foo'
:
'bar'
})
patched_create_order
.
return_value
=
expected_payment_data
# there is no 'processor' parameter in the post payload, so the response should only contain payment form data.
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'contribution'
:
100
}
response
=
self
.
client
.
post
(
reverse
(
'verify_student_create_order'
),
params
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertTrue
(
patched_create_order
.
called
)
# ensure checkout args were correct
args
=
self
.
_get_checkout_args
(
patched_create_order
)
self
.
assertEqual
(
args
[
'user'
],
self
.
user
)
self
.
assertEqual
(
args
[
'course_key'
],
self
.
course
.
id
)
self
.
assertEqual
(
args
[
'course_mode'
]
.
slug
,
'verified'
)
# ensure response data was correct
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
,
{
'foo'
:
'bar'
})
# Verify that the client's session contains the new donation amount
self
.
assertNotIn
(
'donation_for_course'
,
self
.
client
.
session
)
def
_verify_student
(
self
):
@patch
(
'verify_student.views.checkout_with_shoppingcart'
,
return_value
=
EcommerceApiTestMixin
.
PAYMENT_DATA
)
""" Simulate that the student's identity has already been verified. """
class
TestCreateOrderShoppingCart
(
CheckoutTestMixin
,
ModuleStoreTestCase
):
attempt
=
SoftwareSecurePhotoVerification
.
objects
.
create
(
user
=
self
.
user
)
""" Test view behavior when the shoppingcart is used. """
attempt
.
mark_ready
()
attempt
.
submit
()
attempt
.
approve
()
@override_settings
(
ECOMMERCE_API_URL
=
EcommerceApiTestMixin
.
ECOMMERCE_API_URL
,
ECOMMERCE_API_SIGNING_KEY
=
EcommerceApiTestMixin
.
ECOMMERCE_API_SIGNING_KEY
)
def
test_create_order_with_ecommerce_api
(
self
):
""" Verifies that the view communicates with the E-Commerce API to create orders. """
# Keep track of the original number of orders to verify the old code is not being called.
order_count
=
Order
.
objects
.
count
()
# Add SKU to CourseModes
for
course_mode
in
CourseMode
.
objects
.
filter
(
course_id
=
self
.
course
.
id
):
course_mode
.
sku
=
uuid4
()
.
hex
.
decode
(
'ascii'
)
course_mode
.
save
()
# Mock the E-Commerce Service response
with
self
.
mock_create_order
():
self
.
_verify_student
()
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'contribution'
:
100
}
response
=
self
.
_post
(
params
)
# Verify the response is correct.
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
[
'Content-Type'
],
'application/json'
)
self
.
assertEqual
(
json
.
loads
(
response
.
content
),
self
.
ECOMMERCE_API_SUCCESSFUL_BODY
[
'payment_parameters'
])
# Verify old code is not called (e.g. no Order object created in LMS)
def
make_sku
(
self
):
self
.
assertEqual
(
order_count
,
Order
.
objects
.
count
())
""" Checkout is handled by shoppingcart when the course mode's sku is empty. """
return
''
def
_add_course_mode_skus
(
self
):
def
_get_checkout_args
(
self
,
patched_create_order
):
""" Add SKUs to the CourseMode objects for self.course. """
""" Assuming patched_create_order was called, return a mapping containing the call arguments."""
for
course_mode
in
CourseMode
.
objects
.
filter
(
course_id
=
self
.
course
.
id
):
return
dict
(
zip
((
'request'
,
'user'
,
'course_key'
,
'course_mode'
,
'amount'
),
patched_create_order
.
call_args
[
0
]))
course_mode
.
sku
=
uuid4
()
.
hex
.
decode
(
'ascii'
)
course_mode
.
save
()
@override_settings
(
ECOMMERCE_API_URL
=
EcommerceApiTestMixin
.
ECOMMERCE_API_URL
,
ECOMMERCE_API_SIGNING_KEY
=
EcommerceApiTestMixin
.
ECOMMERCE_API_SIGNING_KEY
)
def
test_create_order_with_ecommerce_api_errors
(
self
):
"""
Verifies that the view communicates with the E-Commerce API to create orders, and handles errors
appropriately.
"""
self
.
_add_course_mode_skus
()
with
self
.
mock_create_order
(
side_effect
=
ApiError
):
@override_settings
(
self
.
_verify_student
()
ECOMMERCE_API_URL
=
EcommerceApiTestMixin
.
ECOMMERCE_API_URL
,
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'contribution'
:
100
}
ECOMMERCE_API_SIGNING_KEY
=
EcommerceApiTestMixin
.
ECOMMERCE_API_SIGNING_KEY
self
.
assertRaises
(
ApiError
,
self
.
_post
,
params
)
)
@patch
(
'verify_student.views.checkout_with_ecommerce_service'
,
return_value
=
EcommerceApiTestMixin
.
PAYMENT_DATA
)
class
TestCreateOrderEcommerceService
(
CheckoutTestMixin
,
EcommerceApiTestMixin
,
ModuleStoreTestCase
):
""" Test view behavior when the ecommerce service is used. """
def
make_sku
(
self
):
""" Checkout is handled by the ecommerce service when the course mode's sku is nonempty. """
return
uuid4
()
.
hex
.
decode
(
'ascii'
)
def
_get_checkout_args
(
self
,
patched_create_order
):
""" Assuming patched_create_order was called, return a mapping containing the call arguments."""
return
dict
(
zip
((
'user'
,
'course_key'
,
'course_mode'
,
'processor'
),
patched_create_order
.
call_args
[
0
]))
class
TestCreateOrderView
(
ModuleStoreTestCase
):
class
TestCreateOrderView
(
ModuleStoreTestCase
):
...
@@ -1099,7 +1094,7 @@ class TestCreateOrderView(ModuleStoreTestCase):
...
@@ -1099,7 +1094,7 @@ class TestCreateOrderView(ModuleStoreTestCase):
photo_id_image
=
self
.
IMAGE_DATA
photo_id_image
=
self
.
IMAGE_DATA
)
)
json_response
=
json
.
loads
(
response
.
content
)
json_response
=
json
.
loads
(
response
.
content
)
self
.
assertIsNotNone
(
json_response
.
get
(
'orderNumber'
))
self
.
assertIsNotNone
(
json_response
[
'payment_form_data'
]
.
get
(
'orderNumber'
))
# TODO not canonical
# Verify that the order exists and is configured correctly
# Verify that the order exists and is configured correctly
order
=
Order
.
objects
.
get
(
user
=
self
.
user
)
order
=
Order
.
objects
.
get
(
user
=
self
.
user
)
...
@@ -1135,7 +1130,8 @@ class TestCreateOrderView(ModuleStoreTestCase):
...
@@ -1135,7 +1130,8 @@ class TestCreateOrderView(ModuleStoreTestCase):
url
=
reverse
(
'verify_student_create_order'
)
url
=
reverse
(
'verify_student_create_order'
)
data
=
{
data
=
{
'contribution'
:
contribution
,
'contribution'
:
contribution
,
'course_id'
:
course_id
'course_id'
:
course_id
,
'processor'
:
None
,
}
}
if
face_image
is
not
None
:
if
face_image
is
not
None
:
...
@@ -1149,9 +1145,9 @@ class TestCreateOrderView(ModuleStoreTestCase):
...
@@ -1149,9 +1145,9 @@ class TestCreateOrderView(ModuleStoreTestCase):
if
expect_status_code
==
200
:
if
expect_status_code
==
200
:
json_response
=
json
.
loads
(
response
.
content
)
json_response
=
json
.
loads
(
response
.
content
)
if
expect_success
:
if
expect_success
:
self
.
assert
True
(
json_response
.
get
(
'success'
)
)
self
.
assert
Equal
(
set
(
json_response
.
keys
()),
PAYMENT_DATA_KEYS
)
else
:
else
:
self
.
assertFalse
(
json_response
.
get
(
'success'
)
)
self
.
assertFalse
(
json_response
[
'success'
]
)
return
response
return
response
...
...
lms/djangoapps/verify_student/views.py
View file @
efde11d5
...
@@ -380,6 +380,14 @@ class PayAndVerifyView(View):
...
@@ -380,6 +380,14 @@ class PayAndVerifyView(View):
# Determine the photo verification status
# Determine the photo verification status
verification_good_until
=
self
.
_verification_valid_until
(
request
.
user
)
verification_good_until
=
self
.
_verification_valid_until
(
request
.
user
)
# get available payment processors
if
unexpired_paid_course_mode
.
sku
:
# transaction will be conducted via ecommerce service
processors
=
EcommerceAPI
()
.
get_processors
(
request
.
user
)
else
:
# transaction will be conducted using legacy shopping cart
processors
=
[
settings
.
CC_PROCESSOR_NAME
]
# Render the top-level page
# Render the top-level page
context
=
{
context
=
{
'contribution_amount'
:
contribution_amount
,
'contribution_amount'
:
contribution_amount
,
...
@@ -393,7 +401,7 @@ class PayAndVerifyView(View):
...
@@ -393,7 +401,7 @@ class PayAndVerifyView(View):
'is_active'
:
json
.
dumps
(
request
.
user
.
is_active
),
'is_active'
:
json
.
dumps
(
request
.
user
.
is_active
),
'message_key'
:
message
,
'message_key'
:
message
,
'platform_name'
:
settings
.
PLATFORM_NAME
,
'platform_name'
:
settings
.
PLATFORM_NAME
,
'p
urchase_endpoint'
:
get_purchase_endpoint
()
,
'p
rocessors'
:
processors
,
'requirements'
:
requirements
,
'requirements'
:
requirements
,
'user_full_name'
:
full_name
,
'user_full_name'
:
full_name
,
'verification_deadline'
:
(
'verification_deadline'
:
(
...
@@ -644,26 +652,59 @@ class PayAndVerifyView(View):
...
@@ -644,26 +652,59 @@ class PayAndVerifyView(View):
return
(
has_paid
,
bool
(
is_active
))
return
(
has_paid
,
bool
(
is_active
))
def
c
reate_order_with_ecommerce_service
(
user
,
course_key
,
course_mode
):
# pylint: disable=invalid-name
def
c
heckout_with_ecommerce_service
(
user
,
course_key
,
course_mode
,
processor
):
# pylint: disable=invalid-name
""" Create a new
order
using the E-Commerce API. """
""" Create a new
basket and trigger immediate checkout,
using the E-Commerce API. """
try
:
try
:
api
=
EcommerceAPI
()
api
=
EcommerceAPI
()
# Make an API call to create the order and retrieve the results
# Make an API call to create the order and retrieve the results
_order_number
,
_order_status
,
data
=
api
.
create_order
(
user
,
course_mode
.
sku
)
response_data
=
api
.
create_basket
(
user
,
course_mode
.
sku
,
processor
)
# Pass the payment parameters directly from the API response.
# Pass the payment parameters directly from the API response.
return
HttpResponse
(
json
.
dumps
(
data
[
'payment_parameters'
]),
content_type
=
'application/json
'
)
return
response_data
.
get
(
'payment_data
'
)
except
ApiError
:
except
ApiError
:
params
=
{
'username'
:
user
.
username
,
'mode'
:
course_mode
.
slug
,
'course_id'
:
unicode
(
course_key
)}
params
=
{
'username'
:
user
.
username
,
'mode'
:
course_mode
.
slug
,
'course_id'
:
unicode
(
course_key
)}
log
.
error
(
'Failed to create order for
%(username)
s
%(mode)
s mode of
%(course_id)
s'
,
params
)
log
.
error
(
'Failed to create order for
%(username)
s
%(mode)
s mode of
%(course_id)
s'
,
params
)
raise
raise
def
checkout_with_shoppingcart
(
request
,
user
,
course_key
,
course_mode
,
amount
):
""" Create an order and trigger checkout using shoppingcart."""
cart
=
Order
.
get_cart_for_user
(
user
)
cart
.
clear
()
enrollment_mode
=
course_mode
.
slug
CertificateItem
.
add_to_order
(
cart
,
course_key
,
amount
,
enrollment_mode
)
# Change the order's status so that we don't accidentally modify it later.
# We need to do this to ensure that the parameters we send to the payment system
# match what we store in the database.
# (Ordinarily we would do this client-side when the user submits the form, but since
# the JavaScript on this page does that immediately, we make the change here instead.
# This avoids a second AJAX call and some additional complication of the JavaScript.)
# If a user later re-enters the verification / payment flow, she will create a new order.
cart
.
start_purchase
()
callback_url
=
request
.
build_absolute_uri
(
reverse
(
"shoppingcart.views.postpay_callback"
)
)
payment_data
=
{
'payment_processor_name'
:
settings
.
CC_PROCESSOR_NAME
,
'payment_page_url'
:
get_purchase_endpoint
(),
'payment_form_data'
:
get_signed_purchase_params
(
cart
,
callback_url
=
callback_url
,
extra_data
=
[
unicode
(
course_key
),
course_mode
.
slug
]
),
}
return
payment_data
@require_POST
@require_POST
@login_required
@login_required
def
create_order
(
request
):
def
create_order
(
request
):
"""
"""
Submit PhotoVerification and create a new Order for this verified cert
This endpoint is named 'create_order' for backward compatibility, but its
actual use is to add a single product to the user's cart and request
immediate checkout.
"""
"""
# Only submit photos if photo data is provided by the client.
# Only submit photos if photo data is provided by the client.
# TODO (ECOM-188): Once the A/B test of decoupling verified / payment
# TODO (ECOM-188): Once the A/B test of decoupling verified / payment
...
@@ -724,35 +765,23 @@ def create_order(request):
...
@@ -724,35 +765,23 @@ def create_order(request):
return
HttpResponseBadRequest
(
_
(
"No selected price or selected price is below minimum."
))
return
HttpResponseBadRequest
(
_
(
"No selected price or selected price is below minimum."
))
if
current_mode
.
sku
:
if
current_mode
.
sku
:
return
create_order_with_ecommerce_service
(
request
.
user
,
course_id
,
current_mode
)
# if request.POST doesn't contain 'processor' then the service's default payment processor will be used.
payment_data
=
checkout_with_ecommerce_service
(
# I know, we should check this is valid. All kinds of stuff missing here
request
.
user
,
cart
=
Order
.
get_cart_for_user
(
request
.
user
)
course_id
,
cart
.
clear
()
current_mode
,
enrollment_mode
=
current_mode
.
slug
request
.
POST
.
get
(
'processor'
)
CertificateItem
.
add_to_order
(
cart
,
course_id
,
amount
,
enrollment_mode
)
)
else
:
# Change the order's status so that we don't accidentally modify it later.
payment_data
=
checkout_with_shoppingcart
(
request
,
request
.
user
,
course_id
,
current_mode
,
amount
)
# We need to do this to ensure that the parameters we send to the payment system
# match what we store in the database.
if
'processor'
not
in
request
.
POST
:
# (Ordinarily we would do this client-side when the user submits the form, but since
# (XCOM-214) To be removed after release.
# the JavaScript on this page does that immediately, we make the change here instead.
# the absence of this key in the POST payload indicates that the request was initiated from
# This avoids a second AJAX call and some additional complication of the JavaScript.)
# a stale js client, which expects a response containing only the 'payment_form_data' part of
# If a user later re-enters the verification / payment flow, she will create a new order.
# the payment data result.
cart
.
start_purchase
()
payment_data
=
payment_data
[
'payment_form_data'
]
return
HttpResponse
(
json
.
dumps
(
payment_data
),
content_type
=
"application/json"
)
callback_url
=
request
.
build_absolute_uri
(
reverse
(
"shoppingcart.views.postpay_callback"
)
)
params
=
get_signed_purchase_params
(
cart
,
callback_url
=
callback_url
,
extra_data
=
[
unicode
(
course_id
),
current_mode
.
slug
]
)
params
[
'success'
]
=
True
return
HttpResponse
(
json
.
dumps
(
params
),
content_type
=
"text/json"
)
@require_POST
@require_POST
...
...
lms/static/js/spec/photocapture_spec.js
View file @
efde11d5
...
@@ -4,7 +4,7 @@ define(['backbone', 'jquery', 'js/verify_student/photocapture'],
...
@@ -4,7 +4,7 @@ define(['backbone', 'jquery', 'js/verify_student/photocapture'],
describe
(
"Photo Verification"
,
function
()
{
describe
(
"Photo Verification"
,
function
()
{
beforeEach
(
function
()
{
beforeEach
(
function
()
{
setFixtures
(
'<div id="order-error" style="display: none;"></div><input type="radio" name="contribution" value="35" id="contribution-35" checked="checked"><input type="radio" id="contribution-other" name="contribution" value=""><input type="text" size="9" name="contribution-other-amt" id="contribution-other-amt" value="30"><img id="face_image" src="src="data:image/png;base64,dummy"><img id="photo_id_image" src="src="data:image/png;base64,dummy"><button
id="pay_
button">pay button</button>'
);
setFixtures
(
'<div id="order-error" style="display: none;"></div><input type="radio" name="contribution" value="35" id="contribution-35" checked="checked"><input type="radio" id="contribution-other" name="contribution" value=""><input type="text" size="9" name="contribution-other-amt" id="contribution-other-amt" value="30"><img id="face_image" src="src="data:image/png;base64,dummy"><img id="photo_id_image" src="src="data:image/png;base64,dummy"><button
class="payment-
button">pay button</button>'
);
});
});
it
(
'retake photo'
,
function
()
{
it
(
'retake photo'
,
function
()
{
...
@@ -27,7 +27,7 @@ define(['backbone', 'jquery', 'js/verify_student/photocapture'],
...
@@ -27,7 +27,7 @@ define(['backbone', 'jquery', 'js/verify_student/photocapture'],
});
});
submitToPaymentProcessing
();
submitToPaymentProcessing
();
expect
(
window
.
submitForm
).
toHaveBeenCalled
();
expect
(
window
.
submitForm
).
toHaveBeenCalled
();
expect
(
$
(
"
#pay_
button"
)).
toHaveClass
(
"is-disabled"
);
expect
(
$
(
"
.payment-
button"
)).
toHaveClass
(
"is-disabled"
);
});
});
it
(
'Error during process'
,
function
()
{
it
(
'Error during process'
,
function
()
{
...
@@ -44,7 +44,7 @@ define(['backbone', 'jquery', 'js/verify_student/photocapture'],
...
@@ -44,7 +44,7 @@ define(['backbone', 'jquery', 'js/verify_student/photocapture'],
expect
(
window
.
showSubmissionError
).
toHaveBeenCalled
();
expect
(
window
.
showSubmissionError
).
toHaveBeenCalled
();
// make sure the button isn't disabled
// make sure the button isn't disabled
expect
(
$
(
"
#pay_
button"
)).
not
.
toHaveClass
(
"is-disabled"
);
expect
(
$
(
"
.payment-
button"
)).
not
.
toHaveClass
(
"is-disabled"
);
// but also make sure that it was disabled during the ajax call
// but also make sure that it was disabled during the ajax call
expect
(
$
.
fn
.
addClass
).
toHaveBeenCalledWith
(
"is-disabled"
);
expect
(
$
.
fn
.
addClass
).
toHaveBeenCalledWith
(
"is-disabled"
);
...
...
lms/static/js/spec/student_account/enrollment_spec.js
View file @
efde11d5
...
@@ -5,7 +5,7 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'],
...
@@ -5,7 +5,7 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'],
describe
(
'edx.student.account.EnrollmentInterface'
,
function
()
{
describe
(
'edx.student.account.EnrollmentInterface'
,
function
()
{
var
COURSE_KEY
=
'edX/DemoX/Fall'
,
var
COURSE_KEY
=
'edX/DemoX/Fall'
,
ENROLL_URL
=
'/commerce/
order
s/'
,
ENROLL_URL
=
'/commerce/
basket
s/'
,
FORWARD_URL
=
'/course_modes/choose/edX/DemoX/Fall/'
,
FORWARD_URL
=
'/course_modes/choose/edX/DemoX/Fall/'
,
EMBARGO_MSG_URL
=
'/embargo/blocked-message/enrollment/default/'
;
EMBARGO_MSG_URL
=
'/embargo/blocked-message/enrollment/default/'
;
...
...
lms/static/js/spec/verify_student/make_payment_step_view_spec.js
View file @
efde11d5
...
@@ -11,8 +11,6 @@ define([
...
@@ -11,8 +11,6 @@ define([
describe
(
'edx.verify_student.MakePaymentStepView'
,
function
()
{
describe
(
'edx.verify_student.MakePaymentStepView'
,
function
()
{
var
PAYMENT_URL
=
"/pay"
;
var
PAYMENT_PARAMS
=
{
var
PAYMENT_PARAMS
=
{
orderId
:
"test-order"
,
orderId
:
"test-order"
,
signature
:
"abcd1234"
signature
:
"abcd1234"
...
@@ -21,7 +19,7 @@ define([
...
@@ -21,7 +19,7 @@ define([
var
STEP_DATA
=
{
var
STEP_DATA
=
{
minPrice
:
"12"
,
minPrice
:
"12"
,
currency
:
"usd"
,
currency
:
"usd"
,
p
urchaseEndpoint
:
PAYMENT_URL
,
p
rocessors
:
[
"test-payment-processor"
]
,
courseKey
:
"edx/test/test"
,
courseKey
:
"edx/test/test"
,
courseModeSlug
:
'verified'
courseModeSlug
:
'verified'
};
};
...
@@ -50,15 +48,16 @@ define([
...
@@ -50,15 +48,16 @@ define([
};
};
var
expectPaymentButtonEnabled
=
function
(
isEnabled
)
{
var
expectPaymentButtonEnabled
=
function
(
isEnabled
)
{
var
appearsDisabled
=
$
(
'#pay_button'
).
hasClass
(
'is-disabled'
),
var
el
=
$
(
'.payment-button'
),
isDisabled
=
$
(
'#pay_button'
).
prop
(
'disabled'
);
appearsDisabled
=
el
.
hasClass
(
'is-disabled'
),
isDisabled
=
el
.
prop
(
'disabled'
);
expect
(
!
appearsDisabled
).
toEqual
(
isEnabled
);
expect
(
!
appearsDisabled
).
toEqual
(
isEnabled
);
expect
(
!
isDisabled
).
toEqual
(
isEnabled
);
expect
(
!
isDisabled
).
toEqual
(
isEnabled
);
};
};
var
expectPaymentDisabledBecauseInactive
=
function
()
{
var
expectPaymentDisabledBecauseInactive
=
function
()
{
var
payButton
=
$
(
'
#pay
_button'
);
var
payButton
=
$
(
'
.payment
_button'
);
// Payment button should be hidden
// Payment button should be hidden
expect
(
payButton
.
length
).
toEqual
(
0
);
expect
(
payButton
.
length
).
toEqual
(
0
);
...
@@ -67,21 +66,22 @@ define([
...
@@ -67,21 +66,22 @@ define([
var
goToPayment
=
function
(
requests
,
kwargs
)
{
var
goToPayment
=
function
(
requests
,
kwargs
)
{
var
params
=
{
var
params
=
{
contribution
:
kwargs
.
amount
||
""
,
contribution
:
kwargs
.
amount
||
""
,
course_id
:
kwargs
.
courseId
||
""
course_id
:
kwargs
.
courseId
||
""
,
processor
:
kwargs
.
processor
||
""
};
};
// Click the "go to payment" button
// Click the "go to payment" button
$
(
'
#pay_
button'
).
click
();
$
(
'
.payment-
button'
).
click
();
// Verify that the request was made to the server
// Verify that the request was made to the server
AjaxHelpers
.
expectRequest
(
AjaxHelpers
.
expectPostRequest
(
requests
,
"POST"
,
"/verify_student/create_order/"
,
requests
,
"/verify_student/create_order/"
,
$
.
param
(
params
)
$
.
param
(
params
)
);
);
// Simulate the server response
// Simulate the server response
if
(
kwargs
.
succeeds
)
{
if
(
kwargs
.
succeeds
)
{
AjaxHelpers
.
respondWithJson
(
requests
,
PAYMENT_PARAMS
);
// TODO put fixture responses in the right place
AjaxHelpers
.
respondWithJson
(
requests
,
{
payment_page_url
:
'http://payment-page-url/'
,
payment_form_data
:
{
foo
:
'bar'
}}
);
}
else
{
}
else
{
AjaxHelpers
.
respondWithTextError
(
requests
,
400
,
SERVER_ERROR_MSG
);
AjaxHelpers
.
respondWithTextError
(
requests
,
400
,
SERVER_ERROR_MSG
);
}
}
...
@@ -95,7 +95,7 @@ define([
...
@@ -95,7 +95,7 @@ define([
expect
(
form
.
serialize
()).
toEqual
(
$
.
param
(
params
));
expect
(
form
.
serialize
()).
toEqual
(
$
.
param
(
params
));
expect
(
form
.
attr
(
'method'
)).
toEqual
(
"POST"
);
expect
(
form
.
attr
(
'method'
)).
toEqual
(
"POST"
);
expect
(
form
.
attr
(
'action'
)).
toEqual
(
PAYMENT_URL
);
expect
(
form
.
attr
(
'action'
)).
toEqual
(
'http://payment-page-url/'
);
};
};
beforeEach
(
function
()
{
beforeEach
(
function
()
{
...
@@ -114,9 +114,10 @@ define([
...
@@ -114,9 +114,10 @@ define([
goToPayment
(
requests
,
{
goToPayment
(
requests
,
{
amount
:
STEP_DATA
.
minPrice
,
amount
:
STEP_DATA
.
minPrice
,
courseId
:
STEP_DATA
.
courseKey
,
courseId
:
STEP_DATA
.
courseKey
,
processor
:
STEP_DATA
.
processors
[
0
],
succeeds
:
true
succeeds
:
true
});
});
expectPaymentSubmitted
(
view
,
PAYMENT_PARAMS
);
expectPaymentSubmitted
(
view
,
{
foo
:
'bar'
}
);
});
});
it
(
'by default minimum price is selected if no suggested prices are given'
,
function
()
{
it
(
'by default minimum price is selected if no suggested prices are given'
,
function
()
{
...
@@ -129,9 +130,10 @@ define([
...
@@ -129,9 +130,10 @@ define([
goToPayment
(
requests
,
{
goToPayment
(
requests
,
{
amount
:
STEP_DATA
.
minPrice
,
amount
:
STEP_DATA
.
minPrice
,
courseId
:
STEP_DATA
.
courseKey
,
courseId
:
STEP_DATA
.
courseKey
,
processor
:
STEP_DATA
.
processors
[
0
],
succeeds
:
true
succeeds
:
true
});
});
expectPaymentSubmitted
(
view
,
PAYMENT_PARAMS
);
expectPaymentSubmitted
(
view
,
{
foo
:
'bar'
}
);
});
});
it
(
'min price is always selected even if contribution amount is provided'
,
function
()
{
it
(
'min price is always selected even if contribution amount is provided'
,
function
()
{
...
@@ -156,6 +158,7 @@ define([
...
@@ -156,6 +158,7 @@ define([
goToPayment
(
requests
,
{
goToPayment
(
requests
,
{
amount
:
STEP_DATA
.
minPrice
,
amount
:
STEP_DATA
.
minPrice
,
courseId
:
STEP_DATA
.
courseKey
,
courseId
:
STEP_DATA
.
courseKey
,
processor
:
STEP_DATA
.
processors
[
0
],
succeeds
:
false
succeeds
:
false
});
});
...
...
lms/static/js/student_account/enrollment.js
View file @
efde11d5
...
@@ -9,7 +9,7 @@ var edx = edx || {};
...
@@ -9,7 +9,7 @@ var edx = edx || {};
edx
.
student
.
account
.
EnrollmentInterface
=
{
edx
.
student
.
account
.
EnrollmentInterface
=
{
urls
:
{
urls
:
{
orders
:
'/commerce/order
s/'
,
baskets
:
'/commerce/basket
s/'
,
},
},
headers
:
{
headers
:
{
...
@@ -26,7 +26,7 @@ var edx = edx || {};
...
@@ -26,7 +26,7 @@ var edx = edx || {};
data
=
JSON
.
stringify
(
data_obj
);
data
=
JSON
.
stringify
(
data_obj
);
$
.
ajax
({
$
.
ajax
({
url
:
this
.
urls
.
order
s
,
url
:
this
.
urls
.
basket
s
,
type
:
'POST'
,
type
:
'POST'
,
contentType
:
'application/json; charset=utf-8'
,
contentType
:
'application/json; charset=utf-8'
,
data
:
data
,
data
:
data
,
...
...
lms/static/js/verify_student/pay_and_verify.js
View file @
efde11d5
...
@@ -59,7 +59,7 @@ var edx = edx || {};
...
@@ -59,7 +59,7 @@ var edx = edx || {};
function
(
price
)
{
return
Boolean
(
price
);
}
function
(
price
)
{
return
Boolean
(
price
);
}
),
),
currency
:
el
.
data
(
'course-mode-currency'
),
currency
:
el
.
data
(
'course-mode-currency'
),
p
urchaseEndpoint
:
el
.
data
(
'purchase-endpoint
'
),
p
rocessors
:
el
.
data
(
'processors
'
),
verificationDeadline
:
el
.
data
(
'verification-deadline'
),
verificationDeadline
:
el
.
data
(
'verification-deadline'
),
courseModeSlug
:
el
.
data
(
'course-mode-slug'
),
courseModeSlug
:
el
.
data
(
'course-mode-slug'
),
alreadyVerified
:
el
.
data
(
'already-verified'
),
alreadyVerified
:
el
.
data
(
'already-verified'
),
...
...
lms/static/js/verify_student/photocapture.js
View file @
efde11d5
...
@@ -69,7 +69,7 @@ function refereshPageMessage() {
...
@@ -69,7 +69,7 @@ function refereshPageMessage() {
}
}
var
submitToPaymentProcessing
=
function
()
{
var
submitToPaymentProcessing
=
function
()
{
$
(
"
#pay_
button"
).
addClass
(
'is-disabled'
).
attr
(
'aria-disabled'
,
true
);
$
(
"
.payment-
button"
).
addClass
(
'is-disabled'
).
attr
(
'aria-disabled'
,
true
);
var
contribution_input
=
$
(
"input[name='contribution']:checked"
)
var
contribution_input
=
$
(
"input[name='contribution']:checked"
)
var
contribution
=
0
;
var
contribution
=
0
;
if
(
contribution_input
.
attr
(
'id'
)
==
'contribution-other'
)
{
if
(
contribution_input
.
attr
(
'id'
)
==
'contribution-other'
)
{
...
@@ -96,7 +96,7 @@ var submitToPaymentProcessing = function() {
...
@@ -96,7 +96,7 @@ var submitToPaymentProcessing = function() {
}
}
},
},
error
:
function
(
xhr
,
status
,
error
)
{
error
:
function
(
xhr
,
status
,
error
)
{
$
(
"
#pay_
button"
).
removeClass
(
'is-disabled'
).
attr
(
'aria-disabled'
,
false
);
$
(
"
.payment-
button"
).
removeClass
(
'is-disabled'
).
attr
(
'aria-disabled'
,
false
);
showSubmissionError
()
showSubmissionError
()
}
}
});
});
...
@@ -290,7 +290,7 @@ function waitForFlashLoad(func, flash_object) {
...
@@ -290,7 +290,7 @@ function waitForFlashLoad(func, flash_object) {
$
(
document
).
ready
(
function
()
{
$
(
document
).
ready
(
function
()
{
$
(
".carousel-nav"
).
addClass
(
'sr'
);
$
(
".carousel-nav"
).
addClass
(
'sr'
);
$
(
"
#pay_
button"
).
click
(
function
(){
$
(
"
.payment-
button"
).
click
(
function
(){
analytics
.
pageview
(
"Payment Form"
);
analytics
.
pageview
(
"Payment Form"
);
submitToPaymentProcessing
();
submitToPaymentProcessing
();
});
});
...
@@ -306,7 +306,7 @@ $(document).ready(function() {
...
@@ -306,7 +306,7 @@ $(document).ready(function() {
// prevent browsers from keeping this button checked
// prevent browsers from keeping this button checked
$
(
"#confirm_pics_good"
).
prop
(
"checked"
,
false
)
$
(
"#confirm_pics_good"
).
prop
(
"checked"
,
false
)
$
(
"#confirm_pics_good"
).
change
(
function
()
{
$
(
"#confirm_pics_good"
).
change
(
function
()
{
$
(
"
#pay_
button"
).
toggleClass
(
'disabled'
);
$
(
"
.payment-
button"
).
toggleClass
(
'disabled'
);
$
(
"#reverify_button"
).
toggleClass
(
'disabled'
);
$
(
"#reverify_button"
).
toggleClass
(
'disabled'
);
$
(
"#midcourse_reverify_button"
).
toggleClass
(
'disabled'
);
$
(
"#midcourse_reverify_button"
).
toggleClass
(
'disabled'
);
});
});
...
...
lms/static/js/verify_student/views/make_payment_step_view.js
View file @
efde11d5
...
@@ -3,7 +3,7 @@
...
@@ -3,7 +3,7 @@
*/
*/
var
edx
=
edx
||
{};
var
edx
=
edx
||
{};
(
function
(
$
,
_
,
gettext
)
{
(
function
(
$
,
_
,
gettext
,
interpolate_text
)
{
'use strict'
;
'use strict'
;
edx
.
verify_student
=
edx
.
verify_student
||
{};
edx
.
verify_student
=
edx
.
verify_student
||
{};
...
@@ -28,12 +28,45 @@ var edx = edx || {};
...
@@ -28,12 +28,45 @@ var edx = edx || {};
};
};
},
},
_getProductText
:
function
(
modeSlug
,
isUpgrade
)
{
switch
(
modeSlug
)
{
case
"professional"
:
return
gettext
(
"Professional Education Verified Certificate"
);
case
"no-id-professional"
:
return
gettext
(
"Professional Education"
);
default
:
if
(
isUpgrade
)
{
return
gettext
(
"Verified Certificate upgrade"
);
}
else
{
return
gettext
(
"Verified Certificate"
);
}
}
},
_getPaymentButtonText
:
function
(
processorName
)
{
if
(
processorName
.
toLowerCase
().
substr
(
0
,
11
)
==
'cybersource'
)
{
return
gettext
(
'Pay with Credit Card'
);
}
else
{
// This is mainly for testing as no other processors are supported right now.
// Translators: 'processor' is the name of a third-party payment processing vendor (example: "PayPal")
return
interpolate_text
(
gettext
(
'Pay with {processor}'
),
{
processor
:
processorName
});
}
},
_getPaymentButtonHtml
:
function
(
processorName
)
{
var
self
=
this
;
return
_
.
template
(
'<a class="next action-primary payment-button" id="<%- name %>" tab-index="0"><%- text %></a> '
)({
name
:
processorName
,
text
:
self
.
_getPaymentButtonText
(
processorName
)});
},
postRender
:
function
()
{
postRender
:
function
()
{
var
templateContext
=
this
.
templateContext
(),
var
templateContext
=
this
.
templateContext
(),
hasVisibleReqs
=
_
.
some
(
hasVisibleReqs
=
_
.
some
(
templateContext
.
requirements
,
templateContext
.
requirements
,
function
(
isVisible
)
{
return
isVisible
;
}
function
(
isVisible
)
{
return
isVisible
;
}
);
),
self
=
this
;
// Track a virtual pageview, for easy funnel reconstruction.
// Track a virtual pageview, for easy funnel reconstruction.
window
.
analytics
.
page
(
'payment'
,
this
.
templateName
);
window
.
analytics
.
page
(
'payment'
,
this
.
templateName
);
...
@@ -59,25 +92,41 @@ var edx = edx || {};
...
@@ -59,25 +92,41 @@ var edx = edx || {};
this
.
setPaymentEnabled
(
true
);
this
.
setPaymentEnabled
(
true
);
}
}
// render the name of the product being paid for
$
(
'div.payment-buttons span.product-name'
).
append
(
self
.
_getProductText
(
templateContext
.
courseModeSlug
,
templateContext
.
upgrade
)
);
// create a button for each payment processor
_
.
each
(
templateContext
.
processors
,
function
(
processorName
)
{
$
(
'div.payment-buttons'
).
append
(
self
.
_getPaymentButtonHtml
(
processorName
)
);
});
// Handle payment submission
// Handle payment submission
$
(
'
#pay_
button'
).
on
(
'click'
,
_
.
bind
(
this
.
createOrder
,
this
)
);
$
(
'
.payment-
button'
).
on
(
'click'
,
_
.
bind
(
this
.
createOrder
,
this
)
);
},
},
setPaymentEnabled
:
function
(
isEnabled
)
{
setPaymentEnabled
:
function
(
isEnabled
)
{
if
(
_
.
isUndefined
(
isEnabled
)
)
{
if
(
_
.
isUndefined
(
isEnabled
)
)
{
isEnabled
=
true
;
isEnabled
=
true
;
}
}
$
(
'
#pay_
button'
)
$
(
'
.payment-
button'
)
.
toggleClass
(
'is-disabled'
,
!
isEnabled
)
.
toggleClass
(
'is-disabled'
,
!
isEnabled
)
.
prop
(
'disabled'
,
!
isEnabled
)
.
prop
(
'disabled'
,
!
isEnabled
)
.
attr
(
'aria-disabled'
,
!
isEnabled
);
.
attr
(
'aria-disabled'
,
!
isEnabled
);
},
},
createOrder
:
function
()
{
// This function invokes the create_order endpoint. It will either create an order in
// the lms' shoppingcart or a basket in Otto, depending on which backend the request course
// mode is configured to use. In either case, the checkout process will be triggered,
// and the expected response will consist of an appropriate payment processor endpoint for
// redirection, along with parameters to be passed along in the request.
createOrder
:
function
(
event
)
{
var
paymentAmount
=
this
.
getPaymentAmount
(),
var
paymentAmount
=
this
.
getPaymentAmount
(),
postData
=
{
postData
=
{
'processor'
:
event
.
target
.
id
,
'contribution'
:
paymentAmount
,
'contribution'
:
paymentAmount
,
'course_id'
:
this
.
stepData
.
courseKey
,
'course_id'
:
this
.
stepData
.
courseKey
};
};
// Disable the payment button to prevent multiple submissions
// Disable the payment button to prevent multiple submissions
...
@@ -98,21 +147,21 @@ var edx = edx || {};
...
@@ -98,21 +147,21 @@ var edx = edx || {};
},
},
handleCreateOrderResponse
:
function
(
payment
Params
)
{
handleCreateOrderResponse
:
function
(
payment
Data
)
{
// At this point, the
order
has been created on the server,
// At this point, the
basket
has been created on the server,
// and we've received signed payment parameters.
// and we've received signed payment parameters.
// We need to dynamically construct a form using
// We need to dynamically construct a form using
// these parameters, then submit it to the payment processor.
// these parameters, then submit it to the payment processor.
// This will send the user to a
hosted order page,
// This will send the user to a
n externally-hosted page
// where she can
enter credit card information
.
// where she can
proceed with payment
.
var
form
=
$
(
'#payment-processor-form'
);
var
form
=
$
(
'#payment-processor-form'
);
$
(
'input'
,
form
).
remove
();
$
(
'input'
,
form
).
remove
();
form
.
attr
(
'action'
,
this
.
stepData
.
purchaseEndpoint
);
form
.
attr
(
'action'
,
paymentData
.
payment_page_url
);
form
.
attr
(
'method'
,
'POST'
);
form
.
attr
(
'method'
,
'POST'
);
_
.
each
(
payment
Params
,
function
(
value
,
key
)
{
_
.
each
(
payment
Data
.
payment_form_data
,
function
(
value
,
key
)
{
$
(
'<input>'
).
attr
({
$
(
'<input>'
).
attr
({
type
:
'hidden'
,
type
:
'hidden'
,
name
:
key
,
name
:
key
,
...
@@ -200,4 +249,4 @@ var edx = edx || {};
...
@@ -200,4 +249,4 @@ var edx = edx || {};
});
});
})(
jQuery
,
_
,
gettext
);
})(
jQuery
,
_
,
gettext
,
interpolate_text
);
lms/static/sass/_developer.scss
View file @
efde11d5
...
@@ -50,3 +50,22 @@
...
@@ -50,3 +50,22 @@
padding
:
(
$baseline
*
1
.5
)
$baseline
;
padding
:
(
$baseline
*
1
.5
)
$baseline
;
text-align
:
center
;
text-align
:
center
;
}
}
// for verify_student/make_payment_step.underscore
.payment-buttons
{
.purchase
{
float
:
left
;
padding
:
(
$baseline
*.
5
)
0
;
.product-info
,
.product-name
,
.price
{
@extend
%t-ultrastrong
;
color
:
$m-blue-d3
;
}
}
.payment-button
{
float
:
right
;
@include
margin-left
(
(
$baseline
/
2
)
);
}
}
lms/templates/verify_student/make_payment_step.underscore
View file @
efde11d5
...
@@ -98,11 +98,16 @@
...
@@ -98,11 +98,16 @@
<% } %>
<% } %>
<% if ( isActive ) { %>
<% if ( isActive ) { %>
<div class="nav-wizard is-ready center">
<div class="
payment-buttons
nav-wizard is-ready center">
<input type="hidden" name="contribution" value="<%- minPrice %>" />
<input type="hidden" name="contribution" value="<%- minPrice %>" />
<a class="next action-primary" id="pay_button" tab-index="0">
<div class="purchase">
<%- gettext( "Continue to payment" ) %> ($<%- minPrice %>)
<p class="product-info"><span class="product-name"></span> <%- gettext( "price" ) %>: <span class="price">$<%- minPrice %></span></p>
</a>
</div>
<div class="pay-options">
<%
// payment buttons will go here
%>
</div>
</div>
</div>
<% } %>
<% } %>
...
...
lms/templates/verify_student/pay_and_verify.html
View file @
efde11d5
...
@@ -67,7 +67,7 @@ from verify_student.views import PayAndVerifyView
...
@@ -67,7 +67,7 @@ from verify_student.views import PayAndVerifyView
data-course-mode-suggested-prices=
'${course_mode.suggested_prices}'
data-course-mode-suggested-prices=
'${course_mode.suggested_prices}'
data-course-mode-currency=
'${course_mode.currency}'
data-course-mode-currency=
'${course_mode.currency}'
data-contribution-amount=
'${contribution_amount}'
data-contribution-amount=
'${contribution_amount}'
data-p
urchase-endpoint=
'${purchase_endpoint
}'
data-p
rocessors=
'${json.dumps(processors)
}'
data-verification-deadline=
'${verification_deadline}'
data-verification-deadline=
'${verification_deadline}'
data-display-steps=
'${json.dumps(display_steps)}'
data-display-steps=
'${json.dumps(display_steps)}'
data-current-step=
'${current_step}'
data-current-step=
'${current_step}'
...
...
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