forked from orbit-oss/flask
rewrite tutorial docs and example
This commit is contained in:
parent
16d83d6bb4
commit
c3dd7b8e4c
103 changed files with 3327 additions and 2224 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue