Update2

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;

  1. Managing credentials for development purposes
  2. Migrations, migrations, migrations
  3. Adding yummy food images to our app
  4. Checkout! comes to our app
  5. Creating a blog Blueprint part 5

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;

  1. Change my database password to one that works with pydotenv.
  2. 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;

img

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.

img

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;

  1. flask db init
  2. flask db migrate
  3. *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

Update2

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;

  1. $(pyenv) flask db migrate
  2. $(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;

  1. Add items onto a cart
  2. See a complete break down of charges, including taxes
  3. Have the option to accept or cancel at any point before their card is charged
  4. Be able to choose from a variety of payment options
  5. 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 5but it should be clear what we expect from verify; an object with atleast the following members;

  1. A statuscode # a code for describing the status of the verification
  2. 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;

Update1