Last active
May 8, 2019 15:05
-
-
Save lucadealfaro/d492317c6a47ba1045ff6d16bb543a15 to your computer and use it in GitHub Desktop.
A shopping cart implementation using vue.js, stripe, and web2py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
# this file is released under public domain and you can use without limitations | |
# ------------------------------------------------------------------------- | |
# Sample shopping cart implementation. | |
# ------------------------------------------------------------------------- | |
import traceback | |
def index(): | |
""" | |
I am not doing anything here. Look elsewhere. | |
""" | |
return dict() | |
def get_products(): | |
"""Gets the list of products, possibly in response to a query.""" | |
t = request.vars.q.strip() | |
if request.vars.q: | |
q = ((db.product.name.contains(t)) | | |
(db.product.description.contains(t))) | |
else: | |
q = db.product.id > 0 | |
products = db(q).select(db.product.ALL) | |
# Fixes some fields, to make it easy on the client side. | |
for p in products: | |
p.image_url = URL('download', p.image) | |
p.desired_quantity = min(1, p.quantity) | |
p.cart_quantity = 0 | |
return response.json(dict( | |
products=products, | |
)) | |
def purchase(): | |
"""Ajax function called when a customer orders and pays for the cart.""" | |
if not URL.verify(request, hmac_key=session.hmac_key): | |
raise HTTP(500) | |
# Creates the charge. | |
import stripe | |
# Your secret key. | |
stripe.api_key = "sk_test_something" | |
token = json.loads(request.vars.transaction_token) | |
amount = float(request.vars.amount) | |
try: | |
charge = stripe.Charge.create( | |
amount=int(amount * 100), | |
currency="usd", | |
source=token['id'], | |
description="Purchase", | |
) | |
except stripe.error.CardError as e: | |
logger.info("The card has been declined.") | |
logger.info("%r" % traceback.format_exc()) | |
return "nok" | |
db.customer_order.insert( | |
customer_info=request.vars.customer_info, | |
transaction_token=json.dumps(token), | |
cart=request.vars.cart) | |
return "ok" | |
# Normally here we would check that the user is an admin, and do programmatic | |
# APIs to add and remove products to the inventory, etc. | |
@auth.requires_login() | |
def product_management(): | |
q = db.product # This queries for all products. | |
form = SQLFORM.grid( | |
q, | |
editable=True, | |
create=True, | |
user_signature=True, | |
deletable=True, | |
fields=[db.product.product_name, db.product.quantity, db.product.price, | |
db.product.image], | |
details=True, | |
) | |
return dict(form=form) | |
@auth.requires_login() | |
def view_orders(): | |
q = db.customer_order # This queries for all products. | |
db.customer_order.customer_info.represent = lambda v, r: nicefy(v) | |
db.customer_order.transaction_token.represent = lambda v, r: nicefy(v) | |
db.customer_order.cart.represent = lambda v, r: nicefy(v) | |
form = SQLFORM.grid( | |
q, | |
editable=True, | |
create=True, | |
user_signature=True, | |
deletable=True, | |
details=True, | |
) | |
return dict(form=form) | |
def user(): | |
""" | |
exposes: | |
http://..../[app]/default/user/login | |
http://..../[app]/default/user/logout | |
http://..../[app]/default/user/register | |
http://..../[app]/default/user/profile | |
http://..../[app]/default/user/retrieve_password | |
http://..../[app]/default/user/change_password | |
http://..../[app]/default/user/bulk_register | |
use @auth.requires_login() | |
@auth.requires_membership('group name') | |
@auth.requires_permission('read','table name',record_id) | |
to decorate functions that need access control | |
also notice there is http://..../[app]/appadmin/manage/auth to allow administrator to manage users | |
""" | |
return dict(form=auth()) | |
@cache.action() | |
def download(): | |
""" | |
allows downloading of uploaded files | |
http://..../[app]/default/download/[filename] | |
""" | |
return response.download(request, db) | |
def call(): | |
""" | |
exposes services. for example: | |
http://..../[app]/default/call/jsonrpc | |
decorate with @services.jsonrpc the functions to expose | |
supports xml, json, xmlrpc, jsonrpc, amfrpc, rss, csv | |
""" | |
return service() | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is the js for the default/index.html view. | |
var app = function() { | |
var self = {}; | |
Vue.config.silent = false; // show all warnings | |
// Extends an array | |
self.extend = function(a, b) { | |
for (var i = 0; i < b.length; i++) { | |
a.push(b[i]); | |
} | |
}; | |
// Enumerates an array. | |
var enumerate = function(v) { | |
var k=0; | |
return v.map(function(e) {e._idx = k++;}); | |
}; | |
self.get_products = function () { | |
// Gets new products in response to a query, or to an initial page load. | |
$.getJSON(products_url, $.param({q: self.vue.product_search}), function(data) { | |
self.vue.products = data.products; | |
enumerate(self.vue.products); | |
}); | |
}; | |
self.store_cart = function() { | |
localStorage.cart = JSON.stringify(self.vue.cart); | |
}; | |
self.read_cart = function() { | |
if (localStorage.cart) { | |
self.vue.cart = JSON.parse(localStorage.cart); | |
} else { | |
self.vue.cart = []; | |
} | |
self.update_cart(); | |
}; | |
self.inc_desired_quantity = function(product_idx, qty) { | |
// Inc and dec to desired quantity. | |
var p = self.vue.products[product_idx]; | |
p.desired_quantity = Math.max(0, p.desired_quantity + qty); | |
p.desired_quantity = Math.min(p.quantity, p.desired_quantity); | |
}; | |
self.inc_cart_quantity = function(product_idx, qty) { | |
// Inc and dec to desired quantity. | |
var p = self.vue.cart[product_idx]; | |
p.cart_quantity = Math.max(0, p.cart_quantity + qty); | |
p.cart_quantity = Math.min(p.quantity, p.cart_quantity); | |
self.update_cart(); | |
self.store_cart(); | |
}; | |
self.update_cart = function () { | |
enumerate(self.vue.cart); | |
var cart_size = 0; | |
var cart_total = 0; | |
for (var i = 0; i < self.vue.cart.length; i++) { | |
var q = self.vue.cart[i].cart_quantity; | |
if (q > 0) { | |
cart_size++; | |
cart_total += q * self.vue.cart[i].price; | |
} | |
} | |
self.vue.cart_size = cart_size; | |
self.vue.cart_total = cart_total; | |
}; | |
self.buy_product = function(product_idx) { | |
var p = self.vue.products[product_idx]; | |
// I need to put the product in the cart. | |
// Check if it is already there. | |
var already_present = false; | |
var found_idx = 0; | |
for (var i = 0; i < self.vue.cart.length; i++) { | |
if (self.vue.cart[i].id === p.id) { | |
already_present = true; | |
found_idx = i; | |
} | |
} | |
// If it's there, just replace the quantity; otherwise, insert it. | |
if (!already_present) { | |
found_idx = self.vue.cart.length; | |
self.vue.cart.push(p); | |
} | |
self.vue.cart[found_idx].cart_quantity = p.desired_quantity; | |
// Updates the amount of products in the cart. | |
self.update_cart(); | |
self.store_cart(); | |
}; | |
self.customer_info = {} | |
self.goto = function (page) { | |
self.vue.page = page; | |
if (page == 'cart') { | |
// prepares the form. | |
self.stripe_instance = StripeCheckout.configure({ | |
key: 'pk_test_CeE2VVxAs3MWCUDMQpWe8KcX', //put your own publishable key here | |
image: 'https://stripe.com/img/documentation/checkout/marketplace.png', | |
locale: 'auto', | |
token: function(token, args) { | |
console.log('got a token. sending data to localhost.'); | |
self.stripe_token = token; | |
self.customer_info = args; | |
self.send_data_to_server(); | |
} | |
}); | |
}; | |
}; | |
self.pay = function () { | |
self.stripe_instance.open({ | |
name: "Your nice cart", | |
description: "Buy cart content", | |
billingAddress: true, | |
shippingAddress: true, | |
amount: Math.round(self.vue.cart_total * 100), | |
}); | |
}; | |
self.send_data_to_server = function () { | |
console.log("Payment for:", self.customer_info); | |
// Calls the server. | |
$.post(purchase_url, | |
{ | |
customer_info: JSON.stringify(self.customer_info), | |
transaction_token: JSON.stringify(self.stripe_token), | |
amount: self.vue.cart_total, | |
cart: JSON.stringify(self.vue.cart), | |
}, | |
function (data) { | |
// The order was successful. | |
self.vue.cart = []; | |
self.update_cart(); | |
self.store_cart(); | |
self.goto('prod'); | |
$.web2py.flash("Thank you for your purchase"); | |
} | |
); | |
}; | |
self.vue = new Vue({ | |
el: "#vue-div", | |
delimiters: ['${', '}'], | |
unsafeDelimiters: ['!{', '}'], | |
data: { | |
products: [], | |
cart: [], | |
product_search: '', | |
cart_size: 0, | |
cart_total: 0, | |
page: 'prod' | |
}, | |
methods: { | |
get_products: self.get_products, | |
inc_desired_quantity: self.inc_desired_quantity, | |
inc_cart_quantity: self.inc_cart_quantity, | |
buy_product: self.buy_product, | |
goto: self.goto, | |
do_search: self.get_products, | |
pay: self.pay | |
} | |
}); | |
self.get_products(); | |
self.read_cart(); | |
$("#vue-div").show(); | |
return self; | |
}; | |
var APP = null; | |
// This will make everything accessible from the js console; | |
// for instance, self.x above would be accessible as APP.x | |
jQuery(function(){APP = app();}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{{extend 'layout.html'}} | |
{{block head}} | |
<script src="{{=URL('static', 'js/vue.js')}}"></script> | |
<script src="https://checkout.stripe.com/checkout.js"></script> | |
<script> | |
var products_url = "{{=URL('default', 'get_products')}}"; | |
var purchase_url = "{{=URL('default', 'purchase', hmac_key=session.hmac_key)}}" | |
</script> | |
{{end}} | |
<div class="main_content"> | |
<div id="vue-div" style="display:none"> | |
<div class="control_bar container"> | |
<div class="search_div threequarters"> | |
<span v-if="page=='prod'"> | |
<input class="search_input" v-model="product_search"/> | |
<button class="btn" v-on:click="do_search"><i class="fa fa-search fa-lg"></i></button> | |
</span> | |
<span v-if="page=='cart'" class="page_title"><i class="fa fa-shopping-cart"></i> Your Shopping Cart</span> | |
</div> | |
<div class="shopping_button quarter"> | |
<span v-if="page=='prod'"> | |
<button class="btn orange" v-on:click="goto('cart')"> | |
<i class="fa fa-lg fa-shopping-cart"></i> ${cart_size} | |
</button> | |
</span> | |
<span v-if="page=='cart'"> | |
<button class="btn" v-on:click="goto('prod')"> Continue shopping </button> | |
</span> | |
</div> | |
</div> | |
<div v-if="page=='prod'" id="products_list"> | |
<div v-for="product in products" class="container"> | |
<div class="third prod_image"> | |
<img v-bind:src="product.image_url" width="100%" class="product_image"/> | |
</div> | |
<div class="twothirds product_info"> | |
<div class="product_name"><h2>${product.product_name}</h2></div> | |
<div class="product_quantity_price"> | |
<span class="product_price">$ ${product.price}</span> | |
<span class="product_quantity">Quantity in stock: ${product.quantity}</span> | |
<span class="buy_buttons"> | |
<button class="btn btn-secondary" v-on:click="inc_desired_quantity(product._idx, 1)"><i class="fa fa-plus"></i></button> | |
<span class="desired_quantity">${product.desired_quantity}</span> | |
<button class="btn btn-secondary" v-on:click="inc_desired_quantity(product._idx, -1)"><i class="fa fa-minus"></i></button> | |
<button class="btn red" v-on:click="buy_product(product._idx)"><i class="fa fa-lg fa-shopping-cart"></i> Buy</button> | |
</span> | |
</div> | |
<div class="product_description"> | |
<p>${product.description}</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div v-if="page=='cart'" id="cart_list"> | |
<div v-if="cart.length == 0" class="container"> | |
<div class="cart_empty_msg"> | |
Your cart is empty | |
</div> | |
</div> | |
<div v-for="product in cart" class="container"> | |
<div class="col-md-3 prod_image third"> | |
<img v-bind:src="product.image_url" width="100%" class="product_image"/> | |
</div> | |
<div class="col-md-10 product_info twothirds"> | |
<div class="product_name"><h2>${product.product_name}</h2></div> | |
<div class="product_quantity_price"> | |
<span class="product_price">$ ${product.price}</span> | |
<span class="product_quantity">Quantity in stock: ${product.quantity}</span> | |
<span class="buy_buttons"> | |
<button class="btn btn-secondary" v-on:click="inc_cart_quantity(product._idx, 1)"><i class="fa fa-plus"></i></button> | |
<span class="desired_quantity">${product.cart_quantity}</span> | |
<button class="btn btn-secondary" v-on:click="inc_cart_quantity(product._idx, -1)"><i class="fa fa-minus"></i></button> | |
</span> | |
</div> | |
<div class="product_description"> | |
<p>${product.description}</p> | |
</div> | |
</div> | |
</div> | |
<div v-if="cart.length > 0" class="total_price"> | |
Your total price: $ ${cart_total} | |
<button class="btn blue" v-on:click="pay()"><i class="fa fa-lg fa-credit-card"></i> Pay</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script src="{{=URL('static', 'js/default_index.js')}}"></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Define your tables below (or better in another model file) for example | |
# | |
# >>> db.define_table('mytable', Field('myfield', 'string')) | |
# | |
# Fields can be 'string','text','password','integer','double','boolean' | |
# 'date','time','datetime','blob','upload', 'reference TABLENAME' | |
# There is an implicit 'id integer autoincrement' field | |
# Consult manual for more options, validators, etc. | |
import datetime | |
# Product table. | |
db.define_table('product', | |
Field('product_name'), | |
Field('quantity', 'integer'), | |
Field('price', 'float'), | |
Field('image', 'upload'), | |
Field('description', 'text'), | |
) | |
db.product.id.readable = db.product.id.writable = False | |
db.define_table('customer_order', | |
Field('order_date', default=datetime.datetime.utcnow()), | |
Field('customer_info', 'blob'), | |
Field('transaction_token', 'blob'), | |
Field('cart', 'blob'), | |
) | |
# Let's define a secret key for stripe transactions. | |
from gluon.utils import web2py_uuid | |
if session.hmac_key is None: | |
session.hmac_key = web2py_uuid() | |
# after defining tables, uncomment below to enable auditing | |
# auth.enable_record_versioning(db) | |
import json | |
def nicefy(b): | |
if b is None: | |
return 'None' | |
obj = json.loads(b) | |
s = json.dumps(obj, indent=2) | |
return s |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment