Goals;

  1. Add some CSS using the flask-scss extension.
  2. Add HTML templates
  3. Learn how to create and connect a database to our flask application.
  4. Add usersto our database and admin views to the app

This is part 2 of building a restaurant app in flask.
We made a basic flask app in the first part of this series. We will now focus on organizing different parts of the app.

As stated before we shall begin with some backend stuff, like an admin interface and some database setups before diving into other parts of our app.

However, the primary goal of this second part is to setup our app’s frontend.
To that end we shall see how to use organize thr app to use the flask-scss extension, and how to simplify development with other tools, and so on.

Getting started

Navigate to our project and activate our virtual environment; $ source ../myenv/bin/activate

Style Up

Before we take a look at our __init__.py file, let us first install the flask-scss extension and flask-admin;

  1. $(myenv) pip install flask-sscss
  2. $(myenv) pip install flask-admin

Now lets navigate to our app folder:

├── app
│   ├── __init__.py
│   ├── __init__.pyc
│   ├── static
│   │   └── style.css
│   ├── templates
│   │   └── index.html
│   ├── views.py
│   └── views.pyc
└── run.py

And create two new folders;

  1. app/assets/scss
  2. app/static/css

app/assets/scss will house our main .scss files where as app/static/css will contain our compiled css.

Ok so now lets setup flask-scss in our __init__.py file:

from flask import Flask
from flask_scss import Scss
from flask_admin import Admin

raj = Flask(__name__)

Scss(raj, asset_dir='app/assets/scss', static_dir='app/static/css')
admin = Admin(raj)

from app import views

Notice that we initialized our Admin object with our flask app.
We are not done just yet. We have to call the add_views function of that admin object. The views function should take in an object that defines it’s attributes in terms of SQL rows and columns. More on that later.

Now we can’t add css to our site if we don’t have the html so lets create an index.html file in our templates folder.


<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<meta name="description" content="Raj's Restaurant">
	<meta name="keywords" content="HTML, CSS, JavaScript">
	<meta name="author" content="Lehbyte">
	<meta name="application-name" content="Raj's Application">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>{{ title }}</title>
	<link rel="stylesheet" type="text/css" href="./static/css/main.css" />	
</head>
<!-- body -->
<body>
	<main>
		<div class="container">
			<div class="navigation">
				<ul>					
					<li><a href="#">Home</a></li>
					<li><a href="#">About</a></li>
					<li><a href="#">Menu</a></li>
					<li><a href="#">Coupons</a></li>
					<li><a href="#">Support</a></li>
				</ul>
			</div>
			<h1>Welcome to Raj's Restaurant!</h1>
			<div class="wrapper" >
				<input class="search" type="text" id="search" />
				<input class="submit" type="submit" value="Submit" />
			</div>
			<div class="food-grid">
				<div class="food-item">1</div>
				<div class="food-item">2</div>
				<div class="food-item">3</div>
				<div class="food-item">4</div>				
				<div class="food-item">5</div>	
				<div class="food-item">6</div>	
			</div>
		</div>
	</main>
</body>
<!--body -->
</html>

We also need to modify our views.py to render the html file above inder the default route.

from flask import render_template, url_for
from app import raj

@raj.route('/')
@raj.route('/index')
def index():
	return render_template('index.html', title="Raj's Restaurant")

Rember that when linking stylesheets and scripts, searching begins at the root directory of the app, raj in our case. And we need to ensure that we are trying to access a file that actually exists;


<link rel="stylesheet" type="text/css" href="./static/css/main.css" />	

CSS Sprinkles

Lets create some CSS rules to make our app look better. This is what our site is going to look like; Raj

I created three partials;

  1. _menugrid.scss
  2. _navigation.scss
  3. _variables.scss

And a main.scss that includes all three partials. It’s easy to see what each partial is made for. Lets go through them one by one.

_menugrid.scss

.food-grid{
    display: grid;
    grid-gap: 5px;
    grid-template-rows: 400px 400px;
    grid-template-columns: 33% 33% 33%;
    height: inherit;    
    background-color: skyblue;
}
.food-item{
    padding: 2px;
    margin: 2px;    
    background-color: rebeccapurple;
}

We have opted to use a grid system to display food items. The .food-item will contain a picture as well as some description of the food an an add to tray option for the user to add the item to their tray so that they can check out it out later on.

_navigation.scss

a{
    color: $link-color;
    text-decoration: none;
    cursor: pointer;
}
a:hover{
    border-bottom: 1px solid $outline;    
}
a:link, a:visited{
    color: $link-visited;    
}
a:link:active, a:visited:active{
    color: $link-active;
}

.navigation{
    text-align: center;
    font-size: 1.2em;
    font-variant-caps: all-petite-caps;    
}

ul{    
    list-style-type: none;
    display: inline-flex;
    
}

