From e8373944950ba9304822388e8592c357a4a9508e Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 11 Jun 2024 23:16:01 +1000 Subject: [PATCH] Modern attachments (#505) * Minimum API version is now 100 * Remove old API features - Anything below API v100 no longer supported * Reefactor attachment widget to support modern attachment API * Filter and display attachments correctly * Refactor --- lib/api.dart | 39 +++----------- lib/barcode/barcode.dart | 7 +-- lib/inventree/company.dart | 5 +- lib/inventree/model.dart | 38 ++++++++++++-- lib/inventree/part.dart | 9 ++-- lib/inventree/purchase_order.dart | 5 +- lib/inventree/sales_order.dart | 5 +- lib/inventree/stock.dart | 10 ++-- lib/settings/about.dart | 16 +++--- lib/widget/attachment_widget.dart | 29 ++++++++--- lib/widget/company/company_detail.dart | 56 +++++++++------------ lib/widget/drawer.dart | 20 ++++---- lib/widget/order/purchase_order_detail.dart | 4 +- lib/widget/order/sales_order_detail.dart | 4 +- lib/widget/part/part_detail.dart | 28 ++++------- lib/widget/stock/location_display.dart | 16 +++--- lib/widget/stock/stock_detail.dart | 23 +++------ test/api_test.dart | 4 -- 18 files changed, 151 insertions(+), 167 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 1c960f3e..968e43fd 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -187,7 +187,8 @@ class InvenTreeAPI { } // Minimum required API version for server - static const _minApiVersion = 20; + // 2023-03-04 + static const _minApiVersion = 100; bool _strictHttps = false; @@ -282,30 +283,6 @@ class InvenTreeAPI { String get serverVersion => (serverInfo["version"] ?? "") as String; int get apiVersion => (serverInfo["apiVersion"] ?? 1) as int; - // Plugins enabled at API v34 and above - bool get pluginsEnabled => apiVersion >= 34 && (serverInfo["plugins_enabled"] ?? false) as bool; - - // API endpoint for receiving purchase order line items was introduced in v12 - bool get supportsPoReceive => apiVersion >= 12; - - // Notification support requires API v25 or newer - bool get supportsNotifications => isConnected() && apiVersion >= 25; - - // Return True if the API supports 'settings' (requires API v46) - bool get supportsSettings => isConnected() && apiVersion >= 46; - - // Part parameter support requires API v56 or newer - bool get supportsPartParameters => isConnected() && apiVersion >= 56; - - // Supports 'modern' barcode API (v80 or newer) - bool get supportModernBarcodes => isConnected() && apiVersion >= 80; - - // Structural categories requires API v83 or newer - bool get supportsStructuralCategories => isConnected() && apiVersion >= 83; - - // Company attachments require API v95 or newer - bool get supportCompanyAttachments => isConnected() && apiVersion >= 95; - // Consolidated search request API v102 or newer bool get supportsConsolidatedSearch => isConnected() && apiVersion >= 102; @@ -346,7 +323,11 @@ class InvenTreeAPI { bool get supportsCompanyActiveStatus => isConnected() && apiVersion >= 189; // Does the server support the "modern" (consolidated) label printing API? - bool get supportsModernLabelPrinting => isConnected() && apiVersion >= 198; + bool get supportsModernLabelPrinting => isConnected() && apiVersion >= 201; + + // Does the server support the "modern" (consolidated) attachment API? + // Ref: https://github.com/inventree/InvenTree/pull/7420 + bool get supportsModernAttachments => isConnected() && apiVersion >= 207; // Cached list of plugins (refreshed when we connect to the server) List _plugins = []; @@ -1517,7 +1498,6 @@ class InvenTreeAPI { Map _userSettings = {}; Future getGlobalSetting(String key) async { - if (!supportsSettings) return ""; InvenTreeGlobalSetting? setting = _globalSettings[key]; @@ -1543,7 +1523,6 @@ class InvenTreeAPI { } Future getUserSetting(String key) async { - if (!supportsSettings) return ""; InvenTreeUserSetting? setting = _userSettings[key]; @@ -1687,10 +1666,6 @@ class InvenTreeAPI { return; } - if (!supportsNotifications) { - return; - } - InvenTreeNotification().count(filters: {"read": "false"}).then((int n) { notification_counter = n; }); diff --git a/lib/barcode/barcode.dart b/lib/barcode/barcode.dart index fead1b3d..64af533a 100644 --- a/lib/barcode/barcode.dart +++ b/lib/barcode/barcode.dart @@ -341,12 +341,7 @@ class UniqueBarcodeHandler extends BarcodeHandler { } else { String barcode; - if (InvenTreeAPI().supportModernBarcodes) { - barcode = (data["barcode_data"] ?? "") as String; - } else { - // Legacy barcode API - barcode = (data["hash"] ?? data["barcode_hash"] ?? "") as String; - } + barcode = (data["barcode_data"] ?? "") as String; if (barcode.isEmpty) { barcodeFailureTone(); diff --git a/lib/inventree/company.dart b/lib/inventree/company.dart index 94b71b11..b9ac2142 100644 --- a/lib/inventree/company.dart +++ b/lib/inventree/company.dart @@ -106,7 +106,10 @@ class InvenTreeCompanyAttachment extends InvenTreeAttachment { String get REFERENCE_FIELD => "company"; @override - String get URL => "company/attachment/"; + String get REF_MODEL_TYPE => "company"; + + @override + String get URL => InvenTreeAPI().supportsModernAttachments ? "attachment/" : "company/attachment/"; @override InvenTreeModel createFromJson(Map json) => InvenTreeCompanyAttachment.fromJson(json); diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 747dc26f..35cfc121 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -938,9 +938,17 @@ class InvenTreeAttachment extends InvenTreeModel { InvenTreeAttachment.fromJson(Map json) : super.fromJson(json); + @override + String get URL => "attachment/"; + // Override this reference field for any subclasses + // Note: This is used for the *legacy* attachment API String get REFERENCE_FIELD => ""; + // Override this reference field for any subclasses + // Note: This is used for the *modern* attachment API + String get REF_MODEL_TYPE => ""; + String get attachment => getString("attachment"); // Return the filename of the attachment @@ -989,15 +997,39 @@ class InvenTreeAttachment extends InvenTreeModel { } } - Future uploadAttachment(File attachment, int parentId, {String comment = "", Map fields = const {}}) async { + // Return a count of how many attachments exist against the specified model ID + Future countAttachments(int modelId) { + + Map filters = {}; + + if (InvenTreeAPI().supportsModernAttachments) { + filters["model_type"] = REF_MODEL_TYPE; + filters["model_id"] = modelId.toString(); + } else { + filters[REFERENCE_FIELD] = modelId.toString(); + } + + return count(filters: filters); + } + + Future uploadAttachment(File attachment, String modelType, int modelId, {String comment = "", Map fields = const {}}) async { // Ensure that the correct reference field is set Map data = Map.from(fields); - data[REFERENCE_FIELD] = parentId.toString(); + String url = URL; + + if (InvenTreeAPI().supportsModernAttachments) { + // All attachments are stored in a consolidated table + url = "attachment/"; + data["model_id"] = modelId.toString(); + data["model_type"] = modelType; + } else { + data[REFERENCE_FIELD] = modelId.toString(); + } final APIResponse response = await InvenTreeAPI().uploadFile( - URL, + url, attachment, method: "POST", name: "attachment", diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index 61e1c274..11023931 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -36,10 +36,6 @@ class InvenTreePartCategory extends InvenTreeModel { "structural": {}, }; - if (!api.supportsStructuralCategories) { - fields.remove("structural"); - } - return fields; } @@ -475,7 +471,10 @@ class InvenTreePartAttachment extends InvenTreeAttachment { String get REFERENCE_FIELD => "part"; @override - String get URL => "part/attachment/"; + String get REF_MODEL_TYPE => "part"; + + @override + String get URL => InvenTreeAPI().supportsModernAttachments ? "attachment/" : "part/attachment/"; @override InvenTreeModel createFromJson(Map json) => InvenTreePartAttachment.fromJson(json); diff --git a/lib/inventree/purchase_order.dart b/lib/inventree/purchase_order.dart index fab36729..ac8973dc 100644 --- a/lib/inventree/purchase_order.dart +++ b/lib/inventree/purchase_order.dart @@ -237,7 +237,10 @@ class InvenTreePurchaseOrderAttachment extends InvenTreeAttachment { String get REFERENCE_FIELD => "order"; @override - String get URL => "order/po/attachment/"; + String get REF_MODEL_TYPE => "purchaseorder"; + + @override + String get URL => InvenTreeAPI().supportsModernAttachments ? "attachment/" : "order/po/attachment/"; @override InvenTreeModel createFromJson(Map json) => InvenTreePurchaseOrderAttachment.fromJson(json); diff --git a/lib/inventree/sales_order.dart b/lib/inventree/sales_order.dart index b23d8ebf..0002adcb 100644 --- a/lib/inventree/sales_order.dart +++ b/lib/inventree/sales_order.dart @@ -280,6 +280,9 @@ class InvenTreeSalesOrderAttachment extends InvenTreeAttachment { String get REFERENCE_FIELD => "order"; @override - String get URL => "order/so/attachment/"; + String get REF_MODEL_TYPE => "salesorder"; + + @override + String get URL => InvenTreeAPI().supportsModernAttachments ? "attachment/" : "order/so/attachment/"; } diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index b9c40369..4d125533 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -635,11 +635,13 @@ class InvenTreeStockItemAttachment extends InvenTreeAttachment { String get REFERENCE_FIELD => "stock_item"; @override - String get URL => "stock/attachment/"; + String get REF_MODEL_TYPE => "stockitem"; + + @override + String get URL => InvenTreeAPI().supportsModernAttachments ? "attachment/" : "stock/attachment/"; @override InvenTreeModel createFromJson(Map json) => InvenTreeStockItemAttachment.fromJson(json); - } @@ -669,10 +671,6 @@ class InvenTreeStockLocation extends InvenTreeModel { "structural": {}, }; - if (!api.supportsStructuralCategories) { - fields.remove("structural"); - } - return fields; } diff --git a/lib/settings/about.dart b/lib/settings/about.dart index 1bec3763..880da20b 100644 --- a/lib/settings/about.dart +++ b/lib/settings/about.dart @@ -121,15 +121,13 @@ class InvenTreeAboutWidget extends StatelessWidget { ); // Display extra tile if the server supports plugins - if (InvenTreeAPI().pluginsEnabled) { - tiles.add( - ListTile( - title: Text(L10().pluginSupport), - subtitle: Text(L10().pluginSupportDetail), - leading: FaIcon(FontAwesomeIcons.plug), - ) - ); - } + tiles.add( + ListTile( + title: Text(L10().pluginSupport), + subtitle: Text(L10().pluginSupportDetail), + leading: FaIcon(FontAwesomeIcons.plug), + ) + ); } else { tiles.add( diff --git a/lib/widget/attachment_widget.dart b/lib/widget/attachment_widget.dart index 1a31993c..e182df0f 100644 --- a/lib/widget/attachment_widget.dart +++ b/lib/widget/attachment_widget.dart @@ -6,6 +6,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:one_context/one_context.dart"; import "package:url_launcher/url_launcher.dart"; +import "package:inventree/api.dart"; import "package:inventree/l10.dart"; import "package:inventree/app_colors.dart"; @@ -25,10 +26,10 @@ import "package:inventree/widget/refreshable_state.dart"; */ class AttachmentWidget extends StatefulWidget { - const AttachmentWidget(this.attachment, this.referenceId, this.hasUploadPermission) : super(); + const AttachmentWidget(this.attachmentClass, this.modelId, this.hasUploadPermission) : super(); - final InvenTreeAttachment attachment; - final int referenceId; + final InvenTreeAttachment attachmentClass; + final int modelId; final bool hasUploadPermission; @override @@ -74,7 +75,9 @@ class _AttachmentWidgetState extends RefreshableState { if (file == null) return; showLoadingOverlay(context); - final bool result = await widget.attachment.uploadAttachment(file, widget.referenceId); + + final bool result = await widget.attachmentClass.uploadAttachment(file, widget.attachmentClass.MODEL_TYPE, widget.modelId); + hideLoadingOverlay(); if (result) { @@ -131,14 +134,24 @@ class _AttachmentWidgetState extends RefreshableState { @override Future request(BuildContext context) async { - await widget.attachment.list( - filters: { - widget.attachment.REFERENCE_FIELD: widget.referenceId.toString() - } + Map filters = {}; + + if (InvenTreeAPI().supportsModernAttachments) { + filters["model_type"] = widget.attachmentClass.MODEL_TYPE; + filters["model_id"] = widget.modelId.toString(); + } else { + filters[widget.attachmentClass.REFERENCE_FIELD] = widget.modelId.toString(); + } + + await widget.attachmentClass.list( + filters: filters ).then((var results) { attachments.clear(); + print("Found ${results.length} results:"); + for (var result in results) { + print(result.toString()); if (result is InvenTreeAttachment) { attachments.add(result); } diff --git a/lib/widget/company/company_detail.dart b/lib/widget/company/company_detail.dart index 9d098aa5..57cdc388 100644 --- a/lib/widget/company/company_detail.dart +++ b/lib/widget/company/company_detail.dart @@ -188,19 +188,14 @@ class _CompanyDetailState extends RefreshableState { } }); - if (api.supportCompanyAttachments) { - InvenTreeCompanyAttachment().count( - filters: { - "company": widget.company.pk.toString() - } - ).then((value) { - if (mounted) { - setState(() { - attachmentCount = value; - }); - } - }); - } + InvenTreeCompanyAttachment().countAttachments(widget.company.pk) + .then((value) { + if (mounted) { + setState(() { + attachmentCount = value; + }); + } + }); } Future editCompany(BuildContext context) async { @@ -397,25 +392,24 @@ class _CompanyDetailState extends RefreshableState { )); } - if (api.supportCompanyAttachments) { - tiles.add(ListTile( - title: Text(L10().attachments), - leading: FaIcon(FontAwesomeIcons.fileLines, color: COLOR_ACTION), - trailing: attachmentCount > 0 ? Text(attachmentCount.toString()) : null, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AttachmentWidget( - InvenTreeCompanyAttachment(), - widget.company.pk, - InvenTreeCompany().canEdit - ) + + tiles.add(ListTile( + title: Text(L10().attachments), + leading: FaIcon(FontAwesomeIcons.fileLines, color: COLOR_ACTION), + trailing: attachmentCount > 0 ? Text(attachmentCount.toString()) : null, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AttachmentWidget( + InvenTreeCompanyAttachment(), + widget.company.pk, + InvenTreeCompany().canEdit ) - ); - } - )); - } + ) + ); + } + )); return tiles; } diff --git a/lib/widget/drawer.dart b/lib/widget/drawer.dart index f38ad337..5dd47c80 100644 --- a/lib/widget/drawer.dart +++ b/lib/widget/drawer.dart @@ -174,18 +174,16 @@ class InvenTreeDrawer extends StatelessWidget { tiles.add(Divider()); } - if (InvenTreeAPI().supportsNotifications) { - int notification_count = InvenTreeAPI().notification_counter; + int notification_count = InvenTreeAPI().notification_counter; - tiles.add( - ListTile( - leading: FaIcon(FontAwesomeIcons.bell, color: COLOR_ACTION), - trailing: notification_count > 0 ? Text(notification_count.toString()) : null, - title: Text(L10().notifications), - onTap: _notifications, - ) - ); - } + tiles.add( + ListTile( + leading: FaIcon(FontAwesomeIcons.bell, color: COLOR_ACTION), + trailing: notification_count > 0 ? Text(notification_count.toString()) : null, + title: Text(L10().notifications), + onTap: _notifications, + ) + ); tiles.add( ListTile( diff --git a/lib/widget/order/purchase_order_detail.dart b/lib/widget/order/purchase_order_detail.dart index ae7606ad..adc5b47e 100644 --- a/lib/widget/order/purchase_order_detail.dart +++ b/lib/widget/order/purchase_order_detail.dart @@ -228,9 +228,7 @@ class _PurchaseOrderDetailState extends RefreshableState { supportsProjectCodes = api.supportsProjectCodes && await api.getGlobalBooleanSetting("PROJECT_CODES_ENABLED"); - InvenTreeSalesOrderAttachment().count(filters: { - "order": widget.order.pk.toString() - }).then((int value) { + InvenTreeSalesOrderAttachment().countAttachments(widget.order.pk).then((int value) { if (mounted) { setState(() { attachmentCount = value; diff --git a/lib/widget/part/part_detail.dart b/lib/widget/part/part_detail.dart index c310a598..c2e044e6 100644 --- a/lib/widget/part/part_detail.dart +++ b/lib/widget/part/part_detail.dart @@ -90,15 +90,13 @@ class _PartDisplayState extends RefreshableState { List actions = []; if (InvenTreePart().canEdit) { - if (api.supportModernBarcodes) { - actions.add( - customBarcodeAction( - context, this, - widget.part.customBarcode, "part", - widget.part.pk - ) - ); - } + actions.add( + customBarcodeAction( + context, this, + widget.part.customBarcode, "part", + widget.part.pk + ) + ); } return actions; @@ -184,18 +182,10 @@ class _PartDisplayState extends RefreshableState { }); // Request the number of parameters for this part - if (api.supportsPartParameters) { - showParameters = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_PARAMETERS, true) as bool; - } else { - showParameters = false; - } + showParameters = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_PARAMETERS, true) as bool; // Request the number of attachments - InvenTreePartAttachment().count( - filters: { - "part": part.pk.toString(), - } - ).then((int value) { + InvenTreePartAttachment().countAttachments(part.pk).then((int value) { if (mounted) { setState(() { attachmentCount = value; diff --git a/lib/widget/stock/location_display.dart b/lib/widget/stock/location_display.dart index ece7c277..0251667d 100644 --- a/lib/widget/stock/location_display.dart +++ b/lib/widget/stock/location_display.dart @@ -140,15 +140,13 @@ class _LocationDisplayState extends RefreshableState { } // Assign or un-assign barcodes - if (api.supportModernBarcodes) { - actions.add( - customBarcodeAction( - context, this, - location!.customBarcode, "stocklocation", - location!.pk - ) - ); - } + actions.add( + customBarcodeAction( + context, this, + location!.customBarcode, "stocklocation", + location!.pk + ) + ); } return actions; diff --git a/lib/widget/stock/stock_detail.dart b/lib/widget/stock/stock_detail.dart index b98b2d42..b3c829f1 100644 --- a/lib/widget/stock/stock_detail.dart +++ b/lib/widget/stock/stock_detail.dart @@ -183,15 +183,13 @@ class _StockItemDisplayState extends RefreshableState { ) ); - if (api.supportModernBarcodes) { - actions.add( - customBarcodeAction( - context, this, - widget.item.customBarcode, - "stockitem", widget.item.pk - ) - ); - } + actions.add( + customBarcodeAction( + context, this, + widget.item.customBarcode, + "stockitem", widget.item.pk + ) + ); } return actions; @@ -246,12 +244,7 @@ class _StockItemDisplayState extends RefreshableState { } // Request the number of attachments - InvenTreeStockItemAttachment().count( - filters: { - "stock_item": widget.item.pk.toString() - } - ).then((int value) { - + InvenTreeStockItemAttachment().countAttachments(widget.item.pk).then((int value) { if (mounted) { setState(() { attachmentCount = value; diff --git a/test/api_test.dart b/test/api_test.dart index a9c72910..86d52d2e 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -110,10 +110,6 @@ void main() { // Check supported functions assert(api.apiVersion >= 50); - assert(api.supportsSettings); - assert(api.supportsNotifications); - assert(api.supportsPoReceive); - assert(api.serverInstance.isNotEmpty); assert(api.serverVersion.isNotEmpty);