File: views.rst

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

Blueprints and Views
====================

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.


Create a Blueprint
------------------

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.

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.

.. code-block:: python
    :caption: ``flaskr/auth.py``

    import functools

    from flask import (
        Blueprint, flash, g, redirect, render_template, request, session, url_for
    )
    from werkzeug.security import check_password_hash, generate_password_hash

    from flaskr.db import get_db

    bp = Blueprint('auth', __name__, url_prefix='/auth')

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.

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 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':
            username = request.form['username']
            password = request.form['password']
            db = get_db()
            error = None

            if not username:
                error = 'Username is required.'
            elif not password:
                error = 'Password is required.'

            if error is None:
                try:
                    db.execute(
                        "INSERT INTO user (username, password) VALUES (?, ?)",
                        (username, generate_password_hash(password)),
                    )
                    db.commit()
                except db.IntegrityError:
                    error = f"User {username} is already registered."
                else:
                    return redirect(url_for("auth.login"))

            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.

#.  If validation succeeds, insert the new user data into the database.

    -   :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*.

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

    -   An :exc:`sqlite3.IntegrityError` will occur if the username
        already exists, which should be shown to the user as another
        validation error.

#.  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 a 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.

    :meth:`~sqlite3.Cursor.fetchone` returns one row from the query.
    If the query returned no results, it returns ``None``. Later,
    :meth:`~sqlite3.Cursor.fetchall` will be used, which returns a list
    of all results.

#.  :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.clear()
        return redirect(url_for('index'))


Require Authentication in Other Views
-------------------------------------

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