@media only screen and (max-width: 432px){
    ul{        
        list-style: none;   
        li{
            margin: 3px;
            padding: 3px;
            border: 2px solid black;
        }
        &:hover{
            background-color: black;
        }
    }
}

_variables.scss

$link-color: darkgreen;
$link-visited: black;
$link-active: seagreen;

$bg-color: snow;
$outline: gray;

$search-height: 50px;
$sradius: 25px;

main.scss

Finally, here is our main.scss

@import url('https://fonts.googleapis.com/css?family=Josefin+Sans&display=swap');
*{
    margin: 5px;
    padding: 5px;
    box-sizing: border-box;
}
html{ font-family: 'Josefin Sans', sans-serif; }
html, body{
    width: 100%;
    height: 100%;
    margin: 0;    
}

@import 'variables';
@import 'navigation';

h1{
    font-size: 48px;
    text-align: center;
    padding-bottom: 25px;
    // text-decoration: underline;
}



body{
    display: flex;
    flex-direction: column;    
    // background-color:$bg-color;
    // background-color: blanchedalmond;    
    background: -webkit-linear-gradient(to bottom, white 20%,burlywood 80%);
    background: linear-gradient(to bottom, white 20%, burlywood 80%); 
    background-attachment: fixed;

}

.wrapper{    
    width: 100%;
    background-color: darken(skyblue,10%);
    text-align: center;
    position: relative;
    height: 60px;
    font-size: 0;    
    // border-radius: 50px;    
}
input{    
    font-size: 13px;
    vertical-align: middle;
    padding: 0px;
    margin:0px;
}
.search{
    padding: 0 30px;
    font-size: 20px;
    width: 40%;
    max-width: 400px;
    height: $search-height;
    border: 1px solid darken(white, 30%);
    border-radius: $sradius 0 0 $sradius;
}
.submit{
    font-size: 20px;
    color: white;
    cursor: pointer;
    border: none;
    background-color: black;
    background-size: 34px 34px;
    border-radius: 0 $sradius $sradius 0;
    padding: 0px $sradius 0px 17px;
    display: inline-block;
    width: 100px;
    height: $search-height;
}
main{
    border: 2px solid black;    
}
.container{
    border: 2px solid black;    
}

@import 'menugrid';

Adding views to our admin page

Right now when we launch our server and navigate to localhost:5000/admin we are greeted with a nav bar with no views.

Lets change that. First we have to configure our app to work with SQLAlchemy Lets edit our __init__.py to include SQLAlchemy by adding these two lines;

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy(raj)

__init__.py

from flask import Flask
from flask_scss import Scss
from flask_admin import Admin
from flask_sqlalchemy import SQLAlchemy

raj = Flask(__name__)
raj.config.from_object('config')
db = SQLAlchemy(raj)

Scss(raj, asset_dir='app/assets/scss', static_dir='app/static/css')
admin = Admin(raj)

from app import models, views

On the last line we import a file - models - which will define the records that we will eventually add to our admin views.

Now we have created an SQLAlchemy object called db. This is what will help us create a database that will subsequetly contain our Users and FoodItems.

We have also defined certain configuration variables in a config.py file for simplicity;

import os
basedir = os.path.abspath(os.path.dirname(__file__))

FLASK_ADMIN_SWATCH = 'cerulean'
TEMPLATES_AUTO_RELOAD = False

SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'raj.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False

WTF_CSRF_ENABLED = True
SECRET_KEY = 'myrestaurant'
WHOOSH_BASE = os.path.join(basedir, 'raj.db')

There are a number of ways in which we can create our database. We can begin a python shell within our virtual environment, import all our models the the config file and call db.create__all() which would create a database called raj.db.

Then we can experiment with adding and remove things to that database within that shell. We have to make sure that adding and remove stuff from our database can be done without any problems.

But of course we can’t add objects whose models don’t exist so let us create some models.

Creating models

In part 1 of this series, we modeled a food item. We defined it as an object of certain attributes, more concretely;

class FoodItem(db.Model):    
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), unique=True, nullable=False)
    desc = db.Column(db.String(250), unique=True, nullable=False)
    price = db.Column(db.Float, unique=True, nullable=False)
    prep_time = db.Column(db.Integer, unique=True, nullable=False)
    #date = 

    def __repr__(self):
        return '<Food Item %r' % self.title

But that is not enough, owners of the restaurant would want to add and remove FoodItems as well as delete users so we need to define an owner;

