Hello once again!
It’s been a while since we last saw each other. Ha ha. Check out that front look! Those are some gorgeous images! Anyway, I’ve got some exciting news but first, lets recap or catch-up.
In part 3 of this series, we saw how to manage users for our app. Specifically we looked into how to create users, blueprints and admins to add backend content to our app. We also experimented a bit with various styles.
The app looks pretty neat on the desktop as well as on mobile screens.
Now the exciting stuff;
- Managing credentials for development purposes
Migrations
,migrations
,migrations
- Adding
yummy
food images to our app Checkout!
comes to our appCreating apart 5blog
Blueprint
Your app should look gorgeous by the end of this post.
Managing credentials for development
In part 2 I suggested we use pydotenv to manage our app secrets, things like the database
password
and so on;
import pydotenv
env = pydotenv.Environment()
SQLALCHEMY_DATABASE_URI = 'postgresql://username:'+env['DB_PASSWORD']+'@localhost:5432/raj_db'
However, there was a problem, my app was not connecting to my database - raj_db
.
I openned up a python shell to test the pydotenv
package and sure enough it didn’t work for mypassword.
I had a number of options;
- Change my database password to one that works with pydotenv.
- Fork
pydotenv
and fix and use it instead of the original one.
I opted to go with option 2.
So I headed over to the git
repository of the pydotenv
package to find our why my password was being split.
This is what I found;
This is the function that parses each line from the .env
file. The code for the parser is wrong!
That is if your password contains the #
symbol.
Secondly, the author doesn’t warn users about this shortcoming. If you’re stuck here it’s better to add a comment stating that the parser does not work for passwords containing the #
symbol.
The solution to me was obvious, replace the #
with a single space.
Of course I had to fork
the original project to do this since the project itself appears to be abandoned. I commited my changes and pushed the fix to my fork
of pydotenv
. I didn’t really have time to do a pull request because I didn’t think my fix was robust enough for people whose passwords might contain spaces.
Incorporating our solution
After testing that the fix worked, it was time to import my own version of pydotenv
.
I did this by generating a requirements.txt
file;(pyenv) pip freeze -r > requirements.txt
Then I opened requirements.txt
and replaced pydotenv
with git+https://github.com/lehbyte/pydotenv.git#egg=pydotenv
Finally, I then ran; (pyenv) pip install -r requirements.txt
and my app was finally able to connect to my database raj.db
.
Of course all of this would have been done by simply navigating to the pydotenv
package within my virtualenv
pyenv
and applying the fix there but then I wouldn’t be able to do a pull request and you wouldn’t be able to download my fix to use. :(
Modifying config.py
Ok since pydotenv
works, it’s time to update our config.py
;
import os
import pydotenv
env = pydotenv.Environment()
basedir = os.path.abspath(os.dirname(__file__))
FLASK_ADMIN_SWATCH = 'cerulean'
TEMPLATES_AUTO_RELOAD = True
SQLALCHEMY_DATABASE_URI = 'postgresql://'+env['USER']+':'+env['PASS']+'@localhost:'+env['PORT']+'/'+env['DB']
SQLACHEMY_TRACK_MODIFICATIONS = False
WTF_CSRF_ENABLED=True
SECRET_KEY='mysecretkey'
.env
FLASK_APP=app
FLASK_ENV=development
USER=your_username
PASS=your_password
PORT=5432
DB=your_database
Migrations, migrations, migrations
Usually when we change something in our models.py
we have to delete and recreate our database from scratch all over again.
This is very inconvenient during development so we need a tool that will make the changes for us without deleting and recreating the entire database.
That tool is flask-migrate
. Let’s install it;
(pyenv) pip install flask-migrate
Now lets initialize our app for migrations;
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_admin import Admin
from app import raj
db = SQLAlchemy(raj)
migrate = Migrate(raj, db)
admin = Admin(raj)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return '<User %r>' % self.username
Once installed we have to follow the following step to get it setup;
flask db init
flask db migrate
- *
flask db upgrade
The last step is not necessary after the first migration
but it is important after every subsequent migration
.
Once you have done that let us make some changes to our models.py
to see if this will work.
If you remember, we created a food Item in part 2 part 2;
Let us add one thing; image
and remove unique on everything else;
Updated model.py
class FoodItem(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
desc = db.Column(db.String(250), nullable=False)
price = db.Column(db.Float, nullable=False)
image = db.Column(db.String(50))
prep_time = db.Column(db.Integer, unique=True, nullable=False)
#date =
def __repr__(self):
return '<Food Item %r' % self.title
Now let us migrate
and upgrade
;
(pyenv) flask db migrate
(pyenv) flask db upgrade
If all went well then you should receive 0 errors. Remeber to run those two commands everytime you change something in your models! If you get any problems post them in the comment section below.
Adding food images to our app
Where to find good images.
Go to pexels.com and search fod food. You will find a lot of nice looking images. Download the ones you like and put them in your /static/img/
folder.
Adding the images
Let’s modify our FoodItem
model to include a string field for the image path;
class FoodItem(db.Model):
__tablename__ = 'food_item'
id = db.Column(db.Integer, unique=True, primary_key=True)
title = db.Column(db.String(80), nullable=False)
desc = db.Column(db.String(250), unique=True, nullable=False)
price = db.Column(db.Float, nullable=False)
prep_time = db.Column(db.Integer, nullable=False)
image = db.Column(db.String(250), index=True)
date_added = db.Column(db.DateTime)
Let us migrate those changes;
$(pyenv) flask db migrate
$(pyenv) flask db upgrade
Now run your app $(pyenv) ./run.py
and navigate to localhost:3000/admin/FoodItem
to begin adding items. For the image field just put the name of the image. For example,
burger.jpg
and do that for every food item you add.
Make sure that every food item has an image with the file name of an image that actually exists in your /static/img
folder.
FrontEnd
Let’s load the images into our home page.
First, import FoodItem
from your models
at the top of your views.py
;
app/views.py
...
from app.models import FoodItem
...
Then, update your home
function in your views;
app/views.py
@raj.route('/')
@raj.route('/home')
@raj.route('/index')
@templated()
def home():
# user = g.user
# food_items = app.Models.FoodItem
return dict(title=app_name, user=user, dishes=FoodItem.query.all())
Now we can use dishes in home.html
;
app/templates/home.html
...
<div class="food-grid">
{% for dish in dishes %}
<div class="food-item">
<img src="{{ url_for( 'static', filename='img/' + dish.image ) }}" alt="" />
<div class="food-box">
<p><span>Dish #: </span> {{ dish.id }} </p>
<a class="food-link" href="/add/{{ dish.id }}"> Add to tray </a>
</div>
</div>
{% endfor %}
</div>
...
The images do load but they aren’t responsive, i.e. they do not fit within our menu-grid
system.
For that, we need responsify.js.
Download responsify.js
and responsify.min.js
into your static/js/
folder.
Since both scripts use jQuery
we need to download it as well, then we have to include all scripts at the bottom of our base.html
like this;
app/templates/base.html
...
</main>
<!-- JavaScripts -->
<script
src="https://code.jquery.com/jquery-2.2.4.min.js"
integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44="
crossorigin="anonymous">
</script>
<script src="static/js/responsify.js"></script>
<script src="static/js/main.js"></script>
</body>
<!--body -->
...
Then we can use responsify.js
in our main.js
like so;
app/static/js/main.js
$(document).ready(function(){
$(window.onload(function(){
$('img').responsify();
}));
$(window).resize(function(){
$('img').responsify();
});
});
The Tray
function
app/views.py
@raj.route('/add/<int:dish>', methods=['GET','POST'])
def add():
# associate this dish with the current user
return redirect("previous URL")
What this function will do is associate the current
user with the FoodItem
id that was passed in. In order to do that we have to create a database relationship
between the User
and the FoodItem
.
We will do this in part 5
After making the association we will redirect the user to the previous URL
and flash
them a message stating that the item was added to the chart.
What other things do we need to do? We need to define a checkout process.
Checkout!
Remeber that a user needs to be able to;
- Add items onto a cart
- See a complete break down of charges, including taxes
- Have the option to accept or cancel at any point before their card is charged
- Be able to choose from a variety of payment options
- Receive a receipt of the transaction
Let us try to implement some of these features;
The checkout
function
app/views.py
...
@raj.route('/checkout')
def checkout():
current_user = g.user
subtotal = sum( g.user['tray'] )
taxes = calc_tax( subtotal )
total = subtotal + taxes
return render_html('checkout.html', total=total, subtotal=subtotal, taxes=taxes)
...
sum
calculates the total price of all the food items in the current_user
’s tray.
We will look at how the current user is determined later on.
...
def sum(tray):
total = 0
for dish in tray:
total += dish.price
return total
...
calc_tax
calculates the amount of tax to be paid from the subtotal
obtained previously
Then we calculate the total and send this information to the checkout.html
template via three variables, total
, subtotal
, and taxes
.
The pay
function
This will be the second view the user will be presented with and immediately follows the checkout
view, provided nothing goes wrong there.
In checkout.html
we will need to send the total_price
to the pay
route somehow;
<a href="/pay/{{ total_price }}"> Accept </a>
Now we can use the total_price
to charge the user’s card.
app/views.py
@raj.route('/pay/<float:total_price>')
def pay():
# Choose a method of payment
# PayPal
# SquareCash
# Visa/MasterCard
# Venmo/Zelle/Other
payment = verify( total_price )
paycode = payment.code
if payment.successful:
receipt = generate_receipt( g.user['tray'], total_price )
return render_template('payment_success.html', receipt=receipt, status_code=paycode)
else:
return render_template('payment_failure.html', status_code=paycode)
...
verify
simply checks to see that the payment went through.
We will see how to define this function in part 5
but it should be clear what we expect from verify; an object with atleast the following members;
- A status
code
# a code for describing the status of the verification - A
boolean
successful
to indicate that the payment was successful.
A status code is important for troubleshooting. Payments might fail for a variety of reasons;
- Customers might have insufficient funds in their account
- The banking services might be down due to maintainence e.t.c
- The user might have entered the wrong credentials
- Restaurant owner’s bank might not accept funds from customer’s bank
- e.t.c
A status_code
that describes each problem mentioned above will make troubleshooting for both customer
and restaurant
owner much easier.
payment_succes.html
and payment_failure.html
should be pretty straightforward templates to write.
We shall complete the checkout
part of our app in the next post
until then,
happy coding :)
PS: Here’s what our images look line on mobile;