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

1215

После создания системы комментариев пришло время реализовать теги для постов. Сделаем это с помощью интеграции стороннего приложения 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"

Тест на знание python

Если выполнить код ниже, каков будет результат?
Что делает функция re.match()?
Что выведет этот код?
Как узнать длину списка?
Какой будет результат выполнения этого кода?
Александр
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ. Пишу на популярные темы: веб-разработка, работа с базами данных, data sciense и другие...