From d0077cf9507afbbdd8f25c48a8bc0880a9603ca0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Mar 2023 23:00:19 +1100 Subject: [PATCH] Refactoring for report.api - Adds generic mixins for filtering queryset (based on updates to label.api) - Reduces repeated code a *lot* (cherry picked from commit 5de0b42c414ece7c8a33fa06dd9e5e8facc4dc07) --- InvenTree/label/api.py | 2 +- InvenTree/report/api.py | 577 +++++++++++----------------------------- 2 files changed, 153 insertions(+), 426 deletions(-) diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index fb2e8da433..6550454344 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -45,7 +45,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 diff --git a/InvenTree/report/api.py b/InvenTree/report/api.py index 1c27c16393..583fe484ab 100644 --- a/InvenTree/report/api.py +++ b/InvenTree/report/api.py @@ -44,117 +44,98 @@ 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 + 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 + """ -class PartReportMixin: - """Mixin for extracting part items from query params.""" + queryset = super().filter_queryset(queryset) - def get_parts(self): - """Return a list of requested part objects.""" - parts = [] + items = self.get_items() - params = self.request.query_params + if len(items) > 0: + """At this point, we are basically forced to be inefficient: - for key in ['part', 'part[]', 'parts', 'parts[]']: + We need to compare the 'filters' string of each report template, + and see if it matches against each of the requested items. - if key in params: - parts = params.getlist(key, []) + In practice, this is not too bad. + """ - valid_ids = [] + valid_report_ids = set() - for p in parts: - try: - valid_ids.append(int(p)) - except (ValueError): - continue + for report in queryset.all(): + matches = True - # Extract a valid set of Part objects - valid_parts = part.models.Part.objects.filter(pk__in=valid_ids) + try: + filters = InvenTree.helpers.validateFilterString(report.filters) + except ValidationError: + continue - return valid_parts + 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 class ReportPrintMixin: @@ -263,8 +244,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 +270,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 +297,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 +316,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 + ITE_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 +347,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 = [