django-categories/categories/base.py
2024-05-22 11:09:42 +02:00

186 lines
6.1 KiB
Python

"""
This is the base class on which to build a hierarchical category-like model.
It provides customizable metadata and its own name space.
"""
from django import forms
from django.contrib import admin
from django.db import models
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from mptt.fields import TreeForeignKey
from mptt.managers import TreeManager
from mptt.models import MPTTModel
from .editor.tree_editor import TreeEditor
from .settings import ALLOW_SLUG_CHANGE, SLUG_TRANSLITERATOR
from .utils import slugify
class CategoryManager(models.Manager):
"""
A manager that adds an "active()" method for all active categories.
"""
def active(self):
"""
Only categories that are active.
"""
return self.get_queryset().filter(active=True)
class CategoryBase(MPTTModel):
"""
This base model includes the absolute bare-bones fields and methods.
One could simply subclass this model, do nothing else, and it should work.
"""
parent = TreeForeignKey(
"self",
on_delete=models.CASCADE,
blank=True,
null=True,
related_name="children",
verbose_name=_("parent"),
)
name = models.CharField(max_length=100, verbose_name=_("name"))
slug = models.SlugField(verbose_name=_("slug"), unique=True)
active = models.BooleanField(default=True, verbose_name=_("active"))
objects = CategoryManager()
tree = TreeManager()
def save(self, *args, **kwargs):
"""
Save the category.
While you can activate an item without activating its descendants,
It doesn't make sense that you can deactivate an item and have its
decendants remain active.
Args:
args: generic args
kwargs: generic keyword arguments
"""
if not self.slug:
self.slug = slugify(SLUG_TRANSLITERATOR(self.name))[:50]
super(CategoryBase, self).save(*args, **kwargs)
if not self.active:
for item in self.get_descendants():
if item.active != self.active:
item.active = self.active
item.save()
def __str__(self):
ancestors = self.get_ancestors()
return " > ".join(
[force_str(i.name) for i in ancestors]
+ [
self.name,
]
)
class Meta:
abstract = True
unique_together = ("parent", "name")
ordering = ("tree_id", "lft")
class MPTTMeta:
order_insertion_by = "name"
class CategoryBaseAdminForm(forms.ModelForm):
"""Base admin form for categories."""
def clean_slug(self):
"""Prune and transliterate the slug."""
if not self.cleaned_data.get("slug", None) and (self.instance is None or not ALLOW_SLUG_CHANGE):
self.cleaned_data["slug"] = slugify(SLUG_TRANSLITERATOR(self.cleaned_data["name"]))
return self.cleaned_data["slug"][:50]
def clean(self):
"""Clean the data passed from the admin interface."""
super(CategoryBaseAdminForm, self).clean()
if not self.is_valid():
return self.cleaned_data
opts = self._meta
# Validate slug is valid in that level
kwargs = {}
if self.cleaned_data.get("parent", None) is None:
kwargs["parent__isnull"] = True
else:
kwargs["parent__pk"] = int(self.cleaned_data["parent"].id)
this_level_slugs = [
c["slug"] for c in opts.model.objects.filter(**kwargs).values("id", "slug") if c["id"] != self.instance.id
]
if self.cleaned_data["slug"] in this_level_slugs:
raise forms.ValidationError(_("The slug must be unique among " "the items at its level."))
# Validate Category Parent
# Make sure the category doesn't set itself or any of its children as
# its parent.
if self.cleaned_data.get("parent", None) is None or self.instance.id is None:
return self.cleaned_data
if self.instance.pk:
decendant_ids = self.instance.get_descendants().values_list("id", flat=True)
else:
decendant_ids = []
if self.cleaned_data["parent"].id == self.instance.id:
raise forms.ValidationError(_("You can't set the parent of the " "item to itself."))
elif self.cleaned_data["parent"].id in decendant_ids:
raise forms.ValidationError(_("You can't set the parent of the " "item to a descendant."))
return self.cleaned_data
class CategoryBaseAdmin(TreeEditor, admin.ModelAdmin):
"""Base admin class for categories."""
form = CategoryBaseAdminForm
list_display = ("name", "active")
search_fields = ("name",)
prepopulated_fields = {"slug": ("name",)}
actions = ["activate", "deactivate"]
def get_actions(self, request):
"""Get available actions for the admin interface."""
actions = super(CategoryBaseAdmin, self).get_actions(request)
if "delete_selected" in actions:
del actions["delete_selected"]
return actions
def deactivate(self, request, queryset): # NOQA: queryset is not used.
"""
Set active to False for selected items.
"""
selected_cats = self.model.objects.filter(pk__in=[int(x) for x in request.POST.getlist("_selected_action")])
for item in selected_cats:
if item.active:
item.active = False
item.save()
item.children.all().update(active=False)
deactivate.short_description = _("Deactivate selected categories and their children")
def activate(self, request, queryset): # NOQA: queryset is not used.
"""
Set active to True for selected items.
"""
selected_cats = self.model.objects.filter(pk__in=[int(x) for x in request.POST.getlist("_selected_action")])
for item in selected_cats:
item.active = True
item.save()
item.children.all().update(active=True)
activate.short_description = _("Activate selected categories and their children")