Django База [2023]: Добавление системы тегов #29
В этой статье мы осветим вопрос добавления системы тегов на наш проект Django 4.1. Такая система необходима для того, чтобы искать материалы с одинаковыми тегами.
Если вы хотите выразить благодарность автору сайта, статей и курса по Django, вы можете сделать это по ссылке ниже:
Мы не будем создавать что-то своё, а воспользуемся уже отличным готовым решением django-taggit.
Установка django-taggit
Устанавливаем через терминал с помощью команды pip install django-taggit
Результат установки:
(venv) PS C:\Users\Razilator\Desktop\Base\backend> pip install django-taggit
Collecting django-taggit
Using cached django_taggit-3.1.0-py3-none-any.whl (60 kB)
Requirement already satisfied: Django>=3.2 in c:\users\razilator\desktop\base\venv\lib\site-packages (from django-taggit) (4.1.5)
Requirement already satisfied: asgiref<4,>=3.5.2 in c:\users\razilator\desktop\base\venv\lib\site-packages (from Django>=3.2->django-taggit) (3.6.0)
Requirement already satisfied: sqlparse>=0.2.2 in c:\users\razilator\desktop\base\venv\lib\site-packages (from Django>=3.2->django-taggit) (0.4.3)
Requirement already satisfied: tzdata in c:\users\razilator\desktop\base\venv\lib\site-packages (from Django>=3.2->django-taggit) (2022.7)
Installing collected packages: django-taggit
Successfully installed django-taggit-3.1.0
[notice] A new release of pip available: 22.3 -> 23.0
[notice] To update, run: python.exe -m pip install --upgrade pip
Далее нам необходимо добавить приложение taggit в INSTALLED_APPS в нашем конфиг файле проекта.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sites',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'modules.blog.apps.BlogConfig',
'modules.system.apps.SystemConfig',
'mptt',
'debug_toolbar',
'taggit',
]
Далее нам необходимо провести миграции, для этого в терминале вводим следующую команду: py manage.py migrate
Результат миграций:
(venv) PS C:\Users\Razilator\Desktop\Base\backend> py manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions, sites, system, taggit
Running migrations:
Applying taggit.0001_initial... OK
Applying taggit.0002_auto_20150616_2121... OK
Applying taggit.0003_taggeditem_add_unique_index... OK
Applying taggit.0004_alter_taggeditem_content_type_alter_taggeditem_tag... OK
Applying taggit.0005_auto_20220424_2025... OK
Переходим к добавление тегов в модель.
Добавление django-taggit в модель Article
Всё что нам нужно, это в файле models.py нашего приложения blog импортировать TaggableManager и добавить его в модель Article:
from django.db import models
from taggit.managers import TaggableManager
class Article(models.Model):
"""
Модель постов для сайта
"""
title = models.CharField(verbose_name='Заголовок', max_length=255)
# Другие поля модели...
tags = TaggableManager()
Далее снова проведём миграции, сначала py manage.py makemigrations
, после py manage.py migrate
Результат миграций:
(venv) PS C:\Users\Razilator\Desktop\Base\backend> py manage.py makemigrations
Migrations for 'blog':
modules\blog\migrations\0004_article_tags.py
- Add field tags to article
(venv) PS C:\Users\Razilator\Desktop\Base\backend> py manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions, sites, system, taggit
Running migrations:
Applying blog.0004_article_tags... OK
Проверим в админ-панеле:
Создание представления для статей по тегу
Теперь мы будем и фильтровать по тегу, для этого давайте во views.py нашего приложения blog добавим следующий фрагмент кода:
from django.views.generic import ListView
from taggit.models import Tag
from .models import Article
class ArticleByTagListView(ListView):
model = Article
template_name = 'modules/blog/articles/article-list.html'
context_object_name = 'articles'
paginate_by = 10
tag = None
def get_queryset(self):
self.tag = Tag.objects.get(slug=self.kwargs['tag'])
queryset = Article.objects.all().filter(tags__slug=self.tag.slug)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Статьи по тегу: {self.tag.name}'
return context
Напоминаю, что так выглядит наш весь views.py файл на основе всех предыдущих уроков:
from django.http import JsonResponse
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.shortcuts import render, redirect
from django.core.paginator import Paginator
from taggit.models import Tag
from .models import Article, Category, Comment
from .forms import ArticleCreateForm, ArticleUpdateForm, CommentCreateForm
from ..services.mixins import AuthorRequiredMixin
class ArticleListView(ListView):
model = Article
template_name = 'blog/articles_list.html'
context_object_name = 'articles'
paginate_by = 10
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Главная страница'
return context
class ArticleDetailView(DetailView):
model = Article
template_name = 'blog/articles_detail.html'
context_object_name = 'article'
queryset = model.objects.detail()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = self.object.title
context['form'] = CommentCreateForm
return context
class ArticleByCategoryListView(ListView):
model = Article
template_name = 'blog/articles_list.html'
context_object_name = 'articles'
category = None
def get_queryset(self):
self.category = Category.objects.get(slug=self.kwargs['slug'])
queryset = Article.objects.all().filter(category__slug=self.category.slug)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Статьи из категории: {self.category.title}'
return context
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'])
queryset = Article.objects.all().filter(tags__slug=self.tag.slug)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Статьи по тегу: {self.tag.name}'
return context
class ArticleCreateView(LoginRequiredMixin, CreateView):
"""
Представление: создание материалов на сайте
"""
model = Article
template_name = 'blog/articles_create.html'
form_class = ArticleCreateForm
login_url = 'home'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Добавление статьи на сайт'
return context
def form_valid(self, form):
form.instance.author = self.request.user
form.save()
return super().form_valid(form)
class ArticleUpdateView(AuthorRequiredMixin, SuccessMessageMixin, UpdateView):
"""
Представление: обновления материала на сайте
"""
model = Article
template_name = 'blog/articles_update.html'
context_object_name = 'article'
form_class = ArticleUpdateForm
login_url = 'home'
success_message = 'Материал был успешно обновлен'
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Обновление статьи: {self.object.title}'
return context
def form_valid(self, form):
# form.instance.updater = self.request.user
form.save()
return super().form_valid(form)
class ArticleDeleteView(AuthorRequiredMixin, DeleteView):
"""
Представление: удаления материала
"""
model = Article
success_url = reverse_lazy('home')
context_object_name = 'article'
template_name = 'blog/articles_delete.html'
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Удаление статьи: {self.object.title}'
return context
def articles_list(request):
articles = Article.objects.all()
paginator = Paginator(articles, per_page=2)
page_number = request.GET.get('page')
page_object = paginator.get_page(page_number)
context = {'page_obj': page_object}
return render(request, 'blog/articles_func_list.html', context)
class CommentCreateView(LoginRequiredMixin, CreateView):
model = Comment
form_class = CommentCreateForm
def is_ajax(self):
return self.request.headers.get('X-Requested-With') == 'XMLHttpRequest'
def form_invalid(self, form):
if self.is_ajax():
return JsonResponse({'error': form.errors}, status=400)
return super().form_invalid(form)
def form_valid(self, form):
comment = form.save(commit=False)
comment.article_id = self.kwargs.get('pk')
comment.author = self.request.user
comment.parent_id = form.cleaned_data.get('parent')
comment.save()
if self.is_ajax():
return JsonResponse({
'is_child': comment.is_child_node(),
'id': comment.id,
'author': comment.author.username,
'parent_id': comment.parent_id,
'time_create': comment.time_create.strftime('%Y-%b-%d %H:%M:%S'),
'avatar': comment.author.profile.avatar.url,
'content': comment.content,
'get_absolute_url': comment.author.profile.get_absolute_url()
}, status=200)
return redirect(comment.article.get_absolute_url())
def handle_no_permission(self):
return JsonResponse({'error': 'Необходимо авторизоваться для добавления комментариев'}, status=400)
Отлично. Перейдем к обработке и шаблону.
Обработка представления в urls.py
Далее нам необходимо добавить обработку представления в urls.py приложения blog, добавим для этого следюущую строку:
from django.urls import path
from .views import ArticleListView, ArticleDetailView, ArticleByCategoryListView, articles_list, ArticleCreateView, \
ArticleUpdateView, ArticleDeleteView, CommentCreateView, ArticleByTagListView
urlpatterns = [
path('', ArticleListView.as_view(), name='home'),
path('articles/', articles_list, name='articles_by_page'),
path('articles/create/', ArticleCreateView.as_view(), name='articles_create'),
path('articles/<str:slug>/update/', ArticleUpdateView.as_view(), name='articles_update'),
path('articles/<str:slug>/delete/', ArticleDeleteView.as_view(), name='articles_delete'),
path('articles/<str:slug>/', ArticleDetailView.as_view(), name='articles_detail'),
path('articles/<int:pk>/comments/create/', CommentCreateView.as_view(), name='comment_create_view'),
path('articles/tags/<str:tag>/', ArticleByTagListView.as_view(), name='articles_by_tags'),
path('category/<str:slug>/', ArticleByCategoryListView.as_view(), name="articles_by_category"),
]
Добавление тегов в шаблон
Открываем наш файл полной статьи, а именно templates/blog/articles_detail.html, и редактируем код, добавляя блок card-footer:
{% extends 'main.html' %}
{% load mptt_tags %}
{% block content %}
<div class="card mb-3 border-0 shadow-sm">
<div class="row">
<div class="col-4">
<img src="{{ article.thumbnail.url }}" class="card-img-top" alt="{{ article.title }}" />
</div>
<div class="col-8">
<div class="card-body">
<h5>{{ article.title }}</h5>
<p class="card-text">{{ article.full_description }}</p>
Категория: <a href="{% url 'articles_by_category' article.category.slug %}">{{ article.category.title }}</a> / Добавил: {{ article.author.username }} / <small>{{ article.time_create }}</small>
</div>
</div>
</div>
{% if article.tags.all %}
<div class="card-footer border-0">
Теги записи: {% for tag in article.tags.all %} <a href="{% url 'articles_by_tags' tag.slug %}">{{ tag }}</a>, {% endfor %}
</div>
{% endif %}
</div>
<div class="card border-0">
<div class="card-body">
<h5 class="card-title">
Комментарии
</h5>
{% include 'blog/comments/comments_list.html' %}
</div>
</div>
{% endblock %}
В примере выше мы добавляем цикл по тегам статьи, условие на отображение панели. Также я улучшил внешний вид карточки, добавив классы shadow-sm и border-0.
Проверяем на сайте
Добавим пару тегов к статьям в админ-панеле.
Нажмём на тег и получим список статей по данному тегу:
Дополнительно, убираем N+1
Не забываем про решение проблем с N+1, для этого в менеджер модели Article, в метод detail()
, который мы создавали в этом уроке, добавим в prefetch_related()
- 'tags'
:
class Article(models.Model):
"""
Модель постов для сайта
"""
class ArticleManager(models.Manager):
"""
Кастомный менеджер для модели статей
"""
def all(self):
"""
Список статей (SQL запрос с фильтрацией для страницы списка статей)
"""
return self.get_queryset().select_related('author', 'category').filter(status='published')
def detail(self):
"""
Детальная статья (SQL запрос с фильтрацией для страницы со статьёй)
"""
return self.get_queryset()\
.select_related('author', 'category')\
.prefetch_related('comments', 'comments__author', 'comments__author__profile', 'tags')\
.filter(status='published')
STATUS_OPTIONS = (
('published', 'Опубликовано'),
('draft', 'Черновик')
)
Проверяем SQL детальной статьи: