# -*- coding: utf-8 -*-
# Imports
from django.db import models
from django.utils.translation import ugettext_lazy as _
from .utils import get_ancestors, get_descendants
# Exports
__all__ = (
"ParentTreeModel",
)
# Models
[docs]class ParentTreeModel(models.Model):
"""A simple implementation of tree-like functionality using only a ``parent`` field that refers to ``self``.
Note that some methods will be horribly inefficient, yet effective at finding all of the records associated with the
current instance. If performance is a concern, then you may wish to review the following resources:
- `MPTT`_
- `Treebeard`_
.. _MPTT: https://django-mptt.readthedocs.io/en/latest/index.html
.. _Treebeard: http://django-treebeard.readthedocs.io/en/latest/index.html
"""
cached_level = models.PositiveSmallIntegerField(
_("level"),
blank=True,
null=True
)
parent = models.ForeignKey(
"self",
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name="children",
verbose_name=_("parent")
)
class Meta:
abstract = True
[docs] def get_ancestors(self):
"""Get the parent, grandparent, etc. for the current record.
:rtype: list
.. note::
The list is populated from nearest ancestor to furthest ancestor. Use ``reversed()`` as needed to re-order
the list so that the top-most ancestor is first.
"""
return get_ancestors(self)
[docs] def get_children(self):
"""Get the child records that are directly connected to the current record.
:rtype: django.db.models.QuerySet
"""
# noinspection PyUnresolvedReferences
return self.children.all()
[docs] def get_descendants(self):
"""Get all of the descendants (children, grandchildren, etc.) for this record.
:rtype: list
"""
return get_descendants(self)
[docs] def get_level(self, cached=True):
"""Get the level of the record within the hierarchy.
:param cached: Indicates whether cache should be used.
:type cached: bool
:rtype: int
"""
if cached and self.cached_level:
return self.cached_level
if self.has_parent():
# noinspection PyUnresolvedReferences
return self.parent.get_level() + 1
return 1
[docs] def get_siblings(self):
"""Get the records that are directly connected to the record's parent.
:rtype: django.db.models.QuerySet | None
"""
if not self.has_parent():
return None
# noinspection PyUnresolvedReferences
return self.parent.children.exclude(pk=self.pk)
[docs] def has_children(self):
"""Indicates whether the record has child records associated with it.
:rtype: bool
"""
# noinspection PyUnresolvedReferences
return self.children.exists()
[docs] def has_parent(self):
"""Indicates whether this record has a parent record.
:rtype: bool
"""
# noinspection PyUnresolvedReferences
return self.parent_id is not None
[docs] def has_siblings(self):
"""Indicates whether this record's parent has other child records.
:rtype: bool
"""
if not self.has_parent():
return False
# noinspection PyUnresolvedReferences
return self.parent.children.exclude(pk=self.pk).count() > 1
@property
def level(self):
"""An alias of ``get_level()``."""
return self.get_level()
[docs] def save(self, *args, **kwargs):
"""Automatically save ``cached_level``."""
self.cached_level = self.get_level(cached=False)
super().save(*args, **kwargs)