Tutorial: A simple blog¶
This tutorial will guide you through building the blog present in the
examples/blog
directory. This is a very simple blog that displays
a list of posts and allows an authenticated user to create a new post.
Running the example¶
To run the example, in examples/blog
the following should start
the server, (see Installation first),
$ export QUART_APP=blog:app
$ quart run
the blog is then available at http://localhost:5000/.
1: Structure¶
Quart by default expects the code to be structured in a certain way in order for templates and static file to be found. This means that you should structure the blog as follows,
blog/
blog/static/
blog/static/js/
blog/static/css/
blog/templates/
doing so will also make your project familiar to others, as you follow the same convention.
2: Installation¶
It is always best to run python projects within a pipenv, which should be created and activated as follows,
$ cd blog
$ pipenv install quart
for this blog we will only need Quart. Now pipenv can be activated,
$ pipenv shell
Note
(venv)
is used to indicate when the commands must be run within
the pipenv’s virtualenv.
3: Creating the app¶
We can now create a basic hello world app, in a file called
blog.py
,
from quart import Quart
app = Quart(__name__)
@app.route('/')
async def index():
return 'Hello World'
and run it by the following,
$ export QUART_APP=blog:app
(venv) $ quart run
Note
The QUART_APP
environment variable is assumed to be set for the
rest of this tutorial.
4: Creating the database¶
There are many database management systems to choose from depending upon the needs and requirements. In this case we need only the simplest system, and Python’s standard library includes SQLite making it the easiest.
To initialise the database we need some SQL to create the correct table,
DROP TABLE IF EXISTS post;
CREATE TABLE post (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
'text' TEXT NOT NULL
);
which ensures that the post table exists in this form. This is a
command that will need to be used often, so it should be a cli
command. This is achieved via the following blog.py
additions,
from pathlib import Path
from sqlite3 import dbapi2 as sqlite3
app.config.update({
'DATABASE': app.root_path / 'blog.db',
})
def connect_db():
engine = sqlite3.connect(app.config['DATABASE'])
engine.row_factory = sqlite3.Row
return engine
@app.cli.command('init_db')
def init_db():
"""Create an empty database."""
db = connect_db()
with open(Path(__file__).parent / 'schema.sql', mode='r') as file_:
db.cursor().executescript(file_.read())
db.commit()
which allows,
(venv) $ quart init_db
to run the init_db function, creating a blank database.
Warning
Running the schema or the command will wipe any existing data.
5: Displaying posts in the database¶
With the database existing we can display the posts present in it. To
do so we have to query the database and retrieve the messages, this is
best done in the view-function, with the following code (which replaces
the existing /
view-function in blog.py
),
from quart import render_template, g
def get_db():
if not hasattr(g, 'sqlite_db'):
g.sqlite_db = connect_db()
return g.sqlite_db
@app.route('/', methods=['GET'])
async def posts():
db = get_db()
cur = db.execute(
"""SELECT title, text
FROM post
ORDER BY id DESC""",
)
posts = cur.fetchall()
return await render_template('posts.html', posts=posts)
This posts
view-function returns the awaited result of a template
render, which displays the posts. This template should exist within
the templates
directory and contain the following,
<div class="posts">
{% for post in posts %}
<div><h2>{{ post.title }}</h2>{{ post.text|safe }}</div>
{% else %}
<div>No posts available</div>
{% endfor %}
</div>
in order to nicely render HTML displaying the posts.
6: Creating a new post¶
To allow a visitor to create a blog-post we should accept a POST
request from the browser. This POST request should contain all the
information we need to create a blog-post, namely the title and
text. With this the blog-post can be created with the following
view-function addition to blog.py
,
from quart import redirect, request, url_for
@app.route('/', methods=['POST'])
async def create():
db = get_db()
form = await request.form
db.execute(
"INSERT INTO post (title, text) VALUES (?, ?)",
[form['title'], form['text']],
)
db.commit()
return redirect(url_for('posts'))
the redirect sends the POST request browser to the posts
view-function.
You can test this using curl with the following command,
$ curl -X POST -d "title=Blog Title&text=Text for the blog" localhost:5000/
This is not very helpful to most visitors though, instead we should
use a HTML form. This can be added to the posts.html
template as so,
<form action="{{ url_for('create') }}" method="post" class="create-post">
<p>Title:<input type="text" size="30" name="title">
<p>Text:<textarea name="text" rows="5" cols="40"></textarea>
<p><input type="submit" value="Post">
</dl>
</form>
with the action pointing at out new create
view-function.
7: Authenticating visitors¶
So far we can view and create posts, but so can anyone visiting the site. Ideally we should restrict the ability to create posts to a subset of visitors, notably visitors we allow. Therefore we need to authenticated visitors.
An authenticated visitor is typically different to the other visitors in that they present some proof of authentication. Initially this must be their username and password. Thereafter a market on the cookie is set to indicate they are logged in. With Quart the Session Storage is secure by default, so it can be used as so,
from quart import session
@app.route('/login')
def login():
session['logged_in'] = True
...
@app.route('/posts')
def posts():
if session['logged_in']:
# Do something authenticated
else:
# Do something else
...
@app.route('/logout')
def logout():
session.pop('logged_in', None)
we can also check in the templates if the user is logged in,
<nav>
{% if not session.logged_in %}
<a href="{{ url_for('login') }}">Login</a>
{% else %}
<a href="{{ url_for('logout') }}">Logout</a>
{% endif %}
</nav>
Note
In production you probably want a more sophisticated authentication system, of which Flask-Login is the best example.
8: All together¶
Now that visitors can be authenticated the app needs to offer login and logout view functions alongside checking the authentication status when creating posts. This combined is,
from quart import (
abort, redirect, render_template, request, session,
url_for,
)
app.config.update({
'SECRET_KEY': 'development key',
'USERNAME': 'admin',
'PASSWORD': 'default',
})
@app.route('/', methods=['POST'])
async def create():
if not session.get('logged_in'):
abort(401)
db = get_db()
form = await request.form
db.execute(
"INSERT INTO post (title, text) VALUES (?, ?)",
[form['title'], form['text']],
)
db.commit()
return redirect(url_for('posts'))
@app.route('/login/', methods=['GET', 'POST'])
async def login():
error = None
if request.method == 'POST':
form = await request.form
if form['username'] != app.config['USERNAME']:
error = 'Invalid username'
elif form['password'] != app.config['PASSWORD']:
error = 'Invalid password'
else:
session['logged_in'] = True
return redirect(url_for('posts'))
return await render_template('login.html', error=error)
@app.route('/logout/')
async def logout():
session.pop('logged_in', None)
await flash('You were logged out')
return redirect(url_for('posts'))
Warning
In production don’t store the passwords in plain text, rather use something like bcrypt (salting and hashing).
The login template itself is given as below,
<h2>Login</h2>
{% if error %}<p class="error"><strong>Error:</strong> {{ error }}{% endif %}
<form action="{{ url_for('login') }}" method="post">
<p>Username: <input type="text" name="username">
<p>Password: <input type="password" name="password">
<p><input type="submit" value="Login">
</form>
9: Flashing messages¶
So far every action the visitor completes is silently completed, however we should give the visitor some feedback. This is where flashing messages proves very helpful. For example after login it makes sense to flash if the login was successful, like so,
await flash('You were logged in')
which requires the following jinja addition to every template,
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %}
To avoid repeating ourselves and adding this snippet to every single template, we can instead create a base template and have the other templates inherit from it. We could also have used a template macro, but the base template helps with the styling in the next section. The base template should be,
<!doctype html>
<title>Blog</title>
<h1><a href="{{ url_for('posts') }}">Blog</a></h1>
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %}
<div class="content">
{% block content %}
{% endblock %}
</div>
The other templates can then use this base template via the following construct,
{% extends 'base.html' %}
{% block content %}
...
{% endblock %}
10: Styling¶
The pages can be styled using css, firstly by adding this one line to the base template,
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='blog.css') }}">
and then by adding this stylesheet to static/blog.css
,
body {
background: #f5f5f6;
font-family: sans-serif;
margin: 0;
padding: 0;
}
h1 {
background: #004c40;
padding: 0.2em;
}
...
see the full example for more.
11: Testing¶
You should be testing your apps, and Quart provides testing clients and functionality to make this easy. Using the pytest test framework rather than the stdlib unittest framework makes things easier still, and will be used here. pytest and pytest-asyncio (as required to test asyncio code) can be installed using pipenv,
(venv) $ pipenv install pytest pytest-asyncio
A useful test would be to check that posts are created as expected, which means we need to test against the database. Fortunately pytest offers a tmpdir fixture which is perfect for this, so lets create a test app fixture,
import pytest
from .blog import app, init_db
@pytest.fixture(name='test_app')
def _test_app(tmpdir):
app.config['DATABASE'] = str(tmpdir.join('blog.db'))
init_db()
return app
which we can use in any test function by expecting an argument named
test_app
.
The test itself should be to POST a new blog-post to the create route and then check it exists in the list of posts,
@pytest.mark.asyncio
async def test_create(test_app):
test_client = test_app.test_client()
await test_client.post(
'/login/',
form={
'username': test_app.config['USERNAME'],
'password': test_app.config['PASSWORD']
},
)
response = await test_client.post(
'/', form={'title': 'test_title', 'text': 'test_text'},
)
assert response.status_code == 301
response = await test_client.get('/')
body = await response.get_data(raw=False)
assert 'test_title' in body
assert 'test_text' in body
which is testable via,
(venv) $ pytest
12: Conclusion¶
The example files contain this entire tutorial and a little more, so they are now worth a read. Hopefully you can now go ahead and create your own apps.