Source code for lotus.models.category

from django.core import serializers
from django.db import models
from django.db.models.signals import post_delete, pre_save
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.urls import translate_url, reverse

from treebeard.mp_tree import MP_Node, get_result_class

from smart_media.mixins import SmartFormatMixin
from smart_media.modelfields import SmartMediaField
from smart_media.signals import auto_purge_files_on_change, auto_purge_files_on_delete

from ..choices import get_category_template_choices, get_category_template_default
from ..managers import CategoryManager
from ..exceptions import LanguageMismatchError
from .translated import Translated


[docs] class Category(SmartFormatMixin, MP_Node, Translated): """ Category model. """ original = models.ForeignKey( "self", blank=True, null=True, default=None, on_delete=models.CASCADE, help_text=_( "Mark this category as a translation of an original category." ), ) """ Optional original category when object is a translation. """ modified = models.DateTimeField( _("modification date"), default=timezone.now, editable=False, ) """ Automatic modification date. """ title = models.CharField( _("title"), blank=False, max_length=255, default="", ) """ Required unique title string. """ slug = models.SlugField( _("slug"), max_length=255, ) """ Required unique slug string. """ template = models.CharField( _("template"), blank=False, max_length=150, choices=get_category_template_choices(), default=get_category_template_default(), ) """ Optional custom template path string. """ lead = models.TextField( _("lead"), blank=True, help_text=_( "Lead paragraph, commonly used for SEO purposes in page meta tags." ), ) """ Optional text lead. """ description = models.TextField( _("description"), blank=True, ) """ Optional description string. """ cover = SmartMediaField( verbose_name=_("cover image"), upload_to="lotus/category/cover/%y/%m", max_length=255, blank=True, default="", ) """ Optional cover image file. """ cover_alt_text = models.CharField( _("cover alternative text"), blank=True, max_length=125, default="", ) """ Optional alternative text for cover image. """ COMMON_ORDER_BY = ["title"] """ List of field order commonly used in frontend view/api. """ TREE_ORDER_BY = ["path", "title"] """ List of field order to use with any tree queryset. """ node_order_by = ["title"] """ Treebeard attribute only used for ordering with position name when performing writing operation on categories. .. Warning:: DO NOT CHANGE, it may corrupt tree for already saved data, however ``Category.fix_tree(fix_paths=True)`` should fix corruption. """ objects = CategoryManager() class Meta: ordering = ["title"] verbose_name = _("Category") verbose_name_plural = _("Categories") constraints = [ models.UniqueConstraint( fields=[ "slug", "language" ], name="lotus_unique_cat_slug_lang" ), models.UniqueConstraint( fields=[ "original", "language" ], name="lotus_unique_cat_original_lang" ), ] def __str__(self): return self.title
[docs] def get_absolute_url(self): """ Return absolute URL to the category detail view. Returns: string: An URL. """ return translate_url( reverse("lotus:category-detail", kwargs={"slug": self.slug}), self.language )
[docs] def get_absolute_api_url(self): """ Return absolute URL to the author detail viewset from API. Returns: string: An URL. """ return reverse("lotus-api:category-detail", kwargs={"pk": self.id})
[docs] def get_edit_url(self): """ Return absolute URL to edit category from admin. Returns: string: An URL. """ return reverse("admin:lotus_category_change", args=(self.id,))
[docs] def get_subcategories(self): """ Return category children, results are enforced on category language. Returns: queryset: List of children categories. """ if not self.numchild: return [] return self.get_children().filter(language=self.language).order_by("title")
def get_cover_format(self): return self.media_format(self.cover)
[docs] @classmethod def apply_tree_queryset_filter(cls, queryset, language=None, parent=None, current=None): """ Apply lookups on a queryset to filter items. .. Warning:: With some filter combinations this could return empty results. Arguments: queryset (Queryset): The queryset where to append filters. Keyword Arguments: cls (class object): The model class, typically ``Category``. language (string): Language code to use in queryset filter, every Category in different language will be excluded from results. If not given, all Category from mixed languages are returned. parent (Category): A category object used to start the tree. If not given, we assume than parent is the root of all categories meaning the whole tree will be returned. current (Category): The current category is used to find the tree branch to unfold. When given, only the nodes from the tree branch will be returned along the top level nodes (with the same depth than the parent) and children nodes that are not part of the branch are ignored. Returns: Queryset: The given queryset with possible filters. If no filter arguments are given, the queryset is returned unchanged. """ if language: queryset = queryset.filter(language=language) if not current and not parent: return queryset if not current: return queryset.filter(path__startswith=parent.path) if not parent: top_depth = parent.depth if parent else 1 return queryset.filter( models.Q(depth=top_depth) | models.Q(path__startswith=current.path[0:(Category.steplen * 1)]) ) branch_top_path = current.path[0:(Category.steplen * (parent.depth + 1))] return queryset.filter( models.Q(depth=parent.depth, path__startswith=parent.path) | models.Q(depth__gt=parent.depth, path__startswith=branch_top_path) )
[docs] @classmethod def get_nested_tree(cls, parent=None, language=None, current=None, branch=True, safe=True): """ A convenient method to get a Category tree with language filtered or not. This was based on ``MP_Node.dump_bulk()`` method but opposed to it this method applies some filters on queryset. If language argument is set with "en" only the english objects will be returned from queryset, this means "Item 1", "Item 2" and "Item 3.1". However since "Item 3.1" is a child of "Item 3" that is excluded from language filter, "Items 3.1" won't be in resulting tree because its parent does not exist in results. The output is probably not suitable anymore with ``MP_Node.load_bulk()`` because we keep/add some additional items to a node, opposed to the "dump_bulk" payload. .. Warning:: With some filter combinations this could return an empty list. Arguments: cls (class object): The model class, typically ``Category``. language (string): Language code to use in queryset filter, every Category in different language will be excluded from results. If not given, all Category from mixed languages are returned. parent (Category): A category object used to start the tree. If not given, we assume than parent is the root of all categories meaning the whole tree will be returned. current (Category): Basically the current category is only used to mark a node as "active". But with ``branch`` argument enabled it will be used for the "branch unfolding" mode. branch (boolean): The current category will be used to find the tree branch to unfold. When this is true and ``current`` is given, only the nodes from the tree branch will be returned along the top level nodes (with the same depth than the parent) and children nodes that are not part of the branch are ignored. safe (boolean): Due to the fact we can exclude item from language filter, we can have KeyError exception when building tree. If this argument is true, there won't be any exception for this case and missing key will just be ignored. If false, any exception related to missing node key will be raised. Results: list: Recursive list of Category tree. Each item is dictionnary of node values and it will be something like this: :: { "data": { "language": "en", "original": None, "modified": now, "title": "Item 1", "slug": "item-1", "lead": "", "description": "", "cover": "", }, "id": 1, "active": False, "depth": 1, "path": "0001", "children": [] } The ``children`` is only present if the node has children. """ cls = get_result_class(cls) # Apply filters then the proper tree ordering queryset = cls.apply_tree_queryset_filter( cls._get_serializable_model().objects.all(), language=language, parent=parent, current=current if branch is True else None, ) queryset = queryset.order_by(*cls.TREE_ORDER_BY) ret, lnk = [], {} pk_field = cls._meta.pk.attname for pyobj in serializers.serialize("python", queryset): # django's serializer stores the attributes in 'fields' fields = pyobj["fields"] path = fields["path"] depth = int(len(path) / cls.steplen) newobj = {"data": fields} # Move non data fields out of "data" item newobj[pk_field] = pyobj["pk"] newobj["path"] = fields["path"] newobj["depth"] = fields["depth"] # Add active state newobj["active"] = (current.id == pyobj["pk"]) if current else False # Clean useless fields from item payload del fields["numchild"] del fields["depth"] del fields["path"] # Remove id from data fields if pk_field in fields: del fields[pk_field] if ( (not parent and depth == 1) or (parent and len(path) == len(parent.path)) ): ret.append(newobj) else: parentpath = cls._get_basepath(path, depth - 1) # Ensure unknow path relation don't raise KeyError in case of queryset # filters that drops some nodes linked to granted nodes (mostly with # language filter) # CAUTION: If queryset is not ordered first on 'path' this will drop # some legitimate items if safe and parentpath not in lnk: continue parentobj = lnk[parentpath] if "children" not in parentobj: parentobj["children"] = [] parentobj["children"].append(newobj) lnk[path] = newobj return ret
[docs] def move_into(self, parent): """ Move object as a child of given parent. This is a shortcut around MP_Node.move() method but with positionning forced on 'sorted child' technic because it is the only one that fit to Lotus needs. You should never try to manually set a Category as a child of a parent Category because it will probably not correctly manage the node path rewriting. Raises: LanguageMismatchError: If both object language and parent language are differents. Arguments: parent (Category): A category object to define as the parent. """ # Enforce language validation before performing tree insertion if parent.language != self.language: msg = ( "Object with language '{current_lang}' can not be moved as a child of " "another object with language '{parent_lang}'" ) raise LanguageMismatchError(msg.format( parent_lang=parent.language, current_lang=self.language, )) self.move(parent, pos="sorted-child")
[docs] def save(self, *args, **kwargs): # Auto update 'modified' value on each save self.modified = timezone.now() super().save(*args, **kwargs)
# Connect some signals post_delete.connect( auto_purge_files_on_delete(["cover"]), dispatch_uid="category_cover_on_delete", sender=Category, weak=False, ) pre_save.connect( auto_purge_files_on_change(["cover"]), dispatch_uid="category_cover_on_change", sender=Category, weak=False, )