""" This is the base class on which to build a hierarchical category-like model with customizable metadata and its own name space. """ from django.contrib import admin from django.db import models from django import forms from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from mptt.models import MPTTModel from mptt.fields import TreeForeignKey from mptt.managers import TreeManager from slugify import slugify from .editor.tree_editor import TreeEditor from .settings import ALLOW_SLUG_CHANGE, SLUG_TRANSLITERATOR 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) @python_2_unicode_compatible class CategoryBase(MPTTModel): """ This base model includes the absolute bare bones fields and methods. One could simply subclass this model and do nothing else and it should work. """ parent = TreeForeignKey( 'self', 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): """ 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. """ 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_text(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): def clean_slug(self): if not self.cleaned_data.get('slug', None): if 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): 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 decendant_ids = self.instance.get_descendants().values_list('id', flat=True) 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): form = CategoryBaseAdminForm list_display = ('name', 'active') search_fields = ('name',) prepopulated_fields = {'slug': ('name',)} actions = ['activate', 'deactivate'] def get_actions(self, request): actions = super(CategoryBaseAdmin, self).get_actions(request) if 'delete_selected' in actions: del actions['delete_selected'] return actions def deactivate(self, request, queryset): """ 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): """ 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')