about page changes, refactor processor reply handling
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -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 classname, lc_classname in ORDER_ITEM_SUBTYPES:
|
||||
for cls, 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',
|
||||
}
|
||||
@@ -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 CCProcessorException, CCProcessorDataException, 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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,5 +7,5 @@ class CCProcessorException(PaymentException):
|
||||
class CCProcessorDataException(CCProcessorException):
|
||||
pass
|
||||
|
||||
class CCProcessorWrongAmountException(PaymentException):
|
||||
class CCProcessorWrongAmountException(CCProcessorException):
|
||||
pass
|
||||
@@ -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'),
|
||||
)
|
||||
@@ -1,13 +1,14 @@
|
||||
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
|
||||
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")
|
||||
result = process_postpay_callback(request)
|
||||
if result['success']:
|
||||
return HttpResponseRedirect(reverse('shoppingcart.views.show_receipt', args=[result['order'].id]))
|
||||
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
|
||||
"""
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user