Django 5 для начинающих

Прогресс по курсу:  9/1004

6.3 Разработка представлений на основе классов
1 из 1 шага пройден

Разработка представлений на основе классов

Мы разработали приложение blog, используя представления на основе функций. Такие представления просты и мощны, но Django также позволяет разрабатывать представления с использованием классов.

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

Django предоставляет базовые классы-представления, которые можно использовать для реализации своих собственных представлений. Все они наследуют от класса View, который служит для диспетчеризации HTTP-методов и других распространенных функциональностей.

Зачем использовать представления на основе классов

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

Представления на основе классов позволяют:

  • организовывать исходный код, относящийся к HTTP-методам, таким как GET, POST или PUT, в отдельные методы, не используя ветвление по условию;

  • использовать множественное наследование, чтобы создавать реиспользуемые классы-представления (также именуемые примесями, примесными классами или миксинами).

Использование представления на основе класса для отображения списка постов

В целях понимания того, как писать представления на основе классов, мы создадим новое представление на основе класса, эквивалентное представлению post_list.

Мы создадим класс, который будет наследовать от предлагаемого веб-фреймворком Django типового представления ListView. Представление ListView позволяет перечислять объекты любого типа.

Отредактируйте файл views.py приложения blog, добавив следующий ниже исходный код:

from django.views.generic import ListView


class PostListView(ListView):
    """
    Альтернативное представление списка постов
    """
    queryset = Post.published.all()
    context_object_name = 'posts'
    paginate_by = 3
    template_name = 'blog/post/list.html'


Представление PostListView похоже на разработанное ранее представление post_list.

Мы имплементировали представление, основанное на классе, которое наследует от класса ListView, при этом определив его со следующими атрибутами:

  • атрибут queryset используется для того, чтобы иметь конкретно-прикладной набор запросов QuerySet, не извлекая все объекты. Вместо определения атрибута queryset мы могли бы указать model=Post, и Django сформировал бы для нас типовой набор запросов Post.objects.all();

  • контекстная переменная posts используется для результатов запроса. Если не указано имя контекстного объекта context_object_name, то по умолчанию используется переменная object_list;

  • в атрибуте paginate_by задается постраничная разбивка результатов с возвратом трех объектов на страницу;

  • конкретно-прикладной шаблон используется для прорисовки страницы шаблоном template_name. Если шаблон не задан, то по умолчанию ListView будет использовать blog/post_list.html.

Теперь отредактируйте файл urls.py приложения blog, закомментировав предыдущий шаблон URL-адреса post_list и добавив новый шаблон URL-адреса, используя класс PostListView, как показано ниже:

urlpatterns = [
    # представления поста
    # path('', views.post_list, name='post_list'),
    path('', views.PostListView.as_view(), name='post_list'),
    path('<int:year>/<int:month>/<int:day>/<slug:post>/',
         views.post_detail,
         name='post_detail'),
]

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

Встроенное в Django типовое представление ListView передает запрошенную страницу в переменную с именем page_obj.

В связи с этим необходимо соответствующим образом отредактировать шаблон post/list.html, чтобы вставить разбивщика, используя правильную переменную, как показано ниже:

{% extends "blog/base.html" %}

{% block title %}My Blog{% endblock %}

{% block content %}
    <h1>My Blog</h1>
    {% for post in posts %}
        <h2>
            <a href="{{ post.get_absolute_url }}">
                {{ post.title }}
            </a>
        </h2>
        <p class="date">
            Published {{ post.publish }} by {{ post.author }}
        </p>
        {{ post.body|truncatewords:30|linebreaks }}
    {% endfor %}
    {% include "pagination.html" with page=page_obj %}
{% endblock %}

Пройдите по URL-адресу http://127.0.0.1:8000/blog/ в своем браузере и проверьте, чтобы ссылки с постраничной разбивкой работали должным образом.

Поведение постранично разбитых ссылок должно быть таким же, как и в предыдущем представлении post_list.
Обработка исключений в этом случае немного отличается.
Если попытаться загрузить страницу вне диапазона или передать не целочисленное значение в параметре page, то представление вернет HTTP-ответ с кодом состояния, равным 404 (Страница не найдена), как показано ниже:

Обработка исключений, которая возвращает HTTP-ответ с кодом состояния 404, предусмотрена типовым представлением ListView. Это простой пример того, как писать представления на основе классов.

Введение в представления на основе классов можно почитать на странице https://docs.djangoproject.com/en/4.2/topics/class-based-views/intro/.


  • Комментариев
Будьте вежливы и соблюдайте наши принципы сообщества. Пожалуйста, не оставляйте решения и подсказки в комментариях, для этого есть отдельный форум.
Оставить комментарий

Такой вопрос, какой подход больше используется на практике: представления на основе классов или функций? Почему данный проект мы решили реализовать именно на основе функций?

Изменен Кислинский Роман

@Кислинский_Роман, В некоторых случаях представления на основе функций лучше, а в некоторых случаях представления на основе классов. Огромный плюс разработки через классы, это скорость разработки, меньшее количество кода и возможность наследования. Но, я считаю что нужно придти к этому, тоесть понять почему какие-то вещи вы пишите через CBV, а не через FBV.

Мы пишем проект в этом курсе на основе функций, потому что они более просты в использовании, и новички могут легко понять их и основную концепцию Django. Без опыта работы через FBV можно и не лесть в разработку через CBV. Если использовать метод CBV с его наследованием, то начинающим было бы просто не понятна их работа. Вот даже сравните на данном этапе функцию post_list и класс PostListView, они делают абсолютно одно и тоже, но подход абсолютно другой из-за наследования от базовых классов.

