Spec-Zone.ru › Django 6.0

Встроенные обобщённые представления на основе классов

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

Обобщённые представления Django были разработаны, чтобы облегчить эту задачу. Они выделяют некоторые распространённые идиомы и шаблоны, встречающиеся при разработке представлений, и абстрагируют их, чтобы вы могли быстро создавать типовые представления данных, не написав слишком много кода.

Мы можем распознавать некоторые распространённые задачи, например отображение списка объектов, и написать код, который отображает список любых объектов. Затем нужную модель можно передать в URLconf в качестве дополнительного аргумента.

Django предоставляет обобщённые представления для следующих задач:

  • Отображение страниц со списком и подробностями для одного объекта. Если бы мы создавали приложение для управления конференциями, то TalkListView и RegisteredUserListView были бы примерами представлений со списком. Страница отдельного доклада — это пример того, что мы называем представлением с «подробностями».
  • Отображение объектов, связанных с датами, на архивных страницах по годам, месяцам и дням, а также на соответствующих страницах с подробностями и страницах с «последними» объектами.
  • Предоставление пользователям возможности создавать, обновлять и удалять объекты — с авторизацией или без неё.

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

Расширение обобщённых представлений

Использование обобщённых представлений, несомненно, может значительно ускорить разработку. Однако в большинстве проектов наступает момент, когда возможностей обобщённых представлений становится недостаточно. Действительно, самый распространённый вопрос, который задают новые разработчики Django, — как заставить обобщённые представления обрабатывать более широкий набор ситуаций.

Это одна из причин, по которым обобщённые представления были переработаны для выпуска 1.3: раньше они представляли собой функции-представления с ошеломляющим количеством параметров; теперь же вместо передачи большого объёма настроек в URLconf рекомендуется расширять обобщённые представления путём создания подклассов и переопределения их атрибутов или методов.

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

Дополнительные примеры обобщённых представлений доступны в некоторых сторонних приложениях; при необходимости вы также можете написать собственные.

Обобщённые представления объектов

TemplateView безусловно полезно, однако обобщённые представления Django особенно хороши для отображения данных из вашей базы данных. Поскольку это очень распространённая задача, Django включает несколько встроенных обобщённых представлений, помогающих создавать представления со списком объектов и подробностями об объектах.

Начнём с нескольких примеров отображения списка объектов или отдельного объекта.

Мы будем использовать следующие модели:

# models.py
from django.db import models


class Publisher(models.Model):
    name = models.CharField(max_length=30)
    address = models.CharField(max_length=50)
    city = models.CharField(max_length=60)
    state_province = models.CharField(max_length=30)
    country = models.CharField(max_length=50)
    website = models.URLField()

    class Meta:
        ordering = ["-name"]

    def __str__(self):
        return self.name


class Author(models.Model):
    salutation = models.CharField(max_length=10)
    name = models.CharField(max_length=200)
    email = models.EmailField()
    headshot = models.ImageField(upload_to="author_headshots")

    def __str__(self):
        return self.name


class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField("Author")
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
    publication_date = models.DateField()

Теперь нужно определить представление:

# views.py
from django.views.generic import ListView
from books.models import Publisher


class PublisherListView(ListView):
    model = Publisher

Наконец, подключим это представление в urls:

# urls.py
from django.urls import path
from books.views import PublisherListView

urlpatterns = [
    path("publishers/", PublisherListView.as_view()),
]

Вот и весь код Python, который нам нужно написать. Однако ещё потребуется написать шаблон. Мы могли бы явно указать представлению, какой шаблон использовать, добавив атрибут template_name в представление, но если шаблон явно не задан, Django определит его по имени объекта. В данном случае подразумеваемым шаблоном будет "books/publisher_list.html": часть «books» берётся из имени приложения, в котором определена модель, а часть «publisher» — это имя модели в нижнем регистре.

Примечание

Таким образом, если (например) параметр APP_DIRS бэкенда DjangoTemplates установлен в True в TEMPLATES, шаблон может находиться по пути: /path/to/project/books/templates/books/publisher_list.html

Этот шаблон будет отрендерен с контекстом, содержащим переменную object_list, в которой находятся все объекты издателей. Шаблон может выглядеть так:

{% extends "base.html" %}

