""" Provides a JSON API for the Part app """ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from django.conf import settings from django.db.models import Q, F, Sum, Count from django.db.models.functions import Coalesce from rest_framework import status from rest_framework.response import Response from rest_framework import filters, serializers from rest_framework import generics, permissions from django.conf.urls import url, include from django.urls import reverse import os from decimal import Decimal from .models import Part, PartCategory, BomItem, PartStar from .models import PartParameter, PartParameterTemplate from . import serializers as part_serializers from InvenTree.status_codes import OrderStatus, StockStatus, BuildStatus from InvenTree.views import TreeSerializer from InvenTree.helpers import str2bool, isNull class PartCategoryTree(TreeSerializer): title = "Parts" model = PartCategory @property def root_url(self): return reverse('part-index') def get_items(self): return PartCategory.objects.all().prefetch_related('parts', 'children') class CategoryList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PartCategory objects. - GET: Return a list of PartCategory objects - POST: Create a new PartCategory object """ queryset = PartCategory.objects.all() serializer_class = part_serializers.CategorySerializer permission_classes = [ permissions.IsAuthenticated, ] def get_queryset(self): """ Custom filtering: - Allow filtering by "null" parent to retrieve top-level part categories """ cat_id = self.request.query_params.get('parent', None) queryset = super().get_queryset() if cat_id is not None: # Look for top-level categories if isNull(cat_id): queryset = queryset.filter(parent=None) else: try: cat_id = int(cat_id) queryset = queryset.filter(parent=cat_id) except ValueError: pass return queryset filter_backends = [ DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter, ] filter_fields = [ ] ordering_fields = [ 'name', ] ordering = 'name' search_fields = [ 'name', 'description', ] class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): """ API endpoint for detail view of a single PartCategory object """ serializer_class = part_serializers.CategorySerializer queryset = PartCategory.objects.all() class PartThumbs(generics.ListAPIView): """ API endpoint for retrieving information on available Part thumbnails """ serializer_class = part_serializers.PartThumbSerializer def list(self, reguest, *args, **kwargs): """ Serialize the available Part images. - Images may be used for multiple parts! """ # Get all Parts which have an associated image queryset = Part.objects.all().exclude(image='') # Return the most popular parts first data = queryset.values( 'image', ).annotate(count=Count('image')).order_by('-count') return Response(data) class PartDetail(generics.RetrieveUpdateAPIView): """ API endpoint for detail view of a single Part object """ queryset = Part.objects.all() serializer_class = part_serializers.PartSerializer permission_classes = [ permissions.IsAuthenticated, ] class PartList(generics.ListCreateAPIView): """ API endpoint for accessing a list of Part objects - GET: Return list of objects - POST: Create a new Part object The Part object list can be filtered by: - category: Filter by PartCategory reference - cascade: If true, include parts from sub-categories - starred: Is the part "starred" by the current user? - is_template: Is the part a template part? - variant_of: Filter by variant_of Part reference - assembly: Filter by assembly field - component: Filter by component field - trackable: Filter by trackable field - purchaseable: Filter by purcahseable field - salable: Filter by salable field - active: Filter by active field """ serializer_class = part_serializers.PartSerializer 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! Note: Implementation coped from DRF class CreateModelMixin """ serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) # Record the user who created this Part object part = serializer.save() part.creation_user = request.user part.save() headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def filter_queryset(self, queryset): """ Perform custom filtering of the queryset """ # Perform basic filtering queryset = super().filter_queryset(queryset) # Filter by 'starred' parts? starred = str2bool(self.request.query_params.get('starred', None)) if starred is not None: starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()] if starred: queryset = queryset.filter(pk__in=starred_parts) else: queryset = queryset.exclude(pk__in=starred_parts) # Cascade? cascade = str2bool(self.request.query_params.get('cascade', None)) # Does the user wish to filter by category? cat_id = self.request.query_params.get('category', None) if cat_id is None: # No category filtering if category is not specified pass else: # Category has been specified! if isNull(cat_id): # A 'null' category is the top-level category if cascade is False: # Do not cascade, only list parts in the top-level category queryset = queryset.filter(category=None) else: try: category = PartCategory.objects.get(pk=cat_id) # If '?cascade=true' then include parts which exist in sub-categories if cascade: queryset = queryset.filter(category__in=category.getUniqueChildren()) # Just return parts directly in the requested category else: queryset = queryset.filter(category=cat_id) except (ValueError, PartCategory.DoesNotExist): pass # Annotate calculated data to the queryset # (This will be used for further filtering) queryset = part_serializers.PartSerializer.annotate_queryset(queryset) # Filter by whether the part has stock has_stock = self.request.query_params.get("has_stock", None) if has_stock is not None: has_stock = str2bool(has_stock) if has_stock: queryset = queryset.filter(Q(in_stock__gt=0)) else: 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) 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 queryset = queryset.exclude(minimum_stock=0) # Filter items which have an 'in_stock' level lower than 'minimum_stock' queryset = queryset.filter(Q(in_stock__lt=F('minimum_stock'))) else: # Filter items which have an 'in_stock' level higher than 'minimum_stock' queryset = queryset.filter(Q(in_stock__gte=F('minimum_stock'))) return queryset permission_classes = [ permissions.IsAuthenticated, ] filter_backends = [ DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter, ] filter_fields = [ 'is_template', 'variant_of', 'assembly', 'component', 'trackable', 'purchaseable', 'salable', 'active', ] ordering_fields = [ 'name', ] # Default ordering ordering = 'name' search_fields = [ '$name', 'description', '$IPN', 'keywords', ] class PartStarDetail(generics.RetrieveDestroyAPIView): """ API endpoint for viewing or removing a PartStar object """ queryset = PartStar.objects.all() serializer_class = part_serializers.PartStarSerializer class PartStarList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PartStar objects. - GET: Return list of PartStar objects - POST: Create a new PartStar object """ queryset = PartStar.objects.all() serializer_class = part_serializers.PartStarSerializer def create(self, request, *args, **kwargs): # Override the user field (with the logged-in user) data = request.data.copy() data['user'] = str(request.user.id) serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) permission_classes = [ permissions.IsAuthenticated, ] filter_backends = [ DjangoFilterBackend, filters.SearchFilter ] filter_fields = [ 'part', 'user', ] search_fields = [ 'partname' ] class PartParameterTemplateList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PartParameterTemplate objects. - GET: Return list of PartParameterTemplate objects - POST: Create a new PartParameterTemplate object """ queryset = PartParameterTemplate.objects.all() serializer_class = part_serializers.PartParameterTemplateSerializer permission_classes = [ permissions.IsAuthenticated, ] filter_backends = [ filters.OrderingFilter, ] filter_fields = [ 'name', ] class PartParameterList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PartParameter objects - GET: Return list of PartParameter objects - POST: Create a new PartParameter object """ queryset = PartParameter.objects.all() serializer_class = part_serializers.PartParameterSerializer permission_classes = [ permissions.IsAuthenticated, ] filter_backends = [ DjangoFilterBackend ] filter_fields = [ 'part', 'template', ] class BomList(generics.ListCreateAPIView): """ API endpoint for accessing a list of BomItem objects. - GET: Return list of BomItem objects - POST: Create a new BomItem object """ serializer_class = part_serializers.BomItemSerializer def get_serializer(self, *args, **kwargs): # Do we wish to include extra detail? try: part_detail = str2bool(self.request.GET.get('part_detail', None)) sub_part_detail = str2bool(self.request.GET.get('sub_part_detail', None)) except AttributeError: part_detail = None sub_part_detail = None 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): queryset = BomItem.objects.all() queryset = self.get_serializer_class().setup_eager_loading(queryset) # Filter by part? part = self.request.query_params.get('part', None) if part is not None: queryset = queryset.filter(part=part) # Filter by sub-part? sub_part = self.request.query_params.get('sub_part', None) if sub_part is not None: queryset = queryset.filter(sub_part=sub_part) return queryset permission_classes = [ permissions.IsAuthenticated, ] filter_backends = [ DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter, ] filter_fields = [ ] class BomDetail(generics.RetrieveUpdateDestroyAPIView): """ API endpoint for detail view of a single BomItem object """ queryset = BomItem.objects.all() serializer_class = part_serializers.BomItemSerializer permission_classes = [ permissions.IsAuthenticated, ] class BomItemValidate(generics.UpdateAPIView): """ API endpoint for validating a BomItem """ # Very simple serializers class BomItemValidationSerializer(serializers.Serializer): valid = serializers.BooleanField(default=False) queryset = BomItem.objects.all() serializer_class = BomItemValidationSerializer def update(self, request, *args, **kwargs): """ Perform update request """ partial = kwargs.pop('partial', False) valid = request.data.get('valid', False) instance = self.get_object() serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) if type(instance) == BomItem: instance.validate_hash(valid) return Response(serializer.data) cat_api_urls = [ url(r'^(?P\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'), url(r'^$', CategoryList.as_view(), name='api-part-category-list'), ] part_star_api_urls = [ url(r'^(?P\d+)/?', PartStarDetail.as_view(), name='api-part-star-detail'), # Catchall url(r'^.*$', PartStarList.as_view(), name='api-part-star-list'), ] part_param_api_urls = [ url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'), url(r'^.*$', PartParameterList.as_view(), name='api-part-param-list'), ] part_api_urls = [ url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'), url(r'^category/', include(cat_api_urls)), url(r'^star/', include(part_star_api_urls)), url(r'^parameter/', include(part_param_api_urls)), url(r'^thumbs/', PartThumbs.as_view(), name='api-part-thumbs'), url(r'^(?P\d+)/?', PartDetail.as_view(), name='api-part-detail'), url(r'^.*$', PartList.as_view(), name='api-part-list'), ] bom_item_urls = [ url(r'^validate/?', BomItemValidate.as_view(), name='api-bom-item-validate'), url(r'^.*$', BomDetail.as_view(), name='api-bom-item-detail'), ] bom_api_urls = [ # BOM Item Detail url(r'^(?P\d+)/', include(bom_item_urls)), # Catch-all url(r'^.*$', BomList.as_view(), name='api-bom-list'), ]