@Илья_Перминов, Спасибо за подробное объяснение)

Не понял, а что в итоге с ошибкой 404 в конце страницы? Как обработать это исключение в классе(http://127.0.0.1:8000/blog/?page=asdf)? По функциям вроде разобрались, а тут упомянули, что такая же ошибка есть, но как ее избежать не ясно. Как будто автор на этом моменте пошел заварить кофе и забыл дописать статью)

@Maxim_Lapshin, Изначально мы планировали добавить данный раздел, чтобы показать что, при разработке на классах в джанго, очень много встроенного функционала. Но очень быстро поняли что такие вещи не рассмотреть в нескольких разделах, и это все переросло в 2 модуля и 23 раздела - Блога 2.0. В ближайшее время мы будем переводить курс на Джангу 5, и удалим этот раздел.

А по вашему вопросу, можем использовать вот такой код:

class MyPaginator(Paginator):
    def validate_number(self, number):
        try:
            return super().validate_number(number)
        except EmptyPage:
            if int(number) > 1:
                # return the last page
                return self.num_pages
            elif int(number) < 1:
                # return the first page
                return 1
            else:
                raise


class PostListView(ListView):
    queryset = Post.published.all()
    context_object_name = 'posts'
    paginate_by = 3
    template_name = 'blog/post/list.html'
    paginator_class = MyPaginator

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

Спасибо огромное, я в восторге от вашего ответа🤝
Если мы добавляем записьpath('', views.PostListView.as_view(), name='post_list'), то функция post_list становиться не актуальной, думаю стоит указать, что ее мы тоже должны закомментировать, это конечно не обязательно, но так хоть новички будут понимать, что она уже не нужна по причине добавления класса PostListView который выполняет тоже самое.
# def post_list(request):
#      posts_lst = Post.published.all()
#      paginator = Paginator(posts_lst, 3)
#      page = request.GET.get('page')
#      posts = paginator.get_page(page)
#      return render(request, 'blog/post/list.html', {'posts': posts})

@Александр_Павлов, можно даже не комментировать, в следующем шаге вернём обратно.

Урок был написал до появления 10-11 модуля, чтобы показать пример использования CBV.

Я не понял вот эту строчку - context_object_name = 'posts'. Может кто-то объяснить?

У нас, после запроса к БД (Post.published.all()), QuerySet попадает в переменную queryset. Как эти даные потом оказываются в 'posts', который передается в шаблон? Или я вообще не туда воюю?

@Николай_Попов,

def post_list(request):
    posts = Post.published.all()
    return render(request,
                  'blog/post/list.html',
                  {'posts': posts})

Когда мы работаем на функциях, мы возвращаем в шаблон переменную с QuerySet.

В классах немного иначе.

class PostListView(ListView):
    queryset = Post.published.all()
    context_object_name = 'posts'
    template_name = 'blog/post/list.html'

Мы выполняем QuerySet, далее если мы не укажем в классе переменную context_object_name, то в шаблоне нам придется работать с  переменной object_list(Если не указано имя контекстного объекта context_object_name, то по умолчанию используется переменная object_list) И в шаблоне мы бы писали так: {% for post in object_list %} 

Поэтому мы пишем context_object_name = 'posts', тем самым переопределяя имя Queryset, чтобы иметь более удобное имя для работы в шаблонах. И уже в шаблоне работаем как:  {% for post in posts %}

@Илья_Перминов, благодарю за ответ!

А есть возможность в CBV добавить обработку случаев когда передаются "неправильные" данные в HTTP запросе, по аналогии с тем, что было в FBV?

@Виктор_Русинович, Не совсем понял вопроса. Если мы рассматриваем ListView, то в случае ошибок в части URL мы получим 404 ошибку, это обрабатывается под капотом Django.

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

@Илья_Перминов, извините, попробую сформулировать вопрос иначе: можно ли в CBV настроить обработку исключений: "передан номер страницы, которой нет" и "вместо номера страницы введены буквы", по аналогии с тем, что было в FBV?

@Виктор_Русинович, Да, в данном случае нам нужно переопределить встроенные методы классов. Например у ListView есть следующие методы, переопределив которые, мы можем добавить дополнительную логику, в том числе и обработку ошибок.

@Илья_Перминов, какие конкретно из этих методов требуется переопределить, чтобы добавить обработку ошибок?

@Никита_Айзиков, Смотря какие ошибки мы хотим обрабатывать, по ссылке в прошлом комментарии вы можете посмотреть все методы, и за что они отвечают. Исходя из чего и мы можем их обрабатывать. Например мы можем обработать запрос следующим образом:

class MyView(ListView):
    allow_empty = False

    def get(self, request, *args, **kwargs):
        try:
            return super(MyView, self).get(request, *args, **kwargs)
        except Http404:
            return HttpResponseRedirect("/empty-queryset-url/")

Подскажите, почему мы перевели на CBV только один view из двух (post_detail view оставили функцией)?

Изменен ilya kutaev

@ilya_kutaev, только для примера использования CBV, в дальнейшем вернём обратно.

Это было написано ещё до появления 10-11модуля(а там всё на CBV).

path('', views.PostListView.as_view(), name='post_list'),

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

@Vlad_Kirnats, это подробно рассматривается в 3м уроке 10м модуля.

 

Еще бы вот так бы объясняли) Было бы понятнее

 

И я еще на понял про "разбивщика" в list.html. Где он там?

@Ravshan_Battalov, Разбивщик в смысле постраничная разбивка.

{% include "pagination.html" with page=page_obj %}

По поводу объяснения, мы готовим в этот курс раздел по блогу 2.0, который будет написан на классах. В нем мы подробно все разберем, а этот раздел будет удален.