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,

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,

schema.sql
 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,

blog.py
 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),

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,

templates/posts.html
  <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,

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,

templates/posts.html
 <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,

blog.py
 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,

templates/login.html
 <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,

templates/base.html
 <!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,

templates/base.html
 <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='blog.css') }}">

and then by adding this stylesheet to static/blog.css,

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.