2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-01 04:56:45 +00:00

Adds extra subsctiption functionality for Part and PartCategory

- Allows variants and templates
- Allows categories and sub-categories
- Unit testing
This commit is contained in:
Oliver 2021-11-03 22:57:49 +11:00
parent cf023e2cc1
commit f9a00b7a90
4 changed files with 224 additions and 22 deletions

View File

@ -180,10 +180,6 @@
float: right; float: right;
} }
.starred-part {
color: #ffbb00;
}
.red-cell { .red-cell {
background-color: #ec7f7f; background-color: #ec7f7f;
} }

View File

@ -420,7 +420,7 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
if 'starred' in request.data: if 'starred' in request.data:
starred = str2bool(request.data.get('starred', None)) starred = str2bool(request.data.get('starred', None))
self.get_object().setStarred(request.user, starred) self.get_object().set_subscription(request.user, starred)
response = super().update(request, *args, **kwargs) response = super().update(request, *args, **kwargs)

View File

@ -15,7 +15,7 @@ from django.urls import reverse
from django.db import models, transaction from django.db import models, transaction
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.db.models import Q, Sum, UniqueConstraint from django.db.models import Q, Sum, UniqueConstraint, query
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
@ -201,6 +201,60 @@ class PartCategory(InvenTreeTree):
return prefetch.filter(category=self.id) return prefetch.filter(category=self.id)
def get_subscribers(self, include_parents=True):
"""
Return a list of users who subscribe to this PartCategory
"""
cats = self.get_ancestors(include_self=True)
subscribers = set()
if include_parents:
queryset = PartCategoryStar.objects.filter(
category__pk__in=[cat.pk for cat in cats]
)
else:
queryset = PartCategoryStar.objects.filter(
category=self,
)
for result in queryset:
subscribers.add(result.user)
return [s for s in subscribers]
def is_subscribed_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):
"""
Set the "subscription" status of this PartCategory against the specified user
"""
if not user:
return
if self.is_subscribed_by(user) == status:
return
if status:
PartCategoryStar.objects.create(
category=self,
user=user
)
else:
# Note that this won't actually stop the user being subscribed,
# if the user is subscribed to a parent category
PartCategoryStar.objects.filter(
category=self,
user=user,
).delete()
@receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log') @receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log')
def before_delete_part_category(sender, instance, using, **kwargs): def before_delete_part_category(sender, instance, using, **kwargs):
@ -332,7 +386,7 @@ class Part(MPTTModel):
context = {} context = {}
context['starred'] = self.isStarredBy(request.user) context['starred'] = self.is_subscribed_by(request.user)
context['disabled'] = not self.active context['disabled'] = not self.active
# Pre-calculate complex queries so they only need to be performed once # Pre-calculate complex queries so they only need to be performed once
@ -1040,30 +1094,65 @@ class Part(MPTTModel):
return self.total_stock - self.allocation_count() + self.on_order return self.total_stock - self.allocation_count() + self.on_order
def isStarredBy(self, user): def get_subscribers(self, include_variants=True, include_categories=True):
""" Return True if this part has been starred by a particular user """
try:
PartStar.objects.get(part=self, user=user)
return True
except PartStar.DoesNotExist:
return False
def setStarred(self, user, starred):
""" """
Set the "starred" status of this Part for the given user Return a list of users who are 'subscribed' to this part.
A user may 'subscribe' to this part in the following ways:
a) Subscribing to the part instance directly
b) Subscribing to a template part "above" this part (if it is a variant)
c) Subscribing to the part category that this part belongs to
d) Subscribing to a parent category of the category in c)
"""
subscribers = set()
# Start by looking at direct subscriptions to a Part model
queryset = PartStar.objects.all()
if include_variants:
queryset = queryset.filter(
part__pk__in=[part.pk for part in self.get_ancestors(include_self=True)]
)
else:
queryset = queryset.filter(part=self)
for star in queryset:
subscribers.add(star.user)
if include_categories and self.category:
for sub in self.category.get_subscribers():
subscribers.add(sub)
return [s for s in subscribers]
def is_subscribed_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):
"""
Set the "subscription" status of this Part against the specified user
""" """
if not user: if not user:
return return
# Do not duplicate efforts # Already subscribed?
if self.isStarredBy(user) == starred: if self.is_subscribed_by(user) == status:
return return
if starred: if status:
PartStar.objects.create(part=self, user=user) PartStar.objects.create(part=self, user=user)
else: else:
# Note that this won't actually stop the user being subscribed,
# if the user is subscribed to a parent part or category
PartStar.objects.filter(part=self, user=user).delete() PartStar.objects.filter(part=self, user=user).delete()
def need_to_restock(self): def need_to_restock(self):

