Добавление функциональности тегирования
Теги для статей
Категория у статьи одна, а тегов может быть много. Как и в блоге из модуля 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
Добавьте в модель Article поле тегов: tags = TaggableManager() из taggit.managers.
from taggit.managers import TaggableManager
class Article(models.Model):
title = models.CharField(max_length=255)
tags = TaggableManager()
Напишите 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).
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 = Tag.objects.get(slug=self.kwargs["tag"])
return Article.objects.filter(tags__slug=self.tag.slug)
Почему в get_queryset лучше использовать get_object_or_404(Tag, slug=...), а не Tag.objects.get(...)?
Выведите теги статьи ссылками: цикл по article.tags.all, для каждого
<a href="{% url 'articles_by_tags' tag.slug %}">#{{ tag.name }}</a>.
{% for tag in article.tags.all %}
<a href="{% url 'articles_by_tags' tag.slug %}">#{{ tag.name }}</a>
{% endfor %}
Как django-taggit по умолчанию разбирает строку из поля ввода тегов в форме?
Войдите, чтобы отмечать пройденные уроки.