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
a4f5f4e4
Commit
a4f5f4e4
authored
Aug 13, 2013
by
Jason Bau
Committed by
Diana Huang
Aug 21, 2013
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
about page changes, refactor processor reply handling
parent
0b8f4144
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
162 additions
and
137 deletions
+162
-137
common/lib/xmodule/xmodule/course_module.py
+1
-0
common/static/js/capa/spec/jsinput_spec.js
+0
-70
lms/djangoapps/shoppingcart/models.py
+40
-14
lms/djangoapps/shoppingcart/processors/CyberSource.py
+50
-2
lms/djangoapps/shoppingcart/processors/__init__.py
+24
-8
lms/djangoapps/shoppingcart/processors/exceptions.py
+2
-1
lms/djangoapps/shoppingcart/urls.py
+2
-2
lms/djangoapps/shoppingcart/views.py
+32
-37
lms/templates/courseware/course_about.html
+1
-1
lms/templates/shoppingcart/list.html
+5
-1
lms/templates/shoppingcart/receipt.html
+5
-1
No files found.
common/lib/xmodule/xmodule/course_module.py
View file @
a4f5f4e4
...
...
@@ -338,6 +338,7 @@ class CourseFields(object):
show_timezone
=
Boolean
(
help
=
"True if timezones should be shown on dates in the courseware"
,
scope
=
Scope
.
settings
,
default
=
True
)
enrollment_domain
=
String
(
help
=
"External login method associated with user accounts allowed to register in course"
,
scope
=
Scope
.
settings
)
enrollment_cost
=
Dict
(
scope
=
Scope
.
settings
,
default
=
{
'currency'
:
'usd'
,
'cost'
:
0
})
# An extra property is used rather than the wiki_slug/number because
# there are courses that change the number for different runs. This allows
...
...
common/static/js/capa/spec/jsinput_spec.js
deleted
100644 → 0
View file @
0b8f4144
xdescribe
(
"A jsinput has:"
,
function
()
{
beforeEach
(
function
()
{
$
(
'#fixture'
).
remove
();
$
.
ajax
({
async
:
false
,
url
:
'mainfixture.html'
,
success
:
function
(
data
)
{
$
(
'body'
).
append
(
$
(
data
));
}
});
});
describe
(
"The jsinput constructor"
,
function
(){
var
iframe1
=
$
(
document
).
find
(
'iframe'
)[
0
];
var
testJsElem
=
jsinputConstructor
({
id
:
1
,
elem
:
iframe1
,
passive
:
false
});
it
(
"Returns an object"
,
function
(){
expect
(
typeof
(
testJsElem
)).
toEqual
(
'object'
);
});
it
(
"Adds the object to the jsinput array"
,
function
()
{
expect
(
jsinput
.
exists
(
1
)).
toBe
(
true
);
});
describe
(
"The returned object"
,
function
()
{
it
(
"Has a public 'update' method"
,
function
(){
expect
(
testJsElem
.
update
).
toBeDefined
();
});
it
(
"Returns an 'update' that is idempotent"
,
function
(){
var
orig
=
testJsElem
.
update
();
for
(
var
i
=
0
;
i
++
;
i
<
5
)
{
expect
(
testJsElem
.
update
()).
toEqual
(
orig
);
}
});
it
(
"Changes the parent's inputfield"
,
function
()
{
testJsElem
.
update
();
});
});
});
describe
(
"The walkDOM functions"
,
function
()
{
walkDOM
();
it
(
"Creates (at least) one object per iframe"
,
function
()
{
jsinput
.
arr
.
length
>=
2
;
});
it
(
"Does not create multiple objects with the same id"
,
function
()
{
while
(
jsinput
.
arr
.
length
>
0
)
{
var
elem
=
jsinput
.
arr
.
pop
();
expect
(
jsinput
.
exists
(
elem
.
id
)).
toBe
(
false
);
}
});
});
})
lms/djangoapps/shoppingcart/models.py
View file @
a4f5f4e4
...
...
@@ -123,11 +123,13 @@ class OrderItem(models.Model):
Unfortunately the QuerySet used determines the class to be OrderItem, and not its most specific
subclasses. That means this parent class implementation of purchased_callback needs to act as
a dispatcher to call the callback the proper subclasses, and as such it needs to know about all subclasses.
So please add
a dispatcher to call the callback the proper subclasses, and as such it needs to know about all
possible subclasses.
So keep ORDER_ITEM_SUBTYPES up-to-date
"""
for
cl
assname
,
lc_classname
in
ORDER_ITEM_SUBTYPES
:
for
cl
s
,
lc_classname
in
ORDER_ITEM_SUBTYPES
.
iteritems
()
:
try
:
#Uses https://docs.djangoproject.com/en/1.4/topics/db/models/#multi-table-inheritance to test subclass
sub_instance
=
getattr
(
self
,
lc_classname
)
sub_instance
.
purchased_callback
()
except
(
ObjectDoesNotExist
,
AttributeError
):
...
...
@@ -135,13 +137,18 @@ class OrderItem(models.Model):
.
format
(
lc_classname
))
pass
# Each entry is a tuple of ('ModelName', 'lower_case_model_name')
# See https://docs.djangoproject.com/en/1.4/topics/db/models/#multi-table-inheritance for
# PLEASE KEEP THIS LIST UP_TO_DATE WITH THE SUBCLASSES OF OrderItem
ORDER_ITEM_SUBTYPES
=
[
(
'PaidCourseRegistration'
,
'paidcourseregistration'
)
]
def
is_of_subtype
(
self
,
cls
):
"""
Checks if self is also a type of cls, in addition to being an OrderItem
Uses https://docs.djangoproject.com/en/1.4/topics/db/models/#multi-table-inheritance to test for subclass
"""
if
cls
not
in
ORDER_ITEM_SUBTYPES
:
return
False
try
:
getattr
(
self
,
ORDER_ITEM_SUBTYPES
[
cls
])
return
True
except
(
ObjectDoesNotExist
,
AttributeError
):
return
False
class
PaidCourseRegistration
(
OrderItem
):
...
...
@@ -151,7 +158,16 @@ class PaidCourseRegistration(OrderItem):
course_id
=
models
.
CharField
(
max_length
=
128
,
db_index
=
True
)
@classmethod
def
add_to_order
(
cls
,
order
,
course_id
,
cost
,
currency
=
'usd'
):
def
part_of_order
(
cls
,
order
,
course_id
):
"""
Is the course defined by course_id in the order?
"""
return
course_id
in
[
item
.
paidcourseregistration
.
course_id
for
item
in
order
.
orderitem_set
.
all
()
if
item
.
is_of_subtype
(
PaidCourseRegistration
)]
@classmethod
def
add_to_order
(
cls
,
order
,
course_id
,
cost
=
None
,
currency
=
None
):
"""
A standardized way to create these objects, with sensible defaults filled in.
Will update the cost if called on an order that already carries the course.
...
...
@@ -164,6 +180,10 @@ class PaidCourseRegistration(OrderItem):
item
,
created
=
cls
.
objects
.
get_or_create
(
order
=
order
,
user
=
order
.
user
,
course_id
=
course_id
)
item
.
status
=
order
.
status
item
.
qty
=
1
if
cost
is
None
:
cost
=
course
.
enrollment_cost
[
'cost'
]
if
currency
is
None
:
currency
=
course
.
enrollment_cost
[
'currency'
]
item
.
unit_cost
=
cost
item
.
line_cost
=
cost
item
.
line_desc
=
'Registration for Course: {0}'
.
format
(
get_course_about_section
(
course
,
"title"
))
...
...
@@ -182,9 +202,6 @@ class PaidCourseRegistration(OrderItem):
# throw errors if it doesn't
# use get_or_create here to gracefully handle case where the user is already enrolled in the course, for
# whatever reason.
# Don't really need to create CourseEnrollmentAllowed object, but doing it for bookkeeping and consistency
# with rest of codebase.
CourseEnrollmentAllowed
.
objects
.
get_or_create
(
email
=
self
.
user
.
email
,
course_id
=
self
.
course_id
,
auto_enroll
=
True
)
CourseEnrollment
.
objects
.
get_or_create
(
user
=
self
.
user
,
course_id
=
self
.
course_id
)
log
.
info
(
"Enrolled {0} in paid course {1}, paid ${2}"
.
format
(
self
.
user
.
email
,
self
.
course_id
,
self
.
line_cost
))
...
...
@@ -193,3 +210,11 @@ class PaidCourseRegistration(OrderItem):
tags
=
[
"org:{0}"
.
format
(
org
),
"course:{0}"
.
format
(
course_num
),
"run:{0}"
.
format
(
run
)])
# Each entry is a dictionary of ModelName: 'lower_case_model_name'
# See https://docs.djangoproject.com/en/1.4/topics/db/models/#multi-table-inheritance for
# PLEASE KEEP THIS LIST UP_TO_DATE WITH THE SUBCLASSES OF OrderItem
ORDER_ITEM_SUBTYPES
=
{
PaidCourseRegistration
:
'paidcourseregistration'
,
}
\ No newline at end of file
lms/djangoapps/shoppingcart/processors/CyberSource.py
View file @
a4f5f4e4
...
...
@@ -13,7 +13,7 @@ from django.conf import settings
from
django.utils.translation
import
ugettext
as
_
from
mitxmako.shortcuts
import
render_to_string
from
shoppingcart.models
import
Order
from
.exceptions
import
CCProcessorDataException
,
CCProcessorWrongAmountException
from
.exceptions
import
CCProcessor
Exception
,
CCProcessor
DataException
,
CCProcessorWrongAmountException
shared_secret
=
settings
.
CC_PROCESSOR
[
'CyberSource'
]
.
get
(
'SHARED_SECRET'
,
''
)
merchant_id
=
settings
.
CC_PROCESSOR
[
'CyberSource'
]
.
get
(
'MERCHANT_ID'
,
''
)
...
...
@@ -21,6 +21,42 @@ serial_number = settings.CC_PROCESSOR['CyberSource'].get('SERIAL_NUMBER','')
orderPage_version
=
settings
.
CC_PROCESSOR
[
'CyberSource'
]
.
get
(
'ORDERPAGE_VERSION'
,
'7'
)
purchase_endpoint
=
settings
.
CC_PROCESSOR
[
'CyberSource'
]
.
get
(
'PURCHASE_ENDPOINT'
,
''
)
def
process_postpay_callback
(
request
):
"""
The top level call to this module, basically
This function is handed the callback request after the customer has entered the CC info and clicked "buy"
on the external Hosted Order Page.
It is expected to verify the callback and determine if the payment was successful.
It returns {'success':bool, 'order':Order, 'error_html':str}
If successful this function must have the side effect of marking the order purchased and calling the
purchased_callbacks of the cart items.
If unsuccessful this function should not have those side effects but should try to figure out why and
return a helpful-enough error message in error_html.
"""
params
=
request
.
POST
.
dict
()
if
verify_signatures
(
params
):
try
:
result
=
payment_accepted
(
params
)
if
result
[
'accepted'
]:
# SUCCESS CASE first, rest are some sort of oddity
record_purchase
(
params
,
result
[
'order'
])
return
{
'success'
:
True
,
'order'
:
result
[
'order'
],
'error_html'
:
''
}
else
:
return
{
'success'
:
False
,
'order'
:
result
[
'order'
],
'error_html'
:
get_processor_error_html
(
params
)}
except
CCProcessorException
as
e
:
return
{
'success'
:
False
,
'order'
:
None
,
#due to exception we may not have the order
'error_html'
:
get_exception_html
(
params
,
e
)}
else
:
return
{
'success'
:
False
,
'order'
:
None
,
'error_html'
:
get_signature_error_html
(
params
)}
def
hash
(
value
):
"""
Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page
...
...
@@ -48,7 +84,7 @@ def sign(params):
return
params
def
verify
(
params
):
def
verify
_signatures
(
params
):
"""
Verify the signatures accompanying the POST back from Cybersource Hosted Order Page
"""
...
...
@@ -161,6 +197,18 @@ def record_purchase(params, order):
processor_reply_dump
=
json
.
dumps
(
params
)
)
def
get_processor_error_html
(
params
):
"""Have to parse through the error codes for all the other cases"""
return
"<p>ERROR!</p>"
def
get_exception_html
(
params
,
exp
):
"""Return error HTML associated with exception"""
return
"<p>EXCEPTION!</p>"
def
get_signature_error_html
(
params
):
"""Return error HTML associated with signature failure"""
return
"<p>EXCEPTION!</p>"
CARDTYPE_MAP
=
defaultdict
(
lambda
:
"UNKNOWN"
)
CARDTYPE_MAP
.
update
(
...
...
lms/djangoapps/shoppingcart/processors/__init__.py
View file @
a4f5f4e4
...
...
@@ -8,8 +8,32 @@ module = __import__('shoppingcart.processors.' + processor_name,
'render_purchase_form_html'
'payment_accepted'
,
'record_purchase'
,
'process_postpay_callback'
,
])
def
render_purchase_form_html
(
*
args
,
**
kwargs
):
"""
The top level call to this module to begin the purchase.
Given a shopping cart,
Renders the HTML form for display on user's browser, which POSTS to Hosted Processors
Returns the HTML as a string
"""
return
module
.
render_purchase_form_html
(
*
args
,
**
kwargs
)
def
process_postpay_callback
(
*
args
,
**
kwargs
):
"""
The top level call to this module after the purchase.
This function is handed the callback request after the customer has entered the CC info and clicked "buy"
on the external payment page.
It is expected to verify the callback and determine if the payment was successful.
It returns {'success':bool, 'order':Order, 'error_html':str}
If successful this function must have the side effect of marking the order purchased and calling the
purchased_callbacks of the cart items.
If unsuccessful this function should not have those side effects but should try to figure out why and
return a helpful-enough error message in error_html.
"""
return
module
.
process_postpay_callback
(
*
args
,
**
kwargs
)
def
sign
(
*
args
,
**
kwargs
):
"""
Given a dict (or OrderedDict) of parameters to send to the
...
...
@@ -30,14 +54,6 @@ def verify(*args, **kwargs):
"""
return
module
.
sign
(
*
args
,
**
kwargs
)
def
render_purchase_form_html
(
*
args
,
**
kwargs
):
"""
Given a shopping cart,
Renders the HTML form for display on user's browser, which POSTS to Hosted Processors
Returns the HTML as a string
"""
return
module
.
render_purchase_form_html
(
*
args
,
**
kwargs
)
def
payment_accepted
(
*
args
,
**
kwargs
):
"""
Given params returned by the CC processor, check that processor has accepted the payment
...
...
lms/djangoapps/shoppingcart/processors/exceptions.py
View file @
a4f5f4e4
...
...
@@ -7,5 +7,5 @@ class CCProcessorException(PaymentException):
class
CCProcessorDataException
(
CCProcessorException
):
pass
class
CCProcessorWrongAmountException
(
Payment
Exception
):
class
CCProcessorWrongAmountException
(
CCProcessor
Exception
):
pass
\ No newline at end of file
lms/djangoapps/shoppingcart/urls.py
View file @
a4f5f4e4
...
...
@@ -6,7 +6,6 @@ urlpatterns = patterns('shoppingcart.views', # nopep8
url
(
r'^add/course/(?P<course_id>[^/]+/[^/]+/[^/]+)/$'
,
'add_course_to_cart'
),
url
(
r'^clear/$'
,
'clear_cart'
),
url
(
r'^remove_item/$'
,
'remove_item'
),
url
(
r'^purchased/$'
,
'purchased'
),
url
(
r'^postpay_accept_callback/$'
,
'postpay_accept_callback'
),
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'
),
)
\ No newline at end of file
lms/djangoapps/shoppingcart/views.py
View file @
a4f5f4e4
import
logging
from
django.
http
import
HttpResponse
,
HttpResponseRedirect
,
Http404
from
django.http
import
HttpResponse
,
HttpResponseRedirect
,
HttpResponseNotFound
,
HttpResponseForbidden
,
Http404
from
django.
utils.translation
import
ugettext
as
_
from
django.core.urlresolvers
import
reverse
from
django.views.decorators.csrf
import
csrf_exempt
from
django.contrib.auth.decorators
import
login_required
from
student.models
import
CourseEnrollment
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
mitxmako.shortcuts
import
render_to_response
from
.models
import
*
from
.processors
import
verify
,
payment_accepted
,
render_purchase_form_html
,
record_purchase
from
.processors.exceptions
import
CCProcessorDataException
,
CCProcessorWrongAmountException
from
.processors
import
process_postpay_callback
,
render_purchase_form_html
log
=
logging
.
getLogger
(
"shoppingcart"
)
...
...
@@ -16,20 +17,22 @@ def test(request, course_id):
item1
.
purchased_callback
(
request
.
user
.
id
)
return
HttpResponse
(
'OK'
)
@login_required
def
purchased
(
request
):
#verify() -- signatures, total cost match up, etc. Need error handling code (
# If verify fails probaly need to display a contact email/number)
cart
=
Order
.
get_cart_for_user
(
request
.
user
)
cart
.
purchase
()
return
HttpResponseRedirect
(
'/'
)
@login_required
def
add_course_to_cart
(
request
,
course_id
):
if
not
request
.
user
.
is_authenticated
():
return
HttpResponseForbidden
(
_
(
'You must be logged-in to add to a shopping cart'
))
cart
=
Order
.
get_cart_for_user
(
request
.
user
)
# TODO: Catch 500 here for course that does not exist, period
PaidCourseRegistration
.
add_to_order
(
cart
,
course_id
,
200
)
return
HttpResponse
(
"Added"
)
if
PaidCourseRegistration
.
part_of_order
(
cart
,
course_id
):
return
HttpResponseNotFound
(
_
(
'The course {0} is already in your cart.'
.
format
(
course_id
)))
if
CourseEnrollment
.
objects
.
filter
(
user
=
request
.
user
,
course_id
=
course_id
)
.
exists
():
return
HttpResponseNotFound
(
_
(
'You are already registered in course {0}.'
.
format
(
course_id
)))
try
:
PaidCourseRegistration
.
add_to_order
(
cart
,
course_id
)
except
ItemNotFoundError
:
return
HttpResponseNotFound
(
_
(
'The course you requested does not exist.'
))
if
request
.
method
==
'GET'
:
return
HttpResponseRedirect
(
reverse
(
'shoppingcart.views.show_cart'
))
return
HttpResponse
(
_
(
"Course added to cart."
))
@login_required
def
show_cart
(
request
):
...
...
@@ -62,31 +65,23 @@ def remove_item(request):
return
HttpResponse
(
'OK'
)
@csrf_exempt
def
postpay_
accept_
callback
(
request
):
def
postpay_callback
(
request
):
"""
Receives the POST-back from processor and performs the validation and displays a receipt
and does some other stuff
HANDLES THE ACCEPT AND REVIEW CASES
Receives the POST-back from processor.
Mainly this calls the processor-specific code to check if the payment was accepted, and to record the order
if it was, and to generate an error page.
If successful this function should have the side effect of changing the "cart" into a full "order" in the DB.
The cart can then render a success page which links to receipt pages.
If unsuccessful the order will be left untouched and HTML messages giving more detailed error info will be
returned.
"""
# TODO: Templates and logic for all error cases and the REVIEW CASE
params
=
request
.
POST
.
dict
()
if
verify
(
params
):
try
:
result
=
payment_accepted
(
params
)
if
result
[
'accepted'
]:
# ACCEPTED CASE first
record_purchase
(
params
,
result
[
'order'
])
#render_receipt
result
=
process_postpay_callback
(
request
)
if
result
[
'success'
]:
return
HttpResponseRedirect
(
reverse
(
'shoppingcart.views.show_receipt'
,
args
=
[
result
[
'order'
]
.
id
]))
else
:
return
HttpResponse
(
"CC Processor has not accepted the payment."
)
except
CCProcessorWrongAmountException
:
return
HttpResponse
(
"Charged the wrong amount, contact our user support"
)
except
CCProcessorDataException
:
return
HttpResponse
(
"Exception: the processor returned invalid data"
)
else
:
return
HttpResponse
(
"There has been a communication problem blah blah. Not Validated"
)
return
render_to_response
(
'shoppingcart.processor_error.html'
,
{
'order'
:
result
[
'order'
],
'error_html'
:
result
[
'error_html'
]})
def
show_receipt
(
request
,
ordernum
):
"""
...
...
@@ -107,7 +102,7 @@ def show_receipt(request, ordernum):
'order_items'
:
order_items
,
'any_refunds'
:
any_refunds
})
def
show_orders
(
request
):
#
def show_orders(request):
"""
Displays all orders of a user
"""
lms/templates/courseware/course_about.html
View file @
a4f5f4e4
...
...
@@ -59,7 +59,6 @@
%
endif
})(
this
)
</script>
...
...
@@ -93,6 +92,7 @@
<strong>
${_("View Courseware")}
</strong>
</a>
%endif
%else:
<a
href=
"#"
class=
"register"
>
${_("Register for {course.display_number_with_default}").format(course=course) | h}
</a>
...
...
lms/templates/shoppingcart/list.html
View file @
a4f5f4e4
...
...
@@ -25,7 +25,7 @@
</tbody>
</table>
<!-- <input id="back_input" type="submit" value="Return" /> -->
${form_html}
% else:
<p>
${_("You have selected no items for purchase.")}
</p>
...
...
@@ -44,6 +44,10 @@
location
.
reload
(
true
);
});
});
$
(
'#back_input'
).
click
(
function
(){
history
.
back
();
});
});
</script>
lms/templates/shoppingcart/receipt.html
View file @
a4f5f4e4
...
...
@@ -6,7 +6,11 @@
<
%
block
name=
"title"
><title>
${_("Receipt for Order")} ${order.id}
</title></
%
block>
% if notification is not UNDEFINED:
<section
class=
"notification"
>
${notification}
</section>
% endif
<section
class=
"container cart-list"
>
<p><h1>
${_(settings.PLATFORM_NAME + " (" + settings.SITE_NAME + ")" + " Electronic Receipt")}
</h1></p>
...
...
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