View File

@ -11,7 +11,7 @@ from django.core.exceptions import ValidationError
import os import os
from .models import Part, PartCategory, PartTestTemplate from .models import Part, PartCategory, PartCategoryStar, PartStar, PartTestTemplate
from .models import rename_part_image from .models import rename_part_image
from .templatetags import inventree_extras from .templatetags import inventree_extras
@ -347,3 +347,120 @@ class PartSettingsTest(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
part = Part(name='Hello', description='A thing', IPN='IPN123', revision='C') part = Part(name='Hello', description='A thing', IPN='IPN123', revision='C')
part.full_clean() part.full_clean()
class PartSubscriptionTests(TestCase):
fixtures = [
'location',
'category',
'part',
]
def setUp(self):
# Create a user for auth
user = get_user_model()
self.user = user.objects.create_user(
username='testuser',
email='test@testing.com',
password='password',
is_staff=True
)
# electronics / IC / MCU
self.category = PartCategory.objects.get(pk=4)
self.part = Part.objects.create(
category=self.category,
name='STM32F103',
description='Currently worth a lot of money',
is_template=True,
)
def test_part_subcription(self):
"""
Test basic subscription against a part
"""
# First check that the user is *not* subscribed to the part
self.assertFalse(self.part.is_subscribed_by(self.user))
# Now, subscribe directly to the part
self.part.set_subscription(self.user, True)
self.assertEqual(PartStar.objects.count(), 1)
self.assertTrue(self.part.is_subscribed_by(self.user))
# Now, unsubscribe
self.part.set_subscription(self.user, False)
self.assertFalse(self.part.is_subscribed_by(self.user))
def test_variant_subscription(self):
"""
Test subscription against a parent part
"""
# Construct a sub-part to star against
sub_part = Part.objects.create(
name='sub_part',
description='a sub part',
variant_of=self.part,
)
self.assertFalse(sub_part.is_subscribed_by(self.user))
# Subscribe to the "parent" part
self.part.set_subscription(self.user, True)
self.assertTrue(self.part.is_subscribed_by(self.user))
self.assertTrue(sub_part.is_subscribed_by(self.user))
def test_category_subscription(self):
"""
Test subscription against a PartCategory
"""
self.assertEqual(PartCategoryStar.objects.count(), 0)
self.assertFalse(self.part.is_subscribed_by(self.user))
self.assertFalse(self.category.is_subscribed_by(self.user))
# Subscribe to the direct parent category
self.category.set_subscription(self.user, True)
self.assertEqual(PartStar.objects.count(), 0)
self.assertEqual(PartCategoryStar.objects.count(), 1)
self.assertTrue(self.category.is_subscribed_by(self.user))
self.assertTrue(self.part.is_subscribed_by(self.user))
# Check that the "parent" category is not starred
self.assertFalse(self.category.parent.is_subscribed_by(self.user))
# Un-subscribe
self.category.set_subscription(self.user, False)
self.assertFalse(self.category.is_subscribed_by(self.user))
self.assertFalse(self.part.is_subscribed_by(self.user))
def test_parent_category_subscription(self):
"""
Check that a parent category can be subscribed to
"""
# Top-level "electronics" category
cat = PartCategory.objects.get(pk=1)
cat.set_subscription(self.user, True)
# Check base category
self.assertTrue(cat.is_subscribed_by(self.user))
# Check lower level category
self.assertTrue(self.category.is_subscribed_by(self.user))
# Check part
self.assertTrue(self.part.is_subscribed_by(self.user))