mirror of https://github.com/pallets/flask.git
325 lines
10 KiB
ReStructuredText
325 lines
10 KiB
ReStructuredText
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
|
|
================= ========== ===================
|