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

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

6.6 Добавление функциональности тегирования
2 из 2 шагов пройдено

Откройте шаблон blog/post/list.html и добавьте в него следующий ниже исходный код 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="tags">Tags: {{ post.tags.all|join:", " }}</p>
        <p class="date">
            Published {{ post.publish }} by {{ post.author }}
        </p>
        {{ post.body|truncatewords:30|linebreaks }}
    {% endfor %}
    {% include "pagination.html" with page=page_obj %}
{% endblock %}

Шаблонный фильтр join работает так же, как метод Python string.join(), чтобы конкатенировать элементы с заданной строкой.

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

Далее мы отредактируем представление post_list, чтобы пользователи имели возможность отображать список всех постов, помеченных конкретным тегом.

Откройте views.py файл приложения blog, импортируйте модель Tag из приложения django-taggit и измените представление post_list, как показано ниже, чтобы при необходимости фильтровать посты по тегу:

from taggit.models import Tag

def post_list(request, tag_slug=None):
    post_list = Post.published.all()

    tag = None
    if tag_slug:
        tag = get_object_or_404(Tag, slug=tag_slug)
        post_list = post_list.filter(tags__in=[tag])

    # Постраничная разбивка с 3 постами на страницу
    paginator = Paginator(post_list, 3)
    page_number = request.GET.get('page', 1)
    try:
        posts = paginator.page(page_number)
    except PageNotAnInteger:
        # Если page_number не целое число, то
        # выдать первую страницу
        posts = paginator.page(1)
    except EmptyPage:
        # Если page_number находится вне диапазона, то
        # выдать последнюю страницу результатов
        posts = paginator.page(paginator.num_pages)
    return render(request,
                  'blog/post/list.html',
                  {'posts': posts,
                   'tag': tag})

Теперь представление post_list работает следующим образом:

  1. Представление принимает опциональный параметр tag_slug, значение которого по умолчанию равно None. Этот параметр будет передан в URL-адресе.

  2. Внутри указанного представления формируется изначальный набор запросов, извлекающий все опубликованные посты, и если имеется слаг данного тега, то берется объект Tag с данным слагом, используя функцию сокращенного доступа get_object_or_404().

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

  4. Наконец, теперь функция render() передает новую переменную tag в шаблон.

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


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

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

Добавьте следующий ниже дополнительный шаблон URL-адреса, чтобы отображать список постов по тегу:

path('tag/<slug:tag_slug>/',
      views.post_list, name='post_list_by_tag'),

Как вы видите, оба шаблона указывают на одно и то же представление, но у них разные имена.

Первый шаблон будет вызывать представление post_list без каких-либо опциональных параметров,
тогда как второй шаблон будет вызывать это представление с параметром tag_slug.

Конвертер путей slug используется для сочетания с параметром, представленным строковым литералом в нижнем регистре с буквами или цифрами ASCII, а также символами дефиса и подчеркивания.

Теперь файл urls.py приложения blog должен выглядеть следующим образом:

from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [
    # представления поста
    path('', views.post_list, name='post_list'),
    # path('', views.PostListView.as_view(), name='post_list'),

    path('tag/<slug:tag_slug>/',
          views.post_list, name='post_list_by_tag'),

    path('<int:year>/<int:month>/<int:day>/<slug:post>/',
          views.post_detail,
          name='post_detail'),

    path('<int:post_id>/share/',
          views.post_share, name='post_share'),

    path('<int:post_id>/comment/',
          views.post_comment, name='post_comment'),
]

Поскольку используется представление post_list, отредактируйте шаблон blog/post/list.html, видоизменив постраничную разбивку, чтобы использовать объект posts:

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

Добавьте в шаблон blog/post/list.html следующие ниже строки:

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

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

{% block content %}
    <h1>My Blog</h1>

    {% if tag %}
        <h2>Posts tagged with "{{ tag.name }}"</h2>
    {% endif %}

    {% for post in posts %}
        <h2>
            <a href="{{ post.get_absolute_url }}">
                {{ post.title }}
            </a>
        </h2>
        <p class="tags">Tags: {{ post.tags.all|join:", " }}</p>
        <p class="date">
            Published {{ post.publish }} by {{ post.author }}
        </p>
        {{ post.body|truncatewords:30|linebreaks }}
    {% endfor %}
    {% include "pagination.html" with page=posts %}
{% endblock %}

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

Теперь отредактируйте шаблон blog/post/list.html, изменив вид отображения тегов, как показано ниже:

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

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

{% block content %}
    <h1>My Blog</h1>

    {% if tag %}
        <h2>Posts tagged with "{{ tag.name }}"</h2>
    {% endif %}


    {% for post in posts %}
        <h2>
            <a href="{{ post.get_absolute_url }}">
                {{ post.title }}
            </a>
        </h2>
        <p class="tags">
            Tags:
            {% for tag in post.tags.all %}
                <a href="{% url 'blog:post_list_by_tag' tag.slug %}">
                    {{ tag.name }}
                </a>
                {% if not forloop.last %}, {% endif %}
            {% endfor %}
        </p>
        <p class="date">
            Published {{ post.publish }} by {{ post.author }}
        </p>
        {{ post.body|truncatewords:30|linebreaks }}
    {% endfor %}
    {% include "pagination.html" with page=posts %}
{% endblock %}

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

