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.""" - - queryset = StockItemLabel.objects.all() - serializer_class = StockItemLabelSerializer - - ITEM_MODEL = StockItem - ITEM_KEY = 'item' + pass -class StockLocationLabelList(LabelListView): +class StockLocationLabelMixin: + """Mixin for StockLocationLabel endpoints""" + + queryset = StockLocationLabel.objects.all() + serializer_class = StockLocationLabelSerializer + + ITEM_MODEL = StockLocation + ITEM_KEY = 'location' + + +class StockLocationLabelList(StockLocationLabelMixin, LabelListView): """API endpoint for viewiing list of StockLocationLabel objects. Filterable by: @@ -313,56 +329,41 @@ class StockLocationLabelList(LabelListView): - location: Filter by a single stock location - locations: Filter by list of stock locations """ - - queryset = StockLocationLabel.objects.all() - serializer_class = StockLocationLabelSerializer - - ITEM_MODEL = StockLocation - ITEM_KEY = 'location' + pass -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): +class PartLabelMixin: + """Mixin for PartLabel endpoints""" + queryset = PartLabel.objects.all() + serializer_class = PartLabelSerializer + + ITEM_MODEL = Part + ITEM_KEY = 'part' + + +class PartLabelList(PartLabelMixin, LabelListView): """API endpoint for viewing list of PartLabel objects.""" - - queryset = PartLabel.objects.all() - serializer_class = PartLabelSerializer - - ITEM_MODEL = Part - ITEM_KEY = 'part' + pass -class PartLabelDetail(RetrieveUpdateDestroyAPI): +class PartLabelDetail(PartLabelMixin, RetrieveUpdateDestroyAPI): """API endpoint for a single PartLabel object.""" - - queryset = PartLabel.objects.all() - serializer_class = PartLabelSerializer + pass -class PartLabelPrint(LabelPrintMixin, RetrieveAPI): +class PartLabelPrint(PartLabelMixin, LabelPrintMixin, RetrieveAPI): """API endpoint for printing a PartLabel object.""" - - queryset = PartLabel.objects.all() - serializer_class = PartLabelSerializer - - ITEM_MODEL = Part - ITEM_KEY = 'part' + 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 = [] + Each subclass *must* have an attribute called 'ITEM_KEY', + which is used to determine what 'key' is used in the query parameters. - 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 - - -class BuildReportMixin: - """Mixin for extracting Build items from query params.""" - - def get_builds(self): - """Return a list of requested Build objects.""" - builds = [] - - params = self.request.query_params - - for key in ['build', 'build[]', 'builds', 'builds[]']: - - if key in params: - builds = params.getlist(key, []) - - break - - valid_ids = [] - - for b in builds: - 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! + This mixin defines a 'get_items' method which provides a generic implementation + to return a list of matching database model instances """ - def get_orders(self): - """Return a list of order objects.""" - orders = [] + # Database model for instances to actually be "printed" against this report template + ITEM_MODEL = None - params = self.request.query_params + # Default key for looking up database model instances + ITEM_KEY = 'item' - for key in ['order', 'order[]', 'orders', 'orders[]']: - if key in params: - orders = params.getlist(key, []) + def get_items(self): + """Return a list of database objects from query parameters""" + + if not self.ITEM_MODEL: + raise NotImplementedError(f"ITEM_MODEL attribute not defined for {__class__}") + + 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 o in orders: + for id in ids: try: - valid_ids.append(int(o)) - except (ValueError): + valid_ids.append(int(id)) + except ValueError: pass - valid_orders = self.OrderModel.objects.filter(pk__in=valid_ids) + # Filter queryset by matching ID values + return self.ITEM_MODEL.objects.filter(pk__in=valid_ids) - return valid_orders - - -class PartReportMixin: - """Mixin for extracting part items from query params.""" - - def get_parts(self): - """Return a list of requested part objects.""" - parts = [] - - params = self.request.query_params - - for key in ['part', 'part[]', 'parts', 'parts[]']: - - if key in params: - parts = params.getlist(key, []) - - valid_ids = [] - - for p in parts: - try: - valid_ids.append(int(p)) - except (ValueError): - continue - - # Extract a valid set of Part objects - valid_parts = part.models.Part.objects.filter(pk__in=valid_ids) - - return valid_parts + def filter_queryset(self, queryset): + """Filter the queryset based on the provided report ID values. + + As each 'report' instance may optionally define its own filters, + the resulting queryset is the 'union' of the two + """ + + queryset = super().filter_queryset(queryset) + + items = self.get_items() + + 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. + + In practice, this is not too bad. + """ + + valid_report_ids = set() + + for report in queryset.all(): + matches = True + + try: + filters = InvenTree.helpers.validateFilterString(report.filters) + except ValidationError: + continue + + for item in items: + item_query = self.ITEM_MODEL.objects.filter(pk=item.pk) + + try: + if not item_query.filter(**filters).exists(): + matches = False + break + except FieldError: + matches = False + break + + # Matched all items + if matches: + valid_report_ids.add(report.pk) + + # Reduce queryset to only valid matches + queryset = queryset.filter(pk__in=[pk for pk in valid_report_ids]) + + 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,8 +252,25 @@ class ReportPrintMixin: inline=inline, ) + def get(self, request, *args, **kwargs): + """Default implementation of GET for a print endpoint. -class StockItemTestReportList(ReportListView, StockItemReportMixin): + Note that it expects the class has defined a get_items() method + """ + items = self.get_items() + return self.print(request, items) + + +class StockItemTestReportMixin(ReportFilterMixin): + """Mixin for StockItemTestReport report template""" + + ITEM_MODEL = StockItem + ITEM_KEY = 'item' + queryset = TestReport.objects.all() + serializer_class = TestReportSerializer + + +class StockItemTestReportList(StockItemTestReportMixin, ReportListView): """API endpoint for viewing list of TestReport objects. Filterable by: @@ -272,73 +278,17 @@ class StockItemTestReportList(ReportListView, StockItemReportMixin): - enabled: Filter by enabled / disabled status - item: Filter by stock item(s) """ - - 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 - - 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 + pass -class StockItemTestReportDetail(RetrieveUpdateDestroyAPI): +class StockItemTestReportDetail(StockItemTestReportMixin, RetrieveUpdateDestroyAPI): """API endpoint for a single TestReport object.""" - - queryset = TestReport.objects.all() - serializer_class = TestReportSerializer + pass -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) """ - - 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 + pass -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.""" - - 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() - - return self.print(request, parts) + pass -class BuildReportList(ReportListView, BuildReportMixin): +class BuildReportMixin(ReportFilterMixin): + """Mixin for the BuildReport report template""" + + ITEM_MODEL = build.models.Build + ITEM_KEY = 'build' + + queryset = BuildReport.objects.all() + serializer_class = BuildReportSerializer + + +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 """ - - 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 + pass -class BuildReportDetail(RetrieveUpdateDestroyAPI): +class BuildReportDetail(BuildReportMixin, RetrieveUpdateDestroyAPI): """API endpoint for a single BuildReport object.""" - - queryset = BuildReport.objects.all() - serializer_class = BuildReportSerializer + pass -class BuildReportPrint(RetrieveAPI, BuildReportMixin, ReportPrintMixin): +class BuildReportPrint(BuildReportMixin, ReportPrintMixin, RetrieveAPI): """API endpoint for printing a BuildReport.""" - - 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) + pass -class PurchaseOrderReportList(ReportListView, OrderReportMixin): +class PurchaseOrderReportMixin(ReportFilterMixin): + """Mixin for the PurchaseOrderReport report template""" + + ITEM_MODEL = order.models.PurchaseOrder + ITEM_KEY = 'order' + + queryset = PurchaseOrderReport.objects.all() + serializer_class = PurchaseOrderReportSerializer + + +class PurchaseOrderReportList(PurchaseOrderReportMixin, ReportListView): """API list endpoint for the PurchaseOrderReport model""" - OrderModel = order.models.PurchaseOrder - - 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 + 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.""" - - 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) + pass -class SalesOrderReportList(ReportListView, OrderReportMixin): +class SalesOrderReportMixin(ReportFilterMixin): + """Mixin for the SalesOrderReport report template""" + + ITEM_MODEL = order.models.SalesOrder + ITEM_KEY = 'order' + + queryset = SalesOrderReport.objects.all() + serializer_class = SalesOrderReportSerializer + + +class SalesOrderReportList(SalesOrderReportMixin, ReportListView): """API list endpoint for the SalesOrderReport model""" - OrderModel = order.models.SalesOrder - - 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 + 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 = [