""" 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.db.models import Q, F, Count 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 from .models import Part, PartCategory, BomItem, PartStar from .models import PartParameter, PartParameterTemplate from . import serializers as part_serializers 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='') # TODO - We should return the thumbnails here, not the full 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 and self.request is not 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 get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) queryset = part_serializers.PartSerializer.prefetch_queryset(queryset) return queryset 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'), ]