Further API cleanup

- Perform a single call to get starred parts for current user and record results
- This provides significant speed improvements
- Remove old manual serializer
- More data prefetching
This commit is contained in:
Oliver Walters 2020-04-19 23:50:41 +10:00
parent 69b8eed028
commit 2621c51a7e
2 changed files with 75 additions and 135 deletions

View File

@ -168,6 +168,29 @@ class PartList(generics.ListCreateAPIView):
queryset = Part.objects.all() queryset = Part.objects.all()
starred_parts = None
def get_serializer(self, *args, **kwargs):
try:
cat_detail = str2bool(self.request.query_params.get('category_detail', False))
except AttributeError:
cat_detail = None
# Ensure the request context is passed through
kwargs['context'] = self.get_serializer_context()
kwargs['category_detail'] = cat_detail
# Pass a list of "starred" parts fo the current user to the serializer
# We do this to reduce the number of database queries required!
if self.starred_parts is None:
self.starred_parts = [star.part for star in self.request.user.starred_parts.all()]
kwargs['starred_parts'] = self.starred_parts
return self.serializer_class(*args, **kwargs)
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
""" Override the default 'create' behaviour: """ Override the default 'create' behaviour:
We wish to save the user who created this part! We wish to save the user who created this part!
@ -249,7 +272,7 @@ class PartList(generics.ListCreateAPIView):
if has_stock: if has_stock:
queryset = queryset.filter(Q(in_stock__gt=0)) queryset = queryset.filter(Q(in_stock__gt=0))
else: else:
queryset = queryset.filter(Q(in_stock_lte=0)) queryset = queryset.filter(Q(in_stock__lte=0))
# If we are filtering by 'low_stock' status # If we are filtering by 'low_stock' status
low_stock = self.request.query_params.get('low_stock', None) low_stock = self.request.query_params.get('low_stock', None)
@ -268,123 +291,6 @@ class PartList(generics.ListCreateAPIView):
return queryset return queryset
def dolist(self, request, *args, **kwargs):
"""
Instead of using the DRF serialiser to LIST,
we serialize the objects manually.
This turns out to be significantly faster.
"""
queryset = self.filter_queryset(self.get_queryset())
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
# Filters for annotations
# "on_order" items should only sum orders which are currently outstanding
order_filter = Q(supplier_parts__purchase_order_line_items__order__status__in=OrderStatus.OPEN)
# "building" should only reference builds which are active
build_filter = Q(builds__status__in=BuildStatus.ACTIVE_CODES)
# Set of fields we wish to serialize
data = queryset.values(
'pk',
'category',
'image',
'name',
'IPN',
'revision',
'description',
'keywords',
'is_template',
'link',
'units',
'minimum_stock',
'trackable',
'assembly',
'component',
'salable',
'active',
).annotate(
# Quantity of items which are "in stock"
in_stock=Coalesce(Sum('stock_items__quantity'), Decimal(0)) #, filter=stock_filter), Decimal(0)),
#on_order=Coalesce(Sum('supplier_parts__purchase_order_line_items__quantity', filter=order_filter), Decimal(0)),
#building=Coalesce(Sum('builds__quantity', filter=build_filter), Decimal(0)),
)
# If we are filtering by 'has_stock' status
has_stock = self.request.query_params.get('has_stock', None)
if has_stock is not None:
has_stock = str2bool(has_stock)
if has_stock:
# Filter items which have a non-null 'in_stock' quantity above zero
data = data.filter(in_stock__gt=0)
else:
# Filter items which a null or zero 'in_stock' quantity
data = data.filter(Q(in_stock__lte=0))
# If we are filtering by 'low_stock' status
low_stock = self.request.query_params.get('low_stock', None)
if low_stock is not None:
low_stock = str2bool(low_stock)
if low_stock:
# Ignore any parts which do not have a specified 'minimum_stock' level
data = data.exclude(minimum_stock=0)
# Filter items which have an 'in_stock' level lower than 'minimum_stock'
data = data.filter(Q(in_stock__lt=F('minimum_stock')))
else:
# Filter items which have an 'in_stock' level higher than 'minimum_stock'
data = data.filter(Q(in_stock__gte=F('minimum_stock')))
# Get a list of the parts that this user has starred
starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()]
# Reduce the number of lookups we need to do for the part categories
categories = {}
for item in data:
if item['image']:
# Is this part 'starred' for the current user?
item['starred'] = item['pk'] in starred_parts
img = item['image']
# Use the 'thumbnail' image here instead of the full-size image
# Note: The full-size image is used when requesting the /api/part/<x>/ endpoint
if img:
fn, ext = os.path.splitext(img)
thumb = "{fn}.thumbnail{ext}".format(fn=fn, ext=ext)
thumb = os.path.join(settings.MEDIA_URL, thumb)
else:
thumb = ''
item['thumbnail'] = thumb
del 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)
permission_classes = [ permission_classes = [
permissions.IsAuthenticated, permissions.IsAuthenticated,
] ]
@ -410,6 +316,7 @@ class PartList(generics.ListCreateAPIView):
'name', 'name',
] ]
# Default ordering
ordering = 'name' ordering = 'name'
search_fields = [ search_fields = [
@ -538,7 +445,9 @@ class BomList(generics.ListCreateAPIView):
kwargs['part_detail'] = part_detail kwargs['part_detail'] = part_detail
kwargs['sub_part_detail'] = sub_part_detail kwargs['sub_part_detail'] = sub_part_detail
# Ensure the request context is passed through!
kwargs['context'] = self.get_serializer_context() kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs) return self.serializer_class(*args, **kwargs)
def get_queryset(self): def get_queryset(self):

View File

@ -84,8 +84,29 @@ class PartSerializer(InvenTreeModelSerializer):
Used when displaying all details of a single component. Used when displaying all details of a single component.
""" """
def __init__(self, *args, **kwargs):
"""
Custom initialization method for PartSerializer,
so that we can optionally pass extra fields based on the query.
"""
self.starred_parts = kwargs.pop('starred_parts', [])
category_detail = kwargs.pop('category_detail', False)
super().__init__(*args, **kwargs)
if category_detail is not True:
self.fields.pop('category_detail')
@staticmethod @staticmethod
def prefetch_queryset(queryset): def prefetch_queryset(queryset):
"""
Prefetch related database tables,
to reduce database hits.
"""
return queryset.prefetch_related( return queryset.prefetch_related(
'category', 'category',
'stock_items', 'stock_items',
@ -93,7 +114,10 @@ class PartSerializer(InvenTreeModelSerializer):
'builds', 'builds',
'supplier_parts', 'supplier_parts',
'supplier_parts__purchase_order_line_items', 'supplier_parts__purchase_order_line_items',
'supplier_parts__purcahes_order_line_items__order' 'supplier_parts__purcahes_order_line_items__order',
'starred_users',
'starred_user__user',
'starred_user__part',
) )
@staticmethod @staticmethod
@ -141,22 +165,30 @@ class PartSerializer(InvenTreeModelSerializer):
return queryset return queryset
def get_starred(self, part):
"""
Return "true" if the part is starred by the current user.
"""
return part in self.starred_parts
# Extra detail for the category
category_detail = CategorySerializer(source='category', many=False, read_only=True)
# Calculated fields
in_stock = serializers.FloatField(read_only=True) in_stock = serializers.FloatField(read_only=True)
ordering = serializers.FloatField(read_only=True) ordering = serializers.FloatField(read_only=True)
building = serializers.FloatField(read_only=True) building = serializers.FloatField(read_only=True)
image = serializers.CharField(source='get_image_url', read_only=True)
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
starred = serializers.SerializerMethodField()
# TODO - Include annotation for the following fields:
# allocated_stock = serializers.FloatField(source='allocation_count', read_only=True) # allocated_stock = serializers.FloatField(source='allocation_count', read_only=True)
# bom_items = serializers.IntegerField(source='bom_count', read_only=True) # bom_items = serializers.IntegerField(source='bom_count', read_only=True)
#building = serializers.FloatField(source='quantity_being_built', read_only=False)
#category_name = serializers.CharField(source='category_path', read_only=True)
image = serializers.CharField(source='get_image_url', read_only=True)
#on_order = serializers.FloatField(read_only=True)
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
url = serializers.CharField(source='get_absolute_url', read_only=True)
# used_in = serializers.IntegerField(source='used_in_count', read_only=True) # used_in = serializers.IntegerField(source='used_in_count', read_only=True)
# TODO - Include a 'category_detail' field which serializers the category object
class Meta: class Meta:
model = Part model = Part
partial = True partial = True
@ -165,9 +197,8 @@ class PartSerializer(InvenTreeModelSerializer):
# 'allocated_stock', # 'allocated_stock',
'assembly', 'assembly',
# 'bom_items', # 'bom_items',
#'building',
'category', 'category',
#'category_name', 'category_detail',
'component', 'component',
'description', 'description',
'full_name', 'full_name',
@ -179,18 +210,18 @@ class PartSerializer(InvenTreeModelSerializer):
'is_template', 'is_template',
'keywords', 'keywords',
'link', 'link',
'minimum_stock',
'name', 'name',
'notes', 'notes',
#'on_order',
'pk', 'pk',
'purchaseable', 'purchaseable',
'revision',
'salable', 'salable',
'starred',
'thumbnail', 'thumbnail',
'trackable', 'trackable',
#'total_stock',
'units', 'units',
# 'used_in', # 'used_in',
'url', # Link to the part detail page
'variant_of', 'variant_of',
'virtual', 'virtual',
] ]