URL-адрес формируется с помощью тега {% url 'blog:post_list_by_tag' tag.slug %}, используя имя URL-адреса и тег slug в качестве его параметра. Теги отделяются запятыми.

Пройдите по URL-адресу http://127.0.0.1:8000/blog/tag/jazz/ в своем браузере, и вы увидите список постов, отфильтрованных по этому тегу, примерно как показано ниже:


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

Для особо "искушенных", вот вариант добавления тегов с помощью классов:

@Олег_Якушев,  Спасибо! реально заработало.  Переопределение метода get_queryset  можно немного "причесать"

    def get_queryset(self):
        queryset = Post.published.all()
        tag_slug = self.kwargs.get('tag_slug')
        self.tag = None
        if tag_slug:
            self.tag = get_object_or_404(Tag, slug=tag_slug)
            return queryset.filter(tags__in=[self.tag])
        return queryset

Как оказалось, taggit не работает с русскими тегами. Как только он увидит русский тег (или любой другой не из ascii), то сразу же django выдаст вам ошибку "NoReverseMatch" . В документации я нашел следующее:

 By default taggit uses django.utils.text.slugify() to calculate a slug for a given tag. However, if you want to implement your own logic you can override this method.

Там внутри я не особо понял, что происходит, но русские теги очень нужны для моего сайта и я решил попробовать изменить функцию. Сравнил несколько библиотек транслитерации, в итоге установил transliterate и почему-то мне в голову пришла мысль погуглить, которая должна была прийти в самом начале. Как обычно, все решалось за пару секунд - изменить в пути slug на str. Ну и сам ответ - https://ru.stackoverflow.com/questions/1414559/Теги-на-русском-django-taggit

Но теперь у меня другой вопрос. Правильно ли дали ответ на stackoverflow или все-таки попробовать изменить функцию? Если не ошибаюсь, слаги влияют на SEO сайта и безопасность. В моем случае, не думаю, что с безопасностью будут проблемы (там в качестве тегов просто ключевые слова к картинкам), а вот SEO хотелось бы получше. 

@Николай_Попов, Решение из стаковерфлоу вполне рабочее. И не думаю что будут какие-то проблемы с безопасностью.

У меня почему то проблема со строкой 

post_list = post_list.filter(tags__in=[tag])

в ней выскакивает  ошибка. Через try except обрабатываю эту строчку - выдает страницу по except ветке

Можете выложить проект на github? А то на этом моменте почему-то сломалась пагинация

@Кирилл_Смирнов, поменял представление post_list на основе классов, на основе функций и все заработало

Изменен Кирилл Смирнов
Скорее всего пропустили абзац? "Откройте файл urls.py приложения blog, закомментируйте основанный на классе шаблон URL-адреса PostListView и раскомментируйте представление post_list" Если что исходный код к этой главе в разделе 7.12

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

Скорее всего проблема в том, что я что то не сделал, поэтому не важно. Сам сломал, сам починил) уже работает

@Илья_Перминов, у меня тоже сломалась пагинация, после первого изменения шаблона blog/post/list.html на этом шаге.
Позже, когда мы раскомментируем представление post_list на основе классов, все встанет на свои места.
Проверьте, пожалуйста, последовательность воспроизведения кода на этом шаге.

Даже подскажу, что дело в значении переменной page в первом приведенном коде.

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

tags__in=[tag] зачем это выражение, если в tag хранится один объект?

@Шамбер_Егор, Оператор in принимает только итерируемые объекты.

@Илья_Перминов, а всё, я понял, что так более лаконично, ведь мы используем поисковый метод прямо внутри запроса и достаем из поля tags, которое связно с Post, только связанный объект, который мы получили с помощью функции get_object_or_404

@Шамбер_Егор, да, все верно.

У тех у кого отвалилась пагинация в файл  blog/post/list.html  вставьте код этот и все будет работать, так же не используйте модели на основе классов которые автор раньше показывал, это было для ознакомление и не более того!

так же если у вас есть теги но они пустые учитывая что вы их через ORM добавляли, значить вы просто добавили их к посту Draft черновик через админку добавьте новые теги к посту паблишер и все заработает!
 

{% 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="tags">Tags: {{ post.tags.all|join:", " }}</p>
        <p class="date">
            Published {{ post.publish }} by {{ post.author }}
        </p>
        {{ post.body|truncatewords:30|linebreaks }}
    {% endfor %}
    {% include "pagination.html" with page=posts %}
{% endblock %}

@No_Name, Это не совсем корректно. Для использования Paginator совместно с view на основе функции, необходимо передавать ключ 'page_obj' в контекст: https://docs.djangoproject.com/en/5.0/topics/pagination/#using-paginator-in-a-view-function. В случае представлений на основе классов это не требуется, он создается автоматически.

Достаточно будет изменить вызов render() в post_list():

return render(request, 'blog/post/list.html',
              {'posts': posts, 'tag': tag, 'page_obj': paginator.get_page(page_number)})

Авторы забыли его добавить при "откате" от CBV. Все остальное я оставил без изменений, все работает корректно