mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-30 20:46:47 +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:
parent
cf023e2cc1
commit
f9a00b7a90
@ -180,10 +180,6 @@
|
|||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.starred-part {
|
|
||||||
color: #ffbb00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.red-cell {
|
.red-cell {
|
||||||
background-color: #ec7f7f;
|
background-color: #ec7f7f;
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user