Add bleach (#41) (#3204)

* use shims for API view inheritation

* Add mixin for input sanitation

* fix clean operation to fix all string values

* Also clean up dicts
this is to future-proof this function

* Update docstirng

* proof custom methods against XSS through authenticated users
This commit is contained in:
Matthias Mair 2022-06-16 02:01:53 +02:00 committed by GitHub
parent f8a2760955
commit e83995b4f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 310 additions and 178 deletions

View File

@ -6,10 +6,12 @@ from django.http import JsonResponse
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 @@ class BulkDeleteMixin:
)
class ListCreateDestroyAPIView(BulkDeleteMixin, generics.ListCreateAPIView):
class ListCreateDestroyAPIView(BulkDeleteMixin, ListCreateAPI):
"""Custom API endpoint which provides BulkDelete functionality in addition to List and Create"""
...

View File

@ -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."""

View File

@ -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.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAP
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 @@ class BuildFilter(rest_filters.FilterSet):
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 @@ class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
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 @@ class BuildDetail(generics.RetrieveUpdateDestroyAPIView):
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 @@ class BuildOrderContextMixin:
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 @@ class BuildOutputDelete(BuildOrderContextMixin, generics.CreateAPIView):
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()

View File

@ -9,7 +9,7 @@ from django.views.decorators.csrf import csrf_exempt
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.models
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 @@ class WebhookView(CsrfExemptMixin, APIView):
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 @@ class GlobalSettingsPermissions(permissions.BasePermission):
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 @@ class UserSettingsPermissions(permissions.BasePermission):
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 @@ class NotificationUserSettingsList(SettingsList):
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 @@ class NotificationList(BulkDeleteMixin, generics.ListAPIView):
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()

View File

@ -5,10 +5,11 @@ from django.urls import include, re_path
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 @@ from .serializers import (CompanySerializer,
SupplierPriceBreakSerializer)
class CompanyList(generics.ListCreateAPIView):
class CompanyList(ListCreateAPI):
"""API endpoint for accessing a list of Company objects.
Provides two methods:
@ -67,7 +68,7 @@ class CompanyList(generics.ListCreateAPIView):
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 @@ class ManufacturerPartList(ListCreateDestroyAPIView):
]
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 @@ class ManufacturerPartParameterList(ListCreateDestroyAPIView):
]
class ManufacturerPartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
class ManufacturerPartParameterDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of ManufacturerPartParameter model."""
queryset = ManufacturerPartParameter.objects.all()
@ -347,7 +348,7 @@ class SupplierPartList(ListCreateDestroyAPIView):
]
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()

View File

@ -6,11 +6,12 @@ from django.http import HttpResponse, JsonResponse
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 @@ from .serializers import (PartLabelSerializer, StockItemLabelSerializer,
StockLocationLabelSerializer)
class LabelListView(generics.ListAPIView):
class LabelListView(ListAPI):
"""Generic API class for label templates."""
filter_backends = [
@ -275,14 +276,14 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin):
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 @@ class StockLocationLabelList(LabelListView, StockLocationLabelMixin):
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 @@ class PartLabelList(LabelListView, PartLabelMixin):
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()

View File

@ -4,7 +4,7 @@ from django.db.models import F, Q
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 @@ from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
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 PurchaseOrderFilter(rest_filters.FilterSet):
]
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 @@ class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView):
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 @@ class PurchaseOrderContextMixin:
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 @@ class PurchaseOrderMetadata(generics.RetrieveUpdateAPIView):
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 @@ class PurchaseOrderLineItemFilter(rest_filters.FilterSet):
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 @@ class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
]
class PurchaseOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
class PurchaseOrderLineItemDetail(RetrieveUpdateDestroyAPI):
"""Detail API endpoint for PurchaseOrderLineItem object."""
queryset = models.PurchaseOrderLineItem.objects.all()
@ -514,14 +516,14 @@ class PurchaseOrderLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
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 @@ class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
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 @@ class SalesOrderLineItemFilter(rest_filters.FilterSet):
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 @@ class SalesOrderLineItemList(generics.ListCreateAPIView):
]
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 @@ class SalesOrderContextMixin:
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 @@ class SalesOrderMetadata(generics.RetrieveUpdateAPIView):
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 SalesOrderShipmentFilter(rest_filters.FilterSet):
]
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()

View File

@ -14,7 +14,7 @@ from django_filters.rest_framework import DjangoFilterBackend
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 company.models import Company, ManufacturerPart, SupplierPart
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 @@ from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
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 @@ class CategoryList(generics.ListCreateAPIView):
]
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 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
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 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
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 @@ class CategoryMetadata(generics.RetrieveUpdateAPIView):
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 @@ class CategoryParameterList(generics.ListCreateAPIView):
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 @@ class PartTestTemplateList(generics.ListCreateAPIView):
]
class PartThumbs(generics.ListAPIView):
class PartThumbs(ListAPI):
"""API endpoint for retrieving information on available Part thumbnails."""
queryset = Part.objects.all()
@ -415,7 +421,7 @@ class PartThumbs(generics.ListAPIView):
]
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 @@ class PartScheduling(generics.RetrieveAPIView):
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 @@ class PartMetadata(generics.RetrieveUpdateAPIView):
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 @@ class PartSerialNumberDetail(generics.RetrieveAPIView):
return Response(data)
class PartCopyBOM(generics.CreateAPIView):
class PartCopyBOM(CreateAPI):
"""API endpoint for duplicating a BOM."""
queryset = Part.objects.all()
@ -613,7 +619,7 @@ class PartCopyBOM(generics.CreateAPIView):
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 @@ class PartValidateBOM(generics.RetrieveUpdateAPIView):
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 @@ class PartValidateBOM(generics.RetrieveUpdateAPIView):
})
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 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
- 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 @@ class PartFilter(rest_filters.FilterSet):
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 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
"""
# 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 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
# 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 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
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 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
'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 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
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 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView):
]
class PartRelatedList(generics.ListCreateAPIView):
class PartRelatedList(ListCreateAPI):
"""API endpoint for accessing a list of PartRelated objects."""
queryset = PartRelated.objects.all()
@ -1374,14 +1389,14 @@ class PartRelatedList(generics.ListCreateAPIView):
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 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
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 @@ class BomList(ListCreateDestroyAPIView):
]
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 @@ class BomImportUpload(generics.CreateAPIView):
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 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView):
return queryset
class BomItemValidate(generics.UpdateAPIView):
class BomItemValidate(UpdateAPI):
"""API endpoint for validating a BomItem."""
class BomItemValidationSerializer(serializers.Serializer):
@ -1812,11 +1830,13 @@ class BomItemValidate(generics.UpdateAPIView):
"""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 @@ class BomItemValidate(generics.UpdateAPIView):
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()

View File

@ -4,12 +4,14 @@ from django.conf import settings
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.models import PluginConfig, PluginSetting
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 @@ class PluginList(generics.ListAPIView):
]
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 @@ class PluginInstall(generics.CreateAPIView):
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

View File

@ -8,7 +8,7 @@ from django.urls import include, path, re_path
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 common.models
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 @@ from .serializers import (BOMReportSerializer, BuildReportSerializer,
SalesOrderReportSerializer, TestReportSerializer)
class ReportListView(generics.ListAPIView):
class ReportListView(ListAPI):
"""Generic API class for report templates."""
filter_backends = [
@ -330,14 +331,14 @@ class StockItemTestReportList(ReportListView, StockItemReportMixin):
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 @@ class BOMReportList(ReportListView, PartReportMixin):
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 @@ class BuildReportList(ReportListView, BuildReportMixin):
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 @@ class PurchaseOrderReportList(ReportListView, OrderReportMixin):
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 @@ class SalesOrderReportList(ReportListView, OrderReportMixin):
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

View File

@ -12,7 +12,7 @@ from django.utils.translation import gettext_lazy as _
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.api import (APIDownloadMixin, AttachmentMixin,
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 @@ from stock.models import (StockItem, StockItemAttachment, StockItemTestResult,
StockItemTracking, StockLocation)
class StockDetail(generics.RetrieveUpdateDestroyAPIView):
class StockDetail(RetrieveUpdateDestroyAPI):
"""API detail endpoint for Stock object.
get:
@ -78,7 +80,7 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
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 @@ class StockItemContextMixin:
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 @@ class StockAssign(generics.CreateAPIView):
return ctx
class StockMerge(generics.CreateAPIView):
class StockMerge(CreateAPI):
"""API endpoint for merging multiple stock items."""
queryset = StockItem.objects.none()
@ -213,7 +215,7 @@ class StockMerge(generics.CreateAPIView):
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 @@ class StockLocationList(generics.ListCreateAPIView):
]
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 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
# 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 @@ class StockItemTestResultList(ListCreateDestroyAPIView):
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 @@ class StockTrackingList(generics.ListAPIView):
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 @@ class StockTrackingList(generics.ListAPIView):
]
class LocationMetadata(generics.RetrieveUpdateAPIView):
class LocationMetadata(RetrieveUpdateAPI):
"""API endpoint for viewing / updating StockLocation metadata."""
def get_serializer(self, *args, **kwargs):
@ -1324,7 +1330,7 @@ class LocationMetadata(generics.RetrieveUpdateAPIView):
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

View File

@ -5,17 +5,18 @@ from django.core.exceptions import ObjectDoesNotExist
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 @@ class OwnerList(generics.ListAPIView):
return results
class OwnerDetail(generics.RetrieveAPIView):
class OwnerDetail(RetrieveAPI):
"""Detail API endpoint for Owner model.
Cannot edit or delete
@ -107,7 +108,7 @@ class RoleDetails(APIView):
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()