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

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

10.6 Оптимизация SQL запросов и установка Debug-Toolbar
3 из 4 шагов пройдено
0 из 5 баллов  получено

В этом разделе мы рассмотрим добавление своего менеджера в модель Post, добавим для нее фильтрацию по статусу, чтобы не выводить неопубликованные записи. По аналогии с менеджером у блога на функциях, нам необходимо добавить менеджер в этот проект:

class PostManager(models.Manager):
    """
    Кастомный менеджер для модели постов
    """

    def get_queryset(self):
        """
        Список постов (SQL запрос с фильтрацией по статусу опубликованно)
        """
        return super().get_queryset().filter(status='published')


class Post(models.Model):
    # Поля модели

    objects = models.Manager()
    custom = PostManager()

    # Мета классы и методы модели

При работе с блогом на функциях, мы переопределяли функцию get_queryset(), которая выполняется при любых ORM запросах. В этом проекте мы также переопределим функцию get_queryset(), чтобы выводить только опубликованные записи по обращению через менеджер.

Так как мы решили сделать менеджер модели custom, то нам необходимо поменять вызовы модели в представлениях на свой менеджер. Сделать это можно через добавление параметра queryset к PostListViewPostFromCategory:

class PostListView(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    paginate_by = 2
    queryset = Post.custom.all() # Переопределение вызова модели

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['title'] = 'Главная страница'
        return context



class PostFromCategory(ListView):
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    category = None
    paginate_by = 1

    def get_queryset(self):
        self.category = Category.objects.get(slug=self.kwargs['slug'])
        queryset = Post.custom.filter(category__slug=self.category.slug)
        if not queryset:
            sub_cat = Category.objects.filter(parent=self.category)
            queryset = Post.custom.filter(category__in=sub_cat)
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['title'] = f'Записи из категории: {self.category.title}'
        return context


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

И теперь если мы перейдем на главную страницу сайта, то увидим что запись, которая не была опубликована (Статус записи:  Черновик), не отображается в нашем блоге:


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

Хочу поделиться альтернативной реализацией.

Я решил в этой части курса по максимуму использовать модели django-extensions, и кроме наследования модели Post от TitleSlugDescriptionModel (описал в комменте к предыдущему уроку), унаследовался так же от ActivatorModel - в ней реализуется альтернативный подход со статусом и датой активации-деактивации контента, что позволяет определять период времени, когда пост будет активен, плюс использовать два менеджера для активных и неактивных объектов. На этом я, правда, не успокоился, и использовал так же наследование от  TimeStampedModel - в ней реализуются автоматические поля created и modified.
В результате большая часть полей Post унаследована от базовых классов, и реализация выглядит так:

class Post(ActivatorModel, TimeStampedModel, TitleSlugDescriptionModel):
    text = models.TextField(verbose_name='Полный текст записи')
    category = TreeForeignKey('Category', on_delete=models.PROTECT, related_name='posts', verbose_name='Категория')
    thumbnail = models.ImageField(default='default.png', verbose_name='Изображение записи', blank=True,
                                  upload_to='images/thumbnails/%Y/%m/%d/',
                                  validators=[FileExtensionValidator(allowed_extensions=IMAGE_EXTENSIONS)])
    author = models.ForeignKey(to=get_user_model(), verbose_name='Автор', on_delete=models.SET_DEFAULT,
                               related_name='author_posts', default=1)
    updater = models.ForeignKey(to=get_user_model(), verbose_name='Обновил', on_delete=models.SET_NULL, null=True,
                                related_name='updater_posts', blank=True)
    fixed = models.BooleanField(verbose_name='Прикреплено', default=False)

    class Meta:
        ordering = ['-fixed', '-created']
        indexes = [models.Index(fields=['-fixed', '-created', 'status'])]
        verbose_name = 'Статья'
        verbose_name_plural = 'Статьи'

Но сейчас речь только про ActivatorModel. Так как в этом базовом классе уже есть менеджер для активных объектов, в PostListView он используется так:

queryset = Post.objects.active()

И в PostFromCategory аналогичным образом:

    def get_queryset(self):
        self.category = Category.objects.get(slug=self.kwargs['slug'])
        queryset = Post.objects.active().filter(category__slug=self.category.slug)
        if not queryset:
            sub_cat = Category.objects.filter(parent=self.category)
            queryset = Post.objects.active().filter(category__in=sub_cat)
        return queryset

Т.е. ручное кодирование на этом этапе сведено к минимуму.

Понятно, что набор полей в модели отличается, но при этом все работает точно так же, как и у авторов курса:

Не буду утверждать, что "так правильнее", но явно имеет право на жизнь

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

 Я сделал вот такой менеджер:

class PublishedManager(models.Manager):
    """
    Кастомный менеджер, возвращающий только опубликованные записи в модели.
    """

    def get_queryset(self):
        return super().get_queryset().filter(status='published')

В модели:

class Post(models.Model):
    ...
    published = PublishedManager()
    ...

И использование:

class PostListFromCategoryView(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    category = None
    paginate_by = 1
    queryset = Post.published.all()

    def get_queryset(self):
        self.category = Category.objects.get(slug=self.kwargs['slug'])
        queryset = Post.published.filter(category__slug=self.category.slug)

        if not queryset:
            sub_cat = Category.objects.filter(parent=self.category)
            queryset = Post.published.filter(category__in=sub_cat)

        return queryset

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)
        context['title'] = f'Посты из категории {self.category.title}'
        return context

@Корнев_Степан, Старый способ наверное самый лучший, но тут я хотел показать, что мы можем переопределять не только все запросы для менеджера модели, но и отдельные(all и тд), добавляя любую логику.

А по поводу:

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

Не совсем понял, у нас ведь у PostListView и PostFromCategory переопределен параметр queryset на Post.custom.all()

@Илья_Перминов

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

Если PostFromCategory следующего вида:

class PostFromCategory(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    category = None
    paginate_by = 1
    queryset = Post.custom.all()

    def get_queryset(self):
        self.category = Category.objects.get(slug=self.kwargs['slug'])
        queryset = Post.objects.filter(category__slug=self.category.slug)

        if not queryset:
            sub_cat = Category.objects.filter(parent=self.category)
            queryset = Post.objects.filter(category__in=sub_cat)

        return queryset

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)
        context['title'] = f'Посты из категории {self.category.title}'
        return context

То из-за логики в методе get_queryset, при переходе на определенную категорию отображаются неопубликованные посты, хотя не должны(мы так не планировали), даже с учетом того, что queryset мы установили равным Post.custom.all().

Изменен Корнев Степан

@Корнев_Степан, Я понял о чем вы, не внимательно посмотрел код. Исправил в лекции, сделал по старинке)

@Илья_Перминов, Видимо, тут получилось "масло масленое" в результате:

Эти две строки можно без ущерба закомментировать, т.к. они все равно не работают из-за переопределенного метода get_queryset()

@ilya_kutaev, согласен, убрал эти строки.

В первом предложение наверно имелось ввиду 'чтобы не выводить неопубликованные записи' ?

@Ilia_Boiarintsev, да, спасибо, исправил.