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

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`.