Добавление функциональности тегирования

⏱ 25 мин · 🧩 заданий: 5

Теги для статей

Категория у статьи одна, а тегов может быть много. Как и в блоге из модуля 6, используем django-taggit, но теперь теги будут вводить сами пользователи в форме.

pip install django-taggit
INSTALLED_APPS = [
    ...
    "taggit",
]

TAGGIT_CASE_INSENSITIVE = True   # «Django» и «django» — один тег

Модель

from taggit.managers import TaggableManager


class Article(models.Model):
    ...
    tags = TaggableManager()
python manage.py makemigrations
python manage.py migrate

TaggableManager — не обычное поле, а менеджер связи многие-ко-многим через модель TaggedItem:

article.tags.add("django", "python")
article.tags.names()            # ['django', 'python']
article.tags.remove("python")
Article.objects.filter(tags__name__in=["django"])

Теги в форме

Добавьте tags в поля формы статьи:

class ArticleCreateForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ("title", "slug", "category", "short_description", "full_description", "thumbnail", "status", "tags")

taggit сам даст текстовое поле: теги через запятую. Строка с запятыми делится по запятым, без запятых — по пробелам; кавычки объединяют слова в один тег.

Важно: теги — связь M2M, а она сохраняется после сохранения статьи. Если в form_valid вы сохраняете объект через form.save(commit=False), обязательно вызовите form.save_m2m():

def form_valid(self, form):
    article = form.save(commit=False)
    article.author = self.request.user
    article.save()
    form.save_m2m()                      # без этого теги не сохранятся
    return redirect(article.get_absolute_url())

При обычном form.save() (без commit=False) теги сохраняются автоматически.

Статьи по тегу

from django.shortcuts import get_object_or_404
from taggit.models import Tag


class ArticleByTagListView(ListView):
    model = Article
    template_name = "blog/articles_list.html"
    context_object_name = "articles"
    paginate_by = 10
    tag = None

    def get_queryset(self):
        self.tag = get_object_or_404(Tag, slug=self.kwargs["tag"])
        return Article.custom.filter(tags__slug=self.tag.slug).select_related("author", "category")

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["title"] = f"Статьи по тегу: {self.tag.name}"
        return context
path("articles/tags/<str:tag>/", ArticleByTagListView.as_view(), name="articles_by_tags"),

Маршрут articles/tags/... поставьте выше articles/<str:slug>/.

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

TAGGIT_STRIP_UNICODE_WHEN_SLUGIFYING = True

Вывод тегов

На карточке и детальной странице:

{% for tag in article.tags.all %}
  <a href="{% url 'articles_by_tags' tag.slug %}" class="badge bg-secondary text-decoration-none">#{{ tag.name }}</a>
{% endfor %}

В списке статей не забудьте prefetch_related("tags"), иначе будет N+1.

Облако популярных тегов

Тег-включение в blog_tags.py:

from django.db.models import Count
from taggit.models import Tag


@register.inclusion_tag("includes/popular_tags.html")
def popular_tags(count=10):
    tags = Tag.objects.annotate(num_times=Count("taggit_taggeditem_items")).order_by("-num_times")[:count]
    return {"tags": tags}

templates/includes/popular_tags.html:

<div class="card mb-4">
  <div class="card-header">Популярные теги</div>
  <div class="card-body">
    {% for tag in tags %}
      <a href="{% url 'articles_by_tags' tag.slug %}" class="badge bg-light text-dark">#{{ tag.name }} ({{ tag.num_times }})</a>
    {% endfor %}
  </div>
</div>

В main.html в боковой колонке: {% popular_tags %}.

Похожие статьи

Как в блоге: статьи с наибольшим числом общих тегов.

def get_similar_articles(self, obj):
    tag_ids = obj.tags.values_list("id", flat=True)
    return (
        Article.custom.filter(tags__in=tag_ids)
        .exclude(id=obj.id)
        .annotate(same_tags=Count("tags"))
        .order_by("-same_tags", "-time_create")[:4]
    )

Итог

  • tags = TaggableManager() — M2M-связь с тегами, методы add/remove/names.
  • В форме — поле ввода через запятую; при commit=False нужен form.save_m2m().
  • Страница тега — ListView с фильтром tags__slug, облако — annotate(Count(...)).
  • prefetch_related("tags") в списках.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Написать код

Добавьте в модель Article поле тегов: tags = TaggableManager() из taggit.managers.

Задание 2 Написать код

Напишите ArticleByTagListView(ListView): model = Article, шаблон blog/articles_list.html, context_object_name = "articles", paginate_by = 10. В get_queryset сохраните тег self.tag = Tag.objects.get(slug=self.kwargs["tag"]) и верните Article.objects.filter(tags__slug=self.tag.slug).

Задание 3 Один вариант ответа

Почему в get_queryset лучше использовать get_object_or_404(Tag, slug=...), а не Tag.objects.get(...)?

Задание 4 Написать код

Выведите теги статьи ссылками: цикл по article.tags.all, для каждого <a href="{% url 'articles_by_tags' tag.slug %}">#{{ tag.name }}</a>.

Задание 5 Несколько вариантов

Как django-taggit по умолчанию разбирает строку из поля ввода тегов в форме?

Войдите, чтобы отмечать пройденные уроки.