rewrite tutorial docs and example

This commit is contained in:
David Lord 2018-02-09 14:39:05 -08:00
parent 16d83d6bb4
commit c3dd7b8e4c
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
103 changed files with 3327 additions and 2224 deletions

336
docs/tutorial/blog.rst Normal file
View file

@ -0,0 +1,336 @@
.. currentmodule:: flask
Blog Blueprint
==============
You'll use the same techniques you learned about when writing the
authentication blueprint to write the blog blueprint. The blog should
list all posts, allow logged in users to create posts, and allow the
author of a post to edit or delete it.
As you implement each view, keep the development server running. As you
save your changes, try going to the URL in your browser and testing them
out.
The Blueprint
-------------
Define the blueprint and register it in the application factory.
.. code-block:: python
:caption: ``flaskr/blog.py``
from flask import (
Blueprint, flash, g, redirect, render_template, request, url_for
)
from werkzeug.exceptions import abort
from flaskr.auth import login_required
from flaskr.db import get_db
bp = Blueprint('blog', __name__)
Import and register the blueprint from the factory using
:meth:`app.register_blueprint() <Flask.register_blueprint>`. Place the
new code at the end of the factory function before returning the app.
.. code-block:: python
:caption: ``flaskr/__init__.py``
def create_app():
app = ...
# existing code omitted
from . import blog
app.register_blueprint(blog.bp)
app.add_url_rule('/', endpoint='index')
return app
Unlike the auth blueprint, the blog blueprint does not have a
``url_prefix``. So the ``index`` view will be at ``/``, the ``create``
view at ``/create``, and so on. The blog is the main feature of Flaskr,
so it makes sense that the blog index will be the main index.
However, the endpoint for the ``index`` view defined below will be
``blog.index``. Some of the authentication views referred to a plain
``index`` endpoint. :meth:`app.add_url_rule() <Flask.add_url_rule>`
associates the endpoint name ``'index'`` with the ``/`` url so that
``url_for('index')`` or ``url_for('blog.index')`` will both work,
generating the same ``/`` URL either way.
In another application you might give the blog blueprint a
``url_prefix`` and define a separate ``index`` view in the application
factory, similar to the ``hello`` view. Then the ``index`` and
``blog.index`` endpoints and URLs would be different.
Index
-----
The index will show all of the posts, most recent first. A ``JOIN`` is
used so that the author information from the ``user`` table is
available in the result.
.. code-block:: python
:caption: ``flaskr/blog.py``
@bp.route('/')
def index():
db = get_db()
posts = db.execute(
'SELECT p.id, title, body, created, author_id, username'
' FROM post p JOIN user u ON p.author_id = u.id'
' ORDER BY created DESC'
).fetchall()
return render_template('blog/index.html', posts=posts)
.. code-block:: html+jinja
:caption: ``flaskr/templates/blog/index.html``
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Posts{% endblock %}</h1>
{% if g.user %}
<a class="action" href="{{ url_for('blog.create') }}">New</a>
{% endif %}
{% endblock %}
{% block content %}
{% for post in posts %}
<article class="post">
<header>
<div>
<h1>{{ post['title'] }}</h1>
<div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div>
</div>
{% if g.user['id'] == post['author_id'] %}
<a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a>
{% endif %}
</header>
<p class="body">{{ post['body'] }}</p>
</article>
{% if not loop.last %}
<hr>
{% endif %}
{% endfor %}
{% endblock %}
When a user is logged in, the ``header`` block adds a link to the
``create`` view. When the user is the author of a post, they'll see an
"Edit" link to the ``update`` view for that post. ``loop.last`` is a
special variable available inside `Jinja for loops`_. It's used to
display a line after each post except the last one, to visually separate
them.
.. _Jinja for loops: http://jinja.pocoo.org/docs/templates/#for
Create
------
The ``create`` view works the same as the auth ``register`` view. Either
the form is displayed, or the posted data is validated and the post is
added to the database or an error is shown.
The ``login_required`` decorator you wrote earlier is used on the blog
views. A user must be logged in to visit these views, otherwise they
will be redirected to the login page.
.. code-block:: python
:caption: ``flaskr/blog.py``
@bp.route('/create', methods=('GET', 'POST'))
@login_required
def create():
if request.method == 'POST':
title = request.form['title']
body = request.form['body']
error = None
if not title:
error = 'Title is required.'
if error is not None:
flash(error)
else:
db = get_db()
db.execute(
'INSERT INTO post (title, body, author_id)'
' VALUES (?, ?, ?)',
(title, body, g.user['id'])
)
db.commit()
return redirect(url_for('blog.index'))
return render_template('blog/create.html')
.. code-block:: html+jinja
:caption: ``flaskr/templates/blog/create.html``
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}New Post{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="title">Title</label>
<input name="title" id="title" value="{{ request.form['title'] }}" required>
<label for="body">Body</label>
<textarea name="body" id="body">{{ request.form['body'] }}</textarea>
<input type="submit" value="Save">
</form>
{% endblock %}
Update
------
Both the ``update`` and ``delete`` views will need to fetch a ``post``
by ``id`` and check if the author matches the logged in user. To avoid
duplicating code, you can write a function to get the ``post`` and call
it from each view.
.. code-block:: python
:caption: ``flaskr/blog.py``
def get_post(id, check_author=True):
post = get_db().execute(
'SELECT p.id, title, body, created, author_id, username'
' FROM post p JOIN user u ON p.author_id = u.id'
' WHERE p.id = ?',
(id,)
).fetchone()
if post is None:
abort(404, "Post id {0} doesn't exist.".format(id))
if check_author and post['author_id'] != g.user['id']:
abort(403)
return post
:func:`abort` will raise a special exception that returns an HTTP status
code. It takes an optional message to show with the error, otherwise a
default message is used. ``404`` means "Not Found", and ``403`` means
"Forbidden". (``401`` means "Unauthorized", but you redirect to the
login page instead of returning that status.)
The ``check_author`` argument is defined so that the function can be
used to get a ``post`` without checking the author. This would be useful
if you wrote a view to show an individual post on a page, where the user
doesn't matter because they're not modifying the post.
.. code-block:: python
:caption: ``flaskr/blog.py``
@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id):
post = get_post(id)
if request.method == 'POST':
title = request.form['title']
body = request.form['body']
error = None
if not title:
error = 'Title is required.'
if error is not None:
flash(error)
else:
db = get_db()
db.execute(
'UPDATE post SET title = ?, body = ?'
' WHERE id = ?',
(title, body, id)
)
db.commit()
return redirect(url_for('blog.index'))
return render_template('blog/update.html', post=post)
Unlike the views you've written so far, the ``update`` function takes
an argument, ``id``. That corresponds to the ``<int:id>`` in the route.
A real URL will look like ``/1/update``. Flask will capture the ``1``,
ensure it's an :class:`int`, and pass it as the ``id`` argument. If you
don't specify ``int:`` and instead do ``<id>``, it will be a string.
To generate a URL to the update page, :func:`url_for` needs to be passed
the ``id`` so it knows what to fill in:
``url_for('blog.update', id=post['id'])``. This is also in the
``index.html`` file above.
The ``create`` and ``update`` views look very similar. The main
difference is that the ``update`` view uses a ``post`` object and an
``UPDATE`` query instead of an ``INSERT``. With some clever refactoring,
you could use one view and template for both actions, but for the
tutorial it's clearer to keep them separate.
.. code-block:: html+jinja
:caption: ``flaskr/templates/blog/update.html``
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="title">Title</label>
<input name="title" id="title"
value="{{ request.form['title'] or post['title'] }}" required>
<label for="body">Body</label>
<textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
<input type="submit" value="Save">
</form>
<hr>
<form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
<input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');">
</form>
{% endblock %}
This template has two forms. The first posts the edited data to the
current page (``/<id>/update``). The other form contains only a button
and specifies an ``action`` attribute that posts to the delete view
instead. The button uses some JavaScript to show a confirmation dialog
before submitting.
The pattern ``{{ request.form['title'] or post['title'] }}`` is used to
choose what data appears in the form. When the form hasn't been
submitted, the original ``post`` data appears, but if invalid form data
was posted you want to display that so the user can fix the error, so
``request.form`` is used instead. :data:`request` is another variable
that's automatically available in templates.
Delete
------
The delete view doesn't have its own template, the delete button is part
of ``update.html`` and posts to the ``/<id>/delete`` URL. Since there
is no template, it will only handle the ``POST`` method then redirect
to the ``index`` view.
.. code-block:: python
:caption: ``flaskr/blog.py``
@bp.route('/<int:id>/delete', methods=('POST',))
@login_required
def delete(id):
get_post(id)
db = get_db()
db.execute('DELETE FROM post WHERE id = ?', (id,))
db.commit()
return redirect(url_for('blog.index'))
Congratulations, you've now finished writing your application! Take some
time to try out everything in the browser. However, there's still more
to do before the project is complete.
Continue to :doc:`install`.

View file

