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

.. currentmodule:: flask.views

This page introduces using the :class:`View` and :class:`MethodView`
classes to write class-based views.

A class-based view is a class that acts as a view function. Because it
is a class, different instances of the class can be created with
different arguments, to change the behavior of the view. This is also
known as generic, reusable, or pluggable views.

An example of where this is useful is defining a class that creates an
API based on the database model it is initialized with.

For more complex API behavior and customization, look into the various
API extensions for Flask.


Basic Reusable View
-------------------

Let's walk through an example converting a view function to a view
class. We start with a view function that queries a list of users then
renders a template to show the list.

.. code-block:: python

    @app.route("/users/")
    def user_list():
        users = User.query.all()
        return render_template("users.html", users=users)

This works for the user model, but let's say you also had more models
that needed list pages. You'd need to write another view function for
each model, even though the only thing that would change is the model
and template name.

Instead, you can write a :class:`View` subclass that will query a model
and render a template. As the first step, we'll convert the view to a
class without any customization.

.. code-block:: python

    from flask.views import View

    class UserList(View):
        def dispatch_request(self):
            users = User.query.all()
            return render_template("users.html", objects=users)

    app.add_url_rule("/users/", view_func=UserList.as_view("user_list"))

The :meth:`View.dispatch_request` method is the equivalent of the view
function. Calling :meth:`View.as_view` method will create a view
function that can be registered on the app with its
:meth:`~flask.Flask.add_url_rule` method. The first argument to
``as_view`` is the name to use to refer to the view with
:func:`~flask.url_for`.

.. note::

    You can't decorate the class with ``@app.route()`` the way you'd
    do with a basic view function.

Next, we need to be able to register the same view class for different
models and templates, to make it more useful than the original function.
The class will take two arguments, the model and template, and store
them on ``self``. Then ``dispatch_request`` can reference these instead
of hard-coded values.

.. code-block:: python

    class ListView(View):
        def __init__(self, model, template):
            self.model = model
            self.template = template

        def dispatch_request(self):
            items = self.model.query.all()
            return render_template(self.template, items=items)

Remember, we create the view function with ``View.as_view()`` instead of
creating the class directly. Any extra arguments passed to ``as_view``
are then passed when creating the class. Now we can register the same
view to handle multiple models.

.. code-block:: python

    app.add_url_rule(
        "/users/",
        view_func=ListView.as_view("user_list", User, "users.html"),
    )
    app.add_url_rule(
        "/stories/",
        view_func=ListView.as_view("story_list", Story, "stories.html"),
    )


URL Variables
-------------

Any variables captured by the URL are passed as keyword arguments to the
``dispatch_request`` method, as they would be for a regular view
function.

.. code-block:: python

    class DetailView(View):
        def __init__(self, model):
            self.model = model
            self.template = f"{model.__name__.lower()}/detail.html"

        def dispatch_request(self, id)
            item = self.model.query.get_or_404(id)
            return render_template(self.template, item=item)

    app.add_url_rule(
        "/users/<int:id>",
        view_func=DetailView.as_view("user_detail", User)
    )


View Lifetime and ``self``
--------------------------

By default, a new instance of the view class is created every time a
request is handled. This means that it is safe to write other data to
``self`` during the request, since the next request will not see it,
unlike other forms of global state.

However, if your view class needs to do a lot of complex initialization,
doing it for every request is unnecessary and can be inefficient. To
avoid this, set :attr:`View.init_every_request` to ``False``, which will
only create one instance of the class and use it for every request. In
this case, writing to ``self`` is not safe. If you need to store data
during the request, use :data:`~flask.g` instead.

In the ``ListView`` example, nothing writes to ``self`` during the
request, so it is more efficient to create a single instance.

.. code-block:: python

    class ListView(View):
        init_every_request = False

        def __init__(self, model, template):
            self.model = model
            self.template = template

        def dispatch_request(self):
            items = self.model.query.all()
            return render_template(self.template, items=items)

Different instances will still be created each for each ``as_view``
call, but not for each request to those views.


View Decorators
---------------

The view class itself is not the view function. View decorators need to
be applied to the view function returned by ``as_view``, not the class
itself. Set :attr:`View.decorators` to a list of decorators to apply.

