mirror of
https://github.com/jazzband/django-categories.git
synced 2026-03-16 22:30:24 +00:00
186 lines
6.1 KiB
Python
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"))
|
|
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")
|