{% block content %}
    <h2>Publishers</h2>
    <ul>
        {% for publisher in object_list %}
            <li>{{ publisher.name }}</li>
        {% endfor %}
    </ul>
{% endblock %}

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

Удобный контекст шаблона

Вы, возможно, заметили, что в нашем примере шаблона со списком издателей все издатели хранятся в переменной с именем object_list. Это вполне работает, но не слишком «удобно» для авторов шаблонов: им нужно «просто знать», что здесь речь идёт об издателях.

Если вы работаете с объектом модели, это уже сделано за вас. При работе с объектом или queryset Django может заполнить контекст, используя имя класса модели в нижнем регистре. Эта переменная добавляется к стандартной записи object_list, но содержит точно те же данные, то есть publisher_list.

Если такой вариант всё ещё вам не подходит, можно вручную задать имя переменной контекста. Атрибут context_object_name обобщённого представления определяет, какую переменную контекста использовать:

# views.py
from django.views.generic import ListView
from books.models import Publisher


class PublisherListView(ListView):
    model = Publisher
    context_object_name = "my_favorite_publishers"

Всегда полезно задавать понятное значение context_object_name. Ваши коллеги, создающие шаблоны, будут вам благодарны.

Добавление дополнительных данных в контекст

Часто требуется отобразить дополнительную информацию помимо той, которую предоставляет обобщённое представление. Например, представьте, что на странице с подробной информацией об издателе нужно показать список всех его книг. Обобщённое представление DetailView передаёт издателя в контекст, но как получить дополнительную информацию в этом шаблоне?

Ответ — создать подкласс DetailView и предоставить собственную реализацию метода get_context_data. Реализация по умолчанию добавляет отображаемый объект в шаблон, но её можно переопределить, чтобы передать дополнительные данные:

from django.views.generic import DetailView
from books.models import Book, Publisher


class PublisherDetailView(DetailView):
    model = Publisher

    def get_context_data(self, **kwargs):
        # Call the base implementation first to get a context
        context = super().get_context_data(**kwargs)
        # Add in a QuerySet of all the books
        context["book_list"] = Book.objects.all()
        return context

Примечание

Как правило, get_context_data объединяет данные контекста всех родительских классов с данными текущего класса. Чтобы сохранить это поведение в собственных классах при изменении контекста, обязательно вызовите get_context_data суперкласса. Если никакие два класса не пытаются определить один и тот же ключ, это даст ожидаемый результат. Однако если какой-либо класс переопределяет ключ, заданный родительскими классами (после вызова super), то и всем его подклассам потребуется явно задать этот ключ после super, если они хотят гарантированно переопределить значения всех родительских классов. Если у вас возникли трудности, изучите порядок разрешения методов вашего представления.

Также следует учитывать, что данные контекста из обобщённых представлений на основе классов переопределяют данные, предоставленные процессорами контекста; пример см. в разделе get_context_data().

Просмотр подмножеств объектов

Теперь рассмотрим подробнее аргумент model, который мы использовали до сих пор. Аргумент model, указывающий модель базы данных, с которой будет работать представление, доступен во всех обобщённых представлениях, работающих с одним объектом или набором объектов. Однако аргумент model — не единственный способ указать объекты, с которыми будет работать представление: список объектов можно также задать с помощью аргумента queryset:

from django.views.generic import DetailView
from books.models import Publisher


class PublisherDetailView(DetailView):
    context_object_name = "publisher"
    queryset = Publisher.objects.all()

Указание model = Publisher — это сокращённая форма записи queryset = Publisher.objects.all(). Однако, используя queryset для определения отфильтрованного списка объектов, можно точнее указать, какие объекты будут видны в представлении (дополнительную информацию об объектах QuerySet см. в разделе Создание запросов, а полное описание — в справочнике по представлениям на основе классов).

Например, можно упорядочить список книг по дате публикации, расположив сначала самые новые:

from django.views.generic import ListView
from books.models import Book


class BookListView(ListView):
    queryset = Book.objects.order_by("-publication_date")
    context_object_name = "book_list"

Это довольно простой пример, но он хорошо иллюстрирует идею. Обычно требуется не только изменить порядок объектов. Если нужно показать список книг определённого издателя, можно воспользоваться тем же приёмом:

from django.views.generic import ListView
from books.models import Book


