diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py
index 2e434d928d..90543d429d 100644
--- a/InvenTree/part/admin.py
+++ b/InvenTree/part/admin.py
@@ -8,13 +8,7 @@ from import_export.resources import ModelResource
from import_export.fields import Field
import import_export.widgets as widgets
-from .models import PartCategory, Part
-from .models import PartAttachment, PartStar, PartRelated
-from .models import BomItem
-from .models import PartParameterTemplate, PartParameter
-from .models import PartCategoryParameterTemplate
-from .models import PartTestTemplate
-from .models import PartSellPriceBreak, PartInternalPriceBreak
+import part.models as models
from stock.models import StockLocation
from company.models import SupplierPart
@@ -24,7 +18,7 @@ class PartResource(ModelResource):
""" Class for managing Part data import/export """
# ForeignKey fields
- category = Field(attribute='category', widget=widgets.ForeignKeyWidget(PartCategory))
+ category = Field(attribute='category', widget=widgets.ForeignKeyWidget(models.PartCategory))
default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation))
@@ -32,7 +26,7 @@ class PartResource(ModelResource):
category_name = Field(attribute='category__name', readonly=True)
- variant_of = Field(attribute='variant_of', widget=widgets.ForeignKeyWidget(Part))
+ variant_of = Field(attribute='variant_of', widget=widgets.ForeignKeyWidget(models.Part))
suppliers = Field(attribute='supplier_count', readonly=True)
@@ -48,7 +42,7 @@ class PartResource(ModelResource):
building = Field(attribute='quantity_being_built', readonly=True, widget=widgets.IntegerWidget())
class Meta:
- model = Part
+ model = models.Part
skip_unchanged = True
report_skipped = False
clean_model_instances = True
@@ -86,14 +80,14 @@ class PartAdmin(ImportExportModelAdmin):
class PartCategoryResource(ModelResource):
""" Class for managing PartCategory data import/export """
- parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(PartCategory))
+ parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(models.PartCategory))
parent_name = Field(attribute='parent__name', readonly=True)
default_location = Field(attribute='default_location', widget=widgets.ForeignKeyWidget(StockLocation))
class Meta:
- model = PartCategory
+ model = models.PartCategory
skip_unchanged = True
report_skipped = False
clean_model_instances = True
@@ -108,14 +102,14 @@ class PartCategoryResource(ModelResource):
super().after_import(dataset, result, using_transactions, dry_run, **kwargs)
# Rebuild the PartCategory tree(s)
- PartCategory.objects.rebuild()
+ models.PartCategory.objects.rebuild()
class PartCategoryInline(admin.TabularInline):
"""
Inline for PartCategory model
"""
- model = PartCategory
+ model = models.PartCategory
class PartCategoryAdmin(ImportExportModelAdmin):
@@ -146,6 +140,11 @@ class PartStarAdmin(admin.ModelAdmin):
list_display = ('part', 'user')
+class PartCategoryStarAdmin(admin.ModelAdmin):
+
+ list_display = ('category', 'user')
+
+
class PartTestTemplateAdmin(admin.ModelAdmin):
list_display = ('part', 'test_name', 'required')
@@ -159,7 +158,7 @@ class BomItemResource(ModelResource):
bom_id = Field(attribute='pk')
# ID of the parent part
- parent_part_id = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
+ parent_part_id = Field(attribute='part', widget=widgets.ForeignKeyWidget(models.Part))
# IPN of the parent part
parent_part_ipn = Field(attribute='part__IPN', readonly=True)
@@ -168,7 +167,7 @@ class BomItemResource(ModelResource):
parent_part_name = Field(attribute='part__name', readonly=True)
# ID of the sub-part
- part_id = Field(attribute='sub_part', widget=widgets.ForeignKeyWidget(Part))
+ part_id = Field(attribute='sub_part', widget=widgets.ForeignKeyWidget(models.Part))
# IPN of the sub-part
part_ipn = Field(attribute='sub_part__IPN', readonly=True)
@@ -233,7 +232,7 @@ class BomItemResource(ModelResource):
return fields
class Meta:
- model = BomItem
+ model = models.BomItem
skip_unchanged = True
report_skipped = False
clean_model_instances = True
@@ -262,16 +261,16 @@ class ParameterTemplateAdmin(ImportExportModelAdmin):
class ParameterResource(ModelResource):
""" Class for managing PartParameter data import/export """
- part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
+ part = Field(attribute='part', widget=widgets.ForeignKeyWidget(models.Part))
part_name = Field(attribute='part__name', readonly=True)
- template = Field(attribute='template', widget=widgets.ForeignKeyWidget(PartParameterTemplate))
+ template = Field(attribute='template', widget=widgets.ForeignKeyWidget(models.PartParameterTemplate))
template_name = Field(attribute='template__name', readonly=True)
class Meta:
- model = PartParameter
+ model = models.PartParameter
skip_unchanged = True
report_skipped = False
clean_model_instance = True
@@ -292,7 +291,7 @@ class PartCategoryParameterAdmin(admin.ModelAdmin):
class PartSellPriceBreakAdmin(admin.ModelAdmin):
class Meta:
- model = PartSellPriceBreak
+ model = models.PartSellPriceBreak
list_display = ('part', 'quantity', 'price',)
@@ -300,20 +299,21 @@ class PartSellPriceBreakAdmin(admin.ModelAdmin):
class PartInternalPriceBreakAdmin(admin.ModelAdmin):
class Meta:
- model = PartInternalPriceBreak
+ model = models.PartInternalPriceBreak
list_display = ('part', 'quantity', 'price',)
-admin.site.register(Part, PartAdmin)
-admin.site.register(PartCategory, PartCategoryAdmin)
-admin.site.register(PartRelated, PartRelatedAdmin)
-admin.site.register(PartAttachment, PartAttachmentAdmin)
-admin.site.register(PartStar, PartStarAdmin)
-admin.site.register(BomItem, BomItemAdmin)
-admin.site.register(PartParameterTemplate, ParameterTemplateAdmin)
-admin.site.register(PartParameter, ParameterAdmin)
-admin.site.register(PartCategoryParameterTemplate, PartCategoryParameterAdmin)
-admin.site.register(PartTestTemplate, PartTestTemplateAdmin)
-admin.site.register(PartSellPriceBreak, PartSellPriceBreakAdmin)
-admin.site.register(PartInternalPriceBreak, PartInternalPriceBreakAdmin)
+admin.site.register(models.Part, PartAdmin)
+admin.site.register(models.PartCategory, PartCategoryAdmin)
+admin.site.register(models.PartRelated, PartRelatedAdmin)
+admin.site.register(models.PartAttachment, PartAttachmentAdmin)
+admin.site.register(models.PartStar, PartStarAdmin)
+admin.site.register(models.PartCategoryStar, PartCategoryStarAdmin)
+admin.site.register(models.BomItem, BomItemAdmin)
+admin.site.register(models.PartParameterTemplate, ParameterTemplateAdmin)
+admin.site.register(models.PartParameter, ParameterAdmin)
+admin.site.register(models.PartCategoryParameterTemplate, PartCategoryParameterAdmin)
+admin.site.register(models.PartTestTemplate, PartTestTemplateAdmin)
+admin.site.register(models.PartSellPriceBreak, PartSellPriceBreakAdmin)
+admin.site.register(models.PartInternalPriceBreak, PartInternalPriceBreakAdmin)
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 0b754dffe8..20447a4d26 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -58,6 +58,14 @@ class CategoryList(generics.ListCreateAPIView):
queryset = PartCategory.objects.all()
serializer_class = part_serializers.CategorySerializer
+ def get_serializer_context(self):
+
+ ctx = super().get_serializer_context()
+
+ ctx['starred_categories'] = [star.category for star in self.request.user.starred_categories.all()]
+
+ return ctx
+
def filter_queryset(self, queryset):
"""
Custom filtering:
@@ -110,6 +118,18 @@ class CategoryList(generics.ListCreateAPIView):
except (ValueError, PartCategory.DoesNotExist):
pass
+ # Filter by "starred" status
+ starred = params.get('starred', None)
+
+ if starred is not None:
+ starred = str2bool(starred)
+ starred_categories = [star.category.pk for star in self.request.user.starred_categories.all()]
+
+ if starred:
+ queryset = queryset.filter(pk__in=starred_categories)
+ else:
+ queryset = queryset.exclude(pk__in=starred_categories)
+
return queryset
filter_backends = [
@@ -149,6 +169,14 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = part_serializers.CategorySerializer
queryset = PartCategory.objects.all()
+ def get_serializer_context(self):
+
+ ctx = super().get_serializer_context()
+
+ ctx['starred_categories'] = [star.category for star in self.request.user.starred_categories.all()]
+
+ return ctx
+
class CategoryParameterList(generics.ListAPIView):
""" API endpoint for accessing a list of PartCategoryParameterTemplate objects.
@@ -389,7 +417,7 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
# Ensure the request context is passed through
kwargs['context'] = self.get_serializer_context()
- # Pass a list of "starred" parts fo the current user to the serializer
+ # Pass a list of "starred" parts of the current user to the serializer
# We do this to reduce the number of database queries required!
if self.starred_parts is None and self.request is not None:
self.starred_parts = [star.part for star in self.request.user.starred_parts.all()]
@@ -420,7 +448,7 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
if 'starred' in request.data:
starred = str2bool(request.data.get('starred', None))
- self.get_object().set_subscription(request.user, starred)
+ self.get_object().set_starred(request.user, starred)
response = super().update(request, *args, **kwargs)
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 0b99b8dac5..dada6f125b 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -15,7 +15,7 @@ from django.urls import reverse
from django.db import models, transaction
from django.db.utils import IntegrityError
-from django.db.models import Q, Sum, UniqueConstraint, query
+from django.db.models import Q, Sum, UniqueConstraint
from django.db.models.functions import Coalesce
from django.core.validators import MinValueValidator
@@ -102,11 +102,11 @@ class PartCategory(InvenTreeTree):
if cascade:
""" Select any parts which exist in this category or any child categories """
- query = Part.objects.filter(category__in=self.getUniqueChildren(include_self=True))
+ queryset = Part.objects.filter(category__in=self.getUniqueChildren(include_self=True))
else:
- query = Part.objects.filter(category=self.pk)
+ queryset = Part.objects.filter(category=self.pk)
- return query
+ return queryset
@property
def item_count(self):
@@ -224,14 +224,14 @@ class PartCategory(InvenTreeTree):
return [s for s in subscribers]
- def is_subscribed_by(self, user, **kwargs):
+ def is_starred_by(self, user, **kwargs):
"""
Returns True if the specified user subscribes to this category
"""
return user in self.get_subscribers(**kwargs)
- def set_subscription(self, user, status):
+ def set_starred(self, user, status):
"""
Set the "subscription" status of this PartCategory against the specified user
"""
@@ -239,7 +239,7 @@ class PartCategory(InvenTreeTree):
if not user:
return
- if self.is_subscribed_by(user) == status:
+ if self.is_starred_by(user) == status:
return
if status:
@@ -386,9 +386,16 @@ class Part(MPTTModel):
context = {}
- context['starred'] = self.is_subscribed_by(request.user)
context['disabled'] = not self.active
+ # Subscription status
+ context['starred'] = self.is_starred_by(request.user)
+ context['starred_directly'] = context['starred'] and self.is_starred_by(
+ request.user,
+ include_variants=False,
+ include_categories=False
+ )
+
# Pre-calculate complex queries so they only need to be performed once
context['total_stock'] = self.total_stock
@@ -1129,14 +1136,14 @@ class Part(MPTTModel):
return [s for s in subscribers]
- def is_subscribed_by(self, user, **kwargs):
+ def is_starred_by(self, user, **kwargs):
"""
Return True if the specified user subscribes to this part
"""
return user in self.get_subscribers(**kwargs)
- def set_subscription(self, user, status):
+ def set_starred(self, user, status):
"""
Set the "subscription" status of this Part against the specified user
"""
@@ -1145,7 +1152,7 @@ class Part(MPTTModel):
return
# Already subscribed?
- if self.is_subscribed_by(user) == status:
+ if self.is_starred_by(user) == status:
return
if status:
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index ff1fb2c8c6..981d143507 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -33,12 +33,27 @@ from .models import (BomItem, BomItemSubstitute,
class CategorySerializer(InvenTreeModelSerializer):
""" Serializer for PartCategory """
+ def __init__(self, *args, **kwargs):
+
+ self.starred_categories = kwargs.pop('starred_categories', [])
+
+ super().__init__(*args, **kwargs)
+
+ def get_starred(self, category):
+ """
+ Return True if the category is directly "starred" by the current user
+ """
+
+ return category in self.starred_categories
+
url = serializers.CharField(source='get_absolute_url', read_only=True)
parts = serializers.IntegerField(source='item_count', read_only=True)
level = serializers.IntegerField(read_only=True)
+ starred = serializers.SerializerMethodField()
+
class Meta:
model = PartCategory
fields = [
@@ -51,6 +66,7 @@ class CategorySerializer(InvenTreeModelSerializer):
'parent',
'parts',
'pathstring',
+ 'starred',
'url',
]
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index 1dcb509a59..21e26c64c6 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -23,10 +23,14 @@
{% include "admin_button.html" with url=url %}
{% endif %}
-{% if starred %}
+{% if starred_directly %}
+{% elif starred %}
+
{% else %}