added shopping cart list template, embedded form
This commit is contained in:
0
lms/djangoapps/shoppingcart/__init__.py
Normal file
0
lms/djangoapps/shoppingcart/__init__.py
Normal file
68
lms/djangoapps/shoppingcart/inventory_types.py
Normal file
68
lms/djangoapps/shoppingcart/inventory_types.py
Normal file
@@ -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)])
|
||||
3
lms/djangoapps/shoppingcart/models.py
Normal file
3
lms/djangoapps/shoppingcart/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
16
lms/djangoapps/shoppingcart/tests.py
Normal file
16
lms/djangoapps/shoppingcart/tests.py
Normal file
@@ -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)
|
||||
9
lms/djangoapps/shoppingcart/urls.py
Normal file
9
lms/djangoapps/shoppingcart/urls.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.conf.urls import patterns, include, url
|
||||
|
||||
urlpatterns = patterns('shoppingcart.views', # nopep8
|
||||
url(r'^$','show_cart'),
|
||||
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/$','test'),
|
||||
url(r'^add/(?P<course_id>[^/]+/[^/]+/[^/]+)/$','add_course_to_cart'),
|
||||
url(r'^clear/$','clear_cart'),
|
||||
url(r'^remove_item/$', 'remove_item'),
|
||||
)
|
||||
91
lms/djangoapps/shoppingcart/views.py
Normal file
91
lms/djangoapps/shoppingcart/views.py
Normal file
@@ -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
|
||||
46
lms/templates/shoppingcart/list.html
Normal file
46
lms/templates/shoppingcart/list.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
|
||||
<%inherit file="../main.html" />
|
||||
|
||||
<%block name="title"><title>${_("Your Shopping Cart")}</title></%block>
|
||||
|
||||
<section class="container cart-list">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><td>Qty</td><td>Description</td><td>Unit Price</td><td>Price</td></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for idx,item in enumerate(shoppingcart_items):
|
||||
<tr><td>${item.qty}</td><td>${item.line_desc}</td><td>${item.unit_cost}</td><td>${item.line_cost}</td>
|
||||
<td><a data-item-idx="${idx}" class='remove_line_item' href='#'>[x]</a></td></tr>
|
||||
% endfor
|
||||
<tr><td></td><td></td><td></td><td>Total Cost</td></tr>
|
||||
<tr><td></td><td></td><td></td><td>${total_cost}</td></tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form action="https://orderpagetest.ic3.com/hop/orderform.jsp" method="post">
|
||||
% for pk, pv in params.iteritems():
|
||||
<input type="hidden" name="${pk}" value="${pv}" />
|
||||
% endfor
|
||||
<input type="submit" value="Check Out" />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$('a.remove_line_item').click(function(event) {
|
||||
event.preventDefault();
|
||||
var post_url = "${reverse('shoppingcart.views.remove_item')}";
|
||||
$.post(post_url, {idx:$(this).data('item-idx')})
|
||||
.always(function(data){
|
||||
location.reload(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -379,7 +379,7 @@ if settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD'):
|
||||
|
||||
# Shopping cart
|
||||
urlpatterns += (
|
||||
url(r'^shoppingcarttest/(?P<course_id>[^/]+/[^/]+/[^/]+)/$','shoppingcart.views.test'),
|
||||
url(r'^shoppingcart/', include('shoppingcart.urls')),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user