class Owner(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 '<Admin %r>' % self.username

We also need to define who a user is as well.
Obviously, users don’t have the same privileges as administrators/owners.
User’s can’t create new food items, edit or delete them.
They only controll the checkout cart.

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 '<Admin %r>' % self.username

We can also create views to control certain aspects of our User and Owner objects;

class UserView(ModelView):
    page_size = 50
    can_create = True
    can_edit = True
    can_delete = False

And finally, we can add views to our admin page;

admin.add_view(UserView(User, db.session))
admin.add_view(ModelView(FoodItem, db.session))

The full code listing for models.py;

from flask_admin.contrib.sqla import ModelView
from app import admin, db

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

class Admin(User):        
    def __repr__(self):
        return '<Admin %r>' % self.username

class FoodItem(db.Model):    
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(80), unique=True, nullable=False)
    desc = db.Column(db.String(250), unique=True, nullable=False)
    price = db.Column(db.Float, unique=True, nullable=False)
    prep_time = db.Column(db.Integer, unique=True, nullable=False)
    #date = 

    def __repr__(self):
        return '<FoodItem %r' % self.title

class UserView(ModelView):
    page_size = 50
    can_create = True
    can_edit = True
    can_delete = False

# Add views 
admin.add_view(UserView(User, db.session))
admin.add_view(ModelView(FoodItem, db.session))

Obviously our models.py will reside in our app folder, where our views.py is.

Creating our database.

Postgresql

If you have postgresql installed, you can create a database like so;

$ sudo -i
$ su - postrges
$ createdb mydb

Then you can;

psql username or pqsl postgres
\l

To see if your database was created and who owns it.
I created a raj_db database and changed it’s ownership to lehbyte - me.
You can change ownership of any databse to any linux user registered on your machine.

We need to create a database engine for SQLAlchemy;

The Engine is the starting point for any SQLAlchemy application. It’s “home base” for the actual database and its DBAPI, delivered to the SQLAlchemy application through a connection pool and a Dialect, which describes how to talk to a specific kind of database/DBAPI combination.

So how do we create this engine;

engine = create_engine('postgresql://scott:tiger@localhost:5432/mydatabase')

Here scott is the username, tiger is the password and 5432 refers to port on which to connect to mydatabase. But of course we have to change this URI to match our configuration. Let us build our URI or SQLALCHEMY_DATABASE_URI.

We have;

Keys Values
username lehbyte
password mypassword
database raj_db

Thus;

SQLALCHEMY_DATABASE_URI='postgresql://lehbyte:mypassword@localhost:5432/raj_db'

Now we can begin a python shelll;

>>> from app import db, models
>>> import config
>>> db.create_all()

If you don’t get any errors then it means the command db.create_all() was successfull.

If you get any authentication issues, make sure that the password you are using is the actually password associated with your linux account. If you get any connection issues try a different port then close the python session and begin a new one with those changes.

Adding data to our database

Rember that we imported models into our python session so we can create users and food items and add them to our raj_db database.

>>> from app.models import User, FoodItem
>>> kate = User(username='Kate', email='kate@email.com')
>>> guest = User(username='Guest', email='guest@email.com')
>>> kate, guest
>>> (<User 'Kate'>,<User 'Guest'>)
>>> db.session.add(kate)
>>> db.session.add(guest)
>>> db.session.commit()
>>> User.query.all()
>>> [<User 'Kate'>,<User 'Guest'>]

Now you are good to go.

SQLite

In SQLite creating a database is easy.
All we have to do is modify our config.py thus; SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'raj.db')

Start a python shell, import the db and all the other things as before and just run db.create.all()

Alternatively we could just create a create_db.py to handle most of this for us;

# create database
import os
import basedir
from sqlaclchemy import create_engine
engine = create_engine('postgresql://username:mypassword@localhost:5432/mydb')
#engine = create_engine('sqlite:///' + os.path.join(basedir, 'raj.db'))
from app import db
db.create_all()

Security

You may be worried about how we connect to our postgresql database and whether there’s a secure way to do so without exposing our password.

You can use an .env file and include it in .gitignore
Put your password in this .env file;

MY_PASSWORD='mypassword'

Then modify your SQLALCHEMY_DATABASE_URI thus;

import pydotenv
env = pydotenv.Environment()
SQLALCHEMY_DATABASE_URI = 'postgresql://username:'+env['DB_PASSWORD']+'@localhost:5432/mydatabase'

This would be useful if your repo is publicly available.

Concluding part 2

One last thing we need to make sure is that we have flask-scss installed. If it is not, then our scss won’t be compiled into css.

One pain-point with py-scss is that you have to manually reload the page yourself. This is unacceptable especially given the fact that we will be heavily editing and and testing the frontend extensively.

Now you can try to set TEMPLATES_AUTO_RELOAD to True in your config.py but this will only work for html templates and not stylesheets.

Another alternative is to create a gulpfile.js in your static folder and have it execute your run.py script in a child_process but with browserfy enabled. This a tricky thing to do since our python server has to be up for us to use flask-scss. Going full nodejs will only make things worse, unless of course we want to switch the enrire framework to nodejs

Alternatively, we can try to investigate how to work with honcho on this end.

In the third part of this tutorial we shall touch on the following points;

  1. User management
  2. Better frontend
  3. Restricting access to owners
  4. Creating a checkout process
  5. Experimenting with a nodejs solution to browsersync

Until then, happy coding.