File: blog_tutorial.rst

package info (click to toggle)
quart 0.20.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 1,888 kB
  • sloc: python: 8,644; makefile: 42; sh: 17; sql: 6
file content (310 lines) | stat: -rw-r--r-- 8,614 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
.. _blog_tutorial:

Tutorial: Building a simple blog
================================

In this tutorial we will build a simple blog with entries stored in a
database. We'll then render these posts on the server and serve the
HTML directly to the user.

This tutorial is meant to serve as an introduction to building server
rendered websites in Quart. If you want to skip to the end the code is
on `Github <https://github.com/pallets/quart/tree/main/examples/blog>`_.

1: Creating the project
-----------------------

We need to create a project for our blog server, I like to use
`Poetry <https://python-poetry.org>`_ to do this. Poetry is installed
via pip (or via `Brew <https://brew.sh/>`_):

.. code-block:: console

    pip install poetry

We can then use Poetry to create a new blog project:

.. code-block:: console

    poetry new --src blog

Our project can now be developed in the *blog* directory, and all
subsequent commands should be in run the *blog* directory.

2: Adding the dependencies
--------------------------

To start we only need Quart to build the blog server, which we can
install as a dependency of the project by running the following:

.. code-block:: console

    poetry add quart

Poetry will ensure that this dependency is present and the paths are
correct by running:

.. code-block:: console

    poetry install

3: Creating the app
-------------------

We need a Quart app to be our web server, which is created by the
following addition to *src/blog/__init__.py*:

.. code-block:: python
    :caption: src/blog/__init__.py

    from quart import Quart

    app = Quart(__name__)

    def run() -> None:
        app.run()

To make the app easy to run we can call the run method from a poetry
script, by adding the following to *pyproject.toml*:

.. code-block:: toml
    :caption: pyproject.toml

    [tool.poetry.scripts]
    start = "blog:run"

Which allows the following command to start the app:

.. code-block:: console

    poetry run start

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 the following SQL to create the
correct table, as added to *src/blog/schema.sql*:

.. code-block:: sql
    :caption: src/blog/schema.sql

    DROP TABLE IF EXISTS post;
    CREATE TABLE post (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      title TEXT NOT NULL,
      'text' TEXT NOT NULL
    );

Next we need to be able to create the database on command, which
we can do by adding the command code to *src/blog/__init__.py*:

.. code-block:: python
    :caption: src/blog/__init__.py

    from pathlib import Path
    from sqlite3 import dbapi2 as sqlite3

    app.config.update({
      "DATABASE": Path(app.root_path) / "blog.db",
    })

    def _connect_db():
        engine = sqlite3.connect(app.config["DATABASE"])
        engine.row_factory = sqlite3.Row
        return engine

    def init_db():
        db = _connect_db()
        with open(Path(app.root_path) / "schema.sql", mode="r") as file_:
            db.cursor().executescript(file_.read())
        db.commit()

Next we need to update the poetry scripts in *pyproject.toml* to be:

.. code-block:: toml
    :caption: pyproject.toml

    [tool.poetry.scripts]
    init_db = "blog:init_db"
    start = "blog:run"

Now we can run the following to create and update the database:

.. code-block:: console

    poetry run init_db

.. warning::

   Running this command will wipe any existing data.


5: Displaying posts in the database
-----------------------------------

With can now display the posts present in the database. To do so we
first need a template to render the posts as HTML. This is as follows
and should be added to *src/blog/templates/posts.html*:

.. code-block:: html
    :caption: src/blog/templates/posts.html

    <main>
      {% for post in posts %}
        <article>
          <h2>{{ post.title }}</h2>
          <p>{{ post.text|safe }}</p>
        </article>
      {% else %}
        <p>No posts available</p>
      {% endfor %}
    </main>

Now we need a route to query the database, retrieve the messages,
and render the template. As done with the following code which should
be added to *src/blog/__init__.py*:

.. code-block:: python
    :caption: src/blog/__init__.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.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)

6: Creating a new post
----------------------

To create blog posts we first need a form into which a user can enter
the post details. This is done via the following template code that should
be added to *src/blog/templates/create.html*:

.. code-block:: html
    :caption: src/blog/templates/create.html

    <form method="POST" style="display: flex; flex-direction: column; gap: 8px; max-width:400px">
      <label>Title: <input type="text" size="30" name="title" /></label>
      <label>Text: <textarea name="text" rows="5" cols="40"></textarea></label>
      <button type="submit">Create</button>
    </form>

The styling ensures that the elements of the form are arranged
verically with a gap and sensible maximum width.

To allow a visitor to create a blog post we need to accept the POST
request generated by this form in the browser. To do so the following
should be added to *src/blog/__init__.py*:

.. code-block:: python
    :caption: src/blog/__init__.py

    from quart import redirect, request, url_for

    @app.route("/create/", methods=["GET", "POST"])
    async def create():
        if request.method == "POST":
            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"))
        else:
            return await render_template("create.html")

This route handler will render the creation form in response to a GET
request e.g. via navigation in the browser. However, for a POST
request it will extract the form data to create a blog post before
redirecting the user to the page with the posts.

7: Testing
----------

To test our app we need to check that a blog post can be created, and
once done so shows on the posts page. Firstly we need to create a
temporary database for testing, which we can do using a pytest fixture
placed in *tests/conftest.py*:

.. code-block:: python
    :caption: tests/conftest.py

    import pytest

    from blog import app, init_db

    @pytest.fixture(autouse=True)
    def configure_db(tmpdir):
        app.config['DATABASE'] = str(tmpdir.join('blog.db'))
        init_db()

This fixture will run automatically before our tests, thereby setting up
a database we can use in the tests.

To test the creation and display we can add the following to
*tests/test_blog.py*:

.. code-block:: python
    :caption: tests/test_blog.py

    from blog import app

    async def test_create_post():
        test_client = app.test_client()
        response = await test_client.post("/create/", form={"title": "Post", "text": "Text"})
        assert response.status_code == 302
        response = await test_client.get("/")
        text = await response.get_data()
        assert b"<h2>Post</h2>" in text
        assert b"<p>Text</p>" in text

As the test is an async function we need to install `pytest-asyncio
<https://github.com/pytest-dev/pytest-asyncio>`_ by running the
following:

.. code-block:: console

    poetry add --dev pytest-asyncio

Once installed it needs to be configured by adding the following to
*pyproject.toml*:

.. code-block:: toml

    [tool.pytest.ini_options]
    asyncio_mode = "auto"

Finally we can run the tests via this command:

.. code-block:: console

    poetry run pytest tests/

If you are running this in the Quart example folder you'll need to add
a ``-c pyproject.toml`` option to prevent pytest from using the Quart
pytest configuration.

8: Summary
----------

We've built a simple database backed blog server. This should be a
good starting point to building any type of server rendered app.