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
d5dda647
Commit
d5dda647
authored
Jul 10, 2015
by
aamir-khan
Committed by
Awais
Jul 16, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
ECOM-1816: added the provider detail on the receipt page.
parent
ba06ab19
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
344 additions
and
14 deletions
+344
-14
lms/djangoapps/commerce/views.py
+1
-0
lms/static/js/commerce/views/receipt_view.js
+111
-9
lms/static/sass/views/_verification.scss
+36
-0
lms/templates/commerce/checkout_receipt.html
+9
-3
lms/templates/commerce/provider.underscore
+13
-0
lms/templates/commerce/receipt.underscore
+3
-0
openedx/core/djangoapps/credit/api/provider.py
+44
-2
openedx/core/djangoapps/credit/tests/test_api.py
+121
-0
openedx/core/djangoapps/credit/urls.py
+6
-0
No files found.
lms/djangoapps/commerce/views.py
View file @
d5dda647
...
...
@@ -66,5 +66,6 @@ def checkout_receipt(request):
'error_text'
:
error_text
,
'for_help_text'
:
for_help_text
,
'payment_support_email'
:
payment_support_email
,
'username'
:
request
.
user
.
username
}
return
render_to_response
(
'commerce/checkout_receipt.html'
,
context
)
lms/static/js/commerce/views/receipt_view.js
View file @
d5dda647
...
...
@@ -13,7 +13,7 @@ var edx = edx || {};
initialize
:
function
()
{
this
.
useEcommerceApi
=
!!
(
$
.
url
(
'?basket_id'
));
_
.
bindAll
(
this
,
'renderReceipt'
,
'renderError'
);
_
.
bindAll
(
this
,
'renderReceipt'
,
'renderError'
,
'getProviderData'
,
'renderProvider'
);
/* Mix non-conflicting functions from underscore.string (all but include, contains, and reverse) into
* the Underscore namespace.
...
...
@@ -28,17 +28,31 @@ var edx = edx || {};
context
=
{
platformName
:
this
.
$el
.
data
(
'platform-name'
),
verified
:
this
.
$el
.
data
(
'verified'
).
toLowerCase
()
===
'true'
};
},
providerId
;
// Add the receipt info to the template context
this
.
course_key
=
this
.
getOrderCourseKey
(
data
)
this
.
username
=
this
.
$el
.
data
(
'username'
);
_
.
extend
(
context
,
{
receipt
:
this
.
receiptContext
(
data
),
courseKey
:
this
.
getOrderCourseKey
(
data
)
courseKey
:
this
.
course_key
});
this
.
$el
.
html
(
_
.
template
(
templateHtml
,
context
));
this
.
trackLinks
();
providerId
=
this
.
getCreditProviderId
(
data
);
if
(
providerId
)
{
this
.
getProviderData
(
providerId
).
then
(
this
.
renderProvider
,
this
.
renderError
)
}
},
renderProvider
:
function
(
context
)
{
var
templateHtml
=
$
(
"#provider-tpl"
).
html
(),
providerDiv
=
this
.
$el
.
find
(
"#receipt-provider"
);
context
.
course_key
=
this
.
course_key
;
context
.
username
=
this
.
username
;
providerDiv
.
html
(
_
.
template
(
templateHtml
,
context
)).
removeClass
(
'hidden'
);
},
renderError
:
function
()
{
...
...
@@ -80,7 +94,7 @@ var edx = edx || {};
/**
* Retrieve receipt data from Oscar (via LMS).
* @param {int} basketId The basket that was purchased.
* @return {object}
JQuery Promise.
* @return {object} JQuery Promise.
*/
getReceiptData
:
function
(
basketId
)
{
var
urlFormat
=
this
.
useEcommerceApi
?
'/api/commerce/v0/baskets/%s/order/'
:
'/shoppingcart/receipt/%s/'
;
...
...
@@ -91,13 +105,27 @@ var edx = edx || {};
dataType
:
'json'
}).
retry
({
times
:
5
,
timeout
:
2000
,
statusCodes
:
[
404
]});
},
/**
* Retrieve credit provider data from LMS.
* @param {string} provider_id The provider_id of the credit provider.
* @return {object} JQuery Promise.
*/
getProviderData
:
function
(
providerId
)
{
var
providerUrl
=
'/api/credit/v1/providers/%s/'
;
return
$
.
ajax
({
url
:
_
.
sprintf
(
providerUrl
,
providerId
),
type
:
'GET'
,
dataType
:
'json'
}).
retry
({
times
:
5
,
timeout
:
2000
,
statusCodes
:
[
404
]});
},
/**
* Construct the template context from data received
* from the E-Commerce API.
*
* @param {object} order Receipt data received from the server
* @return {object}
Receipt template context.
* @return {object} Receipt template context.
*/
receiptContext
:
function
(
order
)
{
var
self
=
this
,
...
...
@@ -172,13 +200,13 @@ var edx = edx || {};
length
=
order
.
lines
.
length
;
for
(
var
i
=
0
;
i
<
length
;
i
++
)
{
var
line
=
order
.
lines
[
i
],
attribute
_values
=
_
.
filter
(
line
.
product
.
attribute_values
,
function
(
attribute
)
{
attribute
Values
=
_
.
find
(
line
.
product
.
attribute_values
,
function
(
attribute
)
{
return
attribute
.
name
===
'course_key'
});
// This method assumes that all items in the order are related to a single course.
if
(
attribute
_values
.
length
>
0
)
{
return
attribute
_values
[
0
]
[
'value'
];
if
(
attribute
Values
!=
undefined
)
{
return
attribute
Values
[
'value'
];
}
}
}
else
{
...
...
@@ -196,7 +224,30 @@ var edx = edx || {};
formatMoney
:
function
(
moneyStr
)
{
return
Number
(
moneyStr
).
toFixed
(
2
);
}
},
/**
* Check whether the payment is for the credit course or not.
*
* @param {object} order Receipt data received from the server
* @return {string} String of the provider_id or null.
*/
getCreditProviderId
:
function
(
order
)
{
var
attributeValues
,
line
=
order
.
lines
[
0
];
if
(
this
.
useEcommerceApi
)
{
attributeValues
=
_
.
find
(
line
.
product
.
attribute_values
,
function
(
attribute
)
{
return
attribute
.
name
===
'credit_provider'
});
// This method assumes that all items in the order are related to a single course.
if
(
attributeValues
!=
undefined
)
{
return
attributeValues
[
'value'
];
}
}
return
null
;
},
});
new
edx
.
commerce
.
ReceiptView
({
...
...
@@ -204,3 +255,54 @@ var edx = edx || {};
});
})(
jQuery
,
_
,
_
.
str
,
Backbone
);
function
completeOrder
(
event
)
{
var
courseKey
=
$
(
event
).
data
(
"course-key"
),
username
=
$
(
event
).
data
(
"username"
),
providerId
=
$
(
event
).
data
(
"provider"
),
postData
=
{
'course_key'
:
courseKey
,
'username'
:
username
},
errorContainer
=
$
(
"#error-container"
);
analytics
.
track
(
"edx.bi.credit.clicked_complete_credit"
,
{
category
:
"credit"
,
label
:
courseKey
}
);
$
.
ajax
({
url
:
'/api/credit/v1/provider/'
+
providerId
+
'/request/'
,
type
:
'POST'
,
headers
:
{
'X-CSRFToken'
:
$
.
cookie
(
'csrftoken'
)
},
data
:
JSON
.
stringify
(
postData
)
,
context
:
this
,
success
:
function
(
requestData
){
var
form
=
$
(
'#complete-order-form'
);
$
(
'input'
,
form
).
remove
();
form
.
attr
(
'action'
,
requestData
.
url
);
form
.
attr
(
'method'
,
'POST'
);
_
.
each
(
requestData
.
parameters
,
function
(
value
,
key
)
{
$
(
'<input>'
).
attr
({
type
:
'hidden'
,
name
:
key
,
value
:
value
}).
appendTo
(
form
);
});
form
.
submit
();
},
error
:
function
(
xhr
){
errorContainer
.
removeClass
(
"is-hidden"
);
errorContainer
.
removeClass
(
"hidden"
);
}
});
}
lms/static/sass/views/_verification.scss
View file @
d5dda647
...
...
@@ -244,6 +244,42 @@
}
}
}
.report-receipt-provider
{
@extend
%ui-window
;
border-top
:
3px
solid
$credit-color-base
!
important
;
p
{
padding
:
0
$baseline
$baseline
/
2
$baseline
;
overflow
:
auto
;
}
.bold_param
{
padding
:
$baseline
*.
75
$baseline
0
$baseline
;
font-weight
:
600
;
}
.bold_param
span
{
@include
float
(
right
);
}
div
{
padding
:
10px
20px
;
margin
:
0
0
15px
;
overflow
:
auto
;
}
div
span
{
@include
float
(
right
);
padding
:
0
;
margin
:
0
;
}
.complete-course
{
@extend
%btn-pl-primary-base
;
@include
float
(
right
);
&
.archived
{
@extend
%btn-pl-default-base
;
}
}
.custom_instructions
div
{
@include
float
(
left
);
}
}
// ====================
...
...
lms/templates/commerce/checkout_receipt.html
View file @
d5dda647
...
...
@@ -12,6 +12,9 @@ from django.utils.translation import ugettext as _
<script
type=
"text/template"
id=
"receipt-tpl"
>
<%
static
:
include
path
=
"commerce/receipt.underscore"
/>
</script>
<script
type=
"text/template"
id=
"provider-tpl"
>
<%
static
:
include
path
=
"commerce/provider.underscore"
/>
</script>
</
%
block>
<
%
block
name=
"js_extra"
>
...
...
@@ -29,17 +32,18 @@ from django.utils.translation import ugettext as _
<div
id=
"error"
class=
"wrapper-msg wrapper-msg-activate"
>
<div
class=
" msg msg-activate"
>
<i
class=
"msg-icon icon fa fa-exclamation-triangle"
aria-hidden=
"true"
></i>
<div
class=
"msg-content"
>
<h3
class=
"title"
>
<span
class=
"sr"
>
${error_summary}
</span>
${error_summary}
</h3>
%if error_text:
%if error_text:
<div
class=
"copy"
>
<p>
${error_text}
</p>
<br/>
</div>
%endif
%endif
<div
class=
"msg"
>
<p>
${for_help_text}
</p>
</div>
...
...
@@ -50,10 +54,12 @@ from django.utils.translation import ugettext as _
<div
class=
"container"
>
<section
class=
"wrapper carousel"
>
<div
id=
"receipt-container"
class=
"pay-and-verify hidden"
data-is-payment-complete=
'${is_payment_complete}'
data-platform-name=
'${platform_name}'
data-verified=
'${verified}'
>
<div
id=
"receipt-container"
class=
"pay-and-verify hidden"
data-is-payment-complete=
'${is_payment_complete}'
data-platform-name=
'${platform_name}'
data-verified=
'${verified}'
data-username=
'${username}'
>
<h1>
${_("Loading Order Data...")}
</h1>
<span>
${ _("Please wait while we retrieve your order details.") }
</span>
</div>
<form
id=
"complete-order-form"
></form>
</section>
</div>
</
%
block>
lms/templates/commerce/provider.underscore
0 → 100644
View file @
d5dda647
<p class="bold_param">
<%= interpolate(gettext("You still need to visit %s to complete the credit process."), [display_name]) %>
<span><%= interpolate("<img src='%s' alt='%s'></image>", ["", display_name]) %></span>
</p>
<p>
<%= interpolate(gettext("In order to learn credit, %s requires learner to:"), [display_name]) %>
</p>
<div class="custom_instructions">
<div>
<%= fulfillment_instructions %>
<%= interpolate('<button data-provider="%s" data-course-key="%s" data-username="%s" class="complete-course" onClick=completeOrder(this)>%s</button>', [provider_id, course_key, username, gettext( "Complete Order")]) %>
</div>
</div>
lms/templates/commerce/receipt.underscore
View file @
d5dda647
...
...
@@ -73,6 +73,9 @@
</p>
</div>
<% } %>
<div class="report report-receipt report-receipt-provider hidden" id="receipt-provider"></div>
</div>
</div>
<% } else { %>
...
...
openedx/core/djangoapps/credit/api/provider.py
View file @
d5dda647
...
...
@@ -8,6 +8,7 @@ import pytz
import
uuid
from
django.db
import
transaction
from
lms.djangoapps.django_comment_client.utils
import
JsonResponse
from
openedx.core.djangoapps.credit.exceptions
import
(
UserIsNotEligible
,
...
...
@@ -39,7 +40,6 @@ def get_credit_providers(providers_list=None):
Returns:
list of credit providers represented as dictionaries
Response Values:
>>> get_credit_providers(['hogwarts'])
[
...
...
@@ -60,10 +60,52 @@ def get_credit_providers(providers_list=None):
...
]
"""
return
CreditProvider
.
get_credit_providers
(
providers_list
=
providers_list
)
def
get_credit_provider_info
(
request
,
provider_id
):
# pylint: disable=unused-argument
"""Retrieve the 'CreditProvider' model data against provided
credit provider.
Args:
provider_id (str): The identifier for the credit provider
Returns: 'CreditProvider' data dictionary
Example Usage:
>>> get_credit_provider_info("hogwarts")
{
"provider_id": "hogwarts",
"display_name": "Hogwarts School of Witchcraft and Wizardry",
"provider_url": "https://credit.example.com/",
"provider_status_url": "https://credit.example.com/status/",
"provider_description: "A new model for the Witchcraft and Wizardry School System.",
"enable_integration": False,
"fulfillment_instructions": "
<p>In order to fulfill credit, Hogwarts School of Witchcraft and Wizardry requires learners to:</p>
<ul>
<li>Sample instruction abc</li>
<li>Sample instruction xyz</li>
</ul>",
}
"""
credit_provider
=
CreditProvider
.
get_credit_provider
(
provider_id
=
provider_id
)
credit_provider_data
=
{}
if
credit_provider
:
credit_provider_data
=
{
"provider_id"
:
credit_provider
.
provider_id
,
"display_name"
:
credit_provider
.
display_name
,
"provider_url"
:
credit_provider
.
provider_url
,
"provider_status_url"
:
credit_provider
.
provider_status_url
,
"provider_description"
:
credit_provider
.
provider_description
,
"enable_integration"
:
credit_provider
.
enable_integration
,
"fulfillment_instructions"
:
credit_provider
.
fulfillment_instructions
}
return
JsonResponse
(
credit_provider_data
)
@transaction.commit_on_success
def
create_credit_request
(
course_key
,
provider_id
,
username
):
"""
...
...
openedx/core/djangoapps/credit/tests/test_api.py
View file @
d5dda647
...
...
@@ -3,11 +3,16 @@ Tests for the API functions in the credit app.
"""
import
datetime
import
ddt
import
json
from
mock
import
patch
import
pytz
import
unittest
from
django.conf
import
settings
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
django.db
import
connection
,
transaction
from
django.core.urlresolvers
import
reverse
,
NoReverseMatch
from
opaque_keys.edx.keys
import
CourseKey
...
...
@@ -33,6 +38,8 @@ from student.tests.factories import UserFactory
TEST_CREDIT_PROVIDER_SECRET_KEY
=
"931433d583c84ca7ba41784bad3232e6"
from
util.testing
import
UrlResetMixin
@override_settings
(
CREDIT_PROVIDER_SECRET_KEYS
=
{
"hogwarts"
:
TEST_CREDIT_PROVIDER_SECRET_KEY
,
...
...
@@ -691,3 +698,117 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase):
"""Check the user's credit status. """
statuses
=
api
.
get_credit_requests_for_user
(
self
.
USER_INFO
[
"username"
])
self
.
assertEqual
(
statuses
[
0
][
"status"
],
expected_status
)
class
CreditApiFeatureFlagTest
(
UrlResetMixin
,
TestCase
):
"""
Base class to test the credit api urls.
"""
def
setUp
(
self
,
**
kwargs
):
enable_credit_api
=
kwargs
.
get
(
'enable_credit_api'
,
False
)
with
patch
.
dict
(
'django.conf.settings.FEATURES'
,
{
'ENABLE_CREDIT_API'
:
enable_credit_api
}):
super
(
CreditApiFeatureFlagTest
,
self
)
.
setUp
(
'lms.urls'
)
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
class
CreditApiFeatureFlagDisabledTests
(
CreditApiFeatureFlagTest
):
"""
Test Python API for credit provider api with feature flag
'ENABLE_CREDIT_API' disabled.
"""
PROVIDER_ID
=
"hogwarts"
def
setUp
(
self
):
super
(
CreditApiFeatureFlagDisabledTests
,
self
)
.
setUp
(
enable_credit_api
=
False
)
def
test_get_credit_provider_details
(
self
):
"""
Test that 'get_provider_info' api url not found.
"""
with
self
.
assertRaises
(
NoReverseMatch
):
reverse
(
'credit:get_provider_info'
,
args
=
[
self
.
PROVIDER_ID
])
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
class
CreditApiFeatureFlagEnabledTests
(
CreditApiFeatureFlagTest
,
CreditApiTestBase
):
"""
Test Python API for credit provider api with feature flag
'ENABLE_CREDIT_API' enabled.
"""
USER_INFO
=
{
"username"
:
"bob"
,
"email"
:
"bob@example.com"
,
"full_name"
:
"Bob"
,
"mailing_address"
:
"123 Fake Street, Cambridge MA"
,
"country"
:
"US"
,
}
FINAL_GRADE
=
0.95
def
setUp
(
self
):
super
(
CreditApiFeatureFlagEnabledTests
,
self
)
.
setUp
(
enable_credit_api
=
True
)
self
.
user
=
UserFactory
(
username
=
self
.
USER_INFO
[
'username'
],
email
=
self
.
USER_INFO
[
'email'
],
)
self
.
user
.
profile
.
name
=
self
.
USER_INFO
[
'full_name'
]
self
.
user
.
profile
.
mailing_address
=
self
.
USER_INFO
[
'mailing_address'
]
self
.
user
.
profile
.
country
=
self
.
USER_INFO
[
'country'
]
self
.
user
.
profile
.
save
()
# By default, configure the database so that there is a single
# credit requirement that the user has satisfied (minimum grade)
self
.
_configure_credit
()
def
test_get_credit_provider_details
(
self
):
"""Test that credit api method 'test_get_credit_provider_details'
returns dictionary data related to provided credit provider.
"""
expected_result
=
{
"provider_id"
:
self
.
PROVIDER_ID
,
"display_name"
:
self
.
PROVIDER_NAME
,
"provider_url"
:
self
.
PROVIDER_URL
,
"provider_status_url"
:
self
.
PROVIDER_STATUS_URL
,
"provider_description"
:
self
.
PROVIDER_DESCRIPTION
,
"enable_integration"
:
self
.
ENABLE_INTEGRATION
,
"fulfillment_instructions"
:
self
.
FULFILLMENT_INSTRUCTIONS
,
}
path
=
reverse
(
'credit:get_provider_info'
,
kwargs
=
{
'provider_id'
:
self
.
PROVIDER_ID
})
result
=
self
.
client
.
get
(
path
)
result
=
json
.
loads
(
result
.
content
)
self
.
assertEqual
(
result
,
expected_result
)
# now test that user gets empty dict for non existent credit provider
path
=
reverse
(
'credit:get_provider_info'
,
kwargs
=
{
'provider_id'
:
'fake_provider_id'
})
result
=
self
.
client
.
get
(
path
)
result
=
json
.
loads
(
result
.
content
)
self
.
assertEqual
(
result
,
{})
def
_configure_credit
(
self
):
"""
Configure a credit course and its requirements.
By default, add a single requirement (minimum grade)
that the user has satisfied.
"""
credit_course
=
self
.
add_credit_course
()
requirement
=
CreditRequirement
.
objects
.
create
(
course
=
credit_course
,
namespace
=
"grade"
,
name
=
"grade"
,
active
=
True
)
status
=
CreditRequirementStatus
.
objects
.
create
(
username
=
self
.
USER_INFO
[
"username"
],
requirement
=
requirement
,
)
status
.
status
=
"satisfied"
status
.
reason
=
{
"final_grade"
:
self
.
FINAL_GRADE
}
status
.
save
()
CreditEligibility
.
objects
.
create
(
username
=
self
.
USER_INFO
[
'username'
],
course
=
CreditCourse
.
objects
.
get
(
course_key
=
self
.
course_key
)
)
openedx/core/djangoapps/credit/urls.py
View file @
d5dda647
...
...
@@ -3,6 +3,7 @@ URLs for the credit app.
"""
from
django.conf.urls
import
patterns
,
url
from
.api.provider
import
get_credit_provider_info
from
.views
import
create_credit_request
,
credit_provider_callback
,
get_providers_detail
,
get_eligibility_for_user
PROVIDER_ID_PATTERN
=
r'(?P<provider_id>[^/]+)'
...
...
@@ -11,6 +12,11 @@ urlpatterns = patterns(
''
,
url
(
r"^v1/providers/(?P<provider_id>[^/]+)/$"
,
get_credit_provider_info
,
name
=
"get_provider_info"
),
url
(
r"^v1/providers/$"
,
get_providers_detail
,
name
=
"providers_detail"
...
...
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