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
16692741
Commit
16692741
authored
Apr 01, 2015
by
Stephen Sanchez
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
:Allow the receipt page to support Oscar Orders.
parent
2c080af6
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
225 additions
and
11 deletions
+225
-11
lms/djangoapps/commerce/api.py
+41
-7
lms/djangoapps/shoppingcart/tests/test_views.py
+105
-0
lms/djangoapps/shoppingcart/urls.py
+1
-1
lms/djangoapps/shoppingcart/views.py
+78
-3
No files found.
lms/djangoapps/commerce/api.py
View file @
16692741
...
...
@@ -42,6 +42,26 @@ class EcommerceAPI(object):
}
return
jwt
.
encode
(
data
,
self
.
key
)
def
get_order
(
self
,
user
,
order_number
):
"""
Retrieve a paid order.
Arguments
user -- User associated with the requested order.
order_number -- The unique identifier for the order.
Returns a tuple with the order number, order status, API response data.
"""
def
get
():
"""Internal service call to retrieve an order. """
headers
=
{
'Content-Type'
:
'application/json'
,
'Authorization'
:
'JWT {}'
.
format
(
self
.
_get_jwt
(
user
))
}
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
self
.
_call_ecommerce_service
(
get
)
def
create_order
(
self
,
user
,
sku
):
"""
Create a new order.
...
...
@@ -52,21 +72,35 @@ class EcommerceAPI(object):
Returns a tuple with the order number, order status, API response data.
"""
headers
=
{
'Content-Type'
:
'application/json'
,
'Authorization'
:
'JWT {}'
.
format
(
self
.
_get_jwt
(
user
))
}
def
create
():
"""Internal service call to create an order. """
headers
=
{
'Content-Type'
:
'application/json'
,
'Authorization'
:
'JWT {}'
.
format
(
self
.
_get_jwt
(
user
))
}
url
=
'{}/orders/'
.
format
(
self
.
url
)
return
requests
.
post
(
url
,
data
=
json
.
dumps
({
'sku'
:
sku
}),
headers
=
headers
,
timeout
=
self
.
timeout
)
return
self
.
_call_ecommerce_service
(
create
)
@staticmethod
def
_call_ecommerce_service
(
call
):
"""
Makes a call to the E-Commerce Service. There are a number of common errors that could occur across any
request to the E-Commerce Service that this helper method can wrap each call with. This method helps ensure
calls to the E-Commerce Service will conform to the same output.
url
=
'{}/orders/'
.
format
(
self
.
url
)
Arguments
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.
"""
try
:
response
=
requests
.
post
(
url
,
data
=
json
.
dumps
({
'sku'
:
sku
}),
headers
=
headers
,
timeout
=
self
.
timeout
)
response
=
call
(
)
data
=
response
.
json
()
except
Timeout
:
msg
=
'E-Commerce API request timed out.'
log
.
error
(
msg
)
raise
TimeoutError
(
msg
)
except
ValueError
:
msg
=
'E-Commerce API response is not valid JSON.'
log
.
exception
(
msg
)
...
...
lms/djangoapps/shoppingcart/tests/test_views.py
View file @
16692741
...
...
@@ -2,6 +2,8 @@
Tests for Shopping Cart views
"""
from
collections
import
OrderedDict
import
copy
import
mock
import
pytz
from
urlparse
import
urlparse
from
decimal
import
Decimal
...
...
@@ -27,6 +29,9 @@ import ddt
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
commerce.api
import
EcommerceAPI
from
commerce.constants
import
OrderStatus
from
commerce.tests
import
EcommerceApiTestMixin
from
student.roles
import
CourseSalesAdminRole
from
util.date_utils
import
get_default_time_display
from
util.testing
import
UrlResetMixin
...
...
@@ -866,6 +871,106 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
'line_desc'
:
'Honor Code Certificate for course Test Course'
})
@ddt.data
(
0
,
1
,
2
)
@override_settings
(
ECOMMERCE_API_URL
=
EcommerceApiTestMixin
.
ECOMMERCE_API_URL
,
ECOMMERCE_API_SIGNING_KEY
=
EcommerceApiTestMixin
.
ECOMMERCE_API_SIGNING_KEY
)
def
test_show_ecom_receipt_json
(
self
,
num_items
):
# set up the get request to return an order with X number of line items.
# Log in the student. Use a false order ID for the E-Commerce Application.
self
.
login_user
()
url
=
reverse
(
'shoppingcart.views.show_receipt'
,
args
=
[
'EDX-100042'
])
with
self
.
mock_get_order
(
num_items
=
num_items
):
resp
=
self
.
client
.
get
(
url
,
HTTP_ACCEPT
=
"application/json"
)
# Should have gotten a successful response
self
.
assertEqual
(
resp
.
status_code
,
200
)
# Parse the response as JSON and check the contents
json_resp
=
json
.
loads
(
resp
.
content
)
self
.
assertEqual
(
json_resp
.
get
(
'currency'
),
self
.
mock_get_order
.
ORDER
[
'currency'
])
self
.
assertEqual
(
json_resp
.
get
(
'purchase_datetime'
),
'Apr 07, 2015 at 17:59 UTC'
)
self
.
assertEqual
(
json_resp
.
get
(
'total_cost'
),
self
.
mock_get_order
.
ORDER
[
'total_excl_tax'
])
self
.
assertEqual
(
json_resp
.
get
(
'status'
),
self
.
mock_get_order
.
ORDER
[
'status'
])
self
.
assertEqual
(
json_resp
.
get
(
'billed_to'
),
{
'first_name'
:
self
.
mock_get_order
.
ORDER
[
'billing_address'
][
'first_name'
],
'last_name'
:
self
.
mock_get_order
.
ORDER
[
'billing_address'
][
'last_name'
],
'street1'
:
self
.
mock_get_order
.
ORDER
[
'billing_address'
][
'line1'
],
'street2'
:
self
.
mock_get_order
.
ORDER
[
'billing_address'
][
'line2'
],
'city'
:
self
.
mock_get_order
.
ORDER
[
'billing_address'
][
'line4'
],
'state'
:
self
.
mock_get_order
.
ORDER
[
'billing_address'
][
'state'
],
'postal_code'
:
self
.
mock_get_order
.
ORDER
[
'billing_address'
][
'postcode'
],
'country'
:
self
.
mock_get_order
.
ORDER
[
'billing_address'
][
'country'
][
'display_name'
]
})
self
.
assertEqual
(
len
(
json_resp
.
get
(
'items'
)),
num_items
)
for
item
in
json_resp
.
get
(
'items'
):
self
.
assertEqual
(
item
,
{
'unit_cost'
:
self
.
mock_get_order
.
LINE
[
'unit_price_excl_tax'
],
'quantity'
:
self
.
mock_get_order
.
LINE
[
'quantity'
],
'line_cost'
:
self
.
mock_get_order
.
LINE
[
'line_price_excl_tax'
],
'line_desc'
:
self
.
mock_get_order
.
LINE
[
'description'
]
})
class
mock_get_order
(
object
):
# pylint: disable=invalid-name
"""Mocks calls to EcommerceAPI.get_order. """
patch
=
None
ORDER
=
copy
.
deepcopy
(
EcommerceApiTestMixin
.
ECOMMERCE_API_SUCCESSFUL_BODY
)
ORDER
[
'total_excl_tax'
]
=
40.0
ORDER
[
'currency'
]
=
'USD'
ORDER
[
'sources'
]
=
[{
'transactions'
:
[
{
'date_created'
:
'2015-04-07 17:59:06.274587+00:00'
},
{
'date_created'
:
'2015-04-08 13:33:06.150000+00:00'
},
{
'date_created'
:
'2015-04-09 10:45:06.200000+00:00'
},
]}]
ORDER
[
'billing_address'
]
=
{
'first_name'
:
'Philip'
,
'last_name'
:
'Fry'
,
'line1'
:
'Robot Arms Apts'
,
'line2'
:
'22 Robot Street'
,
'line4'
:
'New New York'
,
'state'
:
'NY'
,
'postcode'
:
'11201'
,
'country'
:
{
'display_name'
:
'United States'
,
},
}
LINE
=
{
"title"
:
"Honor Code Certificate for course Test Course"
,
"description"
:
"Honor Code Certificate for course Test Course"
,
"status"
:
"Paid"
,
"line_price_excl_tax"
:
40.0
,
"quantity"
:
1
,
"unit_price_excl_tax"
:
40.0
}
def
__init__
(
self
,
**
kwargs
):
result
=
copy
.
deepcopy
(
self
.
ORDER
)
result
[
'lines'
]
=
[
copy
.
deepcopy
(
self
.
LINE
)
for
_
in
xrange
(
kwargs
[
'num_items'
])]
default_kwargs
=
{
'return_value'
:
(
EcommerceApiTestMixin
.
ORDER_NUMBER
,
OrderStatus
.
COMPLETE
,
result
,
)
}
default_kwargs
.
update
(
kwargs
)
self
.
patch
=
mock
.
patch
.
object
(
EcommerceAPI
,
'get_order'
,
mock
.
Mock
(
**
default_kwargs
))
def
__enter__
(
self
):
self
.
patch
.
start
()
return
self
.
patch
.
new
def
__exit__
(
self
,
exc_type
,
exc_val
,
exc_tb
):
# pylint: disable=unused-argument
self
.
patch
.
stop
()
def
test_show_receipt_json_multiple_items
(
self
):
# Two different item types
PaidCourseRegistration
.
add_to_order
(
self
.
cart
,
self
.
course_key
)
...
...
lms/djangoapps/shoppingcart/urls.py
View file @
16692741
...
...
@@ -5,7 +5,7 @@ urlpatterns = patterns(
'shoppingcart.views'
,
url
(
r'^postpay_callback/$'
,
'postpay_callback'
),
# Both the ~accept and ~reject callback pages are handled here
url
(
r'^receipt/(?P<ordernum>[
0-9]*
)/$'
,
'show_receipt'
),
url
(
r'^receipt/(?P<ordernum>[
-\w]+
)/$'
,
'show_receipt'
),
url
(
r'^donation/$'
,
'donate'
,
name
=
'donation'
),
url
(
r'^csv_report/$'
,
'csv_report'
,
name
=
'payment_csv_report'
),
# These following URLs are only valid if the ENABLE_SHOPPING_CART feature flag is set
...
...
lms/djangoapps/shoppingcart/views.py
View file @
16692741
import
logging
import
datetime
import
decimal
import
dateutil
import
pytz
from
ipware.ip
import
get_ip
from
django.db.models
import
Q
...
...
@@ -12,6 +13,9 @@ from django.http import (
HttpResponseBadRequest
,
HttpResponseForbidden
,
Http404
)
from
django.utils.translation
import
ugettext
as
_
from
commerce.api
import
EcommerceAPI
from
commerce.exceptions
import
InvalidConfigurationError
,
ApiError
from
commerce.http
import
InternalRequestErrorResponse
from
course_modes.models
import
CourseMode
from
util.json_request
import
JsonResponse
from
django.views.decorators.http
import
require_POST
,
require_http_methods
...
...
@@ -820,20 +824,91 @@ def show_receipt(request, ordernum):
Displays a receipt for a particular order.
404 if order is not yet purchased or request.user != order.user
"""
is_json_request
=
'application/json'
in
request
.
META
.
get
(
'HTTP_ACCEPT'
,
""
)
try
:
order
=
Order
.
objects
.
get
(
id
=
ordernum
)
except
Order
.
DoesNotExist
:
raise
Http404
(
'Order not found!'
)
except
(
Order
.
DoesNotExist
,
ValueError
):
if
is_json_request
:
return
_get_external_order
(
request
,
ordernum
)
else
:
raise
Http404
(
'Order not found!'
)
if
order
.
user
!=
request
.
user
or
order
.
status
not
in
[
'purchased'
,
'refunded'
]:
raise
Http404
(
'Order not found!'
)
if
'application/json'
in
request
.
META
.
get
(
'HTTP_ACCEPT'
,
""
)
:
if
is_json_request
:
return
_show_receipt_json
(
order
)
else
:
return
_show_receipt_html
(
request
,
order
)
def
_get_external_order
(
request
,
order_number
):
"""Get the order context from the external E-Commerce Service.
Get information about an order. This function makes a request to the E-Commerce Service to see if there is
order information that can be used to render a receipt for the user.
Args:
request (Request): The request for the the receipt.
order_number (str) : The order number.
Returns:
dict: A serializable dictionary of the receipt page context based on an order returned from the E-Commerce
Service.
"""
try
:
api
=
EcommerceAPI
()
order_number
,
order_status
,
order_data
=
api
.
get_order
(
request
.
user
,
order_number
)
billing
=
order_data
.
get
(
'billing_address'
,
{})
country
=
billing
.
get
(
'country'
,
{})
# In order to get the date this order was paid, we need to check for payment sources, and associated
# transactions.
payment_dates
=
[]
for
source
in
order_data
.
get
(
'sources'
,
[]):
for
transaction
in
source
.
get
(
'transactions'
,
[]):
payment_dates
.
append
(
dateutil
.
parser
.
parse
(
transaction
[
'date_created'
]))
payment_date
=
sorted
(
payment_dates
,
reverse
=
True
)
.
pop
()
order_info
=
{
'orderNum'
:
order_number
,
'currency'
:
order_data
[
'currency'
],
'status'
:
order_status
,
'purchase_datetime'
:
get_default_time_display
(
payment_date
),
'total_cost'
:
order_data
[
'total_excl_tax'
],
'billed_to'
:
{
'first_name'
:
billing
.
get
(
'first_name'
,
''
),
'last_name'
:
billing
.
get
(
'last_name'
,
''
),
'street1'
:
billing
.
get
(
'line1'
,
''
),
'street2'
:
billing
.
get
(
'line2'
,
''
),
'city'
:
billing
.
get
(
'line4'
,
''
),
# 'line4' is the City, from the E-Commerce Service
'state'
:
billing
.
get
(
'state'
,
''
),
'postal_code'
:
billing
.
get
(
'postcode'
,
''
),
'country'
:
country
.
get
(
'display_name'
,
''
),
},
'items'
:
[
{
'quantity'
:
item
[
'quantity'
],
'unit_cost'
:
item
[
'unit_price_excl_tax'
],
'line_cost'
:
item
[
'line_price_excl_tax'
],
'line_desc'
:
item
[
'description'
]
}
for
item
in
order_data
[
'lines'
]
]
}
return
JsonResponse
(
order_info
)
except
InvalidConfigurationError
:
msg
=
u"E-Commerce API not setup. Cannot request Order [{order_number}] for User [{user_id}] "
.
format
(
user_id
=
request
.
user
.
id
,
order_number
=
order_number
)
log
.
debug
(
msg
)
return
JsonResponse
(
status
=
500
,
object
=
{
'error_message'
:
msg
})
except
ApiError
as
err
:
# The API will handle logging of the error.
return
InternalRequestErrorResponse
(
err
.
message
)
def
_show_receipt_json
(
order
):
"""Render the receipt page as JSON.
...
...
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