Vistas basadas en las clases

Esta página presenta el uso de las clases View y MethodView para escribir vistas basadas en clases.

Una vista basada en una clase es una clase que actúa como una función de la vista. Como es una clase, se pueden crear diferentes instancias de la clase con diferentes argumentos, para cambiar el comportamiento de la vista. Esto también se conoce como vistas genéricas, reutilizables o enchufables.

Un ejemplo de que esto es útil es definir una clase que crea una API basada en el modelo de base de datos con el que se inicializa.

Para un comportamiento más complejo de la API y su personalización, busque las diversas extensiones de la API para Flask.

Vista básica reutilizable

Veamos un ejemplo de conversión de una función de vista a una clase de vista. Comenzamos con una función de vista que consulta una lista de usuarios y luego renderiza una plantilla para mostrar la lista.

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

Esto funciona para el modelo de usuario, pero digamos que también tienes más modelos que necesitan páginas de lista. Tendrías que escribir otra función de vista para cada modelo, aunque lo único que cambiaría es el nombre del modelo y de la plantilla.

En su lugar, puedes escribir una subclase View que consultará un modelo y renderizará una plantilla. Como primer paso, convertiremos la vista en una clase sin ninguna personalización.

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"))

El método View.dispatch_request() es el equivalente a la función de vista. La llamada al método View.as_view() creará una función de vista que puede registrarse en la aplicación con su método add_url_rule(). El primer argumento de as_view es el nombre a utilizar para referirse a la vista con url_for().

Nota

No puedes decorar la clase con @app.route() como lo harías con una función de vista básica.

A continuación, tenemos que ser capaces de registrar la misma clase de vista para diferentes modelos y plantillas, para que sea más útil que la función original. La clase tomará dos argumentos, el modelo y la plantilla, y los almacenará en self. Entonces dispatch_request puede hacer referencia a estos en lugar de valores codificados.

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)

Recuerda que creamos la función vista con View.as_view() en lugar de crear la clase directamente. Cualquier argumento extra que se pase a as_view se pasa al crear la clase. Ahora podemos registrar la misma vista para manejar múltiples modelos.

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"),
)

Variables de la URL

Cualquier variable capturada por la URL se pasa como argumentos de palabra clave al método dispatch_request, como lo harían en una función de vista normal.

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

Ver Vida útil y self

Por defecto, se crea una nueva instancia de la clase view cada vez que se gestiona una petición. Esto significa que es seguro escribir otros datos en self durante la petición, ya que la siguiente petición no lo verá, a diferencia de otras formas de estado global.

Sin embargo, si tu clase de vista necesita hacer mucha inicialización compleja, hacerla para cada petición es innecesario y puede ser ineficiente. Para evitar esto, establece View.init_every_request a False, que sólo creará una instancia de la clase y la utilizará para cada petición. En este caso, escribir en self no es seguro. Si necesitas almacenar datos durante la petición, utiliza g en su lugar.

En el ejemplo de ListView, nada se escribe en self durante la petición, por lo que es más eficiente crear una única instancia.

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)

Se seguirán creando diferentes instancias para cada llamada a as_view, pero no para cada petición a esas vistas.

Decoradores de vista

La clase de la vista en sí misma no es la función de la vista. Los decoradores de la vista deben aplicarse a la función de la vista devuelta por as_view, no a la propia clase. Establece View.decorators a una lista de decoradores a aplicar.

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

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

Si no ha establecido decoradores, puede aplicarlos manualmente en su lugar. Esto equivale a:

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

Ten en cuenta que el orden es importante. Si estás acostumbrado al estilo @decorador, esto equivale a:

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

Consejos sobre el método

Un patrón común es registrar una vista con methods=["GET", "POST"], y luego comprobar request.method == "POST" para decidir qué hacer. Establecer View.methods es equivalente a pasar la lista de métodos a add_url_rule o route.

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'))

Esto es equivalente a lo siguiente, excepto que otras subclases pueden heredar o cambiar los métodos.

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

Envío de métodos y APIs

Para las APIs puede ser útil utilizar una función diferente para cada método HTTP. MethodView extiende la clase básica View para enviar a diferentes métodos de la clase basados en el método de solicitud. Cada método HTTP se asigna a un método de la clase con el mismo nombre (en minúsculas).

MethodView establece automáticamente View.methods basándose en los métodos definidos por la clase. Incluso sabe cómo manejar las subclases que sobrescriben o definen otros métodos.

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.

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")

Esto produce las siguientes vistas, ¡una API REST estándar!

URL

Método

Descripción

/users/

GET

Listar todos los usuarios

/users/

POST

Crear un nuevo usuario

/users/<id>

GET

Mostrar un solo usuario

/users/<id>

PATCH

Actualizar un usuario

/users/<id>

DELETE

Eliminar un usuario

/stories/

GET

Lista de todas las historias

/stories/

POST

Crear una nueva historia

/stories/<id>

GET

Mostrar una sola historia

/stories/<id>

PATCH

Actualizar una historia

/stories/<id>

DELETE

Eliminar una historia