Блог на Django #26: Добавление системы тегов

После создания системы комментариев пришло время реализовать теги для постов. Сделаем это с помощью интеграции стороннего приложения Django. Модуль django-taggit — это приложение, состоящее из модели Tag и менеджера для добавления тегов к любой модели. Вот его исходный код: https://github.com/jazzband/django-taggit.

Сперва нужно установить django-taggit с помощью pip, воспользовавшись следующей командой:

pip install django_taggit==0.22.2

Затем откройте файл settings.py проекта mysite и добавьте taggit к настройке INSTALLED_APPS:

INSTALLED_APPS = [ 
    # ... 
    'blog.apps.BlogConfig', 
    'taggit', 
]

Откройте файл models.py приложения blog и добавьте менеджер TaggableManager из django-taggit к модели Post с помощью следующего кода:

from taggit.managers import TaggableManager

class Post(models.Model):
    # ...
    tags = TaggableManager()

Менеджер tags позволяет добавлять, удалять и получать теги от объектов Post.

Используйте следующую команду для создания миграции для изменений модели:

python manage.py makemigrations blog

Должен появиться следующий вывод:

Migrations for 'blog':
  blog\migrations\0003_post_tags.py
    - Add field tags to post

Теперь запустите следующую команду для создания требуемых таблиц базы данных для моделей django-taggit и синхронизации изменений модели:

python manage.py migrate

Появится вывод, подтверждающий примененные миграции:

Applying taggit.0001_initial... OK 
Applying taggit.0002_auto_20150616_2121... OK 
Applying blog.0003_post_tags... OK

База данных теперь готова использовать модели django-taggit. Но сперва нужно разобраться, как работает менеджер tags. Откройте терминал с помощью команды python manage.py shell и введите следующий код. В первую очередь нужно получить один из постов (с ID 1):

>>> from blog.models import Post 
>>> post = Post.objects.get(id=1)

Затем добавьте некоторые теги и попробуйте вернуть их, чтобы проверить, были ли они добавлены:

>>> post.tags.add('music', 'jazz', 'django') 
>>> post.tags.all()
<QuerySet [<Tag: jazz>, <Tag: music>, <Tag: django>]>

Наконец, удалите их и проверьте список еще раз:

>>> post.tags.remove('django') 
>>> post.tags.all()
<QuerySet [<Tag: jazz>, <Tag: music>]>

Это было легко, не так ли? Запустите команду python manage.py runserver для запуска сервера разработки и откройте https://127.0.0.1:8000/admin/taggit/tag в браузере. Отобразится административная страница со списком объектов Tag приложения taggit:

админ-страница со списком объектов Tag

Перейдите на https://127.0.0.1:8000/admin/blog/post/ и кликните по посту, чтобы отредактировать его. Посты теперь включают поле Tags, с помощью которого можно легко их редактировать:

 Посты теперь включают поле Tags

Отредактируем посты в блоге для отображения тегов. Откройте шаблон blog/post/list.html и добавьте следующий код HTML под названием поста:

<p class="tags">Tags: {{ post.tags.all|join:", " }}</p>

Фильтр шаблона join работает так же, как и метод строки join() для объединения элементов с выбранной строкой. Откройте https://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): 
    object_list = Post.published.all() 
    tag = None 

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

    paginator = Paginator(object_list, 3) # 3 поста на каждой странице
    # ...

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

  1. Оно принимает опциональный параметр tag_slug со значением по умолчанию None. Он будет в URL.
  2. В представлении создается первый QuerySet, который получает все опубликованные посты. Если slug указан, то объект Tag можно получить через него с помощью get_object_or_404().
  3. Затем список фильтруется, чтобы остались только те, что включают тег. Это отношение многое-ко-многим, поэтому фильтровать нужно по тегам в списке, который в этом случае содержит всего один элемент.

Стоит напомнить, что QuerySet ленивы. Они будут выполнены только при итерации по списку постов при рендеринге шаблона.

Наконец, нужно изменить функцию render() в нижней части представления для передачи тега tag шаблону. В итоге представление будет выглядеть вот так:

def post_list(request, tag_slug=None):  
    object_list = Post.published.all()  
    tag = None  
  
    if tag_slug:  
        tag = get_object_or_404(Tag, slug=tag_slug)  
        object_list = object_list.filter(tags__in=[tag])  
  
    paginator = Paginator(object_list, 3)  # 3 поста на каждой странице  
    page = request.GET.get('page')  
    try:  
        posts = paginator.page(page)  
    except PageNotAnInteger:  
        # Если страница не является целым числом, поставим первую страницу  
        posts = paginator.page(1)  
    except EmptyPage:  
        # Если страница больше максимальной, доставить последнюю страницу результатов  
        posts = paginator.page(paginator.num_pages)  
    return render(request,  
		  'blog/post/list.html',  
		  {'page': page,  
		  'posts': posts,  
		  'tag': tag})

Откройте файл 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, дефиса и нижнего подчеркивания.

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

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

Добавьте следующие строки над циклом {% for %}:

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

Если пользователь будет заходить в блог, он увидит список постов. Если попробует отфильтровать материалы по конкретному тегу — тег, по которому проходит фильтрация. Измените способ отображения тегов:

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

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


Откройте https://127.0.0.1:8000/blog/ в браузере и нажмите на ссылку тега. Появится список постов с этим тегом:

список постов с тегом "jazz"