.. code-block:: python

    class UserList(View):
        decorators = [cache(minutes=2), login_required]

    app.add_url_rule('/users/', view_func=UserList.as_view())

If you didn't set ``decorators``, you could apply them manually instead.
This is equivalent to:

.. code-block:: python

    view = UserList.as_view("users_list")
    view = cache(minutes=2)(view)
    view = login_required(view)
    app.add_url_rule('/users/', view_func=view)

Keep in mind that order matters. If you're used to ``@decorator`` style,
this is equivalent to:

.. code-block:: python

    @app.route("/users/")
    @login_required
    @cache(minutes=2)
    def user_list():
        ...


Method Hints
------------

A common pattern is to register a view with ``methods=["GET", "POST"]``,
then check ``request.method == "POST"`` to decide what to do. Setting
:attr:`View.methods` is equivalent to passing the list of methods to
``add_url_rule`` or ``route``.

.. code-block:: python

    class MyView(View):
        methods = ["GET", "POST"]

        def dispatch_request(self):
            if request.method == "POST":
                ...
            ...

    app.add_url_rule('/my-view', view_func=MyView.as_view('my-view'))

This is equivalent to the following, except further subclasses can
inherit or change the methods.

.. code-block:: python

    app.add_url_rule(
        "/my-view",
        view_func=MyView.as_view("my-view"),
        methods=["GET", "POST"],
    )


Method Dispatching and APIs
---------------------------

For APIs it can be helpful to use a different function for each HTTP
method. :class:`MethodView` extends the basic :class:`View` to dispatch
to different methods of the class based on the request method. Each HTTP
method maps to a method of the class with the same (lowercase) name.

:class:`MethodView` automatically sets :attr:`View.methods` based on the
methods defined by the class. It even knows how to handle subclasses
that override or define other methods.

We can make a generic ``ItemAPI`` class that provides get (detail),
patch (edit), and delete methods for a given model. A ``GroupAPI`` can
provide get (list) and post (create) methods.

.. code-block:: python

    from flask.views import MethodView

    class ItemAPI(MethodView):
        init_every_request = False

        def __init__(self, model):
            self.model = model
            self.validator = generate_validator(model)

        def _get_item(self, id):
            return self.model.query.get_or_404(id)

        def get(self, id):
            item = self._get_item(id)
            return jsonify(item.to_json())

        def patch(self, id):
            item = self._get_item(id)
            errors = self.validator.validate(item, request.json)

            if errors:
                return jsonify(errors), 400

            item.update_from_json(request.json)
            db.session.commit()
            return jsonify(item.to_json())

        def delete(self, id):
            item = self._get_item(id)
            db.session.delete(item)
            db.session.commit()
            return "", 204

    class GroupAPI(MethodView):
        init_every_request = False

        def __init__(self, model):
            self.model = model
            self.validator = generate_validator(model, create=True)

        def get(self):
            items = self.model.query.all()
            return jsonify([item.to_json() for item in items])

        def post(self):
            errors = self.validator.validate(request.json)

            if errors:
                return jsonify(errors), 400

            db.session.add(self.model.from_json(request.json))
            db.session.commit()
            return jsonify(item.to_json())

    def register_api(app, model, name):
        item = ItemAPI.as_view(f"{name}-item", model)
        group = GroupAPI.as_view(f"{name}-group", model)
        app.add_url_rule(f"/{name}/<int:id>", view_func=item)
        app.add_url_rule(f"/{name}/", view_func=group)

    register_api(app, User, "users")
    register_api(app, Story, "stories")

This produces the following views, a standard REST API!

================= ========== ===================
URL               Method     Description
----------------- ---------- -------------------
``/users/``       ``GET``    List all users
``/users/``       ``POST``   Create a new user
``/users/<id>``   ``GET``    Show a single user
``/users/<id>``   ``PATCH``  Update a user
``/users/<id>``   ``DELETE`` Delete a user
``/stories/``     ``GET``    List all stories
``/stories/``     ``POST``   Create a new story
``/stories/<id>`` ``GET``    Show a single story
``/stories/<id>`` ``PATCH``  Update a story
``/stories/<id>`` ``DELETE`` Delete a story
================= ========== ===================