Skip to content

Instantly share code, notes, and snippets.

@lucadealfaro
Last active May 8, 2019 15:05
Show Gist options
  • Save lucadealfaro/d492317c6a47ba1045ff6d16bb543a15 to your computer and use it in GitHub Desktop.
Save lucadealfaro/d492317c6a47ba1045ff6d16bb543a15 to your computer and use it in GitHub Desktop.
A shopping cart implementation using vue.js, stripe, and web2py
# -*- 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 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();});
{{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>
# 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