diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index fb2e8da433..f07a8f891c 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -4,6 +4,8 @@ from django.conf import settings from django.core.exceptions import FieldError, ValidationError from django.http import HttpResponse, JsonResponse from django.urls import include, re_path +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_page, never_cache from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters @@ -45,7 +47,7 @@ class LabelFilterMixin: ids = [] # Construct a list of possible query parameter value options - # e.g. if self.ITEM_KEY = 'part' -> ['part', 'part', 'parts', parts[]'] + # e.g. if self.ITEM_KEY = 'part' -> ['part', 'part[]', 'parts', parts[]'] for k in [self.ITEM_KEY + x for x in ['', '[]', 's', 's[]']]: if ids := self.request.query_params.getlist(k, []): # Return the first list of matches @@ -134,9 +136,15 @@ class LabelListView(LabelFilterMixin, ListAPI): ] +@method_decorator(cache_page(5), name='dispatch') class LabelPrintMixin(LabelFilterMixin): """Mixin for printing labels.""" + @method_decorator(never_cache) + def dispatch(self, *args, **kwargs): + """Prevent caching when printing report templates""" + return super().dispatch(*args, **kwargs) + def get(self, request, *args, **kwargs): """Perform a GET request against this endpoint to print labels""" return self.print(request, self.get_items()) @@ -270,7 +278,17 @@ class LabelPrintMixin(LabelFilterMixin): ) -class StockItemLabelList(LabelListView): +class StockItemLabelMixin: + """Mixin for StockItemLabel endpoints""" + + queryset = StockItemLabel.objects.all() + serializer_class = StockItemLabelSerializer + + ITEM_MODEL = StockItem + ITEM_KEY = 'item' + + +class StockItemLabelList(StockItemLabelMixin, LabelListView): """API endpoint for viewing list of StockItemLabel objects. Filterable by: @@ -279,32 +297,30 @@ class StockItemLabelList(LabelListView): - item: Filter by single stock item - items: Filter by list of stock items """ - - queryset = StockItemLabel.objects.all() - serializer_class = StockItemLabelSerializer - - ITEM_MODEL = StockItem - ITEM_KEY = 'item' + pass -class StockItemLabelDetail(RetrieveUpdateDestroyAPI): +class StockItemLabelDetail(StockItemLabelMixin, RetrieveUpdateDestroyAPI): """API endpoint for a single StockItemLabel object.""" - - queryset = StockItemLabel.objects.all() - serializer_class = StockItemLabelSerializer + pass -class StockItemLabelPrint(LabelPrintMixin, RetrieveAPI): +class StockItemLabelPrint(StockItemLabelMixin, LabelPrintMixin, RetrieveAPI): """API endpoint for printing a StockItemLabel object.""" + pass - queryset = StockItemLabel.objects.all() - serializer_class = StockItemLabelSerializer - ITEM_MODEL = StockItem - ITEM_KEY = 'item' +class StockLocationLabelMixin: + """Mixin for StockLocationLabel endpoints""" + + queryset = StockLocationLabel.objects.all() + serializer_class = StockLocationLabelSerializer + + ITEM_MODEL = StockLocation + ITEM_KEY = 'location' -class StockLocationLabelList(LabelListView): +class StockLocationLabelList(StockLocationLabelMixin, LabelListView): """API endpoint for viewiing list of StockLocationLabel objects. Filterable by: @@ -313,34 +329,21 @@ class StockLocationLabelList(LabelListView): - location: Filter by a single stock location - locations: Filter by list of stock locations """ + pass - queryset = StockLocationLabel.objects.all() - serializer_class = StockLocationLabelSerializer - ITEM_MODEL = StockLocation - ITEM_KEY = 'location' - - -class StockLocationLabelDetail(RetrieveUpdateDestroyAPI): +class StockLocationLabelDetail(StockLocationLabelMixin, RetrieveUpdateDestroyAPI): """API endpoint for a single StockLocationLabel object.""" - - queryset = StockLocationLabel.objects.all() - serializer_class = StockLocationLabelSerializer + pass -class StockLocationLabelPrint(LabelPrintMixin, RetrieveAPI): +class StockLocationLabelPrint(StockLocationLabelMixin, LabelPrintMixin, RetrieveAPI): """API endpoint for printing a StockLocationLabel object.""" - - queryset = StockLocationLabel.objects.all() - seiralizer_class = StockLocationLabelSerializer - - ITEM_MODEL = StockLocation - ITEM_KEY = 'location' + pass -class PartLabelList(LabelListView): - """API endpoint for viewing list of PartLabel objects.""" - +class PartLabelMixin: + """Mixin for PartLabel endpoints""" queryset = PartLabel.objects.all() serializer_class = PartLabelSerializer @@ -348,21 +351,19 @@ class PartLabelList(LabelListView): ITEM_KEY = 'part' -class PartLabelDetail(RetrieveUpdateDestroyAPI): - """API endpoint for a single PartLabel object.""" - - queryset = PartLabel.objects.all() - serializer_class = PartLabelSerializer +class PartLabelList(PartLabelMixin, LabelListView): + """API endpoint for viewing list of PartLabel objects.""" + pass -class PartLabelPrint(LabelPrintMixin, RetrieveAPI): - """API endpoint for printing a PartLabel object.""" +class PartLabelDetail(PartLabelMixin, RetrieveUpdateDestroyAPI): + """API endpoint for a single PartLabel object.""" + pass - queryset = PartLabel.objects.all() - serializer_class = PartLabelSerializer - ITEM_MODEL = Part - ITEM_KEY = 'part' +class PartLabelPrint(PartLabelMixin, LabelPrintMixin, RetrieveAPI): + """API endpoint for printing a PartLabel object.""" + pass label_api_urls = [ diff --git a/InvenTree/report/api.py b/InvenTree/report/api.py index 1c27c16393..68474bdf31 100644 --- a/InvenTree/report/api.py +++ b/InvenTree/report/api.py @@ -5,7 +5,9 @@ from django.core.files.base import ContentFile from django.http import HttpResponse from django.template.exceptions import TemplateDoesNotExist from django.urls import include, path, re_path +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ +from django.views.decorators.cache import cache_page, never_cache from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters @@ -44,122 +46,109 @@ class ReportListView(ListAPI): ] -class StockItemReportMixin: - """Mixin for extracting stock items from query params.""" +class ReportFilterMixin: + """Mixin for extracting multiple objects from query params. - def get_items(self): - """Return a list of requested stock items.""" - items = [] - - params = self.request.query_params - - for key in ['item', 'item[]', 'items', 'items[]']: - if key in params: - items = params.getlist(key, []) - break - - valid_ids = [] - - for item in items: - try: - valid_ids.append(int(item)) - except (ValueError): - pass - - # List of StockItems which match provided values - valid_items = StockItem.objects.filter(pk__in=valid_ids) - - return valid_items + Each subclass *must* have an attribute called 'ITEM_KEY', + which is used to determine what 'key' is used in the query parameters. + This mixin defines a 'get_items' method which provides a generic implementation + to return a list of matching database model instances + """ -class BuildReportMixin: - """Mixin for extracting Build items from query params.""" + # Database model for instances to actually be "printed" against this report template + ITEM_MODEL = None - def get_builds(self): - """Return a list of requested Build objects.""" - builds = [] + # Default key for looking up database model instances + ITEM_KEY = 'item' - params = self.request.query_params + def get_items(self): + """Return a list of database objects from query parameters""" - for key in ['build', 'build[]', 'builds', 'builds[]']: + if not self.ITEM_MODEL: + raise NotImplementedError(f"ITEM_MODEL attribute not defined for {__class__}") - if key in params: - builds = params.getlist(key, []) + ids = [] + # Construct a list of possible query parameter value options + # e.g. if self.ITEM_KEY = 'order' -> ['order', 'order[]', 'orders', 'orders[]'] + for k in [self.ITEM_KEY + x for x in ['', '[]', 's', 's[]']]: + if ids := self.request.query_params.getlist(k, []): + # Return the first list of matches break + # Next we must validated each provided object ID valid_ids = [] - for b in builds: + for id in ids: try: - valid_ids.append(int(b)) - except (ValueError): - continue - - return build.models.Build.objects.filter(pk__in=valid_ids) - - -class OrderReportMixin: - """Mixin for extracting order items from query params. - - requires the OrderModel class attribute to be set! - """ - - def get_orders(self): - """Return a list of order objects.""" - orders = [] + valid_ids.append(int(id)) + except ValueError: + pass - params = self.request.query_params + # Filter queryset by matching ID values + return self.ITEM_MODEL.objects.filter(pk__in=valid_ids) - for key in ['order', 'order[]', 'orders', 'orders[]']: - if key in params: - orders = params.getlist(key, []) - break + def filter_queryset(self, queryset): + """Filter the queryset based on the provided report ID values. - valid_ids = [] + As each 'report' instance may optionally define its own filters, + the resulting queryset is the 'union' of the two + """ - for o in orders: - try: - valid_ids.append(int(o)) - except (ValueError): - pass + queryset = super().filter_queryset(queryset) - valid_orders = self.OrderModel.objects.filter(pk__in=valid_ids) + items = self.get_items() - return valid_orders + if len(items) > 0: + """At this point, we are basically forced to be inefficient: + We need to compare the 'filters' string of each report template, + and see if it matches against each of the requested items. -class PartReportMixin: - """Mixin for extracting part items from query params.""" + In practice, this is not too bad. + """ - def get_parts(self): - """Return a list of requested part objects.""" - parts = [] + valid_report_ids = set() - params = self.request.query_params + for report in queryset.all(): + matches = True - for key in ['part', 'part[]', 'parts', 'parts[]']: + try: + filters = InvenTree.helpers.validateFilterString(report.filters) + except ValidationError: + continue - if key in params: - parts = params.getlist(key, []) + for item in items: + item_query = self.ITEM_MODEL.objects.filter(pk=item.pk) - valid_ids = [] + try: + if not item_query.filter(**filters).exists(): + matches = False + break + except FieldError: + matches = False + break - for p in parts: - try: - valid_ids.append(int(p)) - except (ValueError): - continue + # Matched all items + if matches: + valid_report_ids.add(report.pk) - # Extract a valid set of Part objects - valid_parts = part.models.Part.objects.filter(pk__in=valid_ids) + # Reduce queryset to only valid matches + queryset = queryset.filter(pk__in=[pk for pk in valid_report_ids]) - return valid_parts + return queryset +@method_decorator(cache_page(5), name='dispatch') class ReportPrintMixin: """Mixin for printing reports.""" + @method_decorator(never_cache) + def dispatch(self, *args, **kwargs): + """Prevent caching when printing report templates""" + return super().dispatch(*args, **kwargs) + def report_callback(self, object, report, request): """Callback function for each object/report combination. @@ -263,82 +252,43 @@ class ReportPrintMixin: inline=inline, ) + def get(self, request, *args, **kwargs): + """Default implementation of GET for a print endpoint. -class StockItemTestReportList(ReportListView, StockItemReportMixin): - """API endpoint for viewing list of TestReport objects. + Note that it expects the class has defined a get_items() method + """ + items = self.get_items() + return self.print(request, items) - Filterable by: - - enabled: Filter by enabled / disabled status - - item: Filter by stock item(s) - """ +class StockItemTestReportMixin(ReportFilterMixin): + """Mixin for StockItemTestReport report template""" + ITEM_MODEL = StockItem + ITEM_KEY = 'item' queryset = TestReport.objects.all() serializer_class = TestReportSerializer - def filter_queryset(self, queryset): - """Custom queryset filtering""" - queryset = super().filter_queryset(queryset) - - # List of StockItem objects to match against - items = self.get_items() - - if len(items) > 0: - """ - We wish to filter by stock items. - - We need to compare the 'filters' string of each report, - and see if it matches against each of the specified stock items. - - TODO: In the future, perhaps there is a way to make this more efficient. - """ - - valid_report_ids = set() - - for report in queryset.all(): - - matches = True - - # Filter string defined for the report object - try: - filters = InvenTree.helpers.validateFilterString(report.filters) - except Exception: - continue - - for item in items: - item_query = StockItem.objects.filter(pk=item.pk) - try: - if not item_query.filter(**filters).exists(): - matches = False - break - except FieldError: - matches = False - break +class StockItemTestReportList(StockItemTestReportMixin, ReportListView): + """API endpoint for viewing list of TestReport objects. - if matches: - valid_report_ids.add(report.pk) - else: - continue + Filterable by: - # Reduce queryset to only valid matches - queryset = queryset.filter(pk__in=[pk for pk in valid_report_ids]) - return queryset + - enabled: Filter by enabled / disabled status + - item: Filter by stock item(s) + """ + pass -class StockItemTestReportDetail(RetrieveUpdateDestroyAPI): +class StockItemTestReportDetail(StockItemTestReportMixin, RetrieveUpdateDestroyAPI): """API endpoint for a single TestReport object.""" + pass - queryset = TestReport.objects.all() - serializer_class = TestReportSerializer - -class StockItemTestReportPrint(RetrieveAPI, StockItemReportMixin, ReportPrintMixin): +class StockItemTestReportPrint(StockItemTestReportMixin, ReportPrintMixin, RetrieveAPI): """API endpoint for printing a TestReport object.""" - queryset = TestReport.objects.all() - serializer_class = TestReportSerializer - def report_callback(self, item, report, request): """Callback to (optionally) save a copy of the generated report""" @@ -355,14 +305,18 @@ class StockItemTestReportPrint(RetrieveAPI, StockItemReportMixin, ReportPrintMix comment=_("Test report") ) - def get(self, request, *args, **kwargs): - """Check if valid stock item(s) have been provided.""" - items = self.get_items() - return self.print(request, items) +class BOMReportMixin(ReportFilterMixin): + """Mixin for BillOfMaterialsReport report template""" + + ITEM_MODEL = part.models.Part + ITEM_KEY = 'part' + + queryset = BillOfMaterialsReport.objects.all() + serializer_class = BOMReportSerializer -class BOMReportList(ReportListView, PartReportMixin): +class BOMReportList(BOMReportMixin, ReportListView): """API endpoint for viewing a list of BillOfMaterialReport objects. Filterably by: @@ -370,80 +324,30 @@ class BOMReportList(ReportListView, PartReportMixin): - enabled: Filter by enabled / disabled status - part: Filter by part(s) """ + pass - queryset = BillOfMaterialsReport.objects.all() - serializer_class = BOMReportSerializer - - def filter_queryset(self, queryset): - """Custom queryset filtering""" - queryset = super().filter_queryset(queryset) - - # List of Part objects to match against - parts = self.get_parts() - - if len(parts) > 0: - """ - We wish to filter by part(s). - - We need to compare the 'filters' string of each report, - and see if it matches against each of the specified parts. - """ - - valid_report_ids = set() - - for report in queryset.all(): - - matches = True - - try: - filters = InvenTree.helpers.validateFilterString(report.filters) - except ValidationError: - # Filters are ill-defined - continue - - for p in parts: - part_query = part.models.Part.objects.filter(pk=p.pk) - try: - if not part_query.filter(**filters).exists(): - matches = False - break - except FieldError: - matches = False - break - - if matches: - valid_report_ids.add(report.pk) - else: - continue - - # Reduce queryset to only valid matches - queryset = queryset.filter(pk__in=[pk for pk in valid_report_ids]) - - return queryset - - -class BOMReportDetail(RetrieveUpdateDestroyAPI): +class BOMReportDetail(BOMReportMixin, RetrieveUpdateDestroyAPI): """API endpoint for a single BillOfMaterialReport object.""" - - queryset = BillOfMaterialsReport.objects.all() - serializer_class = BOMReportSerializer + pass -class BOMReportPrint(RetrieveAPI, PartReportMixin, ReportPrintMixin): +class BOMReportPrint(BOMReportMixin, ReportPrintMixin, RetrieveAPI): """API endpoint for printing a BillOfMaterialReport object.""" + pass - queryset = BillOfMaterialsReport.objects.all() - serializer_class = BOMReportSerializer - def get(self, request, *args, **kwargs): - """Check if valid part item(s) have been provided.""" - parts = self.get_parts() +class BuildReportMixin(ReportFilterMixin): + """Mixin for the BuildReport report template""" - return self.print(request, parts) + ITEM_MODEL = build.models.Build + ITEM_KEY = 'build' + + queryset = BuildReport.objects.all() + serializer_class = BuildReportSerializer -class BuildReportList(ReportListView, BuildReportMixin): +class BuildReportList(BuildReportMixin, ReportListView): """API endpoint for viewing a list of BuildReport objects. Can be filtered by: @@ -451,236 +355,67 @@ class BuildReportList(ReportListView, BuildReportMixin): - enabled: Filter by enabled / disabled status - build: Filter by Build object """ + pass - queryset = BuildReport.objects.all() - serializer_class = BuildReportSerializer - - def filter_queryset(self, queryset): - """Custom queryset filtering""" - queryset = super().filter_queryset(queryset) - - # List of Build objects to match against - builds = self.get_builds() - - if len(builds) > 0: - """ - We wish to filter by Build(s) - - We need to compare the 'filters' string of each report, - and see if it matches against each of the specified parts - - # TODO: This code needs some refactoring! - """ - - valid_build_ids = set() - - for report in queryset.all(): - - matches = True - - try: - filters = InvenTree.helpers.validateFilterString(report.filters) - except ValidationError: - continue - - for b in builds: - build_query = build.models.Build.objects.filter(pk=b.pk) - - try: - if not build_query.filter(**filters).exists(): - matches = False - break - except FieldError: - matches = False - break - - if matches: - valid_build_ids.add(report.pk) - else: - continue - - # Reduce queryset to only valid matches - queryset = queryset.filter(pk__in=[pk for pk in valid_build_ids]) - return queryset - - -class BuildReportDetail(RetrieveUpdateDestroyAPI): +class BuildReportDetail(BuildReportMixin, RetrieveUpdateDestroyAPI): """API endpoint for a single BuildReport object.""" + pass - queryset = BuildReport.objects.all() - serializer_class = BuildReportSerializer - -class BuildReportPrint(RetrieveAPI, BuildReportMixin, ReportPrintMixin): +class BuildReportPrint(BuildReportMixin, ReportPrintMixin, RetrieveAPI): """API endpoint for printing a BuildReport.""" + pass - queryset = BuildReport.objects.all() - serializer_class = BuildReportSerializer - - def get(self, request, *ars, **kwargs): - """Perform a GET action to print the report""" - builds = self.get_builds() - return self.print(request, builds) +class PurchaseOrderReportMixin(ReportFilterMixin): + """Mixin for the PurchaseOrderReport report template""" - -class PurchaseOrderReportList(ReportListView, OrderReportMixin): - """API list endpoint for the PurchaseOrderReport model""" - OrderModel = order.models.PurchaseOrder + ITEM_MODEL = order.models.PurchaseOrder + ITEM_KEY = 'order' queryset = PurchaseOrderReport.objects.all() serializer_class = PurchaseOrderReportSerializer - def filter_queryset(self, queryset): - """Custom queryset filter for the PurchaseOrderReport list""" - queryset = super().filter_queryset(queryset) - - orders = self.get_orders() - - if len(orders) > 0: - """ - We wish to filter by purchase orders. - - We need to compare the 'filters' string of each report, - and see if it matches against each of the specified orders. - - TODO: In the future, perhaps there is a way to make this more efficient. - """ - valid_report_ids = set() - - for report in queryset.all(): - - matches = True - - # Filter string defined for the report object - try: - filters = InvenTree.helpers.validateFilterString(report.filters) - except Exception: - continue - - for o in orders: - order_query = order.models.PurchaseOrder.objects.filter(pk=o.pk) - - try: - if not order_query.filter(**filters).exists(): - matches = False - break - except FieldError: - matches = False - break - - if matches: - valid_report_ids.add(report.pk) - else: - continue - - # Reduce queryset to only valid matches - queryset = queryset.filter(pk__in=[pk for pk in valid_report_ids]) - - return queryset +class PurchaseOrderReportList(PurchaseOrderReportMixin, ReportListView): + """API list endpoint for the PurchaseOrderReport model""" + pass -class PurchaseOrderReportDetail(RetrieveUpdateDestroyAPI): +class PurchaseOrderReportDetail(PurchaseOrderReportMixin, RetrieveUpdateDestroyAPI): """API endpoint for a single PurchaseOrderReport object.""" - - queryset = PurchaseOrderReport.objects.all() - serializer_class = PurchaseOrderReportSerializer + pass -class PurchaseOrderReportPrint(RetrieveAPI, OrderReportMixin, ReportPrintMixin): +class PurchaseOrderReportPrint(PurchaseOrderReportMixin, ReportPrintMixin, RetrieveAPI): """API endpoint for printing a PurchaseOrderReport object.""" + pass - OrderModel = order.models.PurchaseOrder - - queryset = PurchaseOrderReport.objects.all() - serializer_class = PurchaseOrderReportSerializer - - def get(self, request, *args, **kwargs): - """Perform GET request to print the report""" - orders = self.get_orders() - return self.print(request, orders) +class SalesOrderReportMixin(ReportFilterMixin): + """Mixin for the SalesOrderReport report template""" - -class SalesOrderReportList(ReportListView, OrderReportMixin): - """API list endpoint for the SalesOrderReport model""" - OrderModel = order.models.SalesOrder + ITEM_MODEL = order.models.SalesOrder + ITEM_KEY = 'order' queryset = SalesOrderReport.objects.all() serializer_class = SalesOrderReportSerializer - def filter_queryset(self, queryset): - """Custom queryset filtering for the SalesOrderReport API list""" - queryset = super().filter_queryset(queryset) - - orders = self.get_orders() - - if len(orders) > 0: - """ - We wish to filter by purchase orders. - We need to compare the 'filters' string of each report, - and see if it matches against each of the specified orders. - - TODO: In the future, perhaps there is a way to make this more efficient. - """ - - valid_report_ids = set() - - for report in queryset.all(): - - matches = True - - # Filter string defined for the report object - try: - filters = InvenTree.helpers.validateFilterString(report.filters) - except Exception: - continue - - for o in orders: - order_query = order.models.SalesOrder.objects.filter(pk=o.pk) - - try: - if not order_query.filter(**filters).exists(): - matches = False - break - except FieldError: - matches = False - break - - if matches: - valid_report_ids.add(report.pk) - else: - continue - - # Reduce queryset to only valid matches - queryset = queryset.filter(pk__in=[pk for pk in valid_report_ids]) - - return queryset +class SalesOrderReportList(SalesOrderReportMixin, ReportListView): + """API list endpoint for the SalesOrderReport model""" + pass -class SalesOrderReportDetail(RetrieveUpdateDestroyAPI): +class SalesOrderReportDetail(SalesOrderReportMixin, RetrieveUpdateDestroyAPI): """API endpoint for a single SalesOrderReport object.""" - - queryset = SalesOrderReport.objects.all() - serializer_class = SalesOrderReportSerializer + pass -class SalesOrderReportPrint(RetrieveAPI, OrderReportMixin, ReportPrintMixin): +class SalesOrderReportPrint(SalesOrderReportMixin, ReportPrintMixin, RetrieveAPI): """API endpoint for printing a PurchaseOrderReport object.""" - - OrderModel = order.models.SalesOrder - - queryset = SalesOrderReport.objects.all() - serializer_class = SalesOrderReportSerializer - - def get(self, request, *args, **kwargs): - """Perform a GET request to print the report""" - orders = self.get_orders() - - return self.print(request, orders) + pass report_api_urls = [