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):
...
@@ -42,6 +42,26 @@ class EcommerceAPI(object):
}
}
return
jwt
.
encode
(
data
,
self
.
key
)
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
):
def
create_order
(
self
,
user
,
sku
):
"""
"""
Create a new order.
Create a new order.
...
@@ -52,21 +72,35 @@ class EcommerceAPI(object):
...
@@ -52,21 +72,35 @@ class EcommerceAPI(object):
Returns a tuple with the order number, order status, API response data.
Returns a tuple with the order number, order status, API response data.
"""
"""
headers
=
{
def
create
():
'Content-Type'
:
'application/json'
,
"""Internal service call to create an order. """
'Authorization'
:
'JWT {}'
.
format
(
self
.
_get_jwt
(
user
))
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
:
try
:
response
=
requests
.
post
(
url
,
data
=
json
.
dumps
({
'sku'
:
sku
}),
headers
=
headers
,
timeout
=
self
.
timeout
)
response
=
call
(
)
data
=
response
.
json
()
data
=
response
.
json
()
except
Timeout
:
except
Timeout
:
msg
=
'E-Commerce API request timed out.'
msg
=
'E-Commerce API request timed out.'
log
.
error
(
msg
)
log
.
error
(
msg
)
raise
TimeoutError
(
msg
)
raise
TimeoutError
(
msg
)
except
ValueError
:
except
ValueError
:
msg
=
'E-Commerce API response is not valid JSON.'
msg
=
'E-Commerce API response is not valid JSON.'
log
.
exception
(
msg
)
log
.
exception
(
msg
)
...
...
lms/djangoapps/shoppingcart/tests/test_views.py
View file @
16692741
...
@@ -2,6 +2,8 @@
...
@@ -2,6 +2,8 @@
Tests for Shopping Cart views
Tests for Shopping Cart views
"""
"""
from
collections
import
OrderedDict
from
collections
import
OrderedDict
import
copy
import
mock
import
pytz
import
pytz
from
urlparse
import
urlparse
from
urlparse
import
urlparse
from
decimal
import
Decimal
from
decimal
import
Decimal
...
@@ -27,6 +29,9 @@ import ddt
...
@@ -27,6 +29,9 @@ import ddt
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.api
import
EcommerceAPI
from
commerce.constants
import
OrderStatus
from
commerce.tests
import
EcommerceApiTestMixin
from
student.roles
import
CourseSalesAdminRole
from
student.roles
import
CourseSalesAdminRole
from
util.date_utils
import
get_default_time_display
from
util.date_utils
import
get_default_time_display
from
util.testing
import
UrlResetMixin
from
util.testing
import
UrlResetMixin
...
@@ -866,6 +871,106 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
...
@@ -866,6 +871,106 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
'line_desc'
:
'Honor Code Certificate for course Test Course'
'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
):
def
test_show_receipt_json_multiple_items
(
self
):
# Two different item types
# Two different item types
PaidCourseRegistration
.
add_to_order
(
self
.
cart
,
self
.
course_key
)
PaidCourseRegistration
.
add_to_order
(
self
.
cart
,
self
.
course_key
)
...
...
lms/djangoapps/shoppingcart/urls.py
View file @
16692741
...
@@ -5,7 +5,7 @@ urlpatterns = patterns(
...
@@ -5,7 +5,7 @@ urlpatterns = patterns(
'shoppingcart.views'
,
'shoppingcart.views'
,
url
(
r'^postpay_callback/$'
,
'postpay_callback'
),
# Both the ~accept and ~reject callback pages are handled here
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'^donation/$'
,
'donate'
,
name
=
'donation'
),
url
(
r'^csv_report/$'
,
'csv_report'
,
name
=
'payment_csv_report'
),
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
# 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
logging
import
datetime
import
datetime
import
decimal
import
decimal
import
dateutil
import
pytz
import
pytz
from
ipware.ip
import
get_ip
from
ipware.ip
import
get_ip
from
django.db.models
import
Q
from
django.db.models
import
Q
...
@@ -12,6 +13,9 @@ from django.http import (
...
@@ -12,6 +13,9 @@ from django.http import (
HttpResponseBadRequest
,
HttpResponseForbidden
,
Http404
HttpResponseBadRequest
,
HttpResponseForbidden
,
Http404
)
)
from
django.utils.translation
import
ugettext
as
_
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
course_modes.models
import
CourseMode
from
util.json_request
import
JsonResponse
from
util.json_request
import
JsonResponse
from
django.views.decorators.http
import
require_POST
,
require_http_methods
from
django.views.decorators.http
import
require_POST
,
require_http_methods
...
@@ -820,20 +824,91 @@ def show_receipt(request, ordernum):
...
@@ -820,20 +824,91 @@ def show_receipt(request, ordernum):
Displays a receipt for a particular order.
Displays a receipt for a particular order.
404 if order is not yet purchased or request.user != order.user
404 if order is not yet purchased or request.user != order.user
"""
"""
is_json_request
=
'application/json'
in
request
.
META
.
get
(
'HTTP_ACCEPT'
,
""
)
try
:
try
:
order
=
Order
.
objects
.
get
(
id
=
ordernum
)
order
=
Order
.
objects
.
get
(
id
=
ordernum
)
except
Order
.
DoesNotExist
:
except
(
Order
.
DoesNotExist
,
ValueError
):
raise
Http404
(
'Order not found!'
)
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'
]:
if
order
.
user
!=
request
.
user
or
order
.
status
not
in
[
'purchased'
,
'refunded'
]:
raise
Http404
(
'Order not found!'
)
raise
Http404
(
'Order not found!'
)
if
'application/json'
in
request
.
META
.
get
(
'HTTP_ACCEPT'
,
""
)
:
if
is_json_request
:
return
_show_receipt_json
(
order
)
return
_show_receipt_json
(
order
)
else
:
else
:
return
_show_receipt_html
(
request
,
order
)
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
):
def
_show_receipt_json
(
order
):
"""Render the receipt page as JSON.
"""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