From 9e56028091fc9f4819ef7c3072c9bf5d1b05e467 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Wed, 7 Aug 2013 22:53:36 -0700 Subject: [PATCH] added shopping cart list template, embedded form --- lms/djangoapps/shoppingcart/__init__.py | 0 .../shoppingcart/inventory_types.py | 68 ++++++++++++++ lms/djangoapps/shoppingcart/models.py | 3 + lms/djangoapps/shoppingcart/tests.py | 16 ++++ lms/djangoapps/shoppingcart/urls.py | 9 ++ lms/djangoapps/shoppingcart/views.py | 91 +++++++++++++++++++ lms/templates/shoppingcart/list.html | 46 ++++++++++ lms/urls.py | 2 +- 8 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 lms/djangoapps/shoppingcart/__init__.py create mode 100644 lms/djangoapps/shoppingcart/inventory_types.py create mode 100644 lms/djangoapps/shoppingcart/models.py create mode 100644 lms/djangoapps/shoppingcart/tests.py create mode 100644 lms/djangoapps/shoppingcart/urls.py create mode 100644 lms/djangoapps/shoppingcart/views.py create mode 100644 lms/templates/shoppingcart/list.html diff --git a/lms/djangoapps/shoppingcart/__init__.py b/lms/djangoapps/shoppingcart/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/shoppingcart/inventory_types.py b/lms/djangoapps/shoppingcart/inventory_types.py new file mode 100644 index 0000000000..0230760cb5 --- /dev/null +++ b/lms/djangoapps/shoppingcart/inventory_types.py @@ -0,0 +1,68 @@ +import logging +from django.contrib.auth.models import User +from student.views import course_from_id +from student.models import CourseEnrollmentAllowed, CourseEnrollment +from statsd import statsd + +log = logging.getLogger("shoppingcart") + +class InventoryItem(object): + """ + This is the abstract interface for inventory items. + Inventory items are things that fill up the shopping cart. + + Each implementation of InventoryItem should have purchased_callback as + a method and data attributes as defined in __init__ below + """ + def __init__(self): + # Set up default data attribute values + self.qty = 1 + self.unit_cost = 0 # in dollars + self.line_cost = 0 # qty * unit_cost + self.line_desc = "Misc Item" + + def purchased_callback(self, user_id): + """ + This is called on each inventory item in the shopping cart when the + purchase goes through. The parameter provided is the id of the user who + made the purchase. + """ + raise NotImplementedError + + +class PaidCourseRegistration(InventoryItem): + """ + This is an inventory item for paying for a course registration + """ + def __init__(self, course_id, unit_cost): + course = course_from_id(course_id) # actually fetch the course to make sure it exists, use this to + # throw errors if it doesn't + self.qty = 1 + self.unit_cost = unit_cost + self.line_cost = unit_cost + self.course_id = course_id + self.line_desc = "Registration for Course {0}".format(course_id) + + def purchased_callback(self, user_id): + """ + When purchased, this should enroll the user in the course. We are assuming that + course settings for enrollment date are configured such that only if the (user.email, course_id) pair is found in + CourseEnrollmentAllowed will the user be allowed to enroll. Otherwise requiring payment + would in fact be quite silly since there's a clear back door. + """ + user = User.objects.get(id=user_id) + course = course_from_id(self.course_id) # actually fetch the course to make sure it exists, use this to + # 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=user.email, course_id=self.course_id, auto_enroll=True) + CourseEnrollment.objects.get_or_create(user=user, course_id=self.course_id) + + log.info("Enrolled {0} in paid course {1}, paid ${2}".format(user.email, self.course_id, self.line_cost)) + org, course_num, run = self.course_id.split("/") + statsd.increment("shoppingcart.PaidCourseRegistration.purchased_callback.enrollment", + tags=["org:{0}".format(org), + "course:{0}".format(course_num), + "run:{0}".format(run)]) diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py new file mode 100644 index 0000000000..71a8362390 --- /dev/null +++ b/lms/djangoapps/shoppingcart/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/lms/djangoapps/shoppingcart/tests.py b/lms/djangoapps/shoppingcart/tests.py new file mode 100644 index 0000000000..501deb776c --- /dev/null +++ b/lms/djangoapps/shoppingcart/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/lms/djangoapps/shoppingcart/urls.py b/lms/djangoapps/shoppingcart/urls.py new file mode 100644 index 0000000000..47bd3c4c3d --- /dev/null +++ b/lms/djangoapps/shoppingcart/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import patterns, include, url + +urlpatterns = patterns('shoppingcart.views', # nopep8 + url(r'^$','show_cart'), + url(r'^(?P[^/]+/[^/]+/[^/]+)/$','test'), + url(r'^add/(?P[^/]+/[^/]+/[^/]+)/$','add_course_to_cart'), + url(r'^clear/$','clear_cart'), + url(r'^remove_item/$', 'remove_item'), +) \ No newline at end of file diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py new file mode 100644 index 0000000000..e7d09e18b7 --- /dev/null +++ b/lms/djangoapps/shoppingcart/views.py @@ -0,0 +1,91 @@ +import logging +import random +import time +import hmac +import binascii +from hashlib import sha1 + +from collections import OrderedDict +from django.http import HttpResponse +from django.contrib.auth.decorators import login_required +from mitxmako.shortcuts import render_to_response +from .inventory_types import * + +log = logging.getLogger("shoppingcart") + + +def test(request, course_id): + item1 = PaidCourseRegistration(course_id, 200) + item1.purchased_callback(request.user.id) + return HttpResponse('OK') + +@login_required +def add_course_to_cart(request, course_id): + cart = request.session.get('shopping_cart', []) + course_ids_in_cart = [i.course_id for i in cart if isinstance(i, PaidCourseRegistration)] + if course_id not in course_ids_in_cart: + # TODO: Catch 500 here for course that does not exist, period + item = PaidCourseRegistration(course_id, 200) + cart.append(item) + request.session['shopping_cart'] = cart + return HttpResponse('Added') + else: + return HttpResponse("Item exists, not adding") + +@login_required +def show_cart(request): + cart = request.session.get('shopping_cart', []) + total_cost = "{0:0.2f}".format(sum([i.line_cost for i in cart])) + params = OrderedDict() + params['amount'] = total_cost + params['currency'] = 'usd' + params['orderPage_transactionType'] = 'sale' + params['orderNumber'] = "{0:d}".format(random.randint(1, 10000)) + signed_param_dict = cybersource_sign(params) + return render_to_response("shoppingcart/list.html", + {'shoppingcart_items': cart, + 'total_cost': total_cost, + 'params': signed_param_dict, + }) + +@login_required +def clear_cart(request): + request.session['shopping_cart'] = [] + return HttpResponse('Cleared') + +@login_required +def remove_item(request): + # doing this with indexes to replicate the function that generated the list on the HTML page + item_idx = request.REQUEST.get('idx', 'blank') + try: + cart = request.session.get('shopping_cart', []) + cart.pop(int(item_idx)) + request.session['shopping_cart'] = cart + except IndexError, ValueError: + log.exception('Cannot remove element at index {0} from cart'.format(item_idx)) + return HttpResponse('OK') + + +def cybersource_sign(params): + """ + params needs to be an ordered dict, b/c cybersource documentation states that order is important. + Reverse engineered from PHP version provided by cybersource + """ + shared_secret = "ELIDED" + merchant_id = "ELIDED" + serial_number = "ELIDED" + orderPage_version = "7" + params['merchantID'] = merchant_id + params['orderPage_timestamp'] = int(time.time()*1000) + params['orderPage_version'] = orderPage_version + params['orderPage_serialNumber'] = serial_number + fields = ",".join(params.keys()) + values = ",".join(["{0}={1}".format(i,params[i]) for i in params.keys()]) + fields_hash_obj = hmac.new(shared_secret, fields, sha1) + fields_sig = binascii.b2a_base64(fields_hash_obj.digest())[:-1] # last character is a '\n', which we don't want + values += ",signedFieldsPublicSignature=" + fields_sig + values_hash_obj = hmac.new(shared_secret, values, sha1) + params['orderPage_signaturePublic'] = binascii.b2a_base64(values_hash_obj.digest())[:-1] + params['orderPage_signedFields'] = fields + + return params \ No newline at end of file diff --git a/lms/templates/shoppingcart/list.html b/lms/templates/shoppingcart/list.html new file mode 100644 index 0000000000..f3fd26c96b --- /dev/null +++ b/lms/templates/shoppingcart/list.html @@ -0,0 +1,46 @@ +<%! from django.utils.translation import ugettext as _ %> + +<%! from django.core.urlresolvers import reverse %> + +<%inherit file="../main.html" /> + +<%block name="title">${_("Your Shopping Cart")} + +
+ + + + + + % for idx,item in enumerate(shoppingcart_items): + + + % endfor + + + + +
QtyDescriptionUnit PricePrice
${item.qty}${item.line_desc}${item.unit_cost}${item.line_cost}[x]
Total Cost
${total_cost}
+ +
+ % for pk, pv in params.iteritems(): + + % endfor + +
+
+ + + + diff --git a/lms/urls.py b/lms/urls.py index 9034683556..53665f9ef6 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -379,7 +379,7 @@ if settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD'): # Shopping cart urlpatterns += ( - url(r'^shoppingcarttest/(?P[^/]+/[^/]+/[^/]+)/$','shoppingcart.views.test'), + url(r'^shoppingcart/', include('shoppingcart.urls')), )