Древовидные категории в Django

Древовидные категории в 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)

Теперь запустим отладочный сервер и перейдем в редактирование категорий.

Список категорий при использовании Django Mptt Admin

Сейчас категории отображаются более наглядно, так же их можно двигать по дереву мышкой, что гораздо удобней чем было.

Вывод постов и категорий

Осталось добавить вывод постов и категорий, для этого отредактируем файл 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

Хостинг для ваших проектов