2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +00:00
2019-09-08 14:08:49 +10:00

211 lines
6.1 KiB
Python

"""
Generic models which provide extra functionality over base Django model types.
"""
from __future__ import unicode_literals
from django.db import models
from django.contrib.contenttypes.models import ContentType
from rest_framework.exceptions import ValidationError
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from mptt.models import MPTTModel, TreeForeignKey
from .validators import validate_tree_name
class InvenTreeTree(MPTTModel):
""" Provides an abstracted self-referencing tree model for data categories.
- Each Category has one parent Category, which can be blank (for a top-level Category).
- Each Category can have zero-or-more child Categor(y/ies)
Attributes:
name: brief name
description: longer form description
parent: The item immediately above this one. An item with a null parent is a top-level item
"""
class Meta:
abstract = True
unique_together = ('name', 'parent')
class MPTTMeta:
order_insertion_by = ['name']
name = models.CharField(
blank=False,
max_length=100,
unique=True,
validators=[validate_tree_name]
)
description = models.CharField(
blank=False,
max_length=250
)
# When a category is deleted, graft the children onto its parent
parent = models.ForeignKey('self',
on_delete=models.DO_NOTHING,
blank=True,
null=True,
related_name='children')
@property
def item_count(self):
""" Return the number of items which exist *under* this node in the tree.
Here an 'item' is considered to be the 'leaf' at the end of each branch,
and the exact nature here will depend on the class implementation.
The default implementation returns zero
"""
return 0
def getUniqueParents(self, unique=None):
""" Return a flat set of all parent items that exist above this node.
If any parents are repeated (which would be very bad!), the process is halted
"""
item = self
# Prevent infinite regression
max_parents = 500
unique = set()
while item.parent and max_parents > 0:
max_parents -= 1
unique.add(item.parent.id)
item = item.parent
return unique
def getUniqueChildren(self, unique=None, include_self=True):
""" Return a flat set of all child items that exist under this node.
If any child items are repeated, the repetitions are omitted.
"""
if unique is None:
unique = set()
if self.id in unique:
return unique
if include_self:
unique.add(self.id)
# Some magic to get around the limitations of abstract models
contents = ContentType.objects.get_for_model(type(self))
children = contents.get_all_objects_for_this_type(parent=self.id)
for child in children:
child.getUniqueChildren(unique)
return unique
@property
def has_children(self):
""" True if there are any children under this item """
return self.children.count() > 0
def getAcceptableParents(self):
""" Returns a list of acceptable parent items within this model
Acceptable parents are ones which are not underneath this item.
Setting the parent of an item to its own child results in recursion.
"""
contents = ContentType.objects.get_for_model(type(self))
available = contents.get_all_objects_for_this_type()
# List of child IDs
childs = self.getUniqueChildren()
acceptable = [None]
for a in available:
if a.id not in childs:
acceptable.append(a)
return acceptable
@property
def parentpath(self):
""" Get the parent path of this category
Returns:
List of category names from the top level to the parent of this category
"""
if self.parent:
return self.parent.parentpath + [self.parent]
else:
return []
@property
def path(self):
""" Get the complete part of this category.
e.g. ["Top", "Second", "Third", "This"]
Returns:
List of category names from the top level to this category
"""
return self.parentpath + [self]
@property
def pathstring(self):
""" Get a string representation for the path of this item.
e.g. "Top/Second/Third/This"
"""
return '/'.join([item.name for item in self.path])
def clean(self):
""" Custom cleaning
Parent:
Setting the parent of an item to its own child results in an infinite loop.
The parent of an item cannot be set to:
a) Its own ID
b) The ID of any child items that exist underneath it
Name:
Tree node names are limited to a reduced character set
"""
super().clean()
# Parent cannot be set to same ID (this would cause looping)
try:
if self.parent.id == self.id:
raise ValidationError("Category cannot set itself as parent")
except:
pass
# Ensure that the new parent is not already a child
if self.id in self.getUniqueChildren(include_self=False):
raise ValidationError("Category cannot set a child as parent")
def __str__(self):
""" String representation of a category is the full path to that category """
return "{path} - {desc}".format(path=self.pathstring, desc=self.description)
@receiver(pre_delete, sender=InvenTreeTree, dispatch_uid='tree_pre_delete_log')
def before_delete_tree_item(sender, instance, using, **kwargs):
""" Receives pre_delete signal from InvenTreeTree object.
Before an item is deleted, update each child object to point to the parent of the object being deleted.
"""
# Update each tree item below this one
for child in instance.children.all():
child.parent = instance.parent
child.save()