Представления на основе классов

На этой странице рассказывается об использовании классов View и MethodView для написания представлений на основе классов.

Представление на основе класса - это класс, который действует как функция представления. Поскольку это класс, различные экземпляры класса могут быть созданы с различными аргументами, чтобы изменить поведение представления. Это также известно как общие, многократно используемые или подключаемые представления.

Примером того, как это может быть полезно, является определение класса, который создает API на основе модели базы данных, с которой он инициализируется.

Для более сложного поведения и настройки API обратите внимание на различные расширения API для Flask.

Базовое многоразовое представление

Давайте рассмотрим пример преобразования функции представления в класс представления. Мы начнем с функции представления, которая запрашивает список пользователей, а затем отображает шаблон для показа списка.

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

Это работает для модели пользователя, но, допустим, у вас есть еще несколько моделей, которым нужны страницы со списком. Вам придется написать еще одну функцию представления для каждой модели, хотя единственное, что изменится, это имя модели и шаблона.

Вместо этого можно написать подкласс View, который будет запрашивать модель и выводить шаблон. В качестве первого шага мы преобразуем представление в класс без какой-либо настройки.

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

Метод View.dispatch_request() является эквивалентом функции представления. Вызов метода View.as_view() создаст функцию представления, которая может быть зарегистрирована в приложении с помощью метода add_url_rule(). Первым аргументом метода as_view является имя, которое будет использоваться для ссылки на представление с помощью url_for().

Примечание

Вы не можете украсить класс с помощью @app.route() так, как вы бы сделали это с базовой функцией представления.

Далее нам нужно иметь возможность регистрировать один и тот же класс представления для разных моделей и шаблонов, чтобы сделать его более полезным, чем оригинальная функция. Класс будет принимать два аргумента, модель и шаблон, и хранить их в self. Затем dispatch_request может ссылаться на них вместо жестко закодированных значений.

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)

Помните, что мы создаем функцию представления с помощью View.as_view() вместо того, чтобы создавать класс напрямую. Любые дополнительные аргументы, переданные в as_view, затем передаются при создании класса. Теперь мы можем зарегистрировать одно и то же представление для работы с несколькими моделями.

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

Любые переменные, захваченные URL, передаются в качестве аргументов ключевых слов в метод dispatch_request, как это было бы для обычной функции представления.

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

Просмотр времени жизни и self

По умолчанию новый экземпляр класса view создается каждый раз, когда обрабатывается запрос. Это означает, что во время запроса можно безопасно записывать другие данные в self, поскольку следующий запрос их не увидит, в отличие от других форм глобального состояния.

Однако если ваш класс представления должен выполнять много сложной инициализации, делать это для каждого запроса не нужно и может быть неэффективно. Чтобы избежать этого, установите View.init_every_request в False, что позволит создать только один экземпляр класса и использовать его для каждого запроса. В этом случае запись в self небезопасна. Если вам нужно хранить данные во время запроса, используйте вместо этого g.

В примере ListView во время запроса в self ничего не записывается, поэтому эффективнее создать один экземпляр.

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)

Различные экземпляры по-прежнему будут создаваться для каждого вызова as_view, но не для каждого запроса к этим представлениям.

Посмотреть декораторов

Сам класс представления не является функцией представления. Декораторы представления должны применяться к функции представления, возвращаемой as_view, а не к самому классу. Установите View.decorators в список декораторов для применения.

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

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

Если вы не задали decorators, вы можете применить их вручную. Это эквивалентно:

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

Помните, что порядок имеет значение. Если вы привыкли к стилю @decorator, это эквивалентно:

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

Подсказки по методу

Общим шаблоном является регистрация представления с помощью methods=["GET", "POST"], затем проверка request.method == "POST", чтобы решить, что делать. Установка View.methods эквивалентна передаче списка методов в add_url_rule или 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'))

Это эквивалентно следующему, за исключением того, что последующие подклассы могут наследовать или изменять методы.

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

Диспетчеризация методов и API

Для API может быть полезно использовать отдельную функцию для каждого метода HTTP. MethodView расширяет базовую View для отправки к различным методам класса в зависимости от метода запроса. Каждый метод HTTP сопоставляется с методом класса с тем же именем (в нижнем регистре).

MethodView автоматически устанавливает View.methods на основе методов, определенных классом. Он даже знает, как обращаться с подклассами, которые переопределяют или определяют другие методы.

Мы можем создать общий класс ItemAPI, который предоставляет методы get (детализация), patch (редактирование) и delete для данной модели. Класс GroupAPI может предоставлять методы get (список) и post (создание).

from flask.views import MethodView

class ItemAPI(MethodView):
    init_every_request = False

    def __init__(self, model):
        self.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")

В результате получаются следующие представления, стандартный REST API!

URL

Метод

Описание

/users/

GET

Список всех пользователей

/users/

POST

Создание нового пользователя

/users/<id>

GET

Показать одного пользователя

/users/<id>

PATCH

Обновление пользователя

/users/<id>

DELETE

Удалить пользователя

/stories/

GET

Список всех историй

/stories/

POST

Создайте новую историю

/stories/<id>

GET

Показать одну историю

/stories/<id>

PATCH

Обновление истории

/stories/<id>

DELETE

Удалить историю

Вернуться на верх