diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 0183c12c20..c5f6c110ae 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -91,7 +91,7 @@ class VersionView(APIView): }) -class VersionSerializer(serializers.Serializer): +class VersionInformationSerializer(serializers.Serializer): """Serializer for a single version.""" version = serializers.CharField() @@ -101,21 +101,21 @@ class VersionSerializer(serializers.Serializer): latest = serializers.BooleanField() class Meta: - """Meta class for VersionSerializer.""" + """Meta class for VersionInformationSerializer.""" - fields = ['version', 'date', 'gh', 'text', 'latest'] + fields = '__all__' class VersionApiSerializer(serializers.Serializer): """Serializer for the version api endpoint.""" - VersionSerializer(many=True) + VersionInformationSerializer(many=True) class VersionTextView(ListAPI): """Simple JSON endpoint for InvenTree version text.""" - serializer_class = VersionSerializer + serializer_class = VersionInformationSerializer permission_classes = [permissions.IsAdminUser] diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index bc69e28ae8..8d2c02ccfe 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -1,11 +1,15 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 184 +INVENTREE_API_VERSION = 185 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v185 - 2024-03-24 : https://github.com/inventree/InvenTree/pull/6836 + - Remove /plugin/activate endpoint + - Update docstrings and typing for various API endpoints (no functional changes) + v184 - 2024-03-17 : https://github.com/inventree/InvenTree/pull/10464 - Add additional fields for tests (start/end datetime, test station) diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index c9508dd2d9..360794d8a5 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -63,7 +63,7 @@ class SettingsSerializer(InvenTreeModelSerializer): typ = serializers.CharField(read_only=True) - def get_choices(self, obj): + def get_choices(self, obj) -> list: """Returns the choices available for a given item.""" results = [] diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 246fc75fe8..8a0b839064 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -82,7 +82,7 @@ class CompanyDetail(RetrieveUpdateDestroyAPI): class CompanyAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): - """API endpoint for the CompanyAttachment model.""" + """API endpoint for listing, creating and bulk deleting a CompanyAttachment.""" queryset = CompanyAttachment.objects.all() serializer_class = CompanyAttachmentSerializer @@ -215,7 +215,7 @@ class ManufacturerPartDetail(RetrieveUpdateDestroyAPI): class ManufacturerPartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): - """API endpoint for listing (and creating) a ManufacturerPartAttachment (file upload).""" + """API endpoint for listing, creating and bulk deleting a ManufacturerPartAttachment (file upload).""" queryset = ManufacturerPartAttachment.objects.all() serializer_class = ManufacturerPartAttachmentSerializer diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index eee46120f0..8d9735fa4f 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -629,7 +629,7 @@ class PurchaseOrderExtraLineDetail(RetrieveUpdateDestroyAPI): class SalesOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): - """API endpoint for listing (and creating) a SalesOrderAttachment (file upload).""" + """API endpoint for listing, creating and bulk deleting a SalesOrderAttachment (file upload).""" queryset = models.SalesOrderAttachment.objects.all() serializer_class = serializers.SalesOrderAttachmentSerializer @@ -1097,7 +1097,7 @@ class SalesOrderShipmentComplete(CreateAPI): class PurchaseOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): - """API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload).""" + """API endpoint for listing, creating and bulk deleting) a PurchaseOrderAttachment (file upload).""" queryset = models.PurchaseOrderAttachment.objects.all() serializer_class = serializers.PurchaseOrderAttachmentSerializer @@ -1363,7 +1363,7 @@ class ReturnOrderExtraLineDetail(RetrieveUpdateDestroyAPI): class ReturnOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): - """API endpoint for listing (and creating) a ReturnOrderAttachment (file upload).""" + """API endpoint for listing, creating and bulk deleting a ReturnOrderAttachment (file upload).""" queryset = models.ReturnOrderAttachment.objects.all() serializer_class = serializers.ReturnOrderAttachmentSerializer diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 7b7ec1c7e5..fb46913cf1 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -10,6 +10,8 @@ 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 drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field from rest_framework import permissions, serializers, status from rest_framework.exceptions import ValidationError from rest_framework.response import Response @@ -214,6 +216,7 @@ class CategoryFilter(rest_filters.FilterSet): help_text=_('Exclude sub-categories under the specified category'), ) + @extend_schema_field(OpenApiTypes.INT) def filter_exclude_tree(self, queryset, name, value): """Exclude all sub-categories under the specified category.""" # Exclude the specified category @@ -406,7 +409,7 @@ class PartInternalPriceList(ListCreateAPI): class PartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): - """API endpoint for listing (and creating) a PartAttachment (file upload).""" + """API endpoint for listing, creating and bulk deleting a PartAttachment (file upload).""" queryset = PartAttachment.objects.all() serializer_class = part_serializers.PartAttachmentSerializer @@ -1003,6 +1006,7 @@ class PartFilter(rest_filters.FilterSet): method='filter_convert_from', ) + @extend_schema_field(OpenApiTypes.INT) def filter_convert_from(self, queryset, name, part): """Limit the queryset to valid conversion options for the specified part.""" conversion_options = part.get_conversion_options() @@ -1017,6 +1021,7 @@ class PartFilter(rest_filters.FilterSet): method='filter_exclude_tree', ) + @extend_schema_field(OpenApiTypes.INT) def filter_exclude_tree(self, queryset, name, part): """Exclude all parts and variants 'down' from the specified part from the queryset.""" children = part.get_descendants(include_self=True) @@ -1027,6 +1032,7 @@ class PartFilter(rest_filters.FilterSet): label='Ancestor', queryset=Part.objects.all(), method='filter_ancestor' ) + @extend_schema_field(OpenApiTypes.INT) def filter_ancestor(self, queryset, name, part): """Limit queryset to descendants of the specified ancestor part.""" descendants = part.get_descendants(include_self=False) @@ -1044,6 +1050,7 @@ class PartFilter(rest_filters.FilterSet): label='In BOM Of', queryset=Part.objects.all(), method='filter_in_bom' ) + @extend_schema_field(OpenApiTypes.INT) def filter_in_bom(self, queryset, name, part): """Limit queryset to parts in the BOM for the specified part.""" bom_parts = part.get_parts_in_bom() @@ -1528,6 +1535,7 @@ class PartParameterTemplateFilter(rest_filters.FilterSet): queryset=Part.objects.all(), method='filter_part', label=_('Part') ) + @extend_schema_field(OpenApiTypes.INT) def filter_part(self, queryset, name, part): """Filter queryset to include only PartParameterTemplates which are referenced by a part.""" parameters = PartParameter.objects.filter(part=part) @@ -1541,6 +1549,7 @@ class PartParameterTemplateFilter(rest_filters.FilterSet): label=_('Category'), ) + @extend_schema_field(OpenApiTypes.INT) def filter_category(self, queryset, name, category): """Filter queryset to include only PartParameterTemplates which are referenced by parts in this category.""" cats = category.get_descendants(include_self=True) @@ -1828,6 +1837,7 @@ class BomFilter(rest_filters.FilterSet): queryset=Part.objects.all(), method='filter_uses', label=_('Uses') ) + @extend_schema_field(OpenApiTypes.INT) def filter_uses(self, queryset, name, part): """Filter the queryset based on the specified part.""" return queryset.filter(part.get_used_in_bom_item_filter()) diff --git a/InvenTree/plugin/api.py b/InvenTree/plugin/api.py index be16328c0a..f14ab7e719 100644 --- a/InvenTree/plugin/api.py +++ b/InvenTree/plugin/api.py @@ -466,7 +466,6 @@ plugin_api_urls = [ # Plugin management path('reload/', PluginReload.as_view(), name='api-plugin-reload'), path('install/', PluginInstall.as_view(), name='api-plugin-install'), - path('activate/', PluginActivate.as_view(), name='api-plugin-activate'), # Registry status path( 'status/', diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index a0474c551d..b66282f21b 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -90,12 +90,19 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): def test_plugin_activate(self): """Test the plugin activate.""" test_plg = self.plugin_confs.first() + assert test_plg is not None def assert_plugin_active(self, active): - self.assertEqual(PluginConfig.objects.all().first().active, active) + plgs = PluginConfig.objects.all().first() + assert plgs is not None + self.assertEqual(plgs.active, active) # Should not work - not a superuser - response = self.client.post(reverse('api-plugin-activate'), {}, follow=True) + response = self.client.post( + reverse('api-plugin-detail-activate', kwargs={'pk': test_plg.pk}), + {}, + follow=True, + ) self.assertEqual(response.status_code, 403) # Make user superuser @@ -109,7 +116,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): # Activate plugin with detail url assert_plugin_active(self, False) response = self.client.patch( - reverse('api-plugin-detail-activate', kwargs={'pk': test_plg.id}), + reverse('api-plugin-detail-activate', kwargs={'pk': test_plg.pk}), {}, follow=True, ) @@ -123,7 +130,9 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): # Activate plugin assert_plugin_active(self, False) response = self.client.patch( - reverse('api-plugin-activate'), {'pk': test_plg.pk}, follow=True + reverse('api-plugin-detail-activate', kwargs={'pk': test_plg.pk}), + {}, + follow=True, ) self.assertEqual(response.status_code, 200) assert_plugin_active(self, True) @@ -133,6 +142,8 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): url = reverse('admin:plugin_pluginconfig_changelist') test_plg = self.plugin_confs.first() + assert test_plg is not None + # deactivate plugin response = self.client.post( url, @@ -181,6 +192,8 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): """Test the PluginConfig model.""" # check mixin registry plg = self.plugin_confs.first() + assert plg is not None + mixin_dict = plg.mixins() self.assertIn('base', mixin_dict) self.assertDictContainsSubset( @@ -190,6 +203,8 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): # check reload on save with self.assertWarns(Warning) as cm: plg_inactive = self.plugin_confs.filter(active=False).first() + assert plg_inactive is not None + plg_inactive.active = True plg_inactive.save() self.assertEqual(cm.warning.args[0], 'A reload was triggered') @@ -208,7 +223,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): # Wrong with pk with self.assertRaises(NotFound) as exc: - check_plugin(plugin_slug=None, plugin_pk='123') + check_plugin(plugin_slug=None, plugin_pk=123) self.assertEqual(str(exc.exception.detail), "Plugin '123' not installed") def test_plugin_settings(self): @@ -219,6 +234,8 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): # Activate the 'sample' plugin via the API cfg = PluginConfig.objects.filter(key='sample').first() + assert cfg is not None + url = reverse('api-plugin-detail-activate', kwargs={'pk': cfg.pk}) self.client.patch(url, {}, expected_code=200) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index bd5d59fc4e..80ca2efede 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -11,6 +11,8 @@ from django.urls import include, path from django.utils.translation import gettext_lazy as _ from django_filters import rest_framework as rest_filters +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field from rest_framework import status from rest_framework.response import Response from rest_framework.serializers import ValidationError @@ -480,9 +482,17 @@ class StockFilter(rest_filters.FilterSet): # Relationship filters manufacturer = rest_filters.ModelChoiceFilter( label='Manufacturer', - queryset=Company.objects.filter(is_manufacturer=True), - field_name='manufacturer_part__manufacturer', + queryset=Company.objects.all(), + method='filter_manufacturer', ) + + @extend_schema_field(OpenApiTypes.INT) + def filter_manufacturer(self, queryset, name, company): + """Filter by manufacturer.""" + return queryset.filter( + Q(is_manufacturer=True) & Q(manufacturer_part__manufacturer=company) + ) + supplier = rest_filters.ModelChoiceFilter( label='Supplier', queryset=Company.objects.filter(is_supplier=True), @@ -725,6 +735,7 @@ class StockFilter(rest_filters.FilterSet): label='Ancestor', queryset=StockItem.objects.all(), method='filter_ancestor' ) + @extend_schema_field(OpenApiTypes.INT) def filter_ancestor(self, queryset, name, ancestor): """Filter based on ancestor stock item.""" return queryset.filter(parent__in=ancestor.get_descendants(include_self=True)) @@ -735,6 +746,7 @@ class StockFilter(rest_filters.FilterSet): method='filter_category', ) + @extend_schema_field(OpenApiTypes.INT) def filter_category(self, queryset, name, category): """Filter based on part category.""" child_categories = category.get_descendants(include_self=True) @@ -745,6 +757,7 @@ class StockFilter(rest_filters.FilterSet): label=_('BOM Item'), queryset=BomItem.objects.all(), method='filter_bom_item' ) + @extend_schema_field(OpenApiTypes.INT) def filter_bom_item(self, queryset, name, bom_item): """Filter based on BOM item.""" return queryset.filter(bom_item.get_stock_filter()) @@ -753,6 +766,7 @@ class StockFilter(rest_filters.FilterSet): label=_('Part Tree'), queryset=Part.objects.all(), method='filter_part_tree' ) + @extend_schema_field(OpenApiTypes.INT) def filter_part_tree(self, queryset, name, part_tree): """Filter based on part tree.""" return queryset.filter(part__tree_id=part_tree.tree_id) @@ -761,6 +775,7 @@ class StockFilter(rest_filters.FilterSet): label=_('Company'), queryset=Company.objects.all(), method='filter_company' ) + @extend_schema_field(OpenApiTypes.INT) def filter_company(self, queryset, name, company): """Filter by company (either manufacturer or supplier).""" return queryset.filter( @@ -813,6 +828,7 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView): - GET: Return a list of all StockItem objects (with optional query filters) - POST: Create a new StockItem + - DELETE: Delete multiple StockItem objects """ serializer_class = StockSerializers.StockItemSerializer @@ -1199,7 +1215,7 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView): class StockAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): - """API endpoint for listing (and creating) a StockItemAttachment (file upload).""" + """API endpoint for listing, creating and bulk deleting a StockItemAttachment (file upload).""" queryset = StockItemAttachment.objects.all() serializer_class = StockSerializers.StockItemAttachmentSerializer diff --git a/InvenTree/web/urls.py b/InvenTree/web/urls.py index 56043c8ed9..17ac2abea3 100644 --- a/InvenTree/web/urls.py +++ b/InvenTree/web/urls.py @@ -29,11 +29,11 @@ class PreferredSerializer(serializers.Serializer): pui = serializers.SerializerMethodField(read_only=True) cui = serializers.SerializerMethodField(read_only=True) - def get_pui(self, obj): + def get_pui(self, obj) -> bool: """Return true if preferred method is PUI.""" return obj['preferred_method'] == 'pui' - def get_cui(self, obj): + def get_cui(self, obj) -> bool: """Return true if preferred method is CUI.""" return obj['preferred_method'] == 'cui'