@ -1,31 +0,0 @@
.. _tutorial-css:
Step 8: Adding Style
====================
Now that everything else works, it's time to add some style to the
application. Just create a stylesheet called :file:`style.css` in the
:file:`static` folder:
.. sourcecode:: css
body { font-family: sans-serif; background: #eee; }
a, h1, h2 { color: #377ba8; }
h1, h2 { font-family: 'Georgia', serif; margin: 0; }
h1 { border-bottom: 2px solid #eee; }
h2 { font-size: 1.2em; }
.page { margin: 2em auto; width: 35em; border: 5px solid #ccc;
padding: 0.8em; background: white; }
.entries { list-style: none; margin: 0; padding: 0; }
.entries li { margin: 0.8em 1.2em; }
.entries li h2 { margin-left: -1em; }
.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; }
.add-entry dl { font-weight: bold; }
.metanav { text-align: right; font-size: 0.8em; padding: 0.3em;
margin-bottom: 1em; background: #fafafa; }
.flash { background: #cee5F5; padding: 0.5em;
border: 1px solid #aacbe2; }
.error { background: #f0d6d6; padding: 0.5em; }
Continue with :ref:`tutorial-testing`.

213
docs/tutorial/database.rst Normal file
View file

@ -0,0 +1,213 @@
.. currentmodule:: flask
Define and Access the Database
==============================
The application will use a `SQLite`_ database to store users and posts.
Python comes with built-in support for SQLite in the :mod:`sqlite3`
module.
SQLite is convenient because it doesn't require setting up a separate
database server and is built-in to Python. However, if concurrent
requests try to write to the database at the same time, they will slow
down as each write happens sequentially. Small applications won't notice
this. Once you become big, you may want to switch to a different
database.
The tutorial doesn't go into detail about SQL. If you are not familiar
with it, the SQLite docs describe the `language`_.
.. _SQLite: https://sqlite.org/about.html
.. _language: https://sqlite.org/lang.html
Connect to the Database
-----------------------
The first thing to do when working with a SQLite database (and most
other Python database libraries) is to create a connection to it. Any
queries and operations are performed using the connection, which is
closed after the work is finished.
In web applications this connection is typically tied to the request. It
is created at some point when handling a request, and closed before the
response is sent.
.. code-block:: python
:caption: ``flaskr/db.py``
import sqlite3
import click
from flask import current_app, g
from flask.cli import with_appcontext
def get_db():
if 'db' not in g:
g.db = sqlite3.connect(
current_app.config['DATABASE'],
detect_types=sqlite3.PARSE_DECLTYPES
)
g.db.row_factory = sqlite3.Row
return g.db
def close_db(e=None):
db = g.pop('db', None)
if db is not None:
db.close()
:data:`g` is a special object that is unique for each request. It is
used to store data that might be accessed by multiple functions during
the request. The connection is stored and reused instead of creating a
new connection if ``get_db`` is called a second time in the same
request.
:data:`current_app` is another special object that points to the Flask
application handling the request. Since you used an application factory,
there is no application object when writing the rest of your code.
``get_db`` will be called when the application has been created and is
handling a request, so :data:`current_app` can be used.
:func:`sqlite3.connect` establishes a connection to the file pointed at
by the ``DATABASE`` configuration key. This file doesn't have to exist
yet, and won't until you initialize the database later.
:class:`sqlite3.Row` tells the connection to return rows that behave
like dicts. This allows accessing the columns by name.
``close_db`` checks if a connection was created by checking if ``g.db``
was set. If the connection exists, it is closed. Further down you will
tell your application about the ``close_db`` function in the application
factory so that it is called after each request.
Create the Tables
-----------------
In SQLite, data is stored in *tables* and *columns*. These need to be
created before you can store and retrieve data. Flaskr will store users
in the ``user`` table, and posts in the ``post`` table. Create a file
with the SQL commands needed to create empty tables:
.. code-block:: sql
:caption: ``flaskr/schema.sql``
DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS post;
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE post (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author_id INTEGER NOT NULL,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
title TEXT NOT NULL,
body TEXT NOT NULL,
FOREIGN KEY (author_id) REFERENCES user (id)
);
Add the Python functions that will run these SQL commands to the
``db.py`` file:
.. code-block:: python
:caption: ``flaskr/db.py``
def init_db():
db = get_db()
with current_app.open_resource('schema.sql') as f:
db.executescript(f.read().decode('utf8'))
@click.command('init-db')
@with_appcontext
def init_db_command():
"""Clear the existing data and create new tables."""
init_db()
click.echo('Initialized the database.')
:meth:`open_resource() <Flask.open_resource>` opens a file relative to
the ``flaskr`` package, which is useful since you won't necessarily know
where that location is when deploying the application later. ``get_db``
returns a database connection, which is used to execute the commands
read from the file.
:func:`click.command` defines a command line command called ``init-db``
that calls the ``init_db`` function and shows a success message to the
user. You can read :ref:`cli` to learn more about writing commands.
Register with the Application
-----------------------------
The ``close_db`` and ``init_db_command`` functions need to be registered
with the application instance, otherwise they won't be used by the
application. However, since you're using a factory function, that
instance isn't available when writing the functions. Instead, write a
function that takes an application and does the registration.
.. code-block:: python
:caption: ``flaskr/db.py``
def init_app(app):
app.teardown_appcontext(close_db)
app.cli.add_command(init_db_command)
:meth:`app.teardown_appcontext() <Flask.teardown_appcontext>` tells
Flask to call that function when cleaning up after returning the
response.
:meth:`app.cli.add_command() <click.Group.add_command>` adds a new
command that can be called with the ``flask`` command.
Import and call this function from the factory. Place the new code at
the end of the factory function before returning the app.
.. code-block:: python
:caption: ``flaskr/__init__.py``
def create_app():
app = ...
# existing code omitted
from . import db
db.init_app(app)
return app
Initialize the Database File
----------------------------
Now that ``init-db`` has been registered with the app, it can be called
using the ``flask`` command, similar to the ``run`` command from the
previous page.
.. note::
If you're still running the server from the previous page, you can
either stop the server, or run this command in a new terminal. If
you use a new terminal, remember to change to your project directory
and activate the env as described in :ref:`install-activate-env`.
You'll also need to set ``FLASK_APP`` and ``FLASK_ENV`` as shown on
the previous page.
Run the ``init-db`` command:
.. code-block:: none
flask init-db
Initialized the database.
There will now be a ``flaskr.sqlite`` file in the ``instance`` folder in
your project.
Continue to :doc:`views`.

View file

@ -1,78 +0,0 @@
.. _tutorial-dbcon:
Step 4: Database Connections
----------------------------
Let's continue building our code in the ``flaskr.py`` file.
(Scroll to the end of the page for more about project layout.)
You currently have a function for establishing a database connection with
`connect_db`, but by itself, it is not particularly useful. Creating and
closing database connections all the time is very inefficient, so you will
need to keep it around for longer. Because database connections
encapsulate a transaction, you will need to make sure that only one
request at a time uses the connection. An elegant way to do this is by
utilizing the *application context*.
Flask provides two contexts: the *application context* and the
*request context*. For the time being, all you have to know is that there
are special variables that use these. For instance, the
:data:`~flask.request` variable is the request object associated with
the current request, whereas :data:`~flask.g` is a general purpose
variable associated with the current application context. The tutorial
will cover some more details of this later on.
For the time being, all you have to know is that you can store information
safely on the :data:`~flask.g` object.
So when do you put it on there? To do that you can make a helper
function. The first time the function is called, it will create a database
connection for the current context, and successive calls will return the
already established connection::
def get_db():
"""Opens a new database connection if there is none yet for the
current application context.
"""
if not hasattr(g, 'sqlite_db'):
g.sqlite_db = connect_db()
return g.sqlite_db
Now you know how to connect, but how can you properly disconnect? For
that, Flask provides us with the :meth:`~flask.Flask.teardown_appcontext`
decorator. It's executed every time the application context tears down::
@app.teardown_appcontext
def close_db(error):
"""Closes the database again at the end of the request."""
if hasattr(g, 'sqlite_db'):
g.sqlite_db.close()
Functions marked with :meth:`~flask.Flask.teardown_appcontext` are called
every time the app context tears down. What does this mean?
Essentially, the app context is created before the request comes in and is
destroyed (torn down) whenever the request finishes. A teardown can
happen because of two reasons: either everything went well (the error
parameter will be ``None``) or an exception happened, in which case the error
is passed to the teardown function.
Curious about what these contexts mean? Have a look at the
:ref:`app-context` documentation to learn more.
Continue to :ref:`tutorial-dbinit`.
.. hint:: Where do I put this code?
If you've been following along in this tutorial, you might be wondering
where to put the code from this step and the next. A logical place is to
group these module-level functions together, and put your new
``get_db`` and ``close_db`` functions below your existing
``connect_db`` function (following the tutorial line-by-line).
If you need a moment to find your bearings, take a look at how the `example
source`_ is organized. In Flask, you can put all of your application code
into a single Python module. You don't have to, and if your app :ref:`grows
larger <larger-applications>`, it's a good idea not to.
.. _example source:
https://github.com/pallets/flask/tree/master/examples/flaskr/

View file

@ -1,80 +0,0 @@
.. _tutorial-dbinit:
Step 5: Creating The Database
=============================
As outlined earlier, Flaskr is a database powered application, and more
precisely, it is an application powered by a relational database system. Such
systems need a schema that tells them how to store that information.
Before starting the server for the first time, it's important to create
that schema.
Such a schema could be created by piping the ``schema.sql`` file into the
``sqlite3`` command as follows::
sqlite3 /tmp/flaskr.db < schema.sql
However, the downside of this is that it requires the ``sqlite3`` command
to be installed, which is not necessarily the case on every system. This
also requires that you provide the path to the database, which can introduce
errors.
Instead of the ``sqlite3`` command above, it's a good idea to add a function
to our application that initializes the database for you. To do this, you
can create a function and hook it into a :command:`flask` command that
initializes the database.
Take a look at the code segment below. A good place to add this function,
and command, is just below the ``connect_db`` function in :file:`flaskr.py`::
def init_db():
db = get_db()
with app.open_resource('schema.sql', mode='r') as f:
db.cursor().executescript(f.read())
db.commit()
@app.cli.command('initdb')
def initdb_command():
"""Initializes the database."""
init_db()
print('Initialized the database.')
The ``app.cli.command()`` decorator registers a new command with the
:command:`flask` script. When the command executes, Flask will automatically
create an application context which is bound to the right application.
Within the function, you can then access :attr:`flask.g` and other things as
you might expect. When the script ends, the application context tears down
and the database connection is released.
You will want to keep an actual function around that initializes the database,
though, so that we can easily create databases in unit tests later on. (For
more information see :ref:`testing`.)
The :func:`~flask.Flask.open_resource` method of the application object
is a convenient helper function that will open a resource that the
application provides. This function opens a file from the resource
location (the :file:`flaskr/flaskr` folder) and allows you to read from it.
It is used in this example to execute a script on the database connection.
The connection object provided by SQLite can give you a cursor object.
On that cursor, there is a method to execute a complete script. Finally, you
only have to commit the changes. SQLite3 and other transactional
databases will not commit unless you explicitly tell it to.
Now, in a terminal, from the application root directory :file:`flaskr/` it is
possible to create a database with the :command:`flask` script::
flask initdb
Initialized the database.
.. admonition:: Troubleshooting
If you get an exception later on stating that a table cannot be found, check
that you did execute the ``initdb`` command and that your table names are
correct (singular vs. plural, for example).
Continue with :ref:`tutorial-views`

121
docs/tutorial/deploy.rst Normal file
View file

@ -0,0 +1,121 @@
Deploy to Production
====================
This part of the tutorial assumes you have a server that you want to
deploy your application to. It gives an overview of how to create the
distribution file and install it, but won't go into specifics about
what server or software to use. You can set up a new environment on your
development computer to try out the instructions below, but probably
shouldn't use it for hosting a real public application. See
:doc:`/deploying/index` for a list of many different ways to host your
application.
Build and Install
-----------------
When you want to deploy your application elsewhere, you build a
distribution file. The current standard for Python distribution is the
*wheel* format, with the ``.whl`` extension. Make sure the wheel library
is installed first:
.. code-block:: none
pip install wheel
Running ``setup.py`` with Python gives you a command line tool to issue
build-related commands. The ``bdist_wheel`` command will build a wheel
distribution file.
.. code-block:: none
python setup.py bdist_wheel
You can find the file in ``dist/flaskr-1.0.0-py3-none-any.whl``. The
file name is the name of the project, the version, and some tags about
the file can install.
Copy this file to another machine,
:ref:`set up a new virtualenv <install-create-env>`, then install the
file with ``pip``.
.. code-block:: none
pip install flaskr-1.0.0-py3-none-any.whl
Pip will install your project along with its dependencies.
Since this is a different machine, you need to run ``init-db`` again to
create the database in the instance folder.
.. code-block:: none
export FLASK_APP=flaskr
flask init-db
When Flask detects that it's installed (not in editable mode), it uses
a different directory for the instance folder. You can find it at
``venv/var/flaskr-instance`` instead.
Configure the Secret Key
------------------------
In the beginning of the tutorial that you gave a default value for
:data:`SECRET_KEY`. This should be changed to some random bytes in
production. Otherwise, attackers could use the public ``'dev'`` key to
modify the session cookie, or anything else that uses the secret key.
You can use the following command to output a random secret key:
.. code-block:: none
python -c 'import os; print(os.urandom(16))'
b'_5#y2L"F4Q8z\n\xec]/'
Create the ``config.py`` file in the instance folder, which the factory
will read from if it exists. Copy the generated value into it.
.. code-block:: python
:caption: ``venv/var/flaskr-instance/config.py``
SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/'
You can also set any other necessary configuration here, although
``SECRET_KEY`` is the only one needed for Flaskr.
Run with a Production Server
----------------------------
When running publicly rather than in development, you should not use the
built-in development server (``flask run``). The development server is
provided by Werkzeug for convenience, but is not designed to be
particularly efficient, stable, or secure.
Instead, use a production WSGI server. For example, to use `Waitress`_,
first install it in the virtual environment:
.. code-block:: none
pip install waitress
You need to tell Waitress about your application, but it doesn't use
``FLASK_APP`` like ``flask run`` does. You need to tell it to import and
call the application factory to get an application object.
.. code-block:: none
waitress-serve --call 'flaskr:create_app'
Serving on http://0.0.0.0:8080
See :doc:`/deploying/index` for a list of many different ways to host
your application. Waitress is just an example, chosen for the tutorial
because it supports both Windows and Linux. There are many more WSGI
servers and deployment options that you may choose for your project.
.. _Waitress: https://docs.pylonsproject.org/projects/waitress/
Continue to :doc:`next`.

177
docs/tutorial/factory.rst Normal file
View file

@ -0,0 +1,177 @@
.. currentmodule:: flask
Application Setup
=================
A Flask application is an instance of the :class:`Flask` class.
Everything about the application, such as configuration and URLs, will
be registered with this class.
The most straightforward way to create a Flask application is to create
a global :class:`Flask` instance directly at the top of your code, like
how the "Hello, World!" example did on the previous page. While this is
simple and useful in some cases, it can cause some tricky issues as the
project grows.
Instead of creating a :class:`Flask` instance globally, you will create
it inside a function. This function is known as the *application
factory*. Any configuration, registration, and other setup the
application needs will happen inside the function, then the application
will be returned.
The Application Factory
-----------------------
It's time to start coding! Create the ``flaskr`` directory and add the
``__init__.py`` file. The ``__init__.py`` serves double duty: it will
contain the application factory, and it tells Python that the ``flaskr``
directory should be treated as a package.
.. code-block:: none
mkdir flaskr
.. code-block:: python
:caption: ``flaskr/__init__.py``
import os
from flask import Flask
def create_app(test_config=None):
# create and configure the app
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY='dev',
DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'),
)
if test_config is None:
# load the instance config, if it exists, when not testing
app.config.from_pyfile('config.py', silent=True)
else:
# load the test config if passed in
app.config.from_mapping(test_config)
# ensure the instance folder exists
try:
os.makedirs(app.instance_path)
except OSError:
pass
# a simple page that says hello
@app.route('/hello')
def hello():
return 'Hello, World!'
return app
``create_app`` is the application factory function. You'll add to it
later in the tutorial, but it already does a lot.
#. ``app = Flask(__name__, instance_relative_config=True)`` creates the
:class:`Flask` instance.
* ``__name__`` is the name of the current Python module. The app
needs to know where it's located to set up some paths, and
``__name__`` is a convenient way to tell it that.
* ``instance_relative_config=True`` tells the app that
configuration files are relative to the
:ref:`instance folder <instance-folders>`. The instance folder
is located outside the ``flaskr`` package and can hold local
data that shouldn't be committed to version control, such as
configuration secrets and the database file.
#. :meth:`app.config.from_mapping() <Config.from_mapping>` sets
some default configuration that the app will use:
* :data:`SECRET_KEY` is used by Flask and extensions to keep data
safe. It's set to ``'dev'`` to provide a convenient value
during development, but it should be overridden with a random
value when deploying.
* ``DATABASE`` is the path where the SQLite database file will be
saved. It's under
:attr:`app.instance_path <Flask.instance_path>`, which is the
path that Flask has chosen for the instance folder. You'll learn
more about the database in the next section.
#. :meth:`app.config.from_pyfile() <Config.from_pyfile>` overrides
the default configuration with values taken from the ``config.py``
file in the instance folder if it exists. For example, when
deploying, this can be used to set a real ``SECRET_KEY``.
* ``test_config`` can also be passed to the factory, and will be
used instead of the instance configuration. This is so the tests
you'll write later in the tutorial can be configured
independently of any development values you have configured.
#. :func:`os.makedirs` ensures that
:attr:`app.instance_path <Flask.instance_path>` exists. Flask
doesn't create the instance folder automatically, but it needs to be
created because your project will create the SQLite database file
there.
#. :meth:`@app.route() <Flask.route>` creates a simple route so you can
see the application working before getting into the rest of the
tutorial. It creates a connection between the URL ``/hello`` and a
function that returns a response, the string ``'Hello, World!'`` in
this case.
Run The Application
-------------------
Now you can run your application using the ``flask`` command. From the
terminal, tell Flask where to find your application, then run it in
development mode.
Development mode shows an interactive debugger whenever a page raises an
exception, and restarts the server whenever you make changes to the
code. You can leave it running and just reload the browser page as you
follow the tutorial.
For Linux and Mac:
.. code-block:: none
export FLASK_APP=flaskr
export FLASK_ENV=development
flask run
For Windows cmd, use ``set`` instead of ``export``:
.. code-block:: none
set FLASK_APP=flaskr
set FLASK_ENV=development
flask run
For Windows PowerShell, use ``$env:`` instead of ``export``:
.. code-block:: none
$env:FLASK_APP = "flaskr"
$env:FLASK_ENV = "development"
flask run
You'll see output similar to this:
.. code-block:: none
* Serving Flask app "flaskr"
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 855-212-761
Visit http://127.0.0.1:5000/hello in a browser and you should see the
"Hello, World!" message. Congratulations, you're now running your Flask
web application!
Continue to :doc:`database`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View file

@ -1,31 +0,0 @@
.. _tutorial-folders:
Step 0: Creating The Folders
============================
It is recommended to install your Flask application within a virtualenv. Please
read the :ref:`installation` section to set up your environment.
Now that you have installed Flask, you will need to create the folders required
for this tutorial. Your directory structure will look like this::
/flaskr
/flaskr
/static
/templates
The application will be installed and run as Python package. This is the
recommended way to install and run Flask applications. You will see exactly
how to run ``flaskr`` later on in this tutorial.
For now go ahead and create the applications directory structure. In the next
few steps you will be creating the database schema as well as the main module.
As a quick side note, the files inside of the :file:`static` folder are
available to users of the application via HTTP. This is the place where CSS and
JavaScript files go. Inside the :file:`templates` folder, Flask will look for
`Jinja2`_ templates. You will see examples of this later on.
For now you should continue with :ref:`tutorial-schema`.
.. _Jinja2: http://jinja.pocoo.org/

View file

@ -3,31 +3,64 @@
Tutorial
========
Learn by example to develop an application with Python and Flask.
In this tutorial, we will create a simple blogging application. It only
supports one user, only allows text entries, and has no feeds or comments.
While very simple, this example still features everything you need to get
started. In addition to Flask, we will use SQLite for the database, which is
built-in to Python, so there is nothing else you need.
If you want the full source code in advance or for comparison, check out
the `example source`_.
.. _example source: https://github.com/pallets/flask/tree/master/examples/flaskr/
.. toctree::
:maxdepth: 2
:caption: Contents:
:maxdepth: 1
introduction
folders
schema
setup
packaging
dbcon
dbinit
views
templates
css
testing
layout
factory
database
views
templates
static
blog
install
tests
deploy
next
This tutorial will walk you through creating a basic blog application
called Flaskr. Users will be able to register, log in, create posts,
and edit or delete their own posts. You will be able to package and
install the application on other computers.
.. image:: flaskr_index.png
:align: center
:class: screenshot
:alt: screenshot of index page
It's assumed that you're already familiar with Python. The `official
tutorial`_ in the Python docs is a great way to learn or review first.
.. _official tutorial: https://docs.python.org/3/tutorial/
While it's designed to give a good starting point, the tutorial doesn't
cover all of Flask's features. Check out the :ref:`quickstart` for an
overview of what Flask can do, then dive into the docs to find out more.
The tutorial only uses what's provided by Flask and Python. In another
project, you might decide to use :ref:`extensions` or other libraries to
make some tasks simpler.
.. image:: flaskr_login.png
:align: center
:class: screenshot
:alt: screenshot of login page
Flask is flexible. It doesn't require you to use any particular project
or code layout. However, when first starting, it's helpful to use a more
structured approach. This means that the tutorial will require a bit of
boilerplate up front, but it's done to avoid many common pitfalls that
new developers encounter, and it creates a project that's easy to expand
on. Once you become more comfortable with Flask, you can step out of
this structure and take full advantage of Flask's flexibility.
.. image:: flaskr_edit.png
:align: center
:class: screenshot
:alt: screenshot of login page
:gh:`The tutorial project is available as an example in the Flask
repository <examples/tutorial>`, if you want to compare your project
with the final product as you follow the tutorial.
Continue to :doc:`layout`.

113
docs/tutorial/install.rst Normal file
View file

@ -0,0 +1,113 @@
Make the Project Installable
============================
Making your project installable means that you can build a
*distribution* file and install that in another environment, just like
you installed Flask in your project's environment. This makes deploying
your project the same as installing any other library, so you're using
all the standard Python tools to manage everything.
Installing also comes with other benefits that might not be obvious from
the tutorial or as a new Python user, including:
* Currently, Python and Flask understand how to use the ``flaskr``
package only because you're running from your project's directory.
Installing means you can import it no matter where you run from.
* You can manage your project's dependencies just like other packages
do, so ``pip install yourproject.whl`` installs them.
* Test tools can isolate your test environment from your development
environment.
.. note::
This is being introduced late in the tutorial, but in your future
projects you should always start with this.
Describe the Project
--------------------
The ``setup.py`` file describes your project and the files that belong
to it.
.. code-block:: python
:caption: ``setup.py``
from setuptools import find_packages, setup
setup(
name='flaskr',
version='1.0.0',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=[
'flask',
],
)
``packages`` tells Python what package directories (and the Python files
they contain) to include. ``find_packages()`` finds these directories
automatically so you don't have to type them out. To include other
files, such as the static and templates directories,
``include_package_data`` is set. Python needs another file named
``MANIFEST.in`` to tell what this other data is.
.. code-block:: none
:caption: ``MANIFEST.in``
include flaskr/schema.sql
graft flaskr/static
graft flaskr/templates
global-exclude *.pyc
This tells Python to copy everything in the ``static`` and ``templates``
directories, and the ``schema.sql`` file, but to exclude all bytecode
files.
See the `official packaging guide`_ for another explanation of the files
and options used.
.. _official packaging guide: https://packaging.python.org/tutorials/distributing-packages/
Install the Project
-------------------
Use ``pip`` to install your project in the virtual environment.
.. code-block:: none
pip install -e .
This tells pip to find ``setup.py`` in the current directory and install
it in *editable* or *development* mode. Editable mode means that as you
make changes to your local code, you'll only need to re-install if you
change the metadata about the project, such as its dependencies.
You can observe that the project is now installed with ``pip list``.
.. code-block:: none
pip list
Package Version Location
-------------- --------- ----------------------------------
click 6.7
Flask 1.0
flaskr 1.0.0 /home/user/Projects/flask-tutorial
itsdangerous 0.24
Jinja2 2.10
MarkupSafe 1.0
pip 9.0.3
setuptools 39.0.1
Werkzeug 0.14.1
wheel 0.30.0
Nothing changes from how you've been running your project so far.
``FLASK_APP`` is still set to ``flaskr`` and ``flask run`` still runs
the application.
Continue to :doc:`tests`.

View file

@ -1,39 +0,0 @@
.. _tutorial-introduction:
Introducing Flaskr
==================
This tutorial will demonstrate a blogging application named Flaskr, but feel
free to choose your own less Web-2.0-ish name ;) Essentially, it will do the
following things:
1. Let the user sign in and out with credentials specified in the
configuration. Only one user is supported.
2. When the user is logged in, they can add new entries to the page
consisting of a text-only title and some HTML for the text. This HTML
is not sanitized because we trust the user here.
3. The index page shows all entries so far in reverse chronological order
(newest on top) and the user can add new ones from there if logged in.
SQLite3 will be used directly for this application because it's good enough
for an application of this size. For larger applications, however,
it makes a lot of sense to use `SQLAlchemy`_, as it handles database
connections in a more intelligent way, allowing you to target different
relational databases at once and more. You might also want to consider
one of the popular NoSQL databases if your data is more suited for those.
.. warning::
If you're following the tutorial from a specific version of the docs, be
sure to check out the same tag in the repository, otherwise the tutorial
may be different than the example.
Here is a screenshot of the final application:
.. image:: ../_static/flaskr.png
:align: center
:class: screenshot
:alt: screenshot of the final application
Continue with :ref:`tutorial-folders`.
.. _SQLAlchemy: https://www.sqlalchemy.org/

110
docs/tutorial/layout.rst Normal file
View file

@ -0,0 +1,110 @@
Project Layout
==============
Create a project directory and enter it:
.. code-block:: none
mkdir flask-tutorial
cd flask-tutorial
Then follow the :doc:`installation instructions </installation>` to set
up a Python virtual environment and install Flask for your project.
The tutorial will assume you're working from the ``flask-tutorial``
directory from now on. The file names at the top of each code block are
relative to this directory.
----
A Flask application can be as simple as a single file.
.. code-block:: python
:caption: ``hello.py``
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello, World!'
However, as a project get bigger, it becomes overwhelming to keep all
the code in one file. Python projects use *packages* to organize code
into multiple modules that can be imported where needed, and the
tutorial will do this as well.
The project directory will contain:
* ``flaskr/``, a Python package containing your application code and
files.
* ``tests/``, a directory containing test modules.
* ``venv/``, a Python virtual environment where Flask and other
dependencies are installed.
* Installation files telling Python how to install your project.
* Version control config, such as `git`_. You should make a habit of
using some type of version control for all your projects, no matter
the size.
* Any other project files you might add in the future.
.. _git: https://git-scm.com/
By the end, your project layout will look like this:
.. code-block:: none
/home/user/Projects/flask-tutorial
├── flaskr/
│   ├── __init__.py
│   ├── db.py
│   ├── schema.sql
│   ├── auth.py
│   ├── blog.py
│   ├── templates/
│   │ ├── base.html
│   │ ├── auth/
│   │ │   ├── login.html
│   │ │   └── register.html
│   │ └── blog/
│   │ ├── create.html
│   │ ├── index.html
│   │ └── update.html
│   └── static/
│      └── style.css
├── tests/
│   ├── conftest.py
  ├── data.sql
│   ├── test_factory.py
│   ├── test_db.py
│  ├── test_auth.py
│  └── test_blog.py
├── venv/
├── setup.py
└── MANIFEST.in
If you're using version control, the following files that are generated
while running your project should be ignored. There may be other files
based on the editor you use. In general, ignore files that you didn't
write. For example, with git:
.. code-block:: none
:caption: ``.gitignore``
venv/
*.pyc
__pycache__/
instance/
.pytest_cache/
.coverage
htmlcov/
dist/
build/
*.egg-info/
Continue to :doc:`factory`.

38
docs/tutorial/next.rst Normal file
View file

@ -0,0 +1,38 @@
Keep Developing!
================
You've learned about quite a few Flask and Python concepts throughout
the tutorial. Go back and review the tutorial and compare your code with
the steps you took to get there. Compare your project to the
:gh:`example project <examples/tutorial>`, which might look a bit
different due to the step-by-step nature of the tutorial.
There's a lot more to Flask than what you've seen so far. Even so,
you're now equipped to start developing your own web applications. Check
out the :ref:`quickstart` for an overview of what Flask can do, then
dive into the docs to keep learning. Flask uses `Jinja`_, `Click`_,
`Werkzeug`_, and `ItsDangerous`_ behind the scenes, and they all have
their own documentation too. You'll also be interested in
:ref:`extensions` which make tasks like working with the database or
validating form data easier and more powerful.
If you want to keep developing your Flaskr project, here are some ideas
for what to try next:
* A detail view to show a single post. Click a post's title to go to
its page.
* Like / unlike a post.
* Comments.
* Tags. Clicking a tag shows all the posts with that tag.
* A search box that filters the index page by name.
* Paged display. Only show 5 posts per page.
* Upload an image to go along with a post.
* Format posts using Markdown.
* An RSS feed of new posts.
Have fun and make awesome applications!
.. _Jinja: https://palletsprojects.com/p/jinja/
.. _Click: https://palletsprojects.com/p/click/
.. _Werkzeug: https://palletsprojects.com/p/werkzeug/
.. _ItsDangerous: https://palletsprojects.com/p/itsdangerous/

View file

@ -1,108 +0,0 @@
.. _tutorial-packaging:
Step 3: Installing flaskr as a Package
======================================
Flask is now shipped with built-in support for `Click`_. Click provides
Flask with enhanced and extensible command line utilities. Later in this
tutorial you will see exactly how to extend the ``flask`` command line
interface (CLI).
A useful pattern to manage a Flask application is to install your app
following the `Python Packaging Guide`_. Presently this involves
creating two new files; :file:`setup.py` and :file:`MANIFEST.in` in the
projects root directory. You also need to add an :file:`__init__.py`
file to make the :file:`flaskr/flaskr` directory a package. After these
changes, your code structure should be::
/flaskr
/flaskr
__init__.py
/static
/templates
flaskr.py
schema.sql
setup.py
MANIFEST.in
Create the ``setup.py`` file for ``flaskr`` with the following content::
from setuptools import setup
setup(
name='flaskr',
packages=['flaskr'],
include_package_data=True,
install_requires=[
'flask',
],
)
When using setuptools, it is also necessary to specify any special files
that should be included in your package (in the :file:`MANIFEST.in`).
In this case, the static and templates directories need to be included,
as well as the schema.
Create the :file:`MANIFEST.in` and add the following lines::
graft flaskr/templates
graft flaskr/static
include flaskr/schema.sql
Next, to simplify locating the application, create the file,
:file:`flaskr/__init__.py` containing only the following import statement::
from .flaskr import app
This import statement brings the application instance into the top-level
of the application package. When it is time to run the application, the
Flask development server needs the location of the app instance. This
import statement simplifies the location process. Without the above
import statement, the export statement a few steps below would need to be
``export FLASK_APP=flaskr.flaskr``.
At this point you should be able to install the application. As usual, it
is recommended to install your Flask application within a `virtualenv`_.
With that said, from the ``flaskr/`` directory, go ahead and install the
application with::
pip install --editable .
The above installation command assumes that it is run within the projects
root directory, ``flaskr/``. The ``editable`` flag allows editing
source code without having to reinstall the Flask app each time you make
changes. The flaskr app is now installed in your virtualenv (see output
of ``pip freeze``).
With that out of the way, you should be able to start up the application.
Do this on Mac or Linux with the following commands in ``flaskr/``::
export FLASK_APP=flaskr
export FLASK_ENV=development
flask run
(In case you are on Windows you need to use ``set`` instead of ``export``).
Exporting ``FLASK_ENV=development`` turns on all development features
such as enabling the interactive debugger.
*Never leave debug mode activated in a production system*, because it will
allow users to execute code on the server!
You will see a message telling you that server has started along with
the address at which you can access it in a browser.
When you head over to the server in your browser, you will get a 404 error
because we don't have any views yet. That will be addressed a little later,
but first, you should get the database working.
.. admonition:: Externally Visible Server
Want your server to be publicly available? Check out the
:ref:`externally visible server <public-server>` section for more
information.
Continue with :ref:`tutorial-dbcon`.
.. _Click: http://click.pocoo.org
.. _Python Packaging Guide: https://packaging.python.org
.. _virtualenv: https://virtualenv.pypa.io

View file

@ -1,25 +0,0 @@
.. _tutorial-schema:
Step 1: Database Schema
=======================
In this step, you will create the database schema. Only a single table is
needed for this application and it will only support SQLite. All you need to do
is put the following contents into a file named :file:`schema.sql` in the
:file:`flaskr/flaskr` folder:
.. sourcecode:: sql
drop table if exists entries;
create table entries (
id integer primary key autoincrement,
title text not null,
'text' text not null
);
This schema consists of a single table called ``entries``. Each row in
this table has an ``id``, a ``title``, and a ``text``. The ``id`` is an
automatically incrementing integer and a primary key, the other two are
strings that must not be null.
Continue with :ref:`tutorial-setup`.

View file

@ -1,101 +0,0 @@
.. _tutorial-setup:
Step 2: Application Setup Code
==============================
Next, we will create the application module, :file:`flaskr.py`. Just like the
:file:`schema.sql` file you created in the previous step, this file should be
placed inside of the :file:`flaskr/flaskr` folder.
For this tutorial, all the Python code we use will be put into this file
(except for one line in ``__init__.py``, and any testing or optional files you
decide to create).
The first several lines of code in the application module are the needed import
statements. After that there will be a few lines of configuration code.
For small applications like ``flaskr``, it is possible to drop the configuration
directly into the module. However, a cleaner solution is to create a separate
``.py`` file, load that, and import the values from there.
Here are the import statements (in :file:`flaskr.py`)::
import os
import sqlite3
from flask import (Flask, request, session, g, redirect, url_for, abort,
render_template, flash)
The next couple lines will create the actual application instance and
initialize it with the config from the same file in :file:`flaskr.py`::
app = Flask(__name__) # create the application instance :)
app.config.from_object(__name__) # load config from this file , flaskr.py
# Load default config and override config from an environment variable
app.config.update(
DATABASE=os.path.join(app.root_path, 'flaskr.db'),
SECRET_KEY=b'_5#y2L"F4Q8z\n\xec]/',
USERNAME='admin',
PASSWORD='default'
)
app.config.from_envvar('FLASKR_SETTINGS', silent=True)
In the above code, the :class:`~flask.Config` object works similarly to a
dictionary, so it can be updated with new values.
.. admonition:: Database Path
Operating systems know the concept of a current working directory for
each process. Unfortunately, you cannot depend on this in web
applications because you might have more than one application in the
same process.
For this reason the ``app.root_path`` attribute can be used to
get the path to the application. Together with the ``os.path`` module,
files can then easily be found. In this example, we place the
database right next to it.
For a real-world application, it's recommended to use
:ref:`instance-folders` instead.
Usually, it is a good idea to load a separate, environment-specific
configuration file. Flask allows you to import multiple configurations and it
will use the setting defined in the last import. This enables robust
configuration setups. :meth:`~flask.Config.from_envvar` can help achieve
this. ::
app.config.from_envvar('FLASKR_SETTINGS', silent=True)
If you want to do this (not required for this tutorial) simply define the
environment variable :envvar:`FLASKR_SETTINGS` that points to a config file
to be loaded. The silent switch just tells Flask to not complain if no such
environment key is set.
In addition to that, you can use the :meth:`~flask.Config.from_object`
method on the config object and provide it with an import name of a
module. Flask will then initialize the variable from that module. Note
that in all cases, only variable names that are uppercase are considered.
The :data:`SECRET_KEY` is needed to keep the client-side sessions secure.
Choose that key wisely and as hard to guess and complex as possible.
Lastly, add a method that allows for easy connections to the specified
database. ::
def connect_db():
"""Connects to the specific database."""
rv = sqlite3.connect(app.config['DATABASE'])
rv.row_factory = sqlite3.Row
return rv
This can be used to open a connection on request and also from the
interactive Python shell or a script. This will come in handy later.
You can create a simple database connection through SQLite and then tell
it to use the :class:`sqlite3.Row` object to represent rows. This allows
the rows to be treated as if they were dictionaries instead of tuples.
In the next section you will see how to run the application.
Continue with :ref:`tutorial-packaging`.

72
docs/tutorial/static.rst Normal file
View file

@ -0,0 +1,72 @@
Static Files
============
The authentication views and templates work, but they look very plain
right now. Some `CSS`_ can be added to add style to the HTML layout you
constructed. The style won't change, so it's a *static* file rather than
a template.
Flask automatically adds a ``static`` view that takes a path relative
to the ``flaskr/static`` directory and serves it. The ``base.html``
template already has a link to the ``style.css`` file:
.. code-block:: html+jinja
{{ url_for('static', filename='style.css') }}
Besides CSS, other types of static files might be files with JavaScript
functions, or a logo image. They are all placed under the
``flaskr/static`` directory and referenced with
``url_for('static', filename='...')``.
This tutorial isn't focused on how to write CSS, so you can just copy
the following into the ``flaskr/static/style.css`` file:
.. code-block:: css
:caption: ``flaskr/static/style.css``
html { font-family: sans-serif; background: #eee; padding: 1rem; }
body { max-width: 960px; margin: 0 auto; background: white; }
h1 { font-family: serif; color: #377ba8; margin: 1rem 0; }
a { color: #377ba8; }
hr { border: none; border-top: 1px solid lightgray; }
nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; }
nav h1 { flex: auto; margin: 0; }
nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; }
nav ul { display: flex; list-style: none; margin: 0; padding: 0; }
nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; }
.content { padding: 0 1rem 1rem; }
.content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; }
.content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; }
.flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; }
.post > header { display: flex; align-items: flex-end; font-size: 0.85em; }
.post > header > div:first-of-type { flex: auto; }
.post > header h1 { font-size: 1.5em; margin-bottom: 0; }
.post .about { color: slategray; font-style: italic; }
.post .body { white-space: pre-line; }
.content:last-child { margin-bottom: 0; }
.content form { margin: 1em 0; display: flex; flex-direction: column; }
.content label { font-weight: bold; margin-bottom: 0.5em; }
.content input, .content textarea { margin-bottom: 1em; }
.content textarea { min-height: 12em; resize: vertical; }
input.danger { color: #cc2f2e; }
input[type=submit] { align-self: start; min-width: 10em; }
You can find a less compact version of ``style.css`` in the
:gh:`example code <examples/tutorial/flaskr/static/style.css>`.
Go to http://127.0.0.1/auth/login and the page should look like the
screenshot below.
.. image:: flaskr_login.png
:align: center
:class: screenshot
:alt: screenshot of login page
You can read more about CSS from `Mozilla's documentation <CSS_>`_. If
you change a static file, refresh the browser page. If the change
doesn't show up, try clearing your browser's cache.
.. _CSS: https://developer.mozilla.org/docs/Web/CSS
Continue to :doc:`blog`.

View file

@ -1,113 +1,187 @@
.. _tutorial-templates:
.. currentmodule:: flask
Step 7: The Templates
=====================
Templates
=========
Now it is time to start working on the templates. As you may have
noticed, if you make requests with the app running, you will get
an exception that Flask cannot find the templates. The templates
are using `Jinja2`_ syntax and have autoescaping enabled by
default. This means that unless you mark a value in the code with
:class:`~flask.Markup` or with the ``|safe`` filter in the template,
Jinja2 will ensure that special characters such as ``<`` or ``>`` are
escaped with their XML equivalents.
You've written the authentication views for your application, but if
you're running the server and try to go to any of the URLs, you'll see a
``TemplateNotFound`` error. That's because the views are calling
:func:`render_template`, but you haven't written the templates yet.
The template files will be stored in the ``templates`` directory inside
the ``flaskr`` package.
We are also using template inheritance which makes it possible to reuse
the layout of the website in all pages.
Templates are files that contain static data as well as placeholders
for dynamic data. A template is rendered with specific data to produce a
final document. Flask uses the `Jinja`_ template library to render
templates.
Create the follwing three HTML files and place them in the
:file:`templates` folder:
In your application, you will use templates to render `HTML`_ which
will display in the user's browser. In Flask, Jinja is configured to
*autoescape* any data that is rendered in HTML templates. This means
that it's safe to render user input; any characters they've entered that
could mess with the HTML, such as ``<`` and ``>`` will be *escaped* with
*safe* values that look the same in the browser but don't cause unwanted
effects.
.. _Jinja2: http://jinja.pocoo.org/docs/templates
Jinja looks and behaves mostly like Python. Special delimiters are used
to distinguish Jinja syntax from the static data in the template.
Anything between ``{{`` and ``}}`` is an expression that will be output
to the final document. ``{%`` and ``%}`` denotes a control flow
statement like ``if`` and ``for``. Unlike Python, blocks are denoted
by start and end tags rather than indentation since static text within
a block could change indentation.
layout.html
-----------
.. _Jinja: http://jinja.pocoo.org/docs/templates/
.. _HTML: https://developer.mozilla.org/docs/Web/HTML
This template contains the HTML skeleton, the header and a link to log in
(or log out if the user was already logged in). It also displays the
flashed messages if there are any. The ``{% block body %}`` block can be
replaced by a block of the same name (``body``) in a child template.
The :class:`~flask.session` dict is available in the template as well and
you can use that to check if the user is logged in or not. Note that in
Jinja you can access missing attributes and items of objects / dicts which
makes the following code work, even if there is no ``'logged_in'`` key in
the session:
The Base Layout
---------------
.. sourcecode:: html+jinja
Each page in the application will have the same basic layout around a
different body. Instead of writing the entire HTML structure in each
template, each template will *extend* a base template and override
specific sections.
.. code-block:: html+jinja
:caption: ``flaskr/templates/base.html``
<!doctype html>
<title>Flaskr</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
<div class=page>
<title>{% block title %}{% endblock %} - Flaskr</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<nav>
<h1>Flaskr</h1>
<div class=metanav>
{% if not session.logged_in %}
<a href="{{ url_for('login') }}">log in</a>
{% else %}
<a href="{{ url_for('logout') }}">log out</a>
{% endif %}
</div>
{% for message in get_flashed_messages() %}
<div class=flash>{{ message }}</div>
{% endfor %}
{% block body %}{% endblock %}
</div>
show_entries.html
-----------------
This template extends the :file:`layout.html` template from above to display the
messages. Note that the ``for`` loop iterates over the messages we passed
in with the :func:`~flask.render_template` function. Notice that the form is
configured to submit to the `add_entry` view function and use ``POST`` as
HTTP method:
.. sourcecode:: html+jinja
{% extends "layout.html" %}
{% block body %}
{% if session.logged_in %}
<form action="{{ url_for('add_entry') }}" method=post class=add-entry>
<dl>
<dt>Title:
<dd><input type=text size=30 name=title>
<dt>Text:
<dd><textarea name=text rows=5 cols=40></textarea>
<dd><input type=submit value=Share>
</dl>
</form>
{% endif %}
<ul class=entries>
{% for entry in entries %}
<li><h2>{{ entry.title }}</h2>{{ entry.text|safe }}</li>
{% else %}
<li><em>Unbelievable. No entries here so far</em></li>
{% endfor %}
<ul>
{% if g.user %}
<li><span>{{ g.user['username'] }}</span>
<li><a href="{{ url_for('auth.logout') }}">Log Out</a>
{% else %}
<li><a href="{{ url_for('auth.register') }}">Register</a>
<li><a href="{{ url_for('auth.login') }}">Log In</a>
{% endif %}
</ul>
</nav>
<section class="content">
<header>
{% block header %}{% endblock %}
</header>
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% block content %}{% endblock %}
</section>
:data:`g` is automatically available in templates. Based on if
``g.user`` is set (from ``load_logged_in_user``), either the username
and a log out link are displayed, otherwise links to register and log in
are displayed. :func:`url_for` is also automatically available, and is
used to generate URLs to views instead of writing them out manually.
After the page title, and before the content, the template loops over
each message returned by :func:`get_flashed_messages`. You used
:func:`flash` in the views to show error messages, and this is the code
that will display them.
There are three blocks defined here that will be overridden in the other
templates:
#. ``{% block title %}`` will change the title displayed in the
browser's tab and window title.
#. ``{% block header %}`` is similar to ``title`` but will change the
title displayed on the page.
#. ``{% block content %}`` is where the content of each page goes, such
as the login form or a blog post.
The base template is directly in the ``templates`` directory. To keep
the others organized, the templates for a blueprint will be placed in a
directory with the same name as the blueprint.
Register
--------
.. code-block:: html+jinja
:caption: ``flaskr/templates/auth/register.html``
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Register{% endblock %}</h1>
{% endblock %}
login.html
----------
This is the login template, which basically just displays a form to allow
the user to login:
.. sourcecode:: html+jinja
{% extends "layout.html" %}
{% block body %}
<h2>Login</h2>
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
<form action="{{ url_for('login') }}" method=post>
<dl>
<dt>Username:
<dd><input type=text name=username>
<dt>Password:
<dd><input type=password name=password>
<dd><input type=submit value=Login>
</dl>
{% block content %}
<form method="post">
<label for="username">Username</label>
<input name="username" id="username" required>
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
<input type="submit" value="Register">
</form>
{% endblock %}
Continue with :ref:`tutorial-css`.
``{% extends 'base.html' %}`` tells Jinja that this template should
replace the blocks from the base template. All the rendered content must
appear inside ``{% block %}`` tags that override blocks from the base
template.
A useful pattern used here is to place ``{% block title %}`` inside
``{% block header %}``. This will set the title block and then output
the value of it into the header block, so that both the window and page
share the same title without writing it twice.
The ``input`` tags are using the ``required`` attribute here. This tells
the browser not to submit the form until those fields are filled in. If
the user is using an older browser that doesn't support that attribute,
or if they are using something besides a browser to make requests, you
still want to validate the data in the Flask view. It's important to
always fully validate the data on the server, even if the client does
some validation as well.
Log In
------
This is identical to the register template except for the title and
submit button.
.. code-block:: html+jinja
:caption: ``flaskr/templates/auth/login.html``
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Log In{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="username">Username</label>
<input name="username" id="username" required>
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
<input type="submit" value="Log In">
</form>
{% endblock %}
Register A User
---------------
Now that the authentication templates are written, you can register a
user. Make sure the server is still running (``flask run`` if it's not),
then go to http://127.0.0.1:5000/auth/register.
Try clicking the "Register" button without filling out the form and see
that the browser shows an error message. Try removing the ``required``
attributes from the ``register.html`` template and click "Register"
again. Instead of the browser showing an error, the page will reload and
the error from :func:`flash` in the view will be shown.
Fill out a username and password and you'll be redirected to the login
page. Try entering an incorrect username, or the correct username and
incorrect password. If you log in you'll get an error because there's
no ``index`` view to redirect to yet.
Continue to :doc:`static`.

View file

@ -1,96 +0,0 @@
.. _tutorial-testing:
Bonus: Testing the Application
==============================
Now that you have finished the application and everything works as
expected, it's probably not a bad idea to add automated tests to simplify
modifications in the future. The application above is used as a basic
example of how to perform unit testing in the :ref:`testing` section of the
documentation. Go there to see how easy it is to test Flask applications.
Adding tests to flaskr
----------------------
Assuming you have seen the :ref:`testing` section and have either written
your own tests for ``flaskr`` or have followed along with the examples
provided, you might be wondering about ways to organize the project.
One possible and recommended project structure is::
flaskr/
flaskr/
__init__.py
static/
templates/
tests/
test_flaskr.py
setup.py
MANIFEST.in
For now go ahead a create the :file:`tests/` directory as well as the
:file:`test_flaskr.py` file.
Running the tests
-----------------
At this point you can run the tests. Here ``pytest`` will be used.
.. note:: Make sure that ``pytest`` is installed in the same virtualenv
as flaskr. Otherwise ``pytest`` test will not be able to import the
required components to test the application::
pip install -e .
pip install pytest
Run and watch the tests pass, within the top-level :file:`flaskr/`
directory as::
pytest
Testing + setuptools
--------------------
One way to handle testing is to integrate it with ``setuptools``. Here
that requires adding a couple of lines to the :file:`setup.py` file and
creating a new file :file:`setup.cfg`. One benefit of running the tests
this way is that you do not have to install ``pytest``. Go ahead and
update the :file:`setup.py` file to contain::
from setuptools import setup
setup(
name='flaskr',
packages=['flaskr'],
include_package_data=True,
install_requires=[
'flask',
],
setup_requires=[
'pytest-runner',
],
tests_require=[
'pytest',
],
)
Now create :file:`setup.cfg` in the project root (alongside
:file:`setup.py`)::
[aliases]
test=pytest
Now you can run::
python setup.py test
This calls on the alias created in :file:`setup.cfg` which in turn runs
``pytest`` via ``pytest-runner``, as the :file:`setup.py` script has
been called. (Recall the `setup_requires` argument in :file:`setup.py`)
Following the standard rules of test-discovery your tests will be
found, run, and hopefully pass.
This is one possible way to run and manage testing. Here ``pytest`` is
used, but there are other options such as ``nose``. Integrating testing
with ``setuptools`` is convenient because it is not necessary to actually
download ``pytest`` or any other testing framework one might use.

561
docs/tutorial/tests.rst Normal file
View file

@ -0,0 +1,561 @@
.. currentmodule:: flask
Test Coverage
=============
Writing unit tests for your application lets you check that the code
you wrote works the way you expect. Flask provides a test client that
simulates requests to the application and returns the response data.
You should test as much of your code as possible. Code in functions only
runs when the function is called, and code in branches, such as ``if``
blocks, only runs when the condition is met. You want to make sure that
each function is tested with data that covers each branch.
The closer you get to 100% coverage, the more comfortable you can be
that making a change won't unexpectedly change other behavior. However,
100% coverage doesn't guarantee that your application doesn't have bugs.
In particular, it doesn't test how the user interacts with the
application in the browser. Despite this, test coverage is an important
tool to use during development.
.. note::
This is being introduced late in the tutorial, but in your future
projects you should test as you develop.
You'll use `pytest`_ and `coverage`_ to test and measure your code.
Install them both:
.. code-block:: none
pip install pytest coverage
.. _pytest: https://pytest.readthedocs.io/
.. _coverage: https://coverage.readthedocs.io/
Setup and Fixtures
------------------
The test code is located in the ``tests`` directory. This directory is
*next to* the ``flaskr`` package, not inside it. The
``tests/conftest.py`` file contains setup functions called *fixtures*
that each test will use. Tests are in Python modules that start with
``test_``, and each test function in those modules also starts with
``test_``.
Each test will create a new temporary database file and populate some
data that will be used in the tests. Write a SQL file to insert that
data.
.. code-block:: sql
:caption: ``tests/data.sql``
INSERT INTO user (username, password)
VALUES
('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'),
('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79');
INSERT INTO post (title, body, author_id, created)
VALUES
('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00');
The ``app`` fixture will call the factory and pass ``test_config`` to
configure the application and database for testing instead of using your
local development configuration.
.. code-block:: python
:caption: ``tests/conftest.py``
import os
import tempfile
import pytest
from flaskr import create_app
from flaskr.db import get_db, init_db
with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f:
_data_sql = f.read().decode('utf8')
@pytest.fixture
def app():
db_fd, db_path = tempfile.mkstemp()
app = create_app({
'TESTING': True,
'DATABASE': db_path,
})
with app.app_context():
init_db()
get_db().executescript(_data_sql)
yield app
os.close(db_fd)
os.unlink(db_path)
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def runner(app):
return app.test_cli_runner()
:func:`tempfile.mkstemp` creates and opens a temporary file, returning
the file object and the path to it. The ``DATABASE`` path is
overridden so it points to this temporary path instead of the instance
folder. After setting the path, the database tables are created and the
test data is inserted. After the test is over, the temporary file is
closed and removed.
:data:`TESTING` tells Flask that the app is in test mode. Flask changes
some internal behavior so it's easier to test, and other extensions can
also use the flag to make testing them easier.
The ``client`` fixture calls
:meth:`app.test_client() <Flask.test_client>` with the application
object created by the ``app`` fixture. Tests will use the client to make
requests to the application without running the server.
The ``runner`` fixture is similar to ``client``.
:meth:`app.test_cli_runner() <Flask.test_cli_runner>` creates a runner
that can call the Click commands registered with the application.
Pytest uses fixtures by matching their function names with the names
of arguments in the test functions. For example, the ``test_hello``
function you'll write next takes a ``client`` argument. Pytest matches
that with the ``client`` fixture function, calls it, and passes the
returned value to the test function.
Factory
-------
There's not much to test about the factory itself. Most of the code will
be executed for each test already, so if something fails the other tests
will notice.
The only behavior that can change is passing test config. If config is
not passed, there should be some default configuration, otherwise the
configuration should be overridden.
.. code-block:: python
:caption: ``tests/test_factory.py``
from flaskr import create_app
def test_config():
assert not create_app().testing
assert create_app({'TESTING': True}).testing
def test_hello(client):
response = client.get('/hello')
assert response.data == b'Hello, World!'
You added the ``hello`` route as an example when writing the factory at
the beginning of the tutorial. It returns "Hello, World!", so the test
checks that the response data matches.
Database
--------
Within an application context, ``get_db`` should return the same
connection each time it's called. After the context, the connection
should be closed.
.. code-block:: python
:caption: ``tests/test_db.py``
import sqlite3
import pytest
from flaskr.db import get_db
def test_get_close_db(app):
with app.app_context():
db = get_db()
assert db is get_db()
with pytest.raises(sqlite3.ProgrammingError) as e:
db.execute('SELECT 1')
assert 'closed' in str(e)
The ``init-db`` command should call the ``init_db`` function and output
a message.
.. code-block:: python
:caption: ``tests/test_db.py``
def test_init_db_command(runner, monkeypatch):
class Recorder(object):
called = False
def fake_init_db():
Recorder.called = True
monkeypatch.setattr('flaskr.db.init_db', fake_init_db)
result = runner.invoke(args=['init-db'])
assert 'Initialized' in result.output
assert Recorder.called
This test uses Pytest's ``monkeypatch`` fixture to replace the
``init_db`` function with one that records that it's been called. The
``runner`` fixture you wrote above is used to call the ``init-db``
command by name.
Authentication
--------------
For most of the views, a user needs to be logged in. The easiest way to
do this in tests is to make a ``POST`` request to the ``login`` view
with the client. Rather than writing that out every time, you can write
a class with methods to do that, and use a fixture to pass it the client
for each test.
.. code-block:: python
:caption: ``tests/conftest.py``
class AuthActions(object):
def __init__(self, client):
self._client = client
def login(self, username='test', password='test'):
return self._client.post(
'/auth/login',
data={'username': username, 'password': password}
)
def logout(self):
return self._client.get('/auth/logout')
@pytest.fixture
def auth(client):
return AuthActions(client)
With the ``auth`` fixture, you can call ``auth.login()`` in a test to
log in as the ``test`` user, which was inserted as part of the test
data in the ``app`` fixture.
The ``register`` view should render successfully on ``GET``. On ``POST``
with valid form data, it should redirect to the login URL and the user's
data should be in the database. Invalid data should display error
messages.
.. code-block:: python
:caption: ``tests/test_auth.py``
import pytest
from flask import g, session
from flaskr.db import get_db
def test_register(client, app):
assert client.get('/auth/register').status_code == 200
response = client.post(
'/auth/register', data={'username': 'a', 'password': 'a'}
)
assert 'http://localhost/auth/login' == response.headers['Location']
with app.app_context():
assert get_db().execute(
"select * from user where username = 'a'",
).fetchone() is not None
@pytest.mark.parametrize(('username', 'password', 'message'), (
('', '', b'Username is required.'),
('a', '', b'Password is required.'),
('test', 'test', b'already registered'),
))
def test_register_validate_input(client, username, password, message):
response = client.post(
'/auth/register',
data={'username': username, 'password': password}
)
assert message in response.data
:meth:`client.get() <werkzeug.test.Client.get>` makes a ``GET`` request
and returns the :class:`Response` object returned by Flask. Similarly,
:meth:`client.post() <werkzeug.test.Client.post>` makes a ``POST``
request, converting the ``data`` dict into form data.
To test that the page renders successfully, a simple request is made and
checked for a ``200 OK`` :attr:`~Response.status_code`. If
rendering failed, Flask would return a ``500 Internal Server Error``
code.
:attr:`~Response.headers` will have a ``Location`` header with the login
URL when the register view redirects to the login view.
:attr:`~Response.data` contains the body of the response as bytes. If
you expect a certain value to render on the page, check that it's in
``data``. Bytes must be compared to bytes. If you want to compare
Unicode text, use :meth:`get_data(as_text=True) <werkzeug.wrappers.BaseResponse.get_data>`
instead.
``pytest.mark.parametrize`` tells Pytest to run the same test function
with different arguments. You use it here to test different invalid
input and error messages without writing the same code three times.
The tests for the ``login`` view are very similar to those for
``register``. Rather than testing the data in the database,
:data:`session` should have ``user_id`` set after logging in.
.. code-block:: python
:caption: ``tests/test_auth.py``
def test_login(client, auth):
assert client.get('/auth/login').status_code == 200
response = auth.login()
assert response.headers['Location'] == 'http://localhost/'
with client:
client.get('/')
assert session['user_id'] == 1
assert g.user['username'] == 'test'
@pytest.mark.parametrize(('username', 'password', 'message'), (
('a', 'test', b'Incorrect username.'),
('test', 'a', b'Incorrect password.'),
))
def test_login_validate_input(auth, username, password, message):
response = auth.login(username, password)
assert message in response.data
Using ``client`` in a ``with`` block allows accessing context variables
such as :data:`session` after the response is returned. Normally,
accessing ``session`` outside of a request would raise an error.
Testing ``logout`` is the opposite of ``login``. :data:`session` should
not contain ``user_id`` after logging out.
.. code-block:: python
:caption: ``tests/test_auth.py``
def test_logout(client, auth):
auth.login()
with client:
auth.logout()
assert 'user_id' not in session
Blog
----
All the blog views use the ``auth`` fixture you wrote earlier. Call
``auth.login()`` and subsequent requests from the client will be logged
in as the ``test`` user.
The ``index`` view should display information about the post that was
added with the test data. When logged in as the author, there should be
a link to edit the post.
You can also test some more authentication behavior while testing the
``index`` view. When not logged in, each page shows links to log in or
register. When logged in, there's a link to log out.
.. code-block:: python
:caption: ``tests/test_blog.py``
import pytest
from flaskr.db import get_db
def test_index(client, auth):
response = client.get('/')
assert b"Log In" in response.data
assert b"Register" in response.data
auth.login()
response = client.get('/')
assert b'Log Out' in response.data
assert b'test title' in response.data
assert b'by test on 2018-01-01' in response.data
assert b'test\nbody' in response.data
assert b'href="/1/update"' in response.data
A user must be logged in to access the ``create``, ``update``, and
``delete`` views. The logged in user must be the author of the post to
access ``update`` and ``delete``, otherwise a ``403 Forbidden`` status
is returned. If a ``post`` with the given ``id`` doesn't exist,
``update`` and ``delete`` should return ``404 Not Found``.
.. code-block:: python
:caption: ``tests/test_blog.py``
@pytest.mark.parametrize('path', (
'/create',
'/1/update',
'/1/delete',
))
def test_login_required(client, path):
response = client.post(path)
assert response.headers['Location'] == 'http://localhost/auth/login'
def test_author_required(app, client, auth):
# change the post author to another user
with app.app_context():
db = get_db()
db.execute('UPDATE post SET author_id = 2 WHERE id = 1')
db.commit()
auth.login()
# current user can't modify other user's post
assert client.post('/1/update').status_code == 403
assert client.post('/1/delete').status_code == 403
# current user doesn't see edit link
assert b'href="/1/update"' not in client.get('/').data
@pytest.mark.parametrize('path', (
'/2/update',
'/2/delete',
))
def test_exists_required(client, auth, path):
auth.login()
assert client.post(path).status_code == 404
The ``create`` and ``update`` views should render and return a
``200 OK`` status for a ``GET`` request. When valid data is sent in a
``POST`` request, ``create`` should insert the new post data into the
database, and ``update`` should modify the existing data. Both pages
should show an error message on invalid data.
.. code-block:: python
:caption: ``tests/test_blog.py``
def test_create(client, auth, app):
auth.login()
assert client.get('/create').status_code == 200
client.post('/create', data={'title': 'created', 'body': ''})
with app.app_context():
db = get_db()
count = db.execute('SELECT COUNT(id) FROM post').fetchone()[0]
assert count == 2
def test_update(client, auth, app):
auth.login()
assert client.get('/1/update').status_code == 200
client.post('/1/update', data={'title': 'updated', 'body': ''})
with app.app_context():
db = get_db()
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone()
assert post['title'] == 'updated'
@pytest.mark.parametrize('path', (
'/create',
'/1/update',
))
def test_create_update_validate(client, auth, path):
auth.login()
response = client.post(path, data={'title': '', 'body': ''})
assert b'Title is required.' in response.data
The ``delete`` view should redirect to the index URL and the post should
no longer exist in the database.
.. code-block:: python
:caption: ``tests/test_blog.py``
def test_delete(client, auth, app):
auth.login()
response = client.post('/1/delete')
assert response.headers['Location'] == 'http://localhost/'
with app.app_context():
db = get_db()
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone()
assert post is None
Running the Tests
-----------------
Some extra configuration, which is not required but makes running
tests with coverage less verbose, can be added to the project's
``setup.cfg`` file.
.. code-block:: none
:caption: ``setup.cfg``
[tool:pytest]
testpaths = tests
[coverage:run]
branch = True
source =
flaskr
To run the tests, use the ``pytest`` command. It will find and run all
the test functions you've written.
.. code-block:: none
pytest
========================= test session starts ==========================
platform linux -- Python 3.6.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0
rootdir: /home/user/Projects/flask-tutorial, inifile: setup.cfg
collected 23 items
tests/test_auth.py ........ [ 34%]
tests/test_blog.py ............ [ 86%]
tests/test_db.py .. [ 95%]
tests/test_factory.py .. [100%]
====================== 24 passed in 0.64 seconds =======================
If any tests fail, pytest will show the error that was raised. You can
run ``pytest -v`` to get a list of each test function rather than dots.
To measure the code coverage of your tests, use the ``coverage`` command
to run pytest instead of running it directly.
.. code-block:: none
coverage run -m pytest
You can either view a simple coverage report in the terminal:
.. code-block:: none
coverage report
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------
flaskr/__init__.py 21 0 2 0 100%
flaskr/auth.py 54 0 22 0 100%
flaskr/blog.py 54 0 16 0 100%
flaskr/db.py 24 0 4 0 100%
------------------------------------------------------
TOTAL 153 0 44 0 100%
An HTML report allows you to see which lines were covered in each file:
.. code-block:: none
coverage html
This generates files in the ``htmlcov`` directory. Open
``htmlcov/index.html`` in your browser to see the report.
Continue to :doc:`deploy`.

View file

@ -1,118 +1,301 @@
.. _tutorial-views:
.. currentmodule:: flask
Step 6: The View Functions
==========================
Blueprints and Views
====================
Now that the database connections are working, you can start writing the
view functions. You will need four of them; Show Entries, Add New Entry,
Login and Logout. Add the following code snipets to :file:`flaskr.py`.
A view function is the code you write to respond to requests to your
application. Flask uses patterns to match the incoming request URL to
the view that should handle it. The view returns data that Flask turns
into an outgoing response. Flask can also go the other direction and
generate a URL to a view based on its name and arguments.
Show Entries
------------
This view shows all the entries stored in the database. It listens on the
root of the application and will select title and text from the database.
The one with the highest id (the newest entry) will be on top. The rows
returned from the cursor look a bit like dictionaries because we are using
the :class:`sqlite3.Row` row factory.
Create a Blueprint
------------------
The view function will pass the entries to the :file:`show_entries.html`
template and return the rendered one::
A :class:`Blueprint` is a way to organize a group of related views and
other code. Rather than registering views and other code directly with
an application, they are registered with a blueprint. Then the blueprint
is registered with the application when it is available in the factory
function.
@app.route('/')
def show_entries():
db = get_db()
cur = db.execute('select title, text from entries order by id desc')
entries = cur.fetchall()
return render_template('show_entries.html', entries=entries)
Flaskr will have two blueprints, one for authentication functions and
one for the blog posts functions. The code for each blueprint will go
in a separate module. Since the blog needs to know about authentication,
you'll write the authentication one first.
Add New Entry
-------------
.. code-block:: python
:caption: ``flaskr/auth.py``
This view lets the user add new entries if they are logged in. This only
responds to ``POST`` requests; the actual form is shown on the
`show_entries` page. If everything worked out well, it will
:func:`~flask.flash` an information message to the next request and
redirect back to the `show_entries` page::
import functools
@app.route('/add', methods=['POST'])
def add_entry():
if not session.get('logged_in'):
abort(401)
db = get_db()
db.execute('insert into entries (title, text) values (?, ?)',
[request.form['title'], request.form['text']])
db.commit()
flash('New entry was successfully posted')
return redirect(url_for('show_entries'))
from flask import (
Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash
Note that this view checks that the user is logged in (that is, if the
`logged_in` key is present in the session and ``True``).
from flaskr.db import get_db
.. admonition:: Security Note
bp = Blueprint('auth', __name__, url_prefix='/auth')
Be sure to use question marks when building SQL statements, as done in the
example above. Otherwise, your app will be vulnerable to SQL injection when
you use string formatting to build SQL statements.
See :ref:`sqlite3` for more.
This creates a :class:`Blueprint` named ``'auth'``. Like the application
object, the blueprint needs to know where it's defined, so ``__name__``
is passed as the second argument. The ``url_prefix`` will be prepended
to all the URLs associated with the blueprint.
Login and Logout
----------------
Import and register the blueprint from the factory using
:meth:`app.register_blueprint() <Flask.register_blueprint>`. Place the
new code at the end of the factory function before returning the app.
These functions are used to sign the user in and out. Login checks the
username and password against the ones from the configuration and sets the
`logged_in` key for the session. If the user logged in successfully, that
key is set to ``True``, and the user is redirected back to the `show_entries`
page. In addition, a message is flashed that informs the user that he or
she was logged in successfully. If an error occurred, the template is
notified about that, and the user is asked again::
.. code-block:: python
:caption: ``flaskr/__init__.py``
@app.route('/login', methods=['GET', 'POST'])
def login():
error = None
def create_app():
app = ...
# existing code omitted
from . import auth
app.register_blueprint(auth.bp)
return app
The authentication blueprint will have views to register new users and
to log in and log out.
The First View: Register
------------------------
When the user visits the ``/auth/register`` URL, the ``register`` view
will return `HTML`_ with a form for them to fill out. When they submit
the form, it will validate their input and either show the form again
with an error message or create the new user and go to the login page.
.. _HTML: https://developer.mozilla.org/docs/Web/HTML
For now you will just write the view code. On the next page, you'll
write templates to generate the HTML form.
.. code-block:: python
:caption: ``flaskr/auth.py``
@bp.route('/register', methods=('GET', 'POST'))
def register():
if request.method == 'POST':
if request.form['username'] != app.config['USERNAME']:
error = 'Invalid username'
elif request.form['password'] != app.config['PASSWORD']:
error = 'Invalid password'
else:
session['logged_in'] = True
flash('You were logged in')
return redirect(url_for('show_entries'))
return render_template('login.html', error=error)
username = request.form['username']
password = request.form['password']
db = get_db()
error = None
The `logout` function, on the other hand, removes that key from the session
again. There is a neat trick here: if you use the :meth:`~dict.pop` method
of the dict and pass a second parameter to it (the default), the method
will delete the key from the dictionary if present or do nothing when that
key is not in there. This is helpful because now it is not necessary to
check if the user was logged in.
if not username:
error = 'Username is required.'
elif not password:
error = 'Password is required.'
elif db.execute(
'SELECT id FROM user WHERE username = ?', (username,)
).fetchone() is not None:
error = 'User {} is already registered.'.format(username)
::
if error is None:
db.execute(
'INSERT INTO user (username, password) VALUES (?, ?)',
(username, generate_password_hash(password))
)
db.commit()
return redirect(url_for('auth.login'))
@app.route('/logout')
flash(error)
return render_template('auth/register.html')
Here's what the ``register`` view function is doing:
#. :meth:`@bp.route <Blueprint.route>` associates the URL ``/register``
with the ``register`` view function. When Flask receives a request
to ``/auth/register``, it will call the ``register`` view and use
the return value as the response.
#. If the user submitted the form,
:attr:`request.method <Request.method>` will be ``'POST'``. In this
case, start validating the input.
#. :attr:`request.form <Request.form>` is a special type of
:class:`dict` mapping submitted form keys and values. The user will
input their ``username`` and ``password``.
#. Validate that ``username`` and ``password`` are not empty.
#. Validate that ``username`` is not already registered by querying the
database and checking if a result is returned.
:meth:`db.execute <sqlite3.Connection.execute>` takes a SQL query
with ``?`` placeholders for any user input, and a tuple of values
to replace the placeholders with. The database library will take
care of escaping the values so you are not vulnerable to a
*SQL injection attack*.
:meth:`~sqlite3.Cursor.fetchone` returns one row from the query.
If the query returned no results, it returns ``None``. Later,
:meth:`~sqlite3.Cursor.fetchall` is used, which returns a list of
all results.
#. If validation succeeds, insert the new user data into the database.
For security, passwords should never be stored in the database
directly. Instead,
:func:`~werkzeug.security.generate_password_hash` is used to
securely hash the password, and that hash is stored. Since this
query modifies data, :meth:`db.commit() <sqlite3.Connection.commit>`
needs to be called afterwards to save the changes.
#. After storing the user, they are redirected to the login page.
:func:`url_for` generates the URL for the login view based on its
name. This is preferable to writing the URL directly as it allows
you to change the URL later without changing all code that links to
it. :func:`redirect` generates a redirect response to the generated
URL.
#. If validation fails, the error is shown to the user. :func:`flash`
stores messages that can be retrieved when rendering the template.
#. When the user initially navigates to ``auth/register``, or
there was an validation error, an HTML page with the registration
form should be shown. :func:`render_template` will render a template
containing the HTML, which you'll write in the next step of the
tutorial.
Login
-----
This view follows the same pattern as the ``register`` view above.
.. code-block:: python
:caption: ``flaskr/auth.py``
@bp.route('/login', methods=('GET', 'POST'))
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
db = get_db()
error = None
user = db.execute(
'SELECT * FROM user WHERE username = ?', (username,)
).fetchone()
if user is None:
error = 'Incorrect username.'
elif not check_password_hash(user['password'], password):
error = 'Incorrect password.'
if error is None:
session.clear()
session['user_id'] = user['id']
return redirect(url_for('index'))
flash(error)
return render_template('auth/login.html')
There are a few differences from the ``register`` view:
#. The user is queried first and stored in a variable for later use.
#. :func:`~werkzeug.security.check_password_hash` hashes the submitted
password in the same way as the stored hash and securely compares
them. If they match, the password is valid.
#. :data:`session` is a :class:`dict` that stores data across requests.
When validation succeeds, the user's ``id`` is stored in a new
session. The data is stored in a *cookie* that is sent to the
browser, and the browser then sends it back with subsequent requests.
Flask securely *signs* the data so that it can't be tampered with.
Now that the user's ``id`` is stored in the :data:`session`, it will be
available on subsequent requests. At the beginning of each request, if
a user is logged in their information should be loaded and made
available to other views.
.. code-block:: python
:caption: ``flaskr/auth.py``
@bp.before_app_request
def load_logged_in_user():
user_id = session.get('user_id')
if user_id is None:
g.user = None
else:
g.user = get_db().execute(
'SELECT * FROM user WHERE id = ?', (user_id,)
).fetchone()
:meth:`bp.before_app_request() <Blueprint.before_app_request>` registers
a function that runs before the view function, no matter what URL is
requested. ``load_logged_in_user`` checks if a user id is stored in the
:data:`session` and gets that user's data from the database, storing it
on :data:`g.user <g>`, which lasts for the length of the request. If
there is no user id, or if the id doesn't exist, ``g.user`` will be
``None``.
Logout
------
To log out, you need to remove the user id from the :data:`session`.
Then ``load_logged_in_user`` won't load a user on subsequent requests.
.. code-block:: python
:caption: ``flaskr/auth.py``
@bp.route('/logout')
def logout():
session.pop('logged_in', None)
flash('You were logged out')
return redirect(url_for('show_entries'))
.. admonition:: Security Note
Passwords should never be stored in plain text in a production
system. This tutorial uses plain text passwords for simplicity. If you
plan to release a project based off this tutorial out into the world,
passwords should be both `hashed and salted`_ before being stored in a
database or file.
Fortunately, there are Flask extensions for the purpose of
hashing passwords and verifying passwords against hashes, so adding
this functionality is fairly straight forward. There are also
many general python libraries that can be used for hashing.
You can find a list of recommended Flask extensions
`here <http://flask.pocoo.org/extensions/>`_
session.clear()
return redirect(url_for('index'))
Continue with :ref:`tutorial-templates`.
Require Authentication in Other Views
-------------------------------------
.. _hashed and salted: https://blog.codinghorror.com/youre-probably-storing-passwords-incorrectly/
Creating, editing, and deleting blog posts will require a user to be
logged in. A *decorator* can be used to check this for each view it's
applied to.
.. code-block:: python
:caption: ``flaskr/auth.py``
def login_required(view):
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None:
return redirect(url_for('auth.login'))
return view(**kwargs)
return wrapped_view
This decorator returns a new view function that wraps the original view
it's applied to. The new function checks if a user is loaded and
redirects to the login page otherwise. If a user is loaded the original
view is called and continues normally. You'll use this decorator when
writing the blog views.
Endpoints and URLs
------------------
The :func:`url_for` function generates the URL to a view based on a name
and arguments. The name associated with a view is also called the
*endpoint*, and by default it's the same as the name of the view
function.
For example, the ``hello()`` view that was added to the app
factory earlier in the tutorial has the name ``'hello'`` and can be
linked to with ``url_for('hello')``. If it took an argument, which
you'll see later, it would be linked to using
``url_for('hello', who='World')``.
When using a blueprint, the name of the blueprint is prepended to the
name of the function, so the endpoint for the ``login`` function you
wrote above is ``'auth.login'`` because you added it to the ``'auth'``
blueprint.
Continue to :doc:`templates`.