forked from jazzband/django-categories
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathbase.py
186 lines (146 loc) · 6.06 KB
/
base.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
"""
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")