Древовидные категории в Django
В этой заметке я покажу как реализовать дерево категорий на Джанго.
Категории будут связаны друг с другом как дочерние-родительские, категории верхнего уровня не будут иметь родительскую категорию.
Для примера я создам пустое приложение блога (python manage.py startapp blog
) и все действия буду производить в нем.
Перед началом работы установим django-mptt
pip install django-mptt
И не забудем добавить его в INSTALLED_APPS файла settings.py:
INSTALLED_APPS = (
'django.contrib.auth',
# ...
'mptt',
'blog',
)
Создание моделей
Создадим модели Постов и Категорий для нашего приложения blog, в файле models.py добавим:
from django.db import models
from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey
class Post(models.Model):
title = models.CharField(max_length=100, verbose_name='Название')
slug = models.SlugField(max_length=150)
category = TreeForeignKey('Category', on_delete=models.PROTECT, related_name='posts', verbose_name='Категория')
content = models.TextField(verbose_name='Содержание')
def __str__(self):
return self.title
class Meta:
verbose_name = 'Запись'
verbose_name_plural = 'Записи'
class Category(MPTTModel):
title = models.CharField(max_length=50, unique=True, verbose_name='Название')
parent = TreeForeignKey('self', on_delete=models.PROTECT, null=True, blank=True, related_name='children',
db_index=True, verbose_name='Родительская категория')
slug = models.SlugField()
class MPTTMeta:
order_insertion_by = ['title']
class Meta:
unique_together = [['parent', 'slug']]
verbose_name = 'Категория'
verbose_name_plural = 'Категории'
def get_absolute_url(self):
return reverse('post-by-category', args=[str(self.slug)])
def __str__(self):
return self.title
Обратите внимание, модель Category расширяет MPTTModel вместо models.Model.
Далее выполняем миграции для создания таблиц базы данных.
python manage.py makemigrations
python manage.py migrate
Работа с категориями через панель Администратора
Теперь чтобы отобразить посты и категории в панели администратора Django, добавим следующее в файл admin.py нашего приложения blog:
from django.contrib import admin
from mptt.admin import MPTTModelAdmin
from .models import Post, Category
class PostAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("title",)}
admin.site.register(Post, PostAdmin)
class CategoryAdmin(MPTTModelAdmin):
prepopulated_fields = {"slug": ("title",)}
admin.site.register(Category, CategoryAdmin)
Теперь перейдем в админку сайта и добавим несколько категорий разного уровня:
Как видим у категории можно выбрать родителя.
В списке категорий дочерние категории отображаются чуть сдвинутыми вправо относительно родителя, выглядит не очень удобно. Также неудобно перемещать категории по иерархии, для этого потребуется заходить в редактирование конкретной категории и менять ее родителя.
Для себя я решил данное неудобство установкой django-mptt-admin, которая улучшает удобство админки при работе с моделями расширяющими MPTTModel.
Установим это дополнение:
pip install django-mptt-admin
Добавил его в INSTALLED_APPS файла settings.py
INSTALLED_APPS = (
..
'django_mptt_admin',
)
Далее нужно будет внести изменение в admin.py. Сделаем чтобы наш класс CategoryAdmin расширял не MPTTModelAdmin, а DjangoMpttAdmin. Вот полный обновленный код файла admin.py:
from django.contrib import admin
from django_mptt_admin.admin import DjangoMpttAdmin
from .models import Post, Category
class PostAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("title",)}
admin.site.register(Post, PostAdmin)
class CategoryAdmin(DjangoMpttAdmin):
prepopulated_fields = {"slug": ("title",)}
admin.site.register(Category, CategoryAdmin)
Теперь запустим отладочный сервер и перейдем в редактирование категорий.
Сейчас категории отображаются более наглядно, так же их можно двигать по дереву мышкой, что гораздо удобней чем было.
Вывод постов и категорий
Осталось добавить вывод постов и категорий, для этого отредактируем файл views.py следующим образом:
from django.views.generic import ListView
from .models import Post, Category
class CategoryListView(ListView):
model = Category
template_name = "blog/category_list.html"
class PostByCategoryView(ListView):
context_object_name = 'posts'
template_name = 'blog/post_list.html'
def get_queryset(self):
self.category = Category.objects.get(slug=self.kwargs['slug'])
queryset = Post.objects.filter(category=self.category)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = self.category
return context
Создадим шаблоны для вывода списка категорий и постов, но для начала сделаем примитивный базовый шаблон.
templates/blog/base.html:
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
</div>
</body>
</html>
Шаблон для вывода категорий templates/blog/category_list.html:
{% extends "blog/base.html" %}
{% load mptt_tags %}
{% block title %}Список категорий{% endblock %}
{% block content %}
<div>
<div>
<ul>
{% recursetree object_list %}
<li>
<a href="{{node.get_absolute_url}}">{{node.title}}</a>
{% if not node.is_leaf_node %}
<ul class="children">
{{ children }}
</ul>
{% endif %}
</li>
{% endrecursetree %}
</ul>
</div>
</div>
{% endblock %}
Шаблон для вывода постов в конкретной категории templates/blog/post_list.html:
{% extends "blog/base.html" %}
{% block title %}Посты из категории {{title}}{% endblock %}
{% block content %}
<main>
<section>
<div class="container">
<h1>Посты из категории {{title}}</h1>
</div>
</section>
<div>
{% for post in object_list%}
<div>
<h3>{{post}}</h3>
</div>
{% endfor %}
</div>
</main>
{% endblock %}
И наконец осталось отредактировать файл urls.py нашего проекта:
from django.contrib import admin
from django.urls import path
from blog.views import CategoryListView, PostByCategoryView
urlpatterns = [
path('admin/', admin.site.urls),
path('', CategoryListView.as_view(), name='category-list'),
path('<str:slug>/', PostByCategoryView.as_view(), name='post-by-category'),
]
Проверяем
Перед тестированием добавим несколько постов в разные категории через админку, после переходим на главную страницу и видим список наших категорий:
Если нажать на любую из категорий, мы попадаем внутрь нее и видим список постов принадлежащих этой категории:
На этом все )
Документация по Django MPTT
Документация по Django Mptt Admin