diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index ca4d37dc7c1..38a3d559a05 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -6,10 +6,12 @@ from django.utils.translation import gettext_lazy as _ from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, generics, permissions +from rest_framework import filters, permissions from rest_framework.response import Response from rest_framework.serializers import ValidationError +from InvenTree.mixins import ListCreateAPI + from .status import is_worker_running from .version import (inventreeApiVersion, inventreeInstanceName, inventreeVersion) @@ -134,7 +136,7 @@ def delete(self, request, *args, **kwargs): ) -class ListCreateDestroyAPIView(BulkDeleteMixin, generics.ListCreateAPIView): +class ListCreateDestroyAPIView(BulkDeleteMixin, ListCreateAPI): """Custom API endpoint which provides BulkDelete functionality in addition to List and Create""" ... diff --git a/InvenTree/InvenTree/mixins.py b/InvenTree/InvenTree/mixins.py new file mode 100644 index 00000000000..59347b60eb1 --- /dev/null +++ b/InvenTree/InvenTree/mixins.py @@ -0,0 +1,90 @@ +"""Mixins for (API) views in the whole project.""" + +from bleach import clean +from rest_framework import generics, status +from rest_framework.response import Response + + +class CleanMixin(): + """Model mixin class which cleans inputs.""" + + # Define a map of fields avaialble for import + SAFE_FIELDS = {} + + def create(self, request, *args, **kwargs): + """Override to clean data before processing it.""" + serializer = self.get_serializer(data=self.clean_data(request.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) + + def update(self, request, *args, **kwargs): + """Override to clean data before processing it.""" + partial = kwargs.pop('partial', False) + instance = self.get_object() + serializer = self.get_serializer(instance, data=self.clean_data(request.data), partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + if getattr(instance, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} + + return Response(serializer.data) + + def clean_data(self, data: dict) -> dict: + """Clean / snatize data. + + This uses mozillas bleach under the hood to disable certain html tags by + encoding them - this leads to script tags etc. to not work. + The results can be longer then the input; might make some character combinations + `ugly`. Prevents XSS on the server-level. + + Args: + data (dict): Data that should be sanatized. + + Returns: + dict: Profided data sanatized; still in the same order. + """ + clean_data = {} + for k, v in data.items(): + if isinstance(v, str): + ret = clean(v) + elif isinstance(v, dict): + ret = self.clean_data(v) + else: + ret = v + clean_data[k] = ret + return clean_data + + +class ListAPI(generics.ListAPIView): + """View for list API.""" + + +class ListCreateAPI(CleanMixin, generics.ListCreateAPIView): + """View for list and create API.""" + + +class CreateAPI(CleanMixin, generics.CreateAPIView): + """View for create API.""" + + +class RetrieveAPI(generics.RetrieveAPIView): + """View for retreive API.""" + pass + + +class RetrieveUpdateAPI(CleanMixin, generics.RetrieveUpdateAPIView): + """View for retrieve and update API.""" + pass + + +class RetrieveUpdateDestroyAPI(CleanMixin, generics.RetrieveUpdateDestroyAPIView): + """View for retrieve, update and destroy API.""" + + +class UpdateAPI(CleanMixin, generics.UpdateAPIView): + """View for update API.""" diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 295035bd996..746a1c58e95 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -3,7 +3,7 @@ from django.urls import include, re_path from django.utils.translation import gettext_lazy as _ -from rest_framework import filters, generics +from rest_framework import filters from rest_framework.exceptions import ValidationError from django_filters.rest_framework import DjangoFilterBackend @@ -13,6 +13,7 @@ from InvenTree.helpers import str2bool, isNull, DownloadFile from InvenTree.filters import InvenTreeOrderingFilter from InvenTree.status_codes import BuildStatus +from InvenTree.mixins import CreateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI import build.admin import build.serializers @@ -65,7 +66,7 @@ def filter_assigned_to_me(self, queryset, name, value): return queryset -class BuildList(APIDownloadMixin, generics.ListCreateAPIView): +class BuildList(APIDownloadMixin, ListCreateAPI): """API endpoint for accessing a list of Build objects. - GET: Return list of objects (with filters) @@ -200,7 +201,7 @@ def get_serializer(self, *args, **kwargs): return self.serializer_class(*args, **kwargs) -class BuildDetail(generics.RetrieveUpdateDestroyAPIView): +class BuildDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of a Build object.""" queryset = Build.objects.all() @@ -219,7 +220,7 @@ def destroy(self, request, *args, **kwargs): return super().destroy(request, *args, **kwargs) -class BuildUnallocate(generics.CreateAPIView): +class BuildUnallocate(CreateAPI): """API endpoint for unallocating stock items from a build order. - The BuildOrder object is specified by the URL @@ -263,7 +264,7 @@ def get_serializer_context(self): return ctx -class BuildOutputCreate(BuildOrderContextMixin, generics.CreateAPIView): +class BuildOutputCreate(BuildOrderContextMixin, CreateAPI): """API endpoint for creating new build output(s).""" queryset = Build.objects.none() @@ -271,7 +272,7 @@ class BuildOutputCreate(BuildOrderContextMixin, generics.CreateAPIView): serializer_class = build.serializers.BuildOutputCreateSerializer -class BuildOutputComplete(BuildOrderContextMixin, generics.CreateAPIView): +class BuildOutputComplete(BuildOrderContextMixin, CreateAPI): """API endpoint for completing build outputs.""" queryset = Build.objects.none() @@ -279,7 +280,7 @@ class BuildOutputComplete(BuildOrderContextMixin, generics.CreateAPIView): serializer_class = build.serializers.BuildOutputCompleteSerializer -class BuildOutputDelete(BuildOrderContextMixin, generics.CreateAPIView): +class BuildOutputDelete(BuildOrderContextMixin, CreateAPI): """API endpoint for deleting multiple build outputs.""" def get_serializer_context(self): @@ -295,7 +296,7 @@ def get_serializer_context(self): serializer_class = build.serializers.BuildOutputDeleteSerializer -class BuildFinish(BuildOrderContextMixin, generics.CreateAPIView): +class BuildFinish(BuildOrderContextMixin, CreateAPI): """API endpoint for marking a build as finished (completed).""" queryset = Build.objects.none() @@ -303,7 +304,7 @@ class BuildFinish(BuildOrderContextMixin, generics.CreateAPIView): serializer_class = build.serializers.BuildCompleteSerializer -class BuildAutoAllocate(BuildOrderContextMixin, generics.CreateAPIView): +class BuildAutoAllocate(BuildOrderContextMixin, CreateAPI): """API endpoint for 'automatically' allocating stock against a build order. - Only looks at 'untracked' parts @@ -317,7 +318,7 @@ class BuildAutoAllocate(BuildOrderContextMixin, generics.CreateAPIView): serializer_class = build.serializers.BuildAutoAllocationSerializer -class BuildAllocate(BuildOrderContextMixin, generics.CreateAPIView): +class BuildAllocate(BuildOrderContextMixin, CreateAPI): """API endpoint to allocate stock items to a build order. - The BuildOrder object is specified by the URL @@ -333,21 +334,21 @@ class BuildAllocate(BuildOrderContextMixin, generics.CreateAPIView): serializer_class = build.serializers.BuildAllocationSerializer -class BuildCancel(BuildOrderContextMixin, generics.CreateAPIView): +class BuildCancel(BuildOrderContextMixin, CreateAPI): """API endpoint for cancelling a BuildOrder.""" queryset = Build.objects.all() serializer_class = build.serializers.BuildCancelSerializer -class BuildItemDetail(generics.RetrieveUpdateDestroyAPIView): +class BuildItemDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of a BuildItem object.""" queryset = BuildItem.objects.all() serializer_class = build.serializers.BuildItemSerializer -class BuildItemList(generics.ListCreateAPIView): +class BuildItemList(ListCreateAPI): """API endpoint for accessing a list of BuildItem objects. - GET: Return list of objects @@ -442,7 +443,7 @@ class BuildAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): ] -class BuildAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView): +class BuildAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI): """Detail endpoint for a BuildOrderAttachment object.""" queryset = BuildOrderAttachment.objects.all() diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 672f341e8f8..9938b890726 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -9,7 +9,7 @@ from django_filters.rest_framework import DjangoFilterBackend from django_q.tasks import async_task -from rest_framework import filters, generics, permissions, serializers +from rest_framework import filters, permissions, serializers from rest_framework.exceptions import NotAcceptable, NotFound from rest_framework.response import Response from rest_framework.views import APIView @@ -18,6 +18,8 @@ import common.serializers from InvenTree.api import BulkDeleteMixin from InvenTree.helpers import inheritors +from InvenTree.mixins import (CreateAPI, ListAPI, RetrieveAPI, + RetrieveUpdateAPI, RetrieveUpdateDestroyAPI) from plugin.models import NotificationUserSetting from plugin.serializers import NotificationUserSettingSerializer @@ -97,7 +99,7 @@ def _get_webhook(self, endpoint, request, *args, **kwargs): raise NotFound() -class SettingsList(generics.ListAPIView): +class SettingsList(ListAPI): """Generic ListView for settings. This is inheritted by all list views for settings. @@ -145,7 +147,7 @@ def has_permission(self, request, view): return False -class GlobalSettingsDetail(generics.RetrieveUpdateAPIView): +class GlobalSettingsDetail(RetrieveUpdateAPI): """Detail view for an individual "global setting" object. - User must have 'staff' status to view / edit @@ -203,7 +205,7 @@ def has_object_permission(self, request, view, obj): return user == obj.user -class UserSettingsDetail(generics.RetrieveUpdateAPIView): +class UserSettingsDetail(RetrieveUpdateAPI): """Detail view for an individual "user setting" object. - User can only view / edit settings their own settings objects @@ -245,7 +247,7 @@ def filter_queryset(self, queryset): return queryset -class NotificationUserSettingsDetail(generics.RetrieveUpdateAPIView): +class NotificationUserSettingsDetail(RetrieveUpdateAPI): """Detail view for an individual "notification user setting" object. - User can only view / edit settings their own settings objects @@ -259,7 +261,7 @@ class NotificationUserSettingsDetail(generics.RetrieveUpdateAPIView): ] -class NotificationList(BulkDeleteMixin, generics.ListAPIView): +class NotificationList(BulkDeleteMixin, ListAPI): """List view for all notifications of the current user.""" queryset = common.models.NotificationMessage.objects.all() @@ -310,7 +312,7 @@ def filter_delete_queryset(self, queryset, request): return queryset -class NotificationDetail(generics.RetrieveUpdateDestroyAPIView): +class NotificationDetail(RetrieveUpdateDestroyAPI): """Detail view for an individual notification object. - User can only view / delete their own notification objects @@ -323,7 +325,7 @@ class NotificationDetail(generics.RetrieveUpdateDestroyAPIView): ] -class NotificationReadEdit(generics.CreateAPIView): +class NotificationReadEdit(CreateAPI): """General API endpoint to manipulate read state of a notification.""" queryset = common.models.NotificationMessage.objects.all() @@ -360,7 +362,7 @@ class NotificationUnread(NotificationReadEdit): target = False -class NotificationReadAll(generics.RetrieveAPIView): +class NotificationReadAll(RetrieveAPI): """API endpoint to mark all notifications as read.""" queryset = common.models.NotificationMessage.objects.all() diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 2e8544002bf..6abc432c61d 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -5,10 +5,11 @@ from django_filters import rest_framework as rest_filters from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, generics +from rest_framework import filters from InvenTree.api import AttachmentMixin, ListCreateDestroyAPIView from InvenTree.helpers import str2bool +from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI from .models import (Company, ManufacturerPart, ManufacturerPartAttachment, ManufacturerPartParameter, SupplierPart, @@ -20,7 +21,7 @@ SupplierPriceBreakSerializer) -class CompanyList(generics.ListCreateAPIView): +class CompanyList(ListCreateAPI): """API endpoint for accessing a list of Company objects. Provides two methods: @@ -67,7 +68,7 @@ def get_queryset(self): ordering = 'name' -class CompanyDetail(generics.RetrieveUpdateDestroyAPIView): +class CompanyDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail of a single Company object.""" queryset = Company.objects.all() @@ -146,7 +147,7 @@ def get_serializer(self, *args, **kwargs): ] -class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView): +class ManufacturerPartDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of ManufacturerPart object. - GET: Retrieve detail view @@ -173,7 +174,7 @@ class ManufacturerPartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): ] -class ManufacturerPartAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView): +class ManufacturerPartAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI): """Detail endpooint for ManufacturerPartAttachment model.""" queryset = ManufacturerPartAttachment.objects.all() @@ -246,7 +247,7 @@ def filter_queryset(self, queryset): ] -class ManufacturerPartParameterDetail(generics.RetrieveUpdateDestroyAPIView): +class ManufacturerPartParameterDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of ManufacturerPartParameter model.""" queryset = ManufacturerPartParameter.objects.all() @@ -347,7 +348,7 @@ def get_serializer(self, *args, **kwargs): ] -class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView): +class SupplierPartDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of SupplierPart object. - GET: Retrieve detail view @@ -362,7 +363,7 @@ class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView): ] -class SupplierPriceBreakList(generics.ListCreateAPIView): +class SupplierPriceBreakList(ListCreateAPI): """API endpoint for list view of SupplierPriceBreak object. - GET: Retrieve list of SupplierPriceBreak objects @@ -381,7 +382,7 @@ class SupplierPriceBreakList(generics.ListCreateAPIView): ] -class SupplierPriceBreakDetail(generics.RetrieveUpdateDestroyAPIView): +class SupplierPriceBreakDetail(RetrieveUpdateDestroyAPI): """Detail endpoint for SupplierPriceBreak object.""" queryset = SupplierPriceBreak.objects.all() diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index 311e3fe53bf..a2ce1adff77 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -6,11 +6,12 @@ from django.urls import include, re_path from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, generics +from rest_framework import filters from rest_framework.exceptions import NotFound import common.models import InvenTree.helpers +from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI from InvenTree.tasks import offload_task from part.models import Part from plugin.base.label import label as plugin_label @@ -22,7 +23,7 @@ StockLocationLabelSerializer) -class LabelListView(generics.ListAPIView): +class LabelListView(ListAPI): """Generic API class for label templates.""" filter_backends = [ @@ -275,14 +276,14 @@ def filter_queryset(self, queryset): return queryset -class StockItemLabelDetail(generics.RetrieveUpdateDestroyAPIView): +class StockItemLabelDetail(RetrieveUpdateDestroyAPI): """API endpoint for a single StockItemLabel object.""" queryset = StockItemLabel.objects.all() serializer_class = StockItemLabelSerializer -class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin, LabelPrintMixin): +class StockItemLabelPrint(RetrieveAPI, StockItemLabelMixin, LabelPrintMixin): """API endpoint for printing a StockItemLabel object.""" queryset = StockItemLabel.objects.all() @@ -391,14 +392,14 @@ def filter_queryset(self, queryset): return queryset -class StockLocationLabelDetail(generics.RetrieveUpdateDestroyAPIView): +class StockLocationLabelDetail(RetrieveUpdateDestroyAPI): """API endpoint for a single StockLocationLabel object.""" queryset = StockLocationLabel.objects.all() serializer_class = StockLocationLabelSerializer -class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin, LabelPrintMixin): +class StockLocationLabelPrint(RetrieveAPI, StockLocationLabelMixin, LabelPrintMixin): """API endpoint for printing a StockLocationLabel object.""" queryset = StockLocationLabel.objects.all() @@ -483,14 +484,14 @@ def filter_queryset(self, queryset): return queryset -class PartLabelDetail(generics.RetrieveUpdateDestroyAPIView): +class PartLabelDetail(RetrieveUpdateDestroyAPI): """API endpoint for a single PartLabel object.""" queryset = PartLabel.objects.all() serializer_class = PartLabelSerializer -class PartLabelPrint(generics.RetrieveAPIView, PartLabelMixin, LabelPrintMixin): +class PartLabelPrint(RetrieveAPI, PartLabelMixin, LabelPrintMixin): """API endpoint for printing a PartLabel object.""" queryset = PartLabel.objects.all() diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 0826b9fdb08..f90446da0dd 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -4,7 +4,7 @@ from django.urls import include, path, re_path from django_filters import rest_framework as rest_filters -from rest_framework import filters, generics, status +from rest_framework import filters, status from rest_framework.response import Response import order.models as models @@ -14,6 +14,8 @@ ListCreateDestroyAPIView) from InvenTree.filters import InvenTreeOrderingFilter from InvenTree.helpers import DownloadFile, str2bool +from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, + RetrieveUpdateAPI, RetrieveUpdateDestroyAPI) from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus from order.admin import (PurchaseOrderLineItemResource, PurchaseOrderResource, SalesOrderResource) @@ -101,7 +103,7 @@ class Meta: ] -class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView): +class PurchaseOrderList(APIDownloadMixin, ListCreateAPI): """API endpoint for accessing a list of PurchaseOrder objects. - GET: Return list of PurchaseOrder objects (with filters) @@ -114,7 +116,7 @@ class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView): def create(self, request, *args, **kwargs): """Save user information on create.""" - serializer = self.get_serializer(data=request.data) + serializer = self.get_serializer(data=self.clean_data(request.data)) serializer.is_valid(raise_exception=True) item = serializer.save() @@ -254,7 +256,7 @@ def filter_queryset(self, queryset): ordering = '-creation_date' -class PurchaseOrderDetail(generics.RetrieveUpdateDestroyAPIView): +class PurchaseOrderDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of a PurchaseOrder object.""" queryset = models.PurchaseOrder.objects.all() @@ -304,7 +306,7 @@ def get_serializer_context(self): return context -class PurchaseOrderCancel(PurchaseOrderContextMixin, generics.CreateAPIView): +class PurchaseOrderCancel(PurchaseOrderContextMixin, CreateAPI): """API endpoint to 'cancel' a purchase order. The purchase order must be in a state which can be cancelled @@ -315,7 +317,7 @@ class PurchaseOrderCancel(PurchaseOrderContextMixin, generics.CreateAPIView): serializer_class = serializers.PurchaseOrderCancelSerializer -class PurchaseOrderComplete(PurchaseOrderContextMixin, generics.CreateAPIView): +class PurchaseOrderComplete(PurchaseOrderContextMixin, CreateAPI): """API endpoint to 'complete' a purchase order.""" queryset = models.PurchaseOrder.objects.all() @@ -323,7 +325,7 @@ class PurchaseOrderComplete(PurchaseOrderContextMixin, generics.CreateAPIView): serializer_class = serializers.PurchaseOrderCompleteSerializer -class PurchaseOrderIssue(PurchaseOrderContextMixin, generics.CreateAPIView): +class PurchaseOrderIssue(PurchaseOrderContextMixin, CreateAPI): """API endpoint to 'complete' a purchase order.""" queryset = models.PurchaseOrder.objects.all() @@ -331,7 +333,7 @@ class PurchaseOrderIssue(PurchaseOrderContextMixin, generics.CreateAPIView): serializer_class = serializers.PurchaseOrderIssueSerializer -class PurchaseOrderMetadata(generics.RetrieveUpdateAPIView): +class PurchaseOrderMetadata(RetrieveUpdateAPI): """API endpoint for viewing / updating PurchaseOrder metadata.""" def get_serializer(self, *args, **kwargs): @@ -341,7 +343,7 @@ def get_serializer(self, *args, **kwargs): queryset = models.PurchaseOrder.objects.all() -class PurchaseOrderReceive(PurchaseOrderContextMixin, generics.CreateAPIView): +class PurchaseOrderReceive(PurchaseOrderContextMixin, CreateAPI): """API endpoint to receive stock items against a purchase order. - The purchase order is specified in the URL. @@ -405,7 +407,7 @@ def filter_received(self, queryset, name, value): return queryset -class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView): +class PurchaseOrderLineItemList(APIDownloadMixin, ListCreateAPI): """API endpoint for accessing a list of PurchaseOrderLineItem objects. - GET: Return a list of PurchaseOrder Line Item objects @@ -499,7 +501,7 @@ def download_queryset(self, queryset, export_format): ] -class PurchaseOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView): +class PurchaseOrderLineItemDetail(RetrieveUpdateDestroyAPI): """Detail API endpoint for PurchaseOrderLineItem object.""" queryset = models.PurchaseOrderLineItem.objects.all() @@ -514,14 +516,14 @@ def get_queryset(self): return queryset -class PurchaseOrderExtraLineList(GeneralExtraLineList, generics.ListCreateAPIView): +class PurchaseOrderExtraLineList(GeneralExtraLineList, ListCreateAPI): """API endpoint for accessing a list of PurchaseOrderExtraLine objects.""" queryset = models.PurchaseOrderExtraLine.objects.all() serializer_class = serializers.PurchaseOrderExtraLineSerializer -class PurchaseOrderExtraLineDetail(generics.RetrieveUpdateDestroyAPIView): +class PurchaseOrderExtraLineDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of a PurchaseOrderExtraLine object.""" queryset = models.PurchaseOrderExtraLine.objects.all() @@ -543,14 +545,14 @@ class SalesOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): ] -class SalesOrderAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView): +class SalesOrderAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI): """Detail endpoint for SalesOrderAttachment.""" queryset = models.SalesOrderAttachment.objects.all() serializer_class = serializers.SalesOrderAttachmentSerializer -class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView): +class SalesOrderList(APIDownloadMixin, ListCreateAPI): """API endpoint for accessing a list of SalesOrder objects. - GET: Return list of SalesOrder objects (with filters) @@ -562,7 +564,7 @@ class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView): def create(self, request, *args, **kwargs): """Save user information on create.""" - serializer = self.get_serializer(data=request.data) + serializer = self.get_serializer(data=self.clean_data(request.data)) serializer.is_valid(raise_exception=True) item = serializer.save() @@ -695,7 +697,7 @@ def filter_queryset(self, queryset): ordering = '-creation_date' -class SalesOrderDetail(generics.RetrieveUpdateDestroyAPIView): +class SalesOrderDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of a SalesOrder object.""" queryset = models.SalesOrder.objects.all() @@ -754,7 +756,7 @@ def filter_completed(self, queryset, name, value): return queryset -class SalesOrderLineItemList(generics.ListCreateAPIView): +class SalesOrderLineItemList(ListCreateAPI): """API endpoint for accessing a list of SalesOrderLineItem objects.""" queryset = models.SalesOrderLineItem.objects.all() @@ -818,21 +820,21 @@ def get_queryset(self, *args, **kwargs): ] -class SalesOrderExtraLineList(GeneralExtraLineList, generics.ListCreateAPIView): +class SalesOrderExtraLineList(GeneralExtraLineList, ListCreateAPI): """API endpoint for accessing a list of SalesOrderExtraLine objects.""" queryset = models.SalesOrderExtraLine.objects.all() serializer_class = serializers.SalesOrderExtraLineSerializer -class SalesOrderExtraLineDetail(generics.RetrieveUpdateDestroyAPIView): +class SalesOrderExtraLineDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of a SalesOrderExtraLine object.""" queryset = models.SalesOrderExtraLine.objects.all() serializer_class = serializers.SalesOrderExtraLineSerializer -class SalesOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView): +class SalesOrderLineItemDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of a SalesOrderLineItem object.""" queryset = models.SalesOrderLineItem.objects.all() @@ -864,21 +866,21 @@ def get_serializer_context(self): return ctx -class SalesOrderCancel(SalesOrderContextMixin, generics.CreateAPIView): +class SalesOrderCancel(SalesOrderContextMixin, CreateAPI): """API endpoint to cancel a SalesOrder""" queryset = models.SalesOrder.objects.all() serializer_class = serializers.SalesOrderCancelSerializer -class SalesOrderComplete(SalesOrderContextMixin, generics.CreateAPIView): +class SalesOrderComplete(SalesOrderContextMixin, CreateAPI): """API endpoint for manually marking a SalesOrder as "complete".""" queryset = models.SalesOrder.objects.all() serializer_class = serializers.SalesOrderCompleteSerializer -class SalesOrderMetadata(generics.RetrieveUpdateAPIView): +class SalesOrderMetadata(RetrieveUpdateAPI): """API endpoint for viewing / updating SalesOrder metadata.""" def get_serializer(self, *args, **kwargs): @@ -888,14 +890,14 @@ def get_serializer(self, *args, **kwargs): queryset = models.SalesOrder.objects.all() -class SalesOrderAllocateSerials(SalesOrderContextMixin, generics.CreateAPIView): +class SalesOrderAllocateSerials(SalesOrderContextMixin, CreateAPI): """API endpoint to allocation stock items against a SalesOrder, by specifying serial numbers.""" queryset = models.SalesOrder.objects.none() serializer_class = serializers.SalesOrderSerialAllocationSerializer -class SalesOrderAllocate(SalesOrderContextMixin, generics.CreateAPIView): +class SalesOrderAllocate(SalesOrderContextMixin, CreateAPI): """API endpoint to allocate stock items against a SalesOrder. - The SalesOrder is specified in the URL @@ -906,14 +908,14 @@ class SalesOrderAllocate(SalesOrderContextMixin, generics.CreateAPIView): serializer_class = serializers.SalesOrderShipmentAllocationSerializer -class SalesOrderAllocationDetail(generics.RetrieveUpdateDestroyAPIView): +class SalesOrderAllocationDetail(RetrieveUpdateDestroyAPI): """API endpoint for detali view of a SalesOrderAllocation object.""" queryset = models.SalesOrderAllocation.objects.all() serializer_class = serializers.SalesOrderAllocationSerializer -class SalesOrderAllocationList(generics.ListAPIView): +class SalesOrderAllocationList(ListAPI): """API endpoint for listing SalesOrderAllocation objects.""" queryset = models.SalesOrderAllocation.objects.all() @@ -1017,7 +1019,7 @@ class Meta: ] -class SalesOrderShipmentList(generics.ListCreateAPIView): +class SalesOrderShipmentList(ListCreateAPI): """API list endpoint for SalesOrderShipment model.""" queryset = models.SalesOrderShipment.objects.all() @@ -1029,14 +1031,14 @@ class SalesOrderShipmentList(generics.ListCreateAPIView): ] -class SalesOrderShipmentDetail(generics.RetrieveUpdateDestroyAPIView): +class SalesOrderShipmentDetail(RetrieveUpdateDestroyAPI): """API detail endpooint for SalesOrderShipment model.""" queryset = models.SalesOrderShipment.objects.all() serializer_class = serializers.SalesOrderShipmentSerializer -class SalesOrderShipmentComplete(generics.CreateAPIView): +class SalesOrderShipmentComplete(CreateAPI): """API endpoint for completing (shipping) a SalesOrderShipment.""" queryset = models.SalesOrderShipment.objects.all() @@ -1072,7 +1074,7 @@ class PurchaseOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): ] -class PurchaseOrderAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView): +class PurchaseOrderAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI): """Detail endpoint for a PurchaseOrderAttachment.""" queryset = models.PurchaseOrderAttachment.objects.all() diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 7f9b0d150d1..da1403da4d3 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -14,7 +14,7 @@ from djmoney.contrib.exchange.exceptions import MissingRate from djmoney.contrib.exchange.models import convert_money from djmoney.money import Money -from rest_framework import filters, generics, serializers, status +from rest_framework import filters, serializers, status from rest_framework.exceptions import ValidationError from rest_framework.response import Response @@ -25,6 +25,9 @@ from InvenTree.api import (APIDownloadMixin, AttachmentMixin, ListCreateDestroyAPIView) from InvenTree.helpers import DownloadFile, increment, isNull, str2bool +from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI, + RetrieveUpdateAPI, RetrieveUpdateDestroyAPI, + UpdateAPI) from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus, SalesOrderStatus) from part.admin import PartResource @@ -39,7 +42,7 @@ PartTestTemplate) -class CategoryList(generics.ListCreateAPIView): +class CategoryList(ListCreateAPI): """API endpoint for accessing a list of PartCategory objects. - GET: Return a list of PartCategory objects @@ -155,7 +158,7 @@ def filter_queryset(self, queryset): ] -class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): +class CategoryDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of a single PartCategory object.""" serializer_class = part_serializers.CategorySerializer @@ -175,8 +178,11 @@ def get_serializer_context(self): def update(self, request, *args, **kwargs): """Perform 'update' function and mark this part as 'starred' (or not)""" - if 'starred' in request.data: - starred = str2bool(request.data.get('starred', False)) + # Clean up input data + data = self.clean_data(request.data) + + if 'starred' in data: + starred = str2bool(data.get('starred', False)) self.get_object().set_starred(request.user, starred) @@ -185,7 +191,7 @@ def update(self, request, *args, **kwargs): return response -class CategoryMetadata(generics.RetrieveUpdateAPIView): +class CategoryMetadata(RetrieveUpdateAPI): """API endpoint for viewing / updating PartCategory metadata.""" def get_serializer(self, *args, **kwargs): @@ -195,7 +201,7 @@ def get_serializer(self, *args, **kwargs): queryset = PartCategory.objects.all() -class CategoryParameterList(generics.ListCreateAPIView): +class CategoryParameterList(ListCreateAPI): """API endpoint for accessing a list of PartCategoryParameterTemplate objects. - GET: Return a list of PartCategoryParameterTemplate objects @@ -236,14 +242,14 @@ def get_queryset(self): return queryset -class CategoryParameterDetail(generics.RetrieveUpdateDestroyAPIView): +class CategoryParameterDetail(RetrieveUpdateDestroyAPI): """Detail endpoint fro the PartCategoryParameterTemplate model""" queryset = PartCategoryParameterTemplate.objects.all() serializer_class = part_serializers.CategoryParameterTemplateSerializer -class CategoryTree(generics.ListAPIView): +class CategoryTree(ListAPI): """API endpoint for accessing a list of PartCategory objects ready for rendering a tree.""" queryset = PartCategory.objects.all() @@ -258,14 +264,14 @@ class CategoryTree(generics.ListAPIView): ordering = ['level', 'name'] -class PartSalePriceDetail(generics.RetrieveUpdateDestroyAPIView): +class PartSalePriceDetail(RetrieveUpdateDestroyAPI): """Detail endpoint for PartSellPriceBreak model.""" queryset = PartSellPriceBreak.objects.all() serializer_class = part_serializers.PartSalePriceSerializer -class PartSalePriceList(generics.ListCreateAPIView): +class PartSalePriceList(ListCreateAPI): """API endpoint for list view of PartSalePriceBreak model.""" queryset = PartSellPriceBreak.objects.all() @@ -280,14 +286,14 @@ class PartSalePriceList(generics.ListCreateAPIView): ] -class PartInternalPriceDetail(generics.RetrieveUpdateDestroyAPIView): +class PartInternalPriceDetail(RetrieveUpdateDestroyAPI): """Detail endpoint for PartInternalPriceBreak model.""" queryset = PartInternalPriceBreak.objects.all() serializer_class = part_serializers.PartInternalPriceSerializer -class PartInternalPriceList(generics.ListCreateAPIView): +class PartInternalPriceList(ListCreateAPI): """API endpoint for list view of PartInternalPriceBreak model.""" queryset = PartInternalPriceBreak.objects.all() @@ -318,21 +324,21 @@ class PartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): ] -class PartAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView): +class PartAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI): """Detail endpoint for PartAttachment model.""" queryset = PartAttachment.objects.all() serializer_class = part_serializers.PartAttachmentSerializer -class PartTestTemplateDetail(generics.RetrieveUpdateDestroyAPIView): +class PartTestTemplateDetail(RetrieveUpdateDestroyAPI): """Detail endpoint for PartTestTemplate model.""" queryset = PartTestTemplate.objects.all() serializer_class = part_serializers.PartTestTemplateSerializer -class PartTestTemplateList(generics.ListCreateAPIView): +class PartTestTemplateList(ListCreateAPI): """API endpoint for listing (and creating) a PartTestTemplate.""" queryset = PartTestTemplate.objects.all() @@ -372,7 +378,7 @@ def filter_queryset(self, queryset): ] -class PartThumbs(generics.ListAPIView): +class PartThumbs(ListAPI): """API endpoint for retrieving information on available Part thumbnails.""" queryset = Part.objects.all() @@ -415,7 +421,7 @@ def list(self, request, *args, **kwargs): ] -class PartThumbsUpdate(generics.RetrieveUpdateAPIView): +class PartThumbsUpdate(RetrieveUpdateAPI): """API endpoint for updating Part thumbnails.""" queryset = Part.objects.all() @@ -426,7 +432,7 @@ class PartThumbsUpdate(generics.RetrieveUpdateAPIView): ] -class PartScheduling(generics.RetrieveAPIView): +class PartScheduling(RetrieveAPI): """API endpoint for delivering "scheduling" information about a given part via the API. Returns a chronologically ordered list about future "scheduled" events, @@ -560,7 +566,7 @@ def add_schedule_entry(date, quantity, title, label, url): return Response(schedule) -class PartMetadata(generics.RetrieveUpdateAPIView): +class PartMetadata(RetrieveUpdateAPI): """API endpoint for viewing / updating Part metadata.""" def get_serializer(self, *args, **kwargs): @@ -570,7 +576,7 @@ def get_serializer(self, *args, **kwargs): queryset = Part.objects.all() -class PartSerialNumberDetail(generics.RetrieveAPIView): +class PartSerialNumberDetail(RetrieveAPI): """API endpoint for returning extra serial number information about a particular part.""" queryset = Part.objects.all() @@ -595,7 +601,7 @@ def retrieve(self, request, *args, **kwargs): return Response(data) -class PartCopyBOM(generics.CreateAPIView): +class PartCopyBOM(CreateAPI): """API endpoint for duplicating a BOM.""" queryset = Part.objects.all() @@ -613,7 +619,7 @@ def get_serializer_context(self): return ctx -class PartValidateBOM(generics.RetrieveUpdateAPIView): +class PartValidateBOM(RetrieveUpdateAPI): """API endpoint for 'validating' the BOM for a given Part.""" class BOMValidateSerializer(serializers.ModelSerializer): @@ -654,7 +660,10 @@ def update(self, request, *args, **kwargs): partial = kwargs.pop('partial', False) - serializer = self.get_serializer(part, data=request.data, partial=partial) + # Clean up input data before using it + data = self.clean_data(request.data) + + serializer = self.get_serializer(part, data=data, partial=partial) serializer.is_valid(raise_exception=True) part.validate_bom(request.user) @@ -664,7 +673,7 @@ def update(self, request, *args, **kwargs): }) -class PartDetail(generics.RetrieveUpdateDestroyAPIView): +class PartDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of a single Part object.""" queryset = Part.objects.all() @@ -721,8 +730,11 @@ def update(self, request, *args, **kwargs): - If the 'starred' field is provided, update the 'starred' status against current user """ - if 'starred' in request.data: - starred = str2bool(request.data.get('starred', False)) + # Clean input data + data = self.clean_data(request.data) + + if 'starred' in data: + starred = str2bool(data.get('starred', False)) self.get_object().set_starred(request.user, starred) @@ -874,7 +886,7 @@ def filter_in_bom(self, queryset, name, part): virtual = rest_filters.BooleanFilter() -class PartList(APIDownloadMixin, generics.ListCreateAPIView): +class PartList(APIDownloadMixin, ListCreateAPI): """API endpoint for accessing a list of Part objects. - GET: Return list of objects @@ -1003,7 +1015,10 @@ def create(self, request, *args, **kwargs): """ # TODO: Unit tests for this function! - serializer = self.get_serializer(data=request.data) + # Clean up input data + data = self.clean_data(request.data) + + serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) part = serializer.save() @@ -1011,23 +1026,23 @@ def create(self, request, *args, **kwargs): # Optionally copy templates from category or parent category copy_templates = { - 'main': str2bool(request.data.get('copy_category_templates', False)), - 'parent': str2bool(request.data.get('copy_parent_templates', False)) + 'main': str2bool(data.get('copy_category_templates', False)), + 'parent': str2bool(data.get('copy_parent_templates', False)) } part.save(**{'add_category_templates': copy_templates}) # Optionally copy data from another part (e.g. when duplicating) - copy_from = request.data.get('copy_from', None) + copy_from = data.get('copy_from', None) if copy_from is not None: try: original = Part.objects.get(pk=copy_from) - copy_bom = str2bool(request.data.get('copy_bom', False)) - copy_parameters = str2bool(request.data.get('copy_parameters', False)) - copy_image = str2bool(request.data.get('copy_image', True)) + copy_bom = str2bool(data.get('copy_bom', False)) + copy_parameters = str2bool(data.get('copy_parameters', False)) + copy_image = str2bool(data.get('copy_image', True)) # Copy image? if copy_image: @@ -1046,12 +1061,12 @@ def create(self, request, *args, **kwargs): pass # Optionally create initial stock item - initial_stock = str2bool(request.data.get('initial_stock', False)) + initial_stock = str2bool(data.get('initial_stock', False)) if initial_stock: try: - initial_stock_quantity = Decimal(request.data.get('initial_stock_quantity', '')) + initial_stock_quantity = Decimal(data.get('initial_stock_quantity', '')) if initial_stock_quantity <= 0: raise ValidationError({ @@ -1062,7 +1077,7 @@ def create(self, request, *args, **kwargs): 'initial_stock_quantity': [_('Must be a valid quantity')], }) - initial_stock_location = request.data.get('initial_stock_location', None) + initial_stock_location = data.get('initial_stock_location', None) try: initial_stock_location = StockLocation.objects.get(pk=initial_stock_location) @@ -1086,20 +1101,20 @@ def create(self, request, *args, **kwargs): stock_item.save(user=request.user) # Optionally add manufacturer / supplier data to the part - if part.purchaseable and str2bool(request.data.get('add_supplier_info', False)): + if part.purchaseable and str2bool(data.get('add_supplier_info', False)): try: - manufacturer = Company.objects.get(pk=request.data.get('manufacturer', None)) + manufacturer = Company.objects.get(pk=data.get('manufacturer', None)) except Exception: manufacturer = None try: - supplier = Company.objects.get(pk=request.data.get('supplier', None)) + supplier = Company.objects.get(pk=data.get('supplier', None)) except Exception: supplier = None - mpn = str(request.data.get('MPN', '')).strip() - sku = str(request.data.get('SKU', '')).strip() + mpn = str(data.get('MPN', '')).strip() + sku = str(data.get('SKU', '')).strip() # Construct a manufacturer part if manufacturer or mpn: @@ -1347,7 +1362,7 @@ def filter_queryset(self, queryset): ] -class PartRelatedList(generics.ListCreateAPIView): +class PartRelatedList(ListCreateAPI): """API endpoint for accessing a list of PartRelated objects.""" queryset = PartRelated.objects.all() @@ -1374,14 +1389,14 @@ def filter_queryset(self, queryset): return queryset -class PartRelatedDetail(generics.RetrieveUpdateDestroyAPIView): +class PartRelatedDetail(RetrieveUpdateDestroyAPI): """API endpoint for accessing detail view of a PartRelated object.""" queryset = PartRelated.objects.all() serializer_class = part_serializers.PartRelationSerializer -class PartParameterTemplateList(generics.ListCreateAPIView): +class PartParameterTemplateList(ListCreateAPI): """API endpoint for accessing a list of PartParameterTemplate objects. - GET: Return list of PartParameterTemplate objects @@ -1441,14 +1456,14 @@ def filter_queryset(self, queryset): return queryset -class PartParameterTemplateDetail(generics.RetrieveUpdateDestroyAPIView): +class PartParameterTemplateDetail(RetrieveUpdateDestroyAPI): """API endpoint for accessing the detail view for a PartParameterTemplate object""" queryset = PartParameterTemplate.objects.all() serializer_class = part_serializers.PartParameterTemplateSerializer -class PartParameterList(generics.ListCreateAPIView): +class PartParameterList(ListCreateAPI): """API endpoint for accessing a list of PartParameter objects. - GET: Return list of PartParameter objects @@ -1468,7 +1483,7 @@ class PartParameterList(generics.ListCreateAPIView): ] -class PartParameterDetail(generics.RetrieveUpdateDestroyAPIView): +class PartParameterDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of a single PartParameter object.""" queryset = PartParameter.objects.all() @@ -1747,7 +1762,7 @@ def convert_price(price, currency, decimal_places=4): ] -class BomImportUpload(generics.CreateAPIView): +class BomImportUpload(CreateAPI): """API endpoint for uploading a complete Bill of Materials. It is assumed that the BOM has been extracted from a file using the BomExtract endpoint. @@ -1758,7 +1773,10 @@ class BomImportUpload(generics.CreateAPIView): def create(self, request, *args, **kwargs): """Custom create function to return the extracted data.""" - serializer = self.get_serializer(data=request.data) + # Clean up input data + data = self.clean_data(request.data) + + serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) @@ -1768,21 +1786,21 @@ def create(self, request, *args, **kwargs): return Response(data, status=status.HTTP_201_CREATED, headers=headers) -class BomImportExtract(generics.CreateAPIView): +class BomImportExtract(CreateAPI): """API endpoint for extracting BOM data from a BOM file.""" queryset = Part.objects.none() serializer_class = part_serializers.BomImportExtractSerializer -class BomImportSubmit(generics.CreateAPIView): +class BomImportSubmit(CreateAPI): """API endpoint for submitting BOM data from a BOM file.""" queryset = BomItem.objects.none() serializer_class = part_serializers.BomImportSubmitSerializer -class BomDetail(generics.RetrieveUpdateDestroyAPIView): +class BomDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of a single BomItem object.""" queryset = BomItem.objects.all() @@ -1798,7 +1816,7 @@ def get_queryset(self, *args, **kwargs): return queryset -class BomItemValidate(generics.UpdateAPIView): +class BomItemValidate(UpdateAPI): """API endpoint for validating a BomItem.""" class BomItemValidationSerializer(serializers.Serializer): @@ -1812,11 +1830,13 @@ def update(self, request, *args, **kwargs): """Perform update request.""" partial = kwargs.pop('partial', False) - valid = request.data.get('valid', False) + # Clean up input data + data = self.clean_data(request.data) + valid = data.get('valid', False) instance = self.get_object() - serializer = self.get_serializer(instance, data=request.data, partial=partial) + serializer = self.get_serializer(instance, data=data, partial=partial) serializer.is_valid(raise_exception=True) if type(instance) == BomItem: @@ -1825,7 +1845,7 @@ def update(self, request, *args, **kwargs): return Response(serializer.data) -class BomItemSubstituteList(generics.ListCreateAPIView): +class BomItemSubstituteList(ListCreateAPI): """API endpoint for accessing a list of BomItemSubstitute objects.""" serializer_class = part_serializers.BomItemSubstituteSerializer @@ -1843,7 +1863,7 @@ class BomItemSubstituteList(generics.ListCreateAPIView): ] -class BomItemSubstituteDetail(generics.RetrieveUpdateDestroyAPIView): +class BomItemSubstituteDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of a single BomItemSubstitute object.""" queryset = BomItemSubstitute.objects.all() diff --git a/InvenTree/plugin/api.py b/InvenTree/plugin/api.py index 91e3fa51ec5..e043da189a1 100644 --- a/InvenTree/plugin/api.py +++ b/InvenTree/plugin/api.py @@ -4,12 +4,14 @@ from django.urls import include, re_path from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, generics, permissions, status +from rest_framework import filters, permissions, status from rest_framework.exceptions import NotFound from rest_framework.response import Response import plugin.serializers as PluginSerializers from common.api import GlobalSettingsPermissions +from InvenTree.mixins import (CreateAPI, ListAPI, RetrieveUpdateAPI, + RetrieveUpdateDestroyAPI) from plugin.base.action.api import ActionPluginView from plugin.base.barcodes.api import barcode_api_urls from plugin.base.locate.api import LocatePluginView @@ -17,7 +19,7 @@ from plugin.registry import registry -class PluginList(generics.ListAPIView): +class PluginList(ListAPI): """API endpoint for list of PluginConfig objects. - GET: Return a list of all PluginConfig objects @@ -80,7 +82,7 @@ def filter_queryset(self, queryset): ] -class PluginDetail(generics.RetrieveUpdateDestroyAPIView): +class PluginDetail(RetrieveUpdateDestroyAPI): """API detail endpoint for PluginConfig object. get: @@ -97,7 +99,7 @@ class PluginDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = PluginSerializers.PluginConfigSerializer -class PluginInstall(generics.CreateAPIView): +class PluginInstall(CreateAPI): """Endpoint for installing a new plugin.""" queryset = PluginConfig.objects.none() @@ -105,7 +107,10 @@ class PluginInstall(generics.CreateAPIView): def create(self, request, *args, **kwargs): """Install a plugin via the API""" - serializer = self.get_serializer(data=request.data) + # Clean up input data + data = self.clean_data(request.data) + + serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) result = self.perform_create(serializer) result['input'] = serializer.data @@ -117,7 +122,7 @@ def perform_create(self, serializer): return serializer.save() -class PluginSettingList(generics.ListAPIView): +class PluginSettingList(ListAPI): """List endpoint for all plugin related settings. - read only @@ -141,7 +146,7 @@ class PluginSettingList(generics.ListAPIView): ] -class PluginSettingDetail(generics.RetrieveUpdateAPIView): +class PluginSettingDetail(RetrieveUpdateAPI): """Detail endpoint for a plugin-specific setting. Note that these cannot be created or deleted via the API diff --git a/InvenTree/report/api.py b/InvenTree/report/api.py index d8721eb7d7c..f27f3d9f189 100644 --- a/InvenTree/report/api.py +++ b/InvenTree/report/api.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _ from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, generics +from rest_framework import filters from rest_framework.response import Response import build.models @@ -16,6 +16,7 @@ import InvenTree.helpers import order.models import part.models +from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI from stock.models import StockItem, StockItemAttachment from .models import (BillOfMaterialsReport, BuildReport, PurchaseOrderReport, @@ -25,7 +26,7 @@ SalesOrderReportSerializer, TestReportSerializer) -class ReportListView(generics.ListAPIView): +class ReportListView(ListAPI): """Generic API class for report templates.""" filter_backends = [ @@ -330,14 +331,14 @@ def filter_queryset(self, queryset): return queryset -class StockItemTestReportDetail(generics.RetrieveUpdateDestroyAPIView): +class StockItemTestReportDetail(RetrieveUpdateDestroyAPI): """API endpoint for a single TestReport object.""" queryset = TestReport.objects.all() serializer_class = TestReportSerializer -class StockItemTestReportPrint(generics.RetrieveAPIView, StockItemReportMixin, ReportPrintMixin): +class StockItemTestReportPrint(RetrieveAPI, StockItemReportMixin, ReportPrintMixin): """API endpoint for printing a TestReport object.""" queryset = TestReport.objects.all() @@ -427,14 +428,14 @@ def filter_queryset(self, queryset): return queryset -class BOMReportDetail(generics.RetrieveUpdateDestroyAPIView): +class BOMReportDetail(RetrieveUpdateDestroyAPI): """API endpoint for a single BillOfMaterialReport object.""" queryset = BillOfMaterialsReport.objects.all() serializer_class = BOMReportSerializer -class BOMReportPrint(generics.RetrieveAPIView, PartReportMixin, ReportPrintMixin): +class BOMReportPrint(RetrieveAPI, PartReportMixin, ReportPrintMixin): """API endpoint for printing a BillOfMaterialReport object.""" queryset = BillOfMaterialsReport.objects.all() @@ -509,14 +510,14 @@ def filter_queryset(self, queryset): return queryset -class BuildReportDetail(generics.RetrieveUpdateDestroyAPIView): +class BuildReportDetail(RetrieveUpdateDestroyAPI): """API endpoint for a single BuildReport object.""" queryset = BuildReport.objects.all() serializer_class = BuildReportSerializer -class BuildReportPrint(generics.RetrieveAPIView, BuildReportMixin, ReportPrintMixin): +class BuildReportPrint(RetrieveAPI, BuildReportMixin, ReportPrintMixin): """API endpoint for printing a BuildReport.""" queryset = BuildReport.objects.all() @@ -586,14 +587,14 @@ def filter_queryset(self, queryset): return queryset -class PurchaseOrderReportDetail(generics.RetrieveUpdateDestroyAPIView): +class PurchaseOrderReportDetail(RetrieveUpdateDestroyAPI): """API endpoint for a single PurchaseOrderReport object.""" queryset = PurchaseOrderReport.objects.all() serializer_class = PurchaseOrderReportSerializer -class PurchaseOrderReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin): +class PurchaseOrderReportPrint(RetrieveAPI, OrderReportMixin, ReportPrintMixin): """API endpoint for printing a PurchaseOrderReport object.""" OrderModel = order.models.PurchaseOrder @@ -665,14 +666,14 @@ def filter_queryset(self, queryset): return queryset -class SalesOrderReportDetail(generics.RetrieveUpdateDestroyAPIView): +class SalesOrderReportDetail(RetrieveUpdateDestroyAPI): """API endpoint for a single SalesOrderReport object.""" queryset = SalesOrderReport.objects.all() serializer_class = SalesOrderReportSerializer -class SalesOrderReportPrint(generics.RetrieveAPIView, OrderReportMixin, ReportPrintMixin): +class SalesOrderReportPrint(RetrieveAPI, OrderReportMixin, ReportPrintMixin): """API endpoint for printing a PurchaseOrderReport object.""" OrderModel = order.models.SalesOrder diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index e3fe14d6b4d..da8e2b087b8 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -12,7 +12,7 @@ from django_filters import rest_framework as rest_filters from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, generics, status +from rest_framework import filters, status from rest_framework.response import Response from rest_framework.serializers import ValidationError @@ -27,6 +27,8 @@ from InvenTree.filters import InvenTreeOrderingFilter from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull, str2bool) +from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI, + RetrieveUpdateAPI, RetrieveUpdateDestroyAPI) from order.models import PurchaseOrder, SalesOrder, SalesOrderAllocation from order.serializers import PurchaseOrderSerializer from part.models import BomItem, Part, PartCategory @@ -37,7 +39,7 @@ StockItemTracking, StockLocation) -class StockDetail(generics.RetrieveUpdateDestroyAPIView): +class StockDetail(RetrieveUpdateDestroyAPI): """API detail endpoint for Stock object. get: @@ -78,7 +80,7 @@ def get_serializer(self, *args, **kwargs): return self.serializer_class(*args, **kwargs) -class StockMetadata(generics.RetrieveUpdateAPIView): +class StockMetadata(RetrieveUpdateAPI): """API endpoint for viewing / updating StockItem metadata.""" def get_serializer(self, *args, **kwargs): @@ -106,13 +108,13 @@ def get_serializer_context(self): return context -class StockItemSerialize(StockItemContextMixin, generics.CreateAPIView): +class StockItemSerialize(StockItemContextMixin, CreateAPI): """API endpoint for serializing a stock item.""" serializer_class = StockSerializers.SerializeStockItemSerializer -class StockItemInstall(StockItemContextMixin, generics.CreateAPIView): +class StockItemInstall(StockItemContextMixin, CreateAPI): """API endpoint for installing a particular stock item into this stock item. - stock_item.part must be in the BOM for this part @@ -123,25 +125,25 @@ class StockItemInstall(StockItemContextMixin, generics.CreateAPIView): serializer_class = StockSerializers.InstallStockItemSerializer -class StockItemUninstall(StockItemContextMixin, generics.CreateAPIView): +class StockItemUninstall(StockItemContextMixin, CreateAPI): """API endpoint for removing (uninstalling) items from this item.""" serializer_class = StockSerializers.UninstallStockItemSerializer -class StockItemConvert(StockItemContextMixin, generics.CreateAPIView): +class StockItemConvert(StockItemContextMixin, CreateAPI): """API endpoint for converting a stock item to a variant part""" serializer_class = StockSerializers.ConvertStockItemSerializer -class StockItemReturn(StockItemContextMixin, generics.CreateAPIView): +class StockItemReturn(StockItemContextMixin, CreateAPI): """API endpoint for returning a stock item from a customer""" serializer_class = StockSerializers.ReturnStockItemSerializer -class StockAdjustView(generics.CreateAPIView): +class StockAdjustView(CreateAPI): """A generic class for handling stocktake actions. Subclasses exist for: @@ -186,7 +188,7 @@ class StockTransfer(StockAdjustView): serializer_class = StockSerializers.StockTransferSerializer -class StockAssign(generics.CreateAPIView): +class StockAssign(CreateAPI): """API endpoint for assigning stock to a particular customer.""" queryset = StockItem.objects.all() @@ -200,7 +202,7 @@ def get_serializer_context(self): return ctx -class StockMerge(generics.CreateAPIView): +class StockMerge(CreateAPI): """API endpoint for merging multiple stock items.""" queryset = StockItem.objects.none() @@ -213,7 +215,7 @@ def get_serializer_context(self): return ctx -class StockLocationList(generics.ListCreateAPIView): +class StockLocationList(ListCreateAPI): """API endpoint for list view of StockLocation objects. - GET: Return list of StockLocation objects @@ -305,7 +307,7 @@ def filter_queryset(self, queryset): ] -class StockLocationTree(generics.ListAPIView): +class StockLocationTree(ListAPI): """API endpoint for accessing a list of StockLocation objects, ready for rendering as a tree.""" queryset = StockLocation.objects.all() @@ -502,7 +504,8 @@ def create(self, request, *args, **kwargs): # Copy the request data, to side-step "mutability" issues data = OrderedDict() - data.update(request.data) + # Update with cleaned input data + data.update(self.clean_data(request.data)) quantity = data.get('quantity', None) @@ -1067,14 +1070,14 @@ class StockAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): ] -class StockAttachmentDetail(AttachmentMixin, generics.RetrieveUpdateDestroyAPIView): +class StockAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI): """Detail endpoint for StockItemAttachment.""" queryset = StockItemAttachment.objects.all() serializer_class = StockSerializers.StockItemAttachmentSerializer -class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView): +class StockItemTestResultDetail(RetrieveUpdateDestroyAPI): """Detail endpoint for StockItemTestResult.""" queryset = StockItemTestResult.objects.all() @@ -1170,14 +1173,14 @@ def perform_create(self, serializer): test_result.save() -class StockTrackingDetail(generics.RetrieveAPIView): +class StockTrackingDetail(RetrieveAPI): """Detail API endpoint for StockItemTracking model.""" queryset = StockItemTracking.objects.all() serializer_class = StockSerializers.StockTrackingSerializer -class StockTrackingList(generics.ListAPIView): +class StockTrackingList(ListAPI): """API endpoint for list view of StockItemTracking objects. StockItemTracking objects are read-only @@ -1276,7 +1279,10 @@ def create(self, request, *args, **kwargs): Here we override the default 'create' implementation, to save the user information associated with the request object. """ - serializer = self.get_serializer(data=request.data) + # Clean up input data + data = self.clean_data(request.data) + + serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) # Record the user who created this Part object @@ -1314,7 +1320,7 @@ def create(self, request, *args, **kwargs): ] -class LocationMetadata(generics.RetrieveUpdateAPIView): +class LocationMetadata(RetrieveUpdateAPI): """API endpoint for viewing / updating StockLocation metadata.""" def get_serializer(self, *args, **kwargs): @@ -1324,7 +1330,7 @@ def get_serializer(self, *args, **kwargs): queryset = StockLocation.objects.all() -class LocationDetail(generics.RetrieveUpdateDestroyAPIView): +class LocationDetail(RetrieveUpdateDestroyAPI): """API endpoint for detail view of StockLocation object. - GET: Return a single StockLocation object diff --git a/InvenTree/users/api.py b/InvenTree/users/api.py index 11267e4d8ff..db6a629b77b 100644 --- a/InvenTree/users/api.py +++ b/InvenTree/users/api.py @@ -5,17 +5,18 @@ from django.urls import include, path, re_path from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, generics, permissions, status +from rest_framework import filters, permissions, status from rest_framework.authtoken.models import Token from rest_framework.response import Response from rest_framework.views import APIView +from InvenTree.mixins import ListAPI, RetrieveAPI from InvenTree.serializers import UserSerializer from users.models import Owner, RuleSet, check_user_role from users.serializers import OwnerSerializer -class OwnerList(generics.ListAPIView): +class OwnerList(ListAPI): """List API endpoint for Owner model. Cannot create. @@ -54,7 +55,7 @@ def filter_queryset(self, queryset): return results -class OwnerDetail(generics.RetrieveAPIView): +class OwnerDetail(RetrieveAPI): """Detail API endpoint for Owner model. Cannot edit or delete @@ -107,7 +108,7 @@ def get(self, request, *args, **kwargs): return Response(data) -class UserDetail(generics.RetrieveAPIView): +class UserDetail(RetrieveAPI): """Detail endpoint for a single user.""" queryset = User.objects.all() @@ -115,7 +116,7 @@ class UserDetail(generics.RetrieveAPIView): permission_classes = (permissions.IsAuthenticated,) -class UserList(generics.ListAPIView): +class UserList(ListAPI): """List endpoint for detail on all users.""" queryset = User.objects.all()