class AcmeBookListView(ListView):
    context_object_name = "book_list"
    queryset = Book.objects.filter(publisher__name="ACME Publishing")
    template_name = "books/acme_list.html"

Обратите внимание, что вместе с отфильтрованным queryset мы также используем пользовательское имя шаблона. Если этого не сделать, обобщённое представление будет использовать тот же шаблон, что и для обычного списка объектов, а это может быть не тем, что нам нужно.

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

Примечание

Если при запросе /books/acme/ вы получаете ошибку 404, проверьте, действительно ли у вас есть Publisher с именем ‘ACME Publishing’. Для этого случая у обобщённых представлений есть параметр allow_empty. Подробнее см. в справочнике по представлениям на основе классов.

Динамическая фильтрация

Ещё одна распространённая задача — отфильтровать объекты на странице со списком по некоторому параметру из URL. Ранее мы жёстко задавали имя издателя в URLconf, но что, если мы хотим написать представление, отображающее все книги произвольного издателя?

К счастью, у ListView есть метод get_queryset(), который можно переопределить. По умолчанию он возвращает значение атрибута queryset, но мы можем добавить в него дополнительную логику.

Ключевой момент здесь в том, что при вызове представлений на основе классов различные полезные данные сохраняются в self; кроме запроса (self.request), здесь также хранятся позиционные (self.args) и именованные (self.kwargs) аргументы, полученные согласно URLconf.

Здесь у нас есть URLconf с одной захваченной группой:

# urls.py
from django.urls import path
from books.views import PublisherBookListView

urlpatterns = [
    path("books/<publisher>/", PublisherBookListView.as_view()),
]

Затем напишем само представление PublisherBookListView:

# views.py
from django.shortcuts import get_object_or_404
from django.views.generic import ListView
from books.models import Book, Publisher


class PublisherBookListView(ListView):
    template_name = "books/books_by_publisher.html"

    def get_queryset(self):
        self.publisher = get_object_or_404(Publisher, name=self.kwargs["publisher"])
        return Book.objects.filter(publisher=self.publisher)

Использовать get_queryset для добавления логики при выборе queryset так же удобно, как и эффективно. Например, при желании мы могли бы использовать self.request.user для фильтрации по текущему пользователю или для реализации другой, более сложной логики.

Заодно можно добавить издателя в контекст, чтобы использовать его в шаблоне:

# ...


def get_context_data(self, **kwargs):
    # Call the base implementation first to get a context
    context = super().get_context_data(**kwargs)
    # Add in the publisher
    context["publisher"] = self.publisher
    return context

Выполнение дополнительных действий

Последний распространённый шаблон, который мы рассмотрим, — выполнение дополнительных действий до или после вызова обобщённого представления.

Представьте, что в модели Author есть поле last_accessed, с помощью которого мы отслеживаем, когда кто-либо в последний раз просматривал сведения об этом авторе:

# models.py
from django.db import models


class Author(models.Model):
    salutation = models.CharField(max_length=10)
    name = models.CharField(max_length=200)
    email = models.EmailField()
    headshot = models.ImageField(upload_to="author_headshots")
    last_accessed = models.DateTimeField()

Обобщённый класс DetailView ничего не знает об этом поле, но мы снова можем написать пользовательское представление, чтобы обновлять его.

Сначала нужно добавить в URLconf подробное представление автора, указывающее на пользовательское представление:

from django.urls import path
from books.views import AuthorDetailView

urlpatterns = [
    # ...
    path("authors/<int:pk>/", AuthorDetailView.as_view(), name="author-detail"),
]

Затем напишем новое представление: метод get_object получает объект, поэтому мы переопределим его и обернём вызов:

from django.utils import timezone
from django.views.generic import DetailView
from books.models import Author


class AuthorDetailView(DetailView):
    queryset = Author.objects.all()

    def get_object(self):
        obj = super().get_object()
        # Record the last accessed date
        obj.last_accessed = timezone.now()
        obj.save()
        return obj

Примечание

В этом URLconf используется именованная группа pk — это имя по умолчанию, которое DetailView использует для получения значения первичного ключа, по которому фильтруется queryset.

Если вы хотите назвать группу иначе, задайте для представления атрибут pk_url_kwarg.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/6.0/topics/class-based-views/generic-display/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API