diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 57b13c10b3..caf731a392 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -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 diff --git a/common/static/js/capa/spec/jsinput_spec.js b/common/static/js/capa/spec/jsinput_spec.js deleted file mode 100644 index a4a4f6e57d..0000000000 --- a/common/static/js/capa/spec/jsinput_spec.js +++ /dev/null @@ -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); - } - }); - }); -}) diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index 052ecfb888..acc0545ab7 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -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', +} \ No newline at end of file diff --git a/lms/djangoapps/shoppingcart/processors/CyberSource.py b/lms/djangoapps/shoppingcart/processors/CyberSource.py index 17e1511ac6..75ad754237 100644 --- a/lms/djangoapps/shoppingcart/processors/CyberSource.py +++ b/lms/djangoapps/shoppingcart/processors/CyberSource.py @@ -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 "
ERROR!
" + +def get_exception_html(params, exp): + """Return error HTML associated with exception""" + return "EXCEPTION!
" + +def get_signature_error_html(params): + """Return error HTML associated with signature failure""" + return "EXCEPTION!
" + CARDTYPE_MAP = defaultdict(lambda:"UNKNOWN") CARDTYPE_MAP.update( diff --git a/lms/djangoapps/shoppingcart/processors/__init__.py b/lms/djangoapps/shoppingcart/processors/__init__.py index 520c353535..45a6e3114d 100644 --- a/lms/djangoapps/shoppingcart/processors/__init__.py +++ b/lms/djangoapps/shoppingcart/processors/__init__.py @@ -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 diff --git a/lms/djangoapps/shoppingcart/processors/exceptions.py b/lms/djangoapps/shoppingcart/processors/exceptions.py index bc132a3d54..e863688133 100644 --- a/lms/djangoapps/shoppingcart/processors/exceptions.py +++ b/lms/djangoapps/shoppingcart/processors/exceptions.py @@ -7,5 +7,5 @@ class CCProcessorException(PaymentException): class CCProcessorDataException(CCProcessorException): pass -class CCProcessorWrongAmountException(PaymentException): +class CCProcessorWrongAmountException(CCProcessorException): pass \ No newline at end of file diff --git a/lms/djangoapps/shoppingcart/urls.py b/lms/djangoapps/shoppingcart/urls.py index 58e51f0b40..892c66d5bb 100644 --- a/lms/djangoapps/shoppingcart/urls.py +++ b/lms/djangoapps/shoppingcart/urls.py @@ -6,7 +6,6 @@ urlpatterns = patterns('shoppingcart.views', # nopep8 url(r'^add/course/(?P${_("You have selected no items for purchase.")}
@@ -44,6 +44,10 @@ location.reload(true); }); }); + + $('#back_input').click(function(){ + history.back(); + }); }); diff --git a/lms/templates/shoppingcart/receipt.html b/lms/templates/shoppingcart/receipt.html index 01de3b3f83..0386b6b353 100644 --- a/lms/templates/shoppingcart/receipt.html +++ b/lms/templates/shoppingcart/receipt.html @@ -6,7 +6,11 @@ <%block name="title">