mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	Metadata API refactor (#4545)
* Add generic metadata API endpoint (cherry picked from commit 7bbd53fc7647e2bb18d36c8c351e3fc080037ab1) * Refactor metadata endpoints for build models (cherry picked from commit 722b44e1259f1c5b046c7bc4328995b8238fc342) * Update metadata views for company models * labels * orders * part * report * stock
This commit is contained in:
		| @@ -16,7 +16,9 @@ import users.models | ||||
| from InvenTree.mixins import ListCreateAPI | ||||
| from InvenTree.permissions import RolePermission | ||||
| from part.templatetags.inventree_extras import plugins_info | ||||
| from plugin.serializers import MetadataSerializer | ||||
|  | ||||
| from .mixins import RetrieveUpdateAPI | ||||
| from .status import is_worker_running | ||||
| from .version import (inventreeApiVersion, inventreeInstanceName, | ||||
|                       inventreeVersion) | ||||
| @@ -354,3 +356,26 @@ class StatusView(APIView): | ||||
|         } | ||||
|  | ||||
|         return Response(data) | ||||
|  | ||||
|  | ||||
| class MetadataView(RetrieveUpdateAPI): | ||||
|     """Generic API endpoint for reading and editing metadata for a model""" | ||||
|  | ||||
|     MODEL_REF = 'model' | ||||
|  | ||||
|     def get_model_type(self): | ||||
|         """Return the model type associated with this API instance""" | ||||
|         model = self.kwargs.get(self.MODEL_REF, None) | ||||
|  | ||||
|         if model is None: | ||||
|             raise ValidationError(f"MetadataView called without '{self.MODEL_REF}' parameter") | ||||
|  | ||||
|         return model | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         """Return the queryset for this endpoint""" | ||||
|         return self.get_model_type().objects.all() | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance""" | ||||
|         return MetadataSerializer(self.get_model_type(), *args, **kwargs) | ||||
|   | ||||
| @@ -10,13 +10,11 @@ from rest_framework.exceptions import ValidationError | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from django_filters import rest_framework as rest_filters | ||||
|  | ||||
| from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, StatusView | ||||
| from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, MetadataView, StatusView | ||||
| from InvenTree.helpers import str2bool, isNull, DownloadFile | ||||
| from InvenTree.filters import InvenTreeOrderingFilter | ||||
| from InvenTree.status_codes import BuildStatus | ||||
| from InvenTree.mixins import CreateAPI, RetrieveUpdateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI | ||||
|  | ||||
| from plugin.serializers import MetadataSerializer | ||||
| from InvenTree.mixins import CreateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI | ||||
|  | ||||
| import build.admin | ||||
| import build.serializers | ||||
| @@ -292,16 +290,6 @@ class BuildOrderContextMixin: | ||||
|         return ctx | ||||
|  | ||||
|  | ||||
| class BuildOrderMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating BuildOrder metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance""" | ||||
|         return MetadataSerializer(Build, *args, **kwargs) | ||||
|  | ||||
|     queryset = Build.objects.all() | ||||
|  | ||||
|  | ||||
| class BuildOutputCreate(BuildOrderContextMixin, CreateAPI): | ||||
|     """API endpoint for creating new build output(s).""" | ||||
|  | ||||
| @@ -473,16 +461,6 @@ class BuildItemList(ListCreateAPI): | ||||
|     ] | ||||
|  | ||||
|  | ||||
| class BuildItemMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating BuildItem metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance""" | ||||
|         return MetadataSerializer(BuildItem, *args, **kwargs) | ||||
|  | ||||
|     queryset = BuildItem.objects.all() | ||||
|  | ||||
|  | ||||
| class BuildAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): | ||||
|     """API endpoint for listing (and creating) BuildOrderAttachment objects.""" | ||||
|  | ||||
| @@ -516,7 +494,7 @@ build_api_urls = [ | ||||
|     # Build Items | ||||
|     re_path(r'^item/', include([ | ||||
|         path(r'<int:pk>/', include([ | ||||
|             re_path(r'^metadata/', BuildItemMetadata.as_view(), name='api-build-item-metadata'), | ||||
|             re_path(r'^metadata/', MetadataView.as_view(), {'model': BuildItem}, name='api-build-item-metadata'), | ||||
|             re_path(r'^.*$', BuildItemDetail.as_view(), name='api-build-item-detail'), | ||||
|         ])), | ||||
|         re_path(r'^.*$', BuildItemList.as_view(), name='api-build-item-list'), | ||||
| @@ -532,7 +510,7 @@ build_api_urls = [ | ||||
|         re_path(r'^finish/', BuildFinish.as_view(), name='api-build-finish'), | ||||
|         re_path(r'^cancel/', BuildCancel.as_view(), name='api-build-cancel'), | ||||
|         re_path(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'), | ||||
|         re_path(r'^metadata/', BuildOrderMetadata.as_view(), name='api-build-metadata'), | ||||
|         re_path(r'^metadata/', MetadataView.as_view(), {'model': Build}, name='api-build-metadata'), | ||||
|         re_path(r'^.*$', BuildDetail.as_view(), name='api-build-detail'), | ||||
|     ])), | ||||
|  | ||||
|   | ||||
| @@ -8,12 +8,11 @@ from django_filters.rest_framework import DjangoFilterBackend | ||||
| from rest_framework import filters | ||||
|  | ||||
| import part.models | ||||
| from InvenTree.api import AttachmentMixin, ListCreateDestroyAPIView | ||||
| from InvenTree.api import (AttachmentMixin, ListCreateDestroyAPIView, | ||||
|                            MetadataView) | ||||
| from InvenTree.filters import InvenTreeOrderingFilter | ||||
| from InvenTree.helpers import str2bool | ||||
| from InvenTree.mixins import (ListCreateAPI, RetrieveUpdateAPI, | ||||
|                               RetrieveUpdateDestroyAPI) | ||||
| from plugin.serializers import MetadataSerializer | ||||
| from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI | ||||
|  | ||||
| from .models import (Company, CompanyAttachment, Contact, ManufacturerPart, | ||||
|                      ManufacturerPartAttachment, ManufacturerPartParameter, | ||||
| @@ -87,16 +86,6 @@ class CompanyDetail(RetrieveUpdateDestroyAPI): | ||||
|         return queryset | ||||
|  | ||||
|  | ||||
| class CompanyMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating Company metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance for a Company""" | ||||
|         return MetadataSerializer(Company, *args, **kwargs) | ||||
|  | ||||
|     queryset = Company.objects.all() | ||||
|  | ||||
|  | ||||
| class CompanyAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): | ||||
|     """API endpoint for the CompanyAttachment model""" | ||||
|  | ||||
| @@ -231,16 +220,6 @@ class ManufacturerPartDetail(RetrieveUpdateDestroyAPI): | ||||
|     serializer_class = ManufacturerPartSerializer | ||||
|  | ||||
|  | ||||
| class ManufacturerPartMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating ManufacturerPart metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance for a Company""" | ||||
|         return MetadataSerializer(ManufacturerPart, *args, **kwargs) | ||||
|  | ||||
|     queryset = ManufacturerPart.objects.all() | ||||
|  | ||||
|  | ||||
| class ManufacturerPartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): | ||||
|     """API endpoint for listing (and creating) a ManufacturerPartAttachment (file upload).""" | ||||
|  | ||||
| @@ -470,16 +449,6 @@ class SupplierPartDetail(RetrieveUpdateDestroyAPI): | ||||
|     ] | ||||
|  | ||||
|  | ||||
| class SupplierPartMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating SupplierPart metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance for a Company""" | ||||
|         return MetadataSerializer(SupplierPart, *args, **kwargs) | ||||
|  | ||||
|     queryset = SupplierPart.objects.all() | ||||
|  | ||||
|  | ||||
| class SupplierPriceBreakFilter(rest_filters.FilterSet): | ||||
|     """Custom API filters for the SupplierPriceBreak list endpoint""" | ||||
|  | ||||
| @@ -567,7 +536,7 @@ manufacturer_part_api_urls = [ | ||||
|     ])), | ||||
|  | ||||
|     re_path(r'^(?P<pk>\d+)/?', include([ | ||||
|         re_path('^metadata/', ManufacturerPartMetadata.as_view(), name='api-manufacturer-part-metadata'), | ||||
|         re_path('^metadata/', MetadataView.as_view(), {'model': ManufacturerPart}, name='api-manufacturer-part-metadata'), | ||||
|         re_path('^.*$', ManufacturerPartDetail.as_view(), name='api-manufacturer-part-detail'), | ||||
|     ])), | ||||
|  | ||||
| @@ -579,7 +548,7 @@ manufacturer_part_api_urls = [ | ||||
| supplier_part_api_urls = [ | ||||
|  | ||||
|     re_path(r'^(?P<pk>\d+)/?', include([ | ||||
|         re_path('^metadata/', SupplierPartMetadata.as_view(), name='api-supplier-part-metadata'), | ||||
|         re_path('^metadata/', MetadataView.as_view(), {'model': SupplierPart}, name='api-supplier-part-metadata'), | ||||
|         re_path('^.*$', SupplierPartDetail.as_view(), name='api-supplier-part-detail'), | ||||
|     ])), | ||||
|  | ||||
| @@ -601,7 +570,7 @@ company_api_urls = [ | ||||
|     ])), | ||||
|  | ||||
|     re_path(r'^(?P<pk>\d+)/?', include([ | ||||
|         re_path(r'^metadata/', CompanyMetadata.as_view(), name='api-company-metadata'), | ||||
|         re_path(r'^metadata/', MetadataView.as_view(), {'model': Company}, name='api-company-metadata'), | ||||
|         re_path(r'^.*$', CompanyDetail.as_view(), name='api-company-detail'), | ||||
|     ])), | ||||
|  | ||||
|   | ||||
| @@ -13,13 +13,12 @@ from rest_framework.exceptions import NotFound | ||||
|  | ||||
| import common.models | ||||
| import InvenTree.helpers | ||||
| from InvenTree.mixins import (ListAPI, RetrieveAPI, RetrieveUpdateAPI, | ||||
|                               RetrieveUpdateDestroyAPI) | ||||
| from InvenTree.api import MetadataView | ||||
| 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 | ||||
| from plugin.registry import registry | ||||
| from plugin.serializers import MetadataSerializer | ||||
| from stock.models import StockItem, StockLocation | ||||
|  | ||||
| from .models import PartLabel, StockItemLabel, StockLocationLabel | ||||
| @@ -307,16 +306,6 @@ class StockItemLabelDetail(StockItemLabelMixin, RetrieveUpdateDestroyAPI): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class StockItemLabelMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating StockItemLabel metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance for a Company""" | ||||
|         return MetadataSerializer(StockItemLabel, *args, **kwargs) | ||||
|  | ||||
|     queryset = StockItemLabel.objects.all() | ||||
|  | ||||
|  | ||||
| class StockItemLabelPrint(StockItemLabelMixin, LabelPrintMixin, RetrieveAPI): | ||||
|     """API endpoint for printing a StockItemLabel object.""" | ||||
|     pass | ||||
| @@ -349,16 +338,6 @@ class StockLocationLabelDetail(StockLocationLabelMixin, RetrieveUpdateDestroyAPI | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class StockLocationLabelMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating StockLocationLabel metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance for a Company""" | ||||
|         return MetadataSerializer(StockLocationLabel, *args, **kwargs) | ||||
|  | ||||
|     queryset = StockLocationLabel.objects.all() | ||||
|  | ||||
|  | ||||
| class StockLocationLabelPrint(StockLocationLabelMixin, LabelPrintMixin, RetrieveAPI): | ||||
|     """API endpoint for printing a StockLocationLabel object.""" | ||||
|     pass | ||||
| @@ -378,16 +357,6 @@ class PartLabelList(PartLabelMixin, LabelListView): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class PartLabelMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating PartLabel metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance for a Company""" | ||||
|         return MetadataSerializer(PartLabel, *args, **kwargs) | ||||
|  | ||||
|     queryset = PartLabel.objects.all() | ||||
|  | ||||
|  | ||||
| class PartLabelDetail(PartLabelMixin, RetrieveUpdateDestroyAPI): | ||||
|     """API endpoint for a single PartLabel object.""" | ||||
|     pass | ||||
| @@ -405,7 +374,7 @@ label_api_urls = [ | ||||
|         # Detail views | ||||
|         path(r'<int:pk>/', include([ | ||||
|             re_path(r'print/?', StockItemLabelPrint.as_view(), name='api-stockitem-label-print'), | ||||
|             re_path(r'metadata/', StockItemLabelMetadata.as_view(), name='api-stockitem-label-metadata'), | ||||
|             re_path(r'metadata/', MetadataView.as_view(), {'model': StockItemLabel}, name='api-stockitem-label-metadata'), | ||||
|             re_path(r'^.*$', StockItemLabelDetail.as_view(), name='api-stockitem-label-detail'), | ||||
|         ])), | ||||
|  | ||||
| @@ -418,7 +387,7 @@ label_api_urls = [ | ||||
|         # Detail views | ||||
|         path(r'<int:pk>/', include([ | ||||
|             re_path(r'print/?', StockLocationLabelPrint.as_view(), name='api-stocklocation-label-print'), | ||||
|             re_path(r'metadata/', StockLocationLabelMetadata.as_view(), name='api-stocklocation-label-metadata'), | ||||
|             re_path(r'metadata/', MetadataView.as_view(), {'model': StockLocationLabel}, name='api-stocklocation-label-metadata'), | ||||
|             re_path(r'^.*$', StockLocationLabelDetail.as_view(), name='api-stocklocation-label-detail'), | ||||
|         ])), | ||||
|  | ||||
| @@ -431,7 +400,7 @@ label_api_urls = [ | ||||
|         # Detail views | ||||
|         path(r'<int:pk>/', include([ | ||||
|             re_path(r'^print/', PartLabelPrint.as_view(), name='api-part-label-print'), | ||||
|             re_path(r'^metadata/', PartLabelMetadata.as_view(), name='api-part-label-metadata'), | ||||
|             re_path(r'^metadata/', MetadataView.as_view(), {'model': PartLabel}, name='api-part-label-metadata'), | ||||
|             re_path(r'^.*$', PartLabelDetail.as_view(), name='api-part-label-detail'), | ||||
|         ])), | ||||
|  | ||||
|   | ||||
| @@ -20,11 +20,11 @@ from common.models import InvenTreeSetting | ||||
| from common.settings import settings | ||||
| from company.models import SupplierPart | ||||
| from InvenTree.api import (APIDownloadMixin, AttachmentMixin, | ||||
|                            ListCreateDestroyAPIView, StatusView) | ||||
|                            ListCreateDestroyAPIView, MetadataView, StatusView) | ||||
| from InvenTree.filters import InvenTreeOrderingFilter | ||||
| from InvenTree.helpers import DownloadFile, str2bool | ||||
| from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, | ||||
|                               RetrieveUpdateAPI, RetrieveUpdateDestroyAPI) | ||||
|                               RetrieveUpdateDestroyAPI) | ||||
| from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus, | ||||
|                                     ReturnOrderStatus, SalesOrderStatus) | ||||
| from order.admin import (PurchaseOrderExtraLineResource, | ||||
| @@ -32,7 +32,6 @@ from order.admin import (PurchaseOrderExtraLineResource, | ||||
|                          ReturnOrderResource, SalesOrderExtraLineResource, | ||||
|                          SalesOrderLineItemResource, SalesOrderResource) | ||||
| from part.models import Part | ||||
| from plugin.serializers import MetadataSerializer | ||||
| from users.models import Owner | ||||
|  | ||||
|  | ||||
| @@ -385,16 +384,6 @@ class PurchaseOrderIssue(PurchaseOrderContextMixin, CreateAPI): | ||||
|     serializer_class = serializers.PurchaseOrderIssueSerializer | ||||
|  | ||||
|  | ||||
| class PurchaseOrderMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating PurchaseOrder metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance for a PurchaseOrder""" | ||||
|         return MetadataSerializer(models.PurchaseOrder, *args, **kwargs) | ||||
|  | ||||
|     queryset = models.PurchaseOrder.objects.all() | ||||
|  | ||||
|  | ||||
| class PurchaseOrderReceive(PurchaseOrderContextMixin, CreateAPI): | ||||
|     """API endpoint to receive stock items against a PurchaseOrder. | ||||
|  | ||||
| @@ -557,16 +546,6 @@ class PurchaseOrderLineItemDetail(PurchaseOrderLineItemMixin, RetrieveUpdateDest | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class PurchaseOrderLineItemMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating PurchaseOrderLineItem metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance for a Company""" | ||||
|         return MetadataSerializer(models.PurchaseOrderLineItem, *args, **kwargs) | ||||
|  | ||||
|     queryset = models.PurchaseOrderLineItem.objects.all() | ||||
|  | ||||
|  | ||||
| class PurchaseOrderExtraLineList(GeneralExtraLineList, ListCreateAPI): | ||||
|     """API endpoint for accessing a list of PurchaseOrderExtraLine objects.""" | ||||
|  | ||||
| @@ -590,16 +569,6 @@ class PurchaseOrderExtraLineDetail(RetrieveUpdateDestroyAPI): | ||||
|     serializer_class = serializers.PurchaseOrderExtraLineSerializer | ||||
|  | ||||
|  | ||||
| class PurchaseOrderExtraLineItemMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating PurchaseOrderExtraLineItem metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance""" | ||||
|         return MetadataSerializer(models.PurchaseOrderExtraLine, *args, **kwargs) | ||||
|  | ||||
|     queryset = models.PurchaseOrderExtraLine.objects.all() | ||||
|  | ||||
|  | ||||
| class SalesOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): | ||||
|     """API endpoint for listing (and creating) a SalesOrderAttachment (file upload)""" | ||||
|  | ||||
| @@ -873,16 +842,6 @@ class SalesOrderLineItemDetail(SalesOrderLineItemMixin, RetrieveUpdateDestroyAPI | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class SalesOrderLineItemMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating SalesOrderLineItem metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance""" | ||||
|         return MetadataSerializer(models.SalesOrderLineItem, *args, **kwargs) | ||||
|  | ||||
|     queryset = models.SalesOrderLineItem.objects.all() | ||||
|  | ||||
|  | ||||
| class SalesOrderExtraLineList(GeneralExtraLineList, ListCreateAPI): | ||||
|     """API endpoint for accessing a list of SalesOrderExtraLine objects.""" | ||||
|  | ||||
| @@ -906,16 +865,6 @@ class SalesOrderExtraLineDetail(RetrieveUpdateDestroyAPI): | ||||
|     serializer_class = serializers.SalesOrderExtraLineSerializer | ||||
|  | ||||
|  | ||||
| class SalesOrderExtraLineItemMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating SalesOrderExtraLineItem metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance""" | ||||
|         return MetadataSerializer(models.SalesOrderExtraLine, *args, **kwargs) | ||||
|  | ||||
|     queryset = models.SalesOrderExtraLine.objects.all() | ||||
|  | ||||
|  | ||||
| class SalesOrderContextMixin: | ||||
|     """Mixin to add sales order object as serializer context variable.""" | ||||
|  | ||||
| @@ -951,16 +900,6 @@ class SalesOrderComplete(SalesOrderContextMixin, CreateAPI): | ||||
|     serializer_class = serializers.SalesOrderCompleteSerializer | ||||
|  | ||||
|  | ||||
| class SalesOrderMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating SalesOrder metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return a metadata serializer for the SalesOrder model""" | ||||
|         return MetadataSerializer(models.SalesOrder, *args, **kwargs) | ||||
|  | ||||
|     queryset = models.SalesOrder.objects.all() | ||||
|  | ||||
|  | ||||
| class SalesOrderAllocateSerials(SalesOrderContextMixin, CreateAPI): | ||||
|     """API endpoint to allocation stock items against a SalesOrder, by specifying serial numbers.""" | ||||
|  | ||||
| @@ -1122,16 +1061,6 @@ class SalesOrderShipmentComplete(CreateAPI): | ||||
|         return ctx | ||||
|  | ||||
|  | ||||
| class SalesOrderShipmentMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating SalesOrderShipment metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return MetadataSerializer instance""" | ||||
|         return MetadataSerializer(models.SalesOrderShipment, *args, **kwargs) | ||||
|  | ||||
|     queryset = models.SalesOrderShipment.objects.all() | ||||
|  | ||||
|  | ||||
| class PurchaseOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): | ||||
|     """API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload)""" | ||||
|  | ||||
| @@ -1604,7 +1533,7 @@ order_api_urls = [ | ||||
|             re_path(r'^cancel/', PurchaseOrderCancel.as_view(), name='api-po-cancel'), | ||||
|             re_path(r'^complete/', PurchaseOrderComplete.as_view(), name='api-po-complete'), | ||||
|             re_path(r'^issue/', PurchaseOrderIssue.as_view(), name='api-po-issue'), | ||||
|             re_path(r'^metadata/', PurchaseOrderMetadata.as_view(), name='api-po-metadata'), | ||||
|             re_path(r'^metadata/', MetadataView.as_view(), {'model': models.PurchaseOrder}, name='api-po-metadata'), | ||||
|             re_path(r'^receive/', PurchaseOrderReceive.as_view(), name='api-po-receive'), | ||||
|  | ||||
|             # PurchaseOrder detail API endpoint | ||||
| @@ -1621,7 +1550,7 @@ order_api_urls = [ | ||||
|     # API endpoints for purchase order line items | ||||
|     re_path(r'^po-line/', include([ | ||||
|         path('<int:pk>/', include([ | ||||
|             re_path(r'^metadata/', PurchaseOrderLineItemMetadata.as_view(), name='api-po-line-metadata'), | ||||
|             re_path(r'^metadata/', MetadataView.as_view(), {'model': models.PurchaseOrderLineItem}, name='api-po-line-metadata'), | ||||
|             re_path(r'^.*$', PurchaseOrderLineItemDetail.as_view(), name='api-po-line-detail'), | ||||
|         ])), | ||||
|         re_path(r'^.*$', PurchaseOrderLineItemList.as_view(), name='api-po-line-list'), | ||||
| @@ -1630,7 +1559,7 @@ order_api_urls = [ | ||||
|     # API endpoints for purchase order extra line | ||||
|     re_path(r'^po-extra-line/', include([ | ||||
|         path('<int:pk>/', include([ | ||||
|             re_path(r'^metadata/', PurchaseOrderExtraLineItemMetadata.as_view(), name='api-po-extra-line-metadata'), | ||||
|             re_path(r'^metadata/', MetadataView.as_view(), {'model': models.PurchaseOrderExtraLine}, name='api-po-extra-line-metadata'), | ||||
|             re_path(r'^.*$', PurchaseOrderExtraLineDetail.as_view(), name='api-po-extra-line-detail'), | ||||
|         ])), | ||||
|         path('', PurchaseOrderExtraLineList.as_view(), name='api-po-extra-line-list'), | ||||
| @@ -1646,7 +1575,7 @@ order_api_urls = [ | ||||
|         re_path(r'^shipment/', include([ | ||||
|             path(r'<int:pk>/', include([ | ||||
|                 path('ship/', SalesOrderShipmentComplete.as_view(), name='api-so-shipment-ship'), | ||||
|                 re_path(r'^metadata/', SalesOrderShipmentMetadata.as_view(), name='api-so-shipment-metadata'), | ||||
|                 re_path(r'^metadata/', MetadataView.as_view(), {'model': models.SalesOrderShipment}, name='api-so-shipment-metadata'), | ||||
|                 re_path(r'^.*$', SalesOrderShipmentDetail.as_view(), name='api-so-shipment-detail'), | ||||
|             ])), | ||||
|             re_path(r'^.*$', SalesOrderShipmentList.as_view(), name='api-so-shipment-list'), | ||||
| @@ -1659,7 +1588,7 @@ order_api_urls = [ | ||||
|             re_path(r'^cancel/', SalesOrderCancel.as_view(), name='api-so-cancel'), | ||||
|             re_path(r'^issue/', SalesOrderIssue.as_view(), name='api-so-issue'), | ||||
|             re_path(r'^complete/', SalesOrderComplete.as_view(), name='api-so-complete'), | ||||
|             re_path(r'^metadata/', SalesOrderMetadata.as_view(), name='api-so-metadata'), | ||||
|             re_path(r'^metadata/', MetadataView.as_view(), {'model': models.SalesOrder}, name='api-so-metadata'), | ||||
|  | ||||
|             # SalesOrder detail endpoint | ||||
|             re_path(r'^.*$', SalesOrderDetail.as_view(), name='api-so-detail'), | ||||
| @@ -1675,7 +1604,7 @@ order_api_urls = [ | ||||
|     # API endpoints for sales order line items | ||||
|     re_path(r'^so-line/', include([ | ||||
|         path('<int:pk>/', include([ | ||||
|             re_path(r'^metadata/', SalesOrderLineItemMetadata.as_view(), name='api-so-line-metadata'), | ||||
|             re_path(r'^metadata/', MetadataView.as_view(), {'model': models.SalesOrderLineItem}, name='api-so-line-metadata'), | ||||
|             re_path(r'^.*$', SalesOrderLineItemDetail.as_view(), name='api-so-line-detail'), | ||||
|         ])), | ||||
|         path('', SalesOrderLineItemList.as_view(), name='api-so-line-list'), | ||||
| @@ -1684,7 +1613,7 @@ order_api_urls = [ | ||||
|     # API endpoints for sales order extra line | ||||
|     re_path(r'^so-extra-line/', include([ | ||||
|         path('<int:pk>/', include([ | ||||
|             re_path(r'^metadata/', SalesOrderExtraLineItemMetadata.as_view(), name='api-so-extra-line-metadata'), | ||||
|             re_path(r'^metadata/', MetadataView.as_view(), {'model': models.SalesOrderExtraLine}, name='api-so-extra-line-metadata'), | ||||
|             re_path(r'^.*$', SalesOrderExtraLineDetail.as_view(), name='api-so-extra-line-detail'), | ||||
|         ])), | ||||
|         path('', SalesOrderExtraLineList.as_view(), name='api-so-extra-line-list'), | ||||
|   | ||||
| @@ -16,7 +16,7 @@ from rest_framework.response import Response | ||||
| import order.models | ||||
| from build.models import Build, BuildItem | ||||
| from InvenTree.api import (APIDownloadMixin, AttachmentMixin, | ||||
|                            ListCreateDestroyAPIView) | ||||
|                            ListCreateDestroyAPIView, MetadataView) | ||||
| from InvenTree.filters import InvenTreeOrderingFilter | ||||
| from InvenTree.helpers import (DownloadFile, increment_serial_number, isNull, | ||||
|                                str2bool, str2int) | ||||
| @@ -28,7 +28,6 @@ from InvenTree.permissions import RolePermission | ||||
| from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus, | ||||
|                                     SalesOrderStatus) | ||||
| from part.admin import PartCategoryResource, PartResource | ||||
| from plugin.serializers import MetadataSerializer | ||||
|  | ||||
| from . import serializers as part_serializers | ||||
| from . import views | ||||
| @@ -230,16 +229,6 @@ class CategoryTree(ListAPI): | ||||
|     ordering = ['level', 'name'] | ||||
|  | ||||
|  | ||||
| class CategoryMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating PartCategory metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return a MetadataSerializer pointing to the referenced PartCategory instance""" | ||||
|         return MetadataSerializer(PartCategory, *args, **kwargs) | ||||
|  | ||||
|     queryset = PartCategory.objects.all() | ||||
|  | ||||
|  | ||||
| class CategoryParameterList(ListCreateAPI): | ||||
|     """API endpoint for accessing a list of PartCategoryParameterTemplate objects. | ||||
|  | ||||
| @@ -698,16 +687,6 @@ class PartRequirements(RetrieveAPI): | ||||
|         return Response(data) | ||||
|  | ||||
|  | ||||
| class PartMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating Part metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Returns a MetadataSerializer instance pointing to the referenced Part""" | ||||
|         return MetadataSerializer(Part, *args, **kwargs) | ||||
|  | ||||
|     queryset = Part.objects.all() | ||||
|  | ||||
|  | ||||
| class PartPricingDetail(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing part pricing data""" | ||||
|  | ||||
| @@ -1415,16 +1394,6 @@ class PartParameterTemplateDetail(RetrieveUpdateDestroyAPI): | ||||
|     serializer_class = part_serializers.PartParameterTemplateSerializer | ||||
|  | ||||
|  | ||||
| class PartParameterTemplateMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating PartParameterTemplate metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return a MetadataSerializer pointing to the referenced PartParameterTemplate instance""" | ||||
|         return MetadataSerializer(PartParameterTemplate, *args, **kwargs) | ||||
|  | ||||
|     queryset = PartParameterTemplate.objects.all() | ||||
|  | ||||
|  | ||||
| class PartParameterList(ListCreateAPI): | ||||
|     """API endpoint for accessing a list of PartParameter objects. | ||||
|  | ||||
| @@ -1886,16 +1855,6 @@ class BomItemSubstituteDetail(RetrieveUpdateDestroyAPI): | ||||
|     serializer_class = part_serializers.BomItemSubstituteSerializer | ||||
|  | ||||
|  | ||||
| class BomItemMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating PartBOM metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return a MetadataSerializer pointing to the referenced PartCategory instance""" | ||||
|         return MetadataSerializer(BomItem, *args, **kwargs) | ||||
|  | ||||
|     queryset = BomItem.objects.all() | ||||
|  | ||||
|  | ||||
| part_api_urls = [ | ||||
|  | ||||
|     # Base URL for PartCategory API endpoints | ||||
| @@ -1910,7 +1869,7 @@ part_api_urls = [ | ||||
|         # Category detail endpoints | ||||
|         path(r'<int:pk>/', include([ | ||||
|  | ||||
|             re_path(r'^metadata/', CategoryMetadata.as_view(), name='api-part-category-metadata'), | ||||
|             re_path(r'^metadata/', MetadataView.as_view(), {'model': PartCategory}, name='api-part-category-metadata'), | ||||
|  | ||||
|             # PartCategory detail endpoint | ||||
|             re_path(r'^.*$', CategoryDetail.as_view(), name='api-part-category-detail'), | ||||
| @@ -1953,7 +1912,7 @@ part_api_urls = [ | ||||
|     re_path(r'^parameter/', include([ | ||||
|         path('template/', include([ | ||||
|             re_path(r'^(?P<pk>\d+)/', include([ | ||||
|                 re_path(r'^metadata/?', PartParameterTemplateMetadata.as_view(), name='api-part-parameter-template-metadata'), | ||||
|                 re_path(r'^metadata/?', MetadataView.as_view(), {'model': PartParameter}, name='api-part-parameter-template-metadata'), | ||||
|                 re_path(r'^.*$', PartParameterTemplateDetail.as_view(), name='api-part-parameter-template-detail'), | ||||
|             ])), | ||||
|             re_path(r'^.*$', PartParameterTemplateList.as_view(), name='api-part-parameter-template-list'), | ||||
| @@ -2000,7 +1959,7 @@ part_api_urls = [ | ||||
|         re_path(r'^bom-validate/', PartValidateBOM.as_view(), name='api-part-bom-validate'), | ||||
|  | ||||
|         # Part metadata | ||||
|         re_path(r'^metadata/', PartMetadata.as_view(), name='api-part-metadata'), | ||||
|         re_path(r'^metadata/', MetadataView.as_view(), {'model': Part}, name='api-part-metadata'), | ||||
|  | ||||
|         # Part pricing | ||||
|         re_path(r'^pricing/', PartPricingDetail.as_view(), name='api-part-pricing'), | ||||
| @@ -2032,7 +1991,7 @@ bom_api_urls = [ | ||||
|     # BOM Item Detail | ||||
|     path(r'<int:pk>/', include([ | ||||
|         re_path(r'^validate/?', BomItemValidate.as_view(), name='api-bom-item-validate'), | ||||
|         re_path(r'^metadata/?', BomItemMetadata.as_view(), name='api-bom-item-metadata'), | ||||
|         re_path(r'^metadata/?', MetadataView.as_view(), {'model': BomItem}, name='api-bom-item-metadata'), | ||||
|         re_path(r'^.*$', BomDetail.as_view(), name='api-bom-item-detail'), | ||||
|     ])), | ||||
|  | ||||
|   | ||||
| @@ -18,9 +18,8 @@ import common.models | ||||
| import InvenTree.helpers | ||||
| import order.models | ||||
| import part.models | ||||
| from InvenTree.mixins import (ListAPI, RetrieveAPI, RetrieveUpdateAPI, | ||||
|                               RetrieveUpdateDestroyAPI) | ||||
| from plugin.serializers import MetadataSerializer | ||||
| from InvenTree.api import MetadataView | ||||
| from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI | ||||
| from stock.models import StockItem, StockItemAttachment | ||||
|  | ||||
| from .models import (BillOfMaterialsReport, BuildReport, PurchaseOrderReport, | ||||
| @@ -449,31 +448,6 @@ class ReturnOrderReportPrint(ReturnOrderReportMixin, ReportPrintMixin, RetrieveA | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class ReportMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating Report metadata.""" | ||||
|     MODEL_REF = 'reportmodel' | ||||
|  | ||||
|     def _get_model(self, *args, **kwargs): | ||||
|         """Return model depending on which report type is requested in get_view constructor.""" | ||||
|         reportmodel = self.kwargs.get(self.MODEL_REF, PurchaseOrderReport) | ||||
|  | ||||
|         if reportmodel not in [PurchaseOrderReport, SalesOrderReport, BuildReport, BillOfMaterialsReport, TestReport]: | ||||
|             raise ValidationError("Invalid report model") | ||||
|         return reportmodel | ||||
|  | ||||
|         # Return corresponding Serializer | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return correct MetadataSerializer instance depending on which model is requested""" | ||||
|         # Get type of report, make sure its one of the allowed values | ||||
|         UseModel = self._get_model(*args, **kwargs) | ||||
|         return MetadataSerializer(UseModel, *args, **kwargs) | ||||
|  | ||||
|     def get_queryset(self, *args, **kwargs): | ||||
|         """Return correct queryset depending on which model is requested""" | ||||
|         UseModel = self._get_model(*args, **kwargs) | ||||
|         return UseModel.objects.all() | ||||
|  | ||||
|  | ||||
| report_api_urls = [ | ||||
|  | ||||
|     # Purchase order reports | ||||
| @@ -481,7 +455,7 @@ report_api_urls = [ | ||||
|         # Detail views | ||||
|         path(r'<int:pk>/', include([ | ||||
|             re_path(r'print/', PurchaseOrderReportPrint.as_view(), name='api-po-report-print'), | ||||
|             re_path(r'metadata/', ReportMetadata.as_view(), {ReportMetadata.MODEL_REF: PurchaseOrderReport}, name='api-po-report-metadata'), | ||||
|             re_path(r'metadata/', MetadataView.as_view(), {'model': PurchaseOrderReport}, name='api-po-report-metadata'), | ||||
|             path('', PurchaseOrderReportDetail.as_view(), name='api-po-report-detail'), | ||||
|         ])), | ||||
|  | ||||
| @@ -494,7 +468,7 @@ report_api_urls = [ | ||||
|         # Detail views | ||||
|         path(r'<int:pk>/', include([ | ||||
|             re_path(r'print/', SalesOrderReportPrint.as_view(), name='api-so-report-print'), | ||||
|             re_path(r'metadata/', ReportMetadata.as_view(), {ReportMetadata.MODEL_REF: SalesOrderReport}, name='api-so-report-metadata'), | ||||
|             re_path(r'metadata/', MetadataView.as_view(), {'model': SalesOrderReport}, name='api-so-report-metadata'), | ||||
|             path('', SalesOrderReportDetail.as_view(), name='api-so-report-detail'), | ||||
|         ])), | ||||
|  | ||||
| @@ -505,6 +479,7 @@ report_api_urls = [ | ||||
|     re_path(r'ro/', include([ | ||||
|         path(r'<int:pk>/', include([ | ||||
|             path(r'print/', ReturnOrderReportPrint.as_view(), name='api-return-order-report-print'), | ||||
|             re_path(r'metadata/', MetadataView.as_view(), {'model': ReturnOrderReport}, name='api-so-report-metadata'), | ||||
|             path('', ReturnOrderReportDetail.as_view(), name='api-return-order-report-detail'), | ||||
|         ])), | ||||
|         path('', ReturnOrderReportList.as_view(), name='api-return-order-report-list'), | ||||
| @@ -515,7 +490,7 @@ report_api_urls = [ | ||||
|         # Detail views | ||||
|         path(r'<int:pk>/', include([ | ||||
|             re_path(r'print/?', BuildReportPrint.as_view(), name='api-build-report-print'), | ||||
|             re_path(r'metadata/', ReportMetadata.as_view(), {ReportMetadata.MODEL_REF: BuildReport}, name='api-build-report-metadata'), | ||||
|             re_path(r'metadata/', MetadataView.as_view(), {'model': BuildReport}, name='api-build-report-metadata'), | ||||
|             re_path(r'^.$', BuildReportDetail.as_view(), name='api-build-report-detail'), | ||||
|         ])), | ||||
|  | ||||
| @@ -529,7 +504,7 @@ report_api_urls = [ | ||||
|         # Detail views | ||||
|         path(r'<int:pk>/', include([ | ||||
|             re_path(r'print/?', BOMReportPrint.as_view(), name='api-bom-report-print'), | ||||
|             re_path(r'metadata/', ReportMetadata.as_view(), {ReportMetadata.MODEL_REF: BillOfMaterialsReport}, name='api-bom-report-metadata'), | ||||
|             re_path(r'metadata/', MetadataView.as_view(), {'model': BillOfMaterialsReport}, name='api-bom-report-metadata'), | ||||
|             re_path(r'^.*$', BOMReportDetail.as_view(), name='api-bom-report-detail'), | ||||
|         ])), | ||||
|  | ||||
| @@ -542,7 +517,7 @@ report_api_urls = [ | ||||
|         # Detail views | ||||
|         path(r'<int:pk>/', include([ | ||||
|             re_path(r'print/?', StockItemTestReportPrint.as_view(), name='api-stockitem-testreport-print'), | ||||
|             re_path(r'metadata/', ReportMetadata.as_view(), {ReportMetadata.MODEL_REF: TestReport}, name='api-stockitem-testreport-metadata'), | ||||
|             re_path(r'metadata/', MetadataView.as_view(), {'report': TestReport}, name='api-stockitem-testreport-metadata'), | ||||
|             re_path(r'^.*$', StockItemTestReportDetail.as_view(), name='api-stockitem-testreport-detail'), | ||||
|         ])), | ||||
|  | ||||
|   | ||||
| @@ -23,13 +23,13 @@ from build.models import Build | ||||
| from company.models import Company, SupplierPart | ||||
| from company.serializers import CompanySerializer, SupplierPartSerializer | ||||
| from InvenTree.api import (APIDownloadMixin, AttachmentMixin, | ||||
|                            ListCreateDestroyAPIView, StatusView) | ||||
|                            ListCreateDestroyAPIView, MetadataView, StatusView) | ||||
| from InvenTree.filters import InvenTreeOrderingFilter | ||||
| from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull, | ||||
|                                str2bool, str2int) | ||||
| from InvenTree.mixins import (CreateAPI, CustomRetrieveUpdateDestroyAPI, | ||||
|                               ListAPI, ListCreateAPI, RetrieveAPI, | ||||
|                               RetrieveUpdateAPI, RetrieveUpdateDestroyAPI) | ||||
|                               RetrieveUpdateDestroyAPI) | ||||
| from InvenTree.status_codes import StockHistoryCode, StockStatus | ||||
| from order.models import (PurchaseOrder, ReturnOrder, SalesOrder, | ||||
|                           SalesOrderAllocation) | ||||
| @@ -37,7 +37,6 @@ from order.serializers import (PurchaseOrderSerializer, ReturnOrderSerializer, | ||||
|                                SalesOrderSerializer) | ||||
| from part.models import BomItem, Part, PartCategory | ||||
| from part.serializers import PartBriefSerializer | ||||
| from plugin.serializers import MetadataSerializer | ||||
| from stock.admin import LocationResource, StockItemResource | ||||
| from stock.models import (StockItem, StockItemAttachment, StockItemTestResult, | ||||
|                           StockItemTracking, StockLocation) | ||||
| @@ -83,16 +82,6 @@ class StockDetail(RetrieveUpdateDestroyAPI): | ||||
|         return self.serializer_class(*args, **kwargs) | ||||
|  | ||||
|  | ||||
| class StockMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating StockItem metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return serializer.""" | ||||
|         return MetadataSerializer(StockItem, *args, **kwargs) | ||||
|  | ||||
|     queryset = StockItem.objects.all() | ||||
|  | ||||
|  | ||||
| class StockItemContextMixin: | ||||
|     """Mixin class for adding StockItem object to serializer context.""" | ||||
|  | ||||
| @@ -1344,16 +1333,6 @@ class StockTrackingList(ListAPI): | ||||
|     ] | ||||
|  | ||||
|  | ||||
| class LocationMetadata(RetrieveUpdateAPI): | ||||
|     """API endpoint for viewing / updating StockLocation metadata.""" | ||||
|  | ||||
|     def get_serializer(self, *args, **kwargs): | ||||
|         """Return serializer.""" | ||||
|         return MetadataSerializer(StockLocation, *args, **kwargs) | ||||
|  | ||||
|     queryset = StockLocation.objects.all() | ||||
|  | ||||
|  | ||||
| class LocationDetail(CustomRetrieveUpdateDestroyAPI): | ||||
|     """API endpoint for detail view of StockLocation object. | ||||
|  | ||||
| @@ -1391,7 +1370,7 @@ stock_api_urls = [ | ||||
|         # Stock location detail endpoints | ||||
|         path(r'<int:pk>/', include([ | ||||
|  | ||||
|             re_path(r'^metadata/', LocationMetadata.as_view(), name='api-location-metadata'), | ||||
|             re_path(r'^metadata/', MetadataView.as_view(), {'model': StockLocation}, name='api-location-metadata'), | ||||
|  | ||||
|             re_path(r'^.*$', LocationDetail.as_view(), name='api-location-detail'), | ||||
|         ])), | ||||
| @@ -1433,7 +1412,7 @@ stock_api_urls = [ | ||||
|     path(r'<int:pk>/', include([ | ||||
|         re_path(r'^convert/', StockItemConvert.as_view(), name='api-stock-item-convert'), | ||||
|         re_path(r'^install/', StockItemInstall.as_view(), name='api-stock-item-install'), | ||||
|         re_path(r'^metadata/', StockMetadata.as_view(), name='api-stock-item-metadata'), | ||||
|         re_path(r'^metadata/', MetadataView.as_view(), {'model': StockItem}, name='api-stock-item-metadata'), | ||||
|         re_path(r'^return/', StockItemReturn.as_view(), name='api-stock-item-return'), | ||||
|         re_path(r'^serialize/', StockItemSerialize.as_view(), name='api-stock-item-serialize'), | ||||
|         re_path(r'^uninstall/', StockItemUninstall.as_view(), name='api-stock-item-uninstall'), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user