From 2621c51a7e4d3ad9874222d407c0fce5a5ca3472 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 19 Apr 2020 23:50:41 +1000 Subject: [PATCH] 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 --- InvenTree/part/api.py | 145 +++++++--------------------------- InvenTree/part/serializers.py | 65 +++++++++++---- 2 files changed, 75 insertions(+), 135 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 6699dff64f..c65e3c673c 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -168,6 +168,29 @@ class PartList(generics.ListCreateAPIView): 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): """ Override the default 'create' behaviour: We wish to save the user who created this part! @@ -249,7 +272,7 @@ class PartList(generics.ListCreateAPIView): if has_stock: queryset = queryset.filter(Q(in_stock__gt=0)) 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 low_stock = self.request.query_params.get('low_stock', None) @@ -268,123 +291,6 @@ class PartList(generics.ListCreateAPIView): 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// 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 = [ permissions.IsAuthenticated, ] @@ -410,6 +316,7 @@ class PartList(generics.ListCreateAPIView): 'name', ] + # Default ordering ordering = 'name' search_fields = [ @@ -538,7 +445,9 @@ class BomList(generics.ListCreateAPIView): kwargs['part_detail'] = part_detail kwargs['sub_part_detail'] = sub_part_detail + # Ensure the request context is passed through! kwargs['context'] = self.get_serializer_context() + return self.serializer_class(*args, **kwargs) def get_queryset(self): diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 184bdcfc11..1b60be121f 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -84,8 +84,29 @@ class PartSerializer(InvenTreeModelSerializer): 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 def prefetch_queryset(queryset): + """ + Prefetch related database tables, + to reduce database hits. + """ + return queryset.prefetch_related( 'category', 'stock_items', @@ -93,7 +114,10 @@ class PartSerializer(InvenTreeModelSerializer): 'builds', 'supplier_parts', '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 @@ -141,33 +165,40 @@ class PartSerializer(InvenTreeModelSerializer): 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) ordering = serializers.FloatField(read_only=True) building = serializers.FloatField(read_only=True) - #allocated_stock = serializers.FloatField(source='allocation_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) + starred = serializers.SerializerMethodField() - # TODO - Include a 'category_detail' field which serializers the category object + # TODO - Include annotation for the following fields: + # allocated_stock = serializers.FloatField(source='allocation_count', read_only=True) + # bom_items = serializers.IntegerField(source='bom_count', read_only=True) + # used_in = serializers.IntegerField(source='used_in_count', read_only=True) class Meta: model = Part partial = True fields = [ 'active', - #'allocated_stock', + # 'allocated_stock', 'assembly', - #'bom_items', - #'building', + # 'bom_items', 'category', - #'category_name', + 'category_detail', 'component', 'description', 'full_name', @@ -179,18 +210,18 @@ class PartSerializer(InvenTreeModelSerializer): 'is_template', 'keywords', 'link', + 'minimum_stock', 'name', 'notes', - #'on_order', 'pk', 'purchaseable', + 'revision', 'salable', + 'starred', 'thumbnail', 'trackable', - #'total_stock', 'units', - #'used_in', - 'url', # Link to the part detail page + # 'used_in', 'variant_of', 'virtual', ]