From 1cbbe9e7c66ea32016023aedddece8c9ae360bbd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 23:39:43 +1000 Subject: [PATCH 1/9] Split tree generation off into a separate function --- InvenTree/InvenTree/views.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 15f93c9133..3df50ac0f8 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -59,19 +59,20 @@ class TreeSerializer(views.APIView): return data - def get(self, request, *args, **kwargs): - - top_items = self.model.objects.filter(parent=None).order_by('name') + def generate_tree(self, items): nodes = [] top_count = 0 + # Construct the top-level items + top_items = [i for i in items if i.parent is None] + for item in top_items: nodes.append(self.itemToJson(item)) top_count += item.item_count - top = { + self.tree = { 'pk': None, 'text': self.title, 'href': self.root_url, @@ -79,8 +80,15 @@ class TreeSerializer(views.APIView): 'tags': [top_count], } + def get(self, request, *args, **kwargs): + """ Respond to a GET request for the Tree """ + + items = self.model.objects.all() + + self.generate_tree(items) + response = { - 'tree': [top] + 'tree': [self.tree] } return JsonResponse(response, safe=False) From b519a1981d9c5cb56769daf0bb1f4df11268256c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 23:39:43 +1000 Subject: [PATCH 2/9] Split tree generation off into a separate function --- InvenTree/InvenTree/views.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 15f93c9133..de9d64be1d 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -59,19 +59,24 @@ class TreeSerializer(views.APIView): return data - def get(self, request, *args, **kwargs): + def get_items(self): - top_items = self.model.objects.filter(parent=None).order_by('name') + return self.model.objects.all() + + def generate_tree(self, items): nodes = [] + # Construct the top-level items + top_items = [i for i in items if i.parent is None] + top_count = 0 for item in top_items: nodes.append(self.itemToJson(item)) top_count += item.item_count - top = { + self.tree = { 'pk': None, 'text': self.title, 'href': self.root_url, @@ -79,8 +84,15 @@ class TreeSerializer(views.APIView): 'tags': [top_count], } + def get(self, request, *args, **kwargs): + """ Respond to a GET request for the Tree """ + + items = self.model.objects.all() + + self.generate_tree(items) + response = { - 'tree': [top] + 'tree': [self.tree] } return JsonResponse(response, safe=False) From 037dc6a0d6aa4ecf8a45144cfa02704c2e89c218 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 17 Jun 2019 23:52:49 +1000 Subject: [PATCH 3/9] Reduced tree time with some better queries --- InvenTree/InvenTree/views.py | 10 ++++++---- InvenTree/part/api.py | 5 +++++ InvenTree/part/models.py | 13 ++++++++++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index de9d64be1d..9179805ed8 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -52,8 +52,10 @@ class TreeSerializer(views.APIView): if item.has_children: nodes = [] + """ for child in item.children.all().order_by('name'): nodes.append(self.itemToJson(child)) + """ data['nodes'] = nodes @@ -63,10 +65,12 @@ class TreeSerializer(views.APIView): return self.model.objects.all() - def generate_tree(self, items): + def generate_tree(self): nodes = [] + items = self.get_items() + # Construct the top-level items top_items = [i for i in items if i.parent is None] @@ -87,9 +91,7 @@ class TreeSerializer(views.APIView): def get(self, request, *args, **kwargs): """ Respond to a GET request for the Tree """ - items = self.model.objects.all() - - self.generate_tree(items) + self.generate_tree() response = { 'tree': [self.tree] diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index d0beee54ce..7f3ad61e75 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -34,6 +34,11 @@ class PartCategoryTree(TreeSerializer): def root_url(self): return reverse('part-index') + def get_items(self): + + print("hello world") + return PartCategory.objects.all().prefetch_related('parts', 'children') + class CategoryList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PartCategory objects. diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 38acae3360..9bf8a78845 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -69,13 +69,20 @@ class PartCategory(InvenTreeTree): return self.partcount @property - def partcount(self): + def partcount(self, cascade=True, active=True): """ Return the total part count under this category (including children of child categories) """ - return len(Part.objects.filter(category__in=self.getUniqueChildren(), - active=True)) + if cascade: + query = Part.objects.filter(category__in=self.getUniqueChildren()) + else: + query = Part.objects.filter(category=self) + + if active: + query = query.filter(active=True) + + return query.count() @property def has_parts(self): From 642660d76ed20540af20f27b7df87ea8b2855221 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 18 Jun 2019 00:09:42 +1000 Subject: [PATCH 4/9] Improved speed of stock tree --- InvenTree/InvenTree/models.py | 17 +++++++++++------ InvenTree/part/models.py | 3 +-- InvenTree/stock/models.py | 12 ++++++++---- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index a7fc93bf3f..4ca630625f 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -62,13 +62,18 @@ class InvenTreeTree(models.Model): If any parents are repeated (which would be very bad!), the process is halted """ - if unique is None: - unique = set() - else: - unique.add(self.id) + item = self - if self.parent and self.parent.id not in unique: - self.parent.getUniqueParents(unique) + # 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 diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 9bf8a78845..26dc54c4f9 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -66,9 +66,8 @@ class PartCategory(InvenTreeTree): @property def item_count(self): - return self.partcount + return self.partcount() - @property def partcount(self, cascade=True, active=True): """ Return the total part count under this category (including children of child categories) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index a53e61f86d..e992b4eb9f 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -49,19 +49,23 @@ class StockLocation(InvenTreeTree): } ) - @property - def stock_item_count(self): + def stock_item_count(self, cascade=True): """ Return the number of StockItem objects which live in or under this category """ - return StockItem.objects.filter(location__in=self.getUniqueChildren()).count() + if cascade: + query = StockItem.objects.filter(location__in=self.getUniqueChildren()) + else: + query = StockItem.objects.filter(location=self) + + return query.count() @property def item_count(self): """ Simply returns the number of stock items in this location. Required for tree view serializer. """ - return self.stock_item_count + return self.stock_item_count() @receiver(pre_delete, sender=StockLocation, dispatch_uid='stocklocation_delete_log') From 16b6ae8d615575d93fe6590d67a6b43995676ec1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 18 Jun 2019 00:59:54 +1000 Subject: [PATCH 5/9] Fixed up some stupid recursion on the Tree model template --- InvenTree/InvenTree/models.py | 47 +++++++------------ InvenTree/InvenTree/validators.py | 8 ++++ InvenTree/InvenTree/views.py | 2 - InvenTree/part/api.py | 6 +-- .../migrations/0008_auto_20190618_0042.py | 19 ++++++++ InvenTree/part/models.py | 8 ++-- InvenTree/stock/api.py | 3 ++ .../migrations/0007_auto_20190618_0042.py | 19 ++++++++ 8 files changed, 73 insertions(+), 39 deletions(-) create mode 100644 InvenTree/part/migrations/0008_auto_20190618_0042.py create mode 100644 InvenTree/stock/migrations/0007_auto_20190618_0042.py diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 4ca630625f..eeee6378c6 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -11,6 +11,8 @@ from rest_framework.exceptions import ValidationError from django.db.models.signals import pre_delete from django.dispatch import receiver +from .validators import validate_tree_name + class InvenTreeTree(models.Model): """ Provides an abstracted self-referencing tree model for data categories. @@ -31,7 +33,8 @@ class InvenTreeTree(models.Model): name = models.CharField( blank=False, max_length=100, - unique=True + unique=True, + validators=[validate_tree_name] ) description = models.CharField( @@ -104,14 +107,6 @@ class InvenTreeTree(models.Model): """ True if there are any children under this item """ return self.children.count() > 0 - @property - def children(self): - """ Return the children of this item """ - contents = ContentType.objects.get_for_model(type(self)) - childs = contents.get_all_objects_for_this_type(parent=self.id) - - return childs - def getAcceptableParents(self): """ Returns a list of acceptable parent items within this model Acceptable parents are ones which are not underneath this item. @@ -164,8 +159,8 @@ class InvenTreeTree(models.Model): """ return '/'.join([item.name for item in self.path]) - def __setattr__(self, attrname, val): - """ Custom Attribute Setting function + def clean(self): + """ Custom cleaning Parent: Setting the parent of an item to its own child results in an infinite loop. @@ -177,28 +172,18 @@ class InvenTreeTree(models.Model): Tree node names are limited to a reduced character set """ - if attrname == 'parent_id': - # If current ID is None, continue - # - This object is just being created - if self.id is None: - pass - # Parent cannot be set to same ID (this would cause looping) - elif val == self.id: - raise ValidationError("Category cannot set itself as parent") - # Null parent is OK - elif val is None: - pass - # Ensure that the new parent is not already a child - else: - kids = self.getUniqueChildren() - if val in kids: - raise ValidationError("Category cannot set a child as parent") + super().clean() - # Prohibit certain characters from tree node names - elif attrname == 'name': - val = val.translate({ord(c): None for c in "!@#$%^&*'\"\\/[]{}<>,|+=~`"}) + # 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 - super(InvenTreeTree, self).__setattr__(attrname, val) + # Ensure that the new parent is not already a child + if self.id in self.getUniqueChildren(): + raise ValidationError("Category cannot set a child as parent") def __str__(self): """ String representation of a category is the full path to that category """ diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py index 0e1622a49a..36eda4d451 100644 --- a/InvenTree/InvenTree/validators.py +++ b/InvenTree/InvenTree/validators.py @@ -17,6 +17,14 @@ def validate_part_name(value): ) +def validate_tree_name(value): + """ Prevent illegal characters in tree item names """ + + for c in "!@#$%^&*'\"\\/[]{}<>,|+=~`\"": + if c in str(value): + raise ValidationError({'name': _('Illegal character in name')}) + + def validate_overage(value): """ Validate that a BOM overage string is properly formatted. diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index f8f20a96b1..a564705ac2 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -52,10 +52,8 @@ class TreeSerializer(views.APIView): if item.has_children: nodes = [] - """ for child in item.children.all().order_by('name'): nodes.append(self.itemToJson(child)) - """ data['nodes'] = nodes diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 7f3ad61e75..bf0555a216 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -35,8 +35,6 @@ class PartCategoryTree(TreeSerializer): return reverse('part-index') def get_items(self): - - print("hello world") return PartCategory.objects.all().prefetch_related('parts', 'children') @@ -112,7 +110,9 @@ class PartList(generics.ListCreateAPIView): if cat_id: try: category = PartCategory.objects.get(pk=cat_id) - parts_list = parts_list.filter(category__in=category.getUniqueChildren()) + cats = [category.id] + cats += [cat for cat in category.getUniqueChildren()] + parts_list = parts_list.filter(category__in=cats) except PartCategory.DoesNotExist: pass diff --git a/InvenTree/part/migrations/0008_auto_20190618_0042.py b/InvenTree/part/migrations/0008_auto_20190618_0042.py new file mode 100644 index 0000000000..c9e0699f9b --- /dev/null +++ b/InvenTree/part/migrations/0008_auto_20190618_0042.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.2 on 2019-06-17 14:42 + +import InvenTree.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0007_auto_20190602_1944'), + ] + + operations = [ + migrations.AlterField( + model_name='partcategory', + name='name', + field=models.CharField(max_length=100, unique=True, validators=[InvenTree.validators.validate_tree_name]), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 26dc54c4f9..7d5476db5d 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -73,10 +73,12 @@ class PartCategory(InvenTreeTree): (including children of child categories) """ + cats = [self.id] + if cascade: - query = Part.objects.filter(category__in=self.getUniqueChildren()) - else: - query = Part.objects.filter(category=self) + cats += [cat for cat in self.getUniqueChildren()] + + query = Part.objects.filter(category__in=cats) if active: query = query.filter(active=True) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 092724407f..8240c6b837 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -38,6 +38,9 @@ class StockCategoryTree(TreeSerializer): def root_url(self): return reverse('stock-index') + def get_items(self): + return StockLocation.objects.all().prefetch_related('stock_items', 'children') + class StockDetail(generics.RetrieveUpdateDestroyAPIView): """ API detail endpoint for Stock object diff --git a/InvenTree/stock/migrations/0007_auto_20190618_0042.py b/InvenTree/stock/migrations/0007_auto_20190618_0042.py new file mode 100644 index 0000000000..862ea94273 --- /dev/null +++ b/InvenTree/stock/migrations/0007_auto_20190618_0042.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.2 on 2019-06-17 14:42 + +import InvenTree.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0006_stockitem_purchase_order'), + ] + + operations = [ + migrations.AlterField( + model_name='stocklocation', + name='name', + field=models.CharField(max_length=100, unique=True, validators=[InvenTree.validators.validate_tree_name]), + ), + ] From 37dba91b4a8fd420946f7a65533c83d1aa5162f6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 18 Jun 2019 01:02:56 +1000 Subject: [PATCH 6/9] Add 'include self' option to getUniqueChildren --- InvenTree/InvenTree/models.py | 5 ++++- InvenTree/part/api.py | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index eeee6378c6..e95b763c05 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -80,7 +80,7 @@ class InvenTreeTree(models.Model): return unique - def getUniqueChildren(self, unique=None): + def getUniqueChildren(self, unique=None, include_self=False): """ Return a flat set of all child items that exist under this node. If any child items are repeated, the repetitions are omitted. """ @@ -88,6 +88,9 @@ class InvenTreeTree(models.Model): if unique is None: unique = set() + if include_self: + unique.add(self.id) + if self.id in unique: return unique diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index bf0555a216..64b08f06b5 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -110,8 +110,7 @@ class PartList(generics.ListCreateAPIView): if cat_id: try: category = PartCategory.objects.get(pk=cat_id) - cats = [category.id] - cats += [cat for cat in category.getUniqueChildren()] + cats = category.getUniqueChildren(include_self=True) parts_list = parts_list.filter(category__in=cats) except PartCategory.DoesNotExist: pass From 79cd05423c400b3a5af26279b06651c2853287fe Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 18 Jun 2019 01:31:30 +1000 Subject: [PATCH 7/9] Custom aggregation of Part API - Reduced full part query from 2.5s to 200ms! --- InvenTree/part/api.py | 60 +++++++++++++++++++++++ InvenTree/static/script/inventree/part.js | 25 +++++++--- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 64b08f06b5..f157851215 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -6,6 +6,9 @@ Provides a JSON API for the Part app from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend +from django.conf import settings + +from django.db.models import Sum from rest_framework import status from rest_framework.response import Response @@ -15,6 +18,8 @@ from rest_framework import generics, permissions from django.conf.urls import url, include from django.urls import reverse +import os + from .models import Part, PartCategory, BomItem, PartStar from .serializers import PartSerializer, BomItemSerializer @@ -99,6 +104,61 @@ class PartList(generics.ListCreateAPIView): serializer_class = PartSerializer + def list(self, request, *args, **kwargs): + """ + Instead of using the DRF serialiser to LIST, + we serialize the objects manuually. + This turns out to be significantly faster. + """ + + queryset = self.filter_queryset(self.get_queryset()) + + data = queryset.values( + 'pk', + 'category', + 'image', + 'name', + 'IPN', + 'description', + 'keywords', + 'is_template', + 'URL', + 'units', + 'trackable', + 'assembly', + 'component', + 'salable', + 'active', + ).annotate( + in_stock=Sum('stock_items__quantity'), + ) + + # TODO - Annotate total being built + # TODO - Annotate total on order + # TODO - Annotate + + # Reduce the number of lookups we need to do for the part categories + categories = {} + + for item in data: + + if item['image']: + item['image'] = os.path.join(settings.MEDIA_URL, item['image']) + + cat_id = item['category'] + + if cat_id: + if cat_id not in categories: + categories[cat_id] = PartCategory.objects.get(pk=cat_id).pathstring + + item['category__name'] = categories[cat_id] + else: + item['category__name'] = None + + + return Response(data) + + def get_queryset(self): # Does the user wish to filter by category? diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js index d56a8d4769..83d21aa074 100644 --- a/InvenTree/static/script/inventree/part.js +++ b/InvenTree/static/script/inventree/part.js @@ -112,16 +112,25 @@ function loadPartTable(table, url, options={}) { } columns.push({ - field: 'full_name', + field: 'name', title: 'Part', sortable: true, formatter: function(value, row, index, field) { - if (row.is_template) { - value = '' + value + ''; + var name = ''; + + if (row.IPN) { + name += row.IPN; + name += ' | '; } - var display = imageHoverIcon(row.image_url) + renderLink(value, row.url); + name += value; + + if (row.is_template) { + name = '' + name + ''; + } + + var display = imageHoverIcon(row.image) + renderLink(name, '/part/' + row.pk + '/'); if (!row.active) { display = display + "INACTIVE"; @@ -146,11 +155,11 @@ function loadPartTable(table, url, options={}) { columns.push({ sortable: true, - field: 'category_name', + field: 'category__name', title: 'Category', formatter: function(value, row, index, field) { if (row.category) { - return renderLink(row.category_name, "/part/category/" + row.category + "/"); + return renderLink(row.category__name, "/part/category/" + row.category + "/"); } else { return ''; @@ -159,13 +168,13 @@ function loadPartTable(table, url, options={}) { }); columns.push({ - field: 'total_stock', + field: 'in_stock', title: 'Stock', searchable: false, sortable: true, formatter: function(value, row, index, field) { if (value) { - return renderLink(value, row.url + 'stock/'); + return renderLink(value, '/part/' + row.pk + '/stock/'); } else { return "No Stock"; From ea0da4c8f8475b48519b5ca24fef6e714865737a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 18 Jun 2019 01:32:29 +1000 Subject: [PATCH 8/9] PEP --- InvenTree/InvenTree/models.py | 2 +- InvenTree/part/api.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index e95b763c05..f3bc9a43cd 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -180,7 +180,7 @@ class InvenTreeTree(models.Model): # 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") + raise ValidationError("Category cannot set itself as parent") except: pass diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index f157851215..64956a60de 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -135,7 +135,6 @@ class PartList(generics.ListCreateAPIView): # TODO - Annotate total being built # TODO - Annotate total on order - # TODO - Annotate # Reduce the number of lookups we need to do for the part categories categories = {} @@ -155,9 +154,7 @@ class PartList(generics.ListCreateAPIView): else: item['category__name'] = None - return Response(data) - def get_queryset(self): From a47be24410543b8fef493f1ca65daa424d4ed3b2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 18 Jun 2019 01:38:43 +1000 Subject: [PATCH 9/9] Fixed test cases --- InvenTree/InvenTree/models.py | 10 ++++------ InvenTree/part/api.py | 3 +-- InvenTree/part/test_category.py | 6 +++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index f3bc9a43cd..646515ce70 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -80,7 +80,7 @@ class InvenTreeTree(models.Model): return unique - def getUniqueChildren(self, unique=None, include_self=False): + 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. """ @@ -88,13 +88,11 @@ class InvenTreeTree(models.Model): if unique is None: unique = set() - if include_self: - unique.add(self.id) - if self.id in unique: return unique - unique.add(self.id) + 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)) @@ -185,7 +183,7 @@ class InvenTreeTree(models.Model): pass # Ensure that the new parent is not already a child - if self.id in self.getUniqueChildren(): + if self.id in self.getUniqueChildren(include_self=False): raise ValidationError("Category cannot set a child as parent") def __str__(self): diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 64956a60de..22f371c379 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -167,8 +167,7 @@ class PartList(generics.ListCreateAPIView): if cat_id: try: category = PartCategory.objects.get(pk=cat_id) - cats = category.getUniqueChildren(include_self=True) - parts_list = parts_list.filter(category__in=cats) + parts_list = parts_list.filter(category__in=category.getUniqueChildren()) except PartCategory.DoesNotExist: pass diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py index ea52396342..3b3fe36b08 100644 --- a/InvenTree/part/test_category.py +++ b/InvenTree/part/test_category.py @@ -82,10 +82,10 @@ class CategoryTest(TestCase): self.assertTrue(self.fasteners.has_parts) self.assertFalse(self.transceivers.has_parts) - self.assertEqual(self.fasteners.partcount, 2) - self.assertEqual(self.capacitors.partcount, 1) + self.assertEqual(self.fasteners.partcount(), 2) + self.assertEqual(self.capacitors.partcount(), 1) - self.assertEqual(self.electronics.partcount, 3) + self.assertEqual(self.electronics.partcount(), 3) def test_delete(self): """ Test that category deletion moves the children properly """