From acf89426cec238ef3837ad7dc409a317a22e6197 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 19 May 2022 20:38:28 +1000 Subject: [PATCH 01/38] Fix for search screen - Change input and controller - Add focus node - Add "searching" indicator --- assets/release_notes.md | 5 +++ lib/l10n/app_en.arb | 3 ++ lib/widget/search.dart | 88 +++++++++++++++++++++++++++++++++-------- 3 files changed, 79 insertions(+), 17 deletions(-) diff --git a/assets/release_notes.md b/assets/release_notes.md index ca14a6c8..e240c26d 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -1,6 +1,11 @@ ## InvenTree App Release Notes --- +### 0.7.1 - May 2022 +--- + +- Fixes issue which prevented text input in search window + ### 0.7.0 - May 2022 --- diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3b13d9cf..f6e60649 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -797,6 +797,9 @@ "description": "search" }, + "searching": "Searching", + "@searching": {}, + "searchLocation": "Search for location", "@searchLocation": {}, diff --git a/lib/widget/search.dart b/lib/widget/search.dart index 2d21fc66..96cf42f9 100644 --- a/lib/widget/search.dart +++ b/lib/widget/search.dart @@ -36,6 +36,19 @@ class _SearchDisplayState extends RefreshableState { final bool hasAppBar; + @override + void initState() { + super.initState(); + + _focusNode = FocusNode(); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + @override String getAppBarTitle(BuildContext context) => L10().search; @@ -52,6 +65,17 @@ class _SearchDisplayState extends RefreshableState { Timer? debounceTimer; + bool isSearching() { + + if (searchController.text.isEmpty) { + return false; + } + + return nSearchResults < 6; + } + + int nSearchResults = 0; + int nPartResults = 0; int nCategoryResults = 0; @@ -64,6 +88,8 @@ class _SearchDisplayState extends RefreshableState { int nPurchaseOrderResults = 0; + late FocusNode _focusNode; + // Callback when the text is being edited // Incorporates a debounce timer to restrict search frequency void onSearchTextChanged(String text, {bool immediate = false}) { @@ -84,6 +110,10 @@ class _SearchDisplayState extends RefreshableState { Future search(String term) async { + setState(() { + nSearchResults = 0; + }); + if (term.isEmpty) { setState(() { // Do not search on an empty string @@ -93,6 +123,8 @@ class _SearchDisplayState extends RefreshableState { nLocationResults = 0; nSupplierResults = 0; nPurchaseOrderResults = 0; + + nSearchResults = 0; }); return; @@ -104,6 +136,8 @@ class _SearchDisplayState extends RefreshableState { ).then((int n) { setState(() { nPartResults = n; + + nSearchResults++; }); }); @@ -113,6 +147,8 @@ class _SearchDisplayState extends RefreshableState { ).then((int n) { setState(() { nCategoryResults = n; + + nSearchResults++; }); }); @@ -122,6 +158,8 @@ class _SearchDisplayState extends RefreshableState { ).then((int n) { setState(() { nStockResults = n; + + nSearchResults++; }); }); @@ -131,6 +169,8 @@ class _SearchDisplayState extends RefreshableState { ).then((int n) { setState(() { nLocationResults = n; + + nSearchResults++; }); }); @@ -143,6 +183,7 @@ class _SearchDisplayState extends RefreshableState { ).then((int n) { setState(() { nSupplierResults = n; + nSearchResults++; }); }); @@ -155,6 +196,7 @@ class _SearchDisplayState extends RefreshableState { ).then((int n) { setState(() { nPurchaseOrderResults = n; + nSearchResults++; }); }); @@ -166,29 +208,31 @@ class _SearchDisplayState extends RefreshableState { // Search input tiles.add( - InputDecorator( + TextFormField( decoration: InputDecoration( - ), - child: ListTile( - title: TextField( - readOnly: false, - decoration: InputDecoration( - helperText: L10().queryEmpty, - ), - controller: searchController, - onChanged: (String text) { - onSearchTextChanged(text); - }, + hintText: L10().queryEmpty, + prefixIcon: IconButton( + icon: FaIcon(FontAwesomeIcons.search), + onPressed: null, ), - trailing: IconButton( + suffixIcon: IconButton( icon: FaIcon(FontAwesomeIcons.backspace, color: Colors.red), onPressed: () { searchController.clear(); onSearchTextChanged("", immediate: true); - }, + _focusNode.requestFocus(); + } ), - ) - ) + ), + readOnly: false, + autofocus: true, + autocorrect: false, + focusNode: _focusNode, + controller: searchController, + onChanged: (String text) { + onSearchTextChanged(text); + }, + ), ); String query = searchController.text; @@ -335,7 +379,17 @@ class _SearchDisplayState extends RefreshableState { ); } - if (results.isEmpty && searchController.text.isNotEmpty) { + if (isSearching()) { + tiles.add( + ListTile( + title: Text(L10().searching), + leading: FaIcon(FontAwesomeIcons.search), + trailing: CircularProgressIndicator(), + ) + ); + } + + if (!isSearching() && results.isEmpty && searchController.text.isNotEmpty) { tiles.add( ListTile( title: Text(L10().queryNoResults), From 941ee5e1725975b6fbba9f4a44c2fb0c70e14286 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 19 May 2022 21:06:20 +1000 Subject: [PATCH 02/38] Prevent "old" search results from mucking up the displayed data - Only accept results if the search term has not changed --- lib/inventree/model.dart | 2 - lib/widget/search.dart | 103 ++++++++++++++++++++------------------- 2 files changed, 53 insertions(+), 52 deletions(-) diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index decb7aaa..550bc2f5 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -216,8 +216,6 @@ class InvenTreeModel { if (response.isValid()) { int n = int.tryParse(response.data["count"].toString()) ?? 0; - - print("${URL} -> ${n} results"); return n; } else { return 0; diff --git a/lib/widget/search.dart b/lib/widget/search.dart index 96cf42f9..1a59237e 100644 --- a/lib/widget/search.dart +++ b/lib/widget/search.dart @@ -105,78 +105,78 @@ class _SearchDisplayState extends RefreshableState { search(text); }); } - } + /* + * Initiate multiple search requests to the server. + * Each request returns at *some point* in the future, + * by which time the search input may have changed, giving unexpected results. + * + * So, each request only causes an update *if* the search term is still the same when it completes + */ Future search(String term) async { setState(() { + // Do not search on an empty string + nPartResults = 0; + nCategoryResults = 0; + nStockResults = 0; + nLocationResults = 0; + nSupplierResults = 0; + nPurchaseOrderResults = 0; + nSearchResults = 0; }); if (term.isEmpty) { - setState(() { - // Do not search on an empty string - nPartResults = 0; - nCategoryResults = 0; - nStockResults = 0; - nLocationResults = 0; - nSupplierResults = 0; - nPurchaseOrderResults = 0; - - nSearchResults = 0; - }); - return; } // Search parts - InvenTreePart().count( - searchQuery: term - ).then((int n) { - setState(() { - nPartResults = n; - - nSearchResults++; - }); + InvenTreePart().count(searchQuery: term).then((int n) { + if (term == searchController.text) { + setState(() { + nPartResults = n; + nSearchResults++; + }); + } }); // Search part categories - InvenTreePartCategory().count( - searchQuery: term, - ).then((int n) { - setState(() { - nCategoryResults = n; - - nSearchResults++; - }); + InvenTreePartCategory().count(searchQuery: term,).then((int n) { + if (term == searchController.text) { + setState(() { + nCategoryResults = n; + nSearchResults++; + }); + } }); // Search stock items - InvenTreeStockItem().count( - searchQuery: term - ).then((int n) { - setState(() { - nStockResults = n; - - nSearchResults++; - }); + InvenTreeStockItem().count(searchQuery: term).then((int n) { + if (term == searchController.text) { + setState(() { + nStockResults = n; + nSearchResults++; + }); + } }); // Search stock locations - InvenTreeStockLocation().count( - searchQuery: term - ).then((int n) { - setState(() { - nLocationResults = n; + InvenTreeStockLocation().count(searchQuery: term).then((int n) { + if (term == searchController.text) { + setState(() { + nLocationResults = n; - nSearchResults++; - }); + nSearchResults++; + }); + } }); + // TDOO: Re-implement this once display for companies has been fixed + /* // Search suppliers - InvenTreeCompany().count( - searchQuery: term, + InvenTreeCompany().count(searchQuery: term, filters: { "is_supplier": "true", }, @@ -186,6 +186,7 @@ class _SearchDisplayState extends RefreshableState { nSearchResults++; }); }); + */ // Search purchase orders InvenTreePurchaseOrder().count( @@ -194,10 +195,12 @@ class _SearchDisplayState extends RefreshableState { "outstanding": "true" } ).then((int n) { - setState(() { - nPurchaseOrderResults = n; - nSearchResults++; - }); + if (term == searchController.text) { + setState(() { + nPurchaseOrderResults = n; + nSearchResults++; + }); + } }); } From 11157b7c77da15ab6578ec48957b8540694579e9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 19 May 2022 21:11:46 +1000 Subject: [PATCH 03/38] Fix search counter --- lib/widget/search.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/widget/search.dart b/lib/widget/search.dart index 1a59237e..c2bc63f5 100644 --- a/lib/widget/search.dart +++ b/lib/widget/search.dart @@ -4,7 +4,6 @@ import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; -import "package:inventree/inventree/company.dart"; import "package:inventree/inventree/purchase_order.dart"; import "package:inventree/widget/part_list.dart"; import "package:inventree/widget/purchase_order_list.dart"; @@ -71,7 +70,7 @@ class _SearchDisplayState extends RefreshableState { return false; } - return nSearchResults < 6; + return nSearchResults < 5; } int nSearchResults = 0; @@ -405,8 +404,11 @@ class _SearchDisplayState extends RefreshableState { } } - return tiles; + if (!_focusNode.hasFocus) { + _focusNode.requestFocus(); + } + return tiles; } @override From a9f794af1fc5ef5339814f28055492379e888a51 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 19 May 2022 23:04:53 +1000 Subject: [PATCH 04/38] New translations app_en.arb (French) --- lib/l10n/fr_FR/app_fr_FR.arb | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/lib/l10n/fr_FR/app_fr_FR.arb b/lib/l10n/fr_FR/app_fr_FR.arb index 82ccc615..e6de6692 100644 --- a/lib/l10n/fr_FR/app_fr_FR.arb +++ b/lib/l10n/fr_FR/app_fr_FR.arb @@ -1,9 +1,21 @@ { "@@locale": "en", + "appTitle": "InvenTree", + "@appTitle": { + "description": "InvenTree application title string" + }, + "ok": "OK", + "@ok": { + "description": "OK" + }, "about": "À propos", "@about": {}, "accountDetails": "Détails du compte", "@accountDetails": {}, + "actions": "Actions", + "@actions": { + "description": "" + }, "actionsNone": "Aucune action disponible", "@actionsNone": {}, "add": "Ajouter", @@ -40,6 +52,8 @@ "@attachmentNonePartDetail": {}, "attachmentSelect": "Sélectionner une pièce jointe", "@attachmentSelect": {}, + "attention": "Avertissement", + "@attention": {}, "availableStock": "Stock disponible", "@availableStock": {}, "barcodeAssign": "Affecter un code-barres", @@ -80,6 +94,8 @@ "@batchCode": {}, "billOfMaterials": "Liste des matériaux", "@billOfMaterials": {}, + "bom": "Liste de matériel", + "@bom": {}, "build": "Assemblage", "@build": {}, "building": "Assemblage en cours", @@ -130,12 +146,16 @@ "@deletePart": {}, "deletePartDetail": "Supprimer cette pièce de la base de données", "@deletePartDetail": {}, + "description": "Description", + "@description": {}, "destroyed": "Détruit", "@destroyed": {}, "details": "Détails", "@details": { "description": "details" }, + "documentation": "Documentation", + "@documentation": {}, "downloading": "Téléchargement du fichier", "@downloading": {}, "downloadError": "Erreur lors du téléchargement", @@ -194,6 +214,7 @@ "@history": { "description": "history" }, + "home": "Page d’accueil", "@homeScreen": {}, "homeScreen": "Ecran d'accueil", "homeScreenSettings": "Configurer les paramètres de l'écran d'accueil", @@ -264,6 +285,8 @@ "@itemInLocation": {}, "keywords": "Mots clés", "@keywords": {}, + "labelTemplate": "Modèle d'étiquette", + "@labelTemplate": {}, "lastStocktake": "Dernier inventaire", "@lastStocktake": {}, "lastUpdated": "Dernière mise à jour", @@ -272,6 +295,10 @@ "@lineItem": {}, "lineItems": "Position", "@lineItems": {}, + "locateItem": "Localiser l'article en stock", + "@locateItem": {}, + "locateLocation": "Localiser l'emplacement du stock", + "@locateLocation": {}, "locationCreate": "Nouvel emplacement", "@locationCreate": {}, "locationCreateDetail": "Créer un nouvel emplacement de stock", @@ -292,6 +319,14 @@ "@name": {}, "notConnected": "Non connecté", "@notConnected": {}, + "notes": "Notes", + "@notes": { + "description": "Notes" + }, + "notifications": "Notifications", + "@notifications": {}, + "notificationsEmpty": "Aucune notification non-lue", + "@notificationsEmpty": {}, "noResponse": "Aucune réponse du serveur", "@noResponse": {}, "noResults": "Aucun résultat", @@ -310,6 +345,8 @@ "@packaging": {}, "packageName": "Nom du package", "@packageName": {}, + "parent": "Niveau superieur", + "@parent": {}, "parentCategory": "Catégorie parent", "@parentCategory": {}, "parentLocation": "Emplacement parent", @@ -362,6 +399,14 @@ "@permissionRequired": {}, "printLabel": "Imprimer l'étiquette", "@printLabel": {}, + "plugin": "Extension", + "@plugin": {}, + "pluginPrinter": "Imprimante", + "@pluginPrinter": {}, + "pluginSupport": "Prise en charge du plugin activée", + "@pluginSupport": {}, + "pluginSupportDetail": "Le serveur supporte des plugins personnalisés", + "@pluginSupportDetail": {}, "printLabelFailure": "Echec de l'impression", "@printLabelFailure": {}, "printLabelSuccess": "Etiquette envoyée à l'imprimante", @@ -384,6 +429,8 @@ "@profileNotSelected": {}, "profileSelect": "Sélectionner le serveur InvenTree", "@profileSelect": {}, + "profileSelectOrCreate": "Sélectionnez un serveur ou créez un nouveau profil", + "@profileSelectOrCreate": {}, "profileTapToCreate": "Appuyer pour créer ou sélectionner un profil", "@profileTapToCreate": {}, "purchaseOrder": "Commande d’achat", @@ -406,6 +453,8 @@ "@quantityInvalid": {}, "quantityPositive": "La quantité doit être positive", "@quantityPositive": {}, + "queryEmpty": "Entrer un critère de recherche", + "@queryEmpty": {}, "queryNoResults": "Pas de résultat pour votre requête", "@queryNoResults": {}, "received": "Reçu", @@ -438,6 +487,8 @@ "@results": {}, "request": "Requête", "@request": {}, + "requestSuccessful": "Recherche reussie", + "@requestSuccessful": {}, "requestingData": "Demande de données", "@requestingData": {}, "required": "Requis", @@ -562,6 +613,10 @@ "@status": {}, "statusCode": "Code d'état", "@statusCode": {}, + "stock": "Stock", + "@stock": { + "description": "stock" + }, "stockDetails": "Quantité actuelle de stock disponible", "@stockDetails": {}, "stockItem": "Article en stock", @@ -636,6 +691,8 @@ "@takePicture": {}, "targetDate": "Date Cible", "@targetDate": {}, + "templatePart": "Modele de composant", + "@templatePart": {}, "testName": "Nom de test", "@testName": {}, "testPassedOrFailed": "Test réussi ou échoué", @@ -704,6 +761,8 @@ "@valueCannotBeEmpty": {}, "valueRequired": "La valeur est requise", "@valueRequired": {}, + "version": "Version", + "@version": {}, "viewSupplierPart": "Voir la pièce du fournisseur", "@viewSupplierPart": {}, "website": "Site web", From cb866fb45c7c7a6647439604f8267a7fc9ca4f8d Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 20 May 2022 11:00:58 +1000 Subject: [PATCH 05/38] New translations app_en.arb (French) --- lib/l10n/fr_FR/app_fr_FR.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/fr_FR/app_fr_FR.arb b/lib/l10n/fr_FR/app_fr_FR.arb index e6de6692..780036af 100644 --- a/lib/l10n/fr_FR/app_fr_FR.arb +++ b/lib/l10n/fr_FR/app_fr_FR.arb @@ -219,13 +219,13 @@ "homeScreen": "Ecran d'accueil", "homeScreenSettings": "Configurer les paramètres de l'écran d'accueil", "@homeScreenSettings": {}, - "homeShowPo": "Afficher les commandes d'achat", + "homeShowPo": "Voir bon de commande", "@homeShowPo": {}, "homeShowSubscribed": "Pièces suivies", "@homeShowSubscribed": {}, "homeShowSubscribedDescription": "Afficher les pièces suivies sur l'écran d'accueil", "@homeShowSubscsribedDescription": {}, - "homeShowPoDescription": "Afficher le bouton de bon de commande sur l'écran d'accueil", + "homeShowPoDescription": "Afficher le bouton de l'ordre d'achat sur l'écran d'accueil", "@homeShowPoDescription": {}, "homeShowSuppliers": "Afficher les fournisseurs", "@homeShowSuppliers": {}, From 8300cde3eca926650c1216c2b9e269210caaf967 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 20 May 2022 11:01:00 +1000 Subject: [PATCH 06/38] New translations app_en.arb (German) --- lib/l10n/de_DE/app_de_DE.arb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/l10n/de_DE/app_de_DE.arb b/lib/l10n/de_DE/app_de_DE.arb index 9fcf7763..099b892e 100644 --- a/lib/l10n/de_DE/app_de_DE.arb +++ b/lib/l10n/de_DE/app_de_DE.arb @@ -285,6 +285,8 @@ "@itemInLocation": {}, "keywords": "Schlüsselwörter", "@keywords": {}, + "labelTemplate": "Label Vorlage", + "@labelTemplate": {}, "lastStocktake": "Letzte Inventur", "@lastStocktake": {}, "lastUpdated": "Letzte Änderung", @@ -539,6 +541,8 @@ "@search": { "description": "search" }, + "searching": "Suche", + "@searching": {}, "searchLocation": "Lagerort suchen", "@searchLocation": {}, "searchParts": "Teile suchen", From cd9af13e2719e8e19b7c48e8cd4766fc58149a15 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 May 2022 23:34:02 +1000 Subject: [PATCH 07/38] Adds an initial unit test --- pubspec.lock | 196 +++++++++++++++++++++++++++++++++++++++--- pubspec.yaml | 7 +- test/api_test.dart | 45 ++++++++++ test/widget_test.dart | 6 +- 4 files changed, 234 insertions(+), 20 deletions(-) create mode 100644 test/api_test.dart diff --git a/pubspec.lock b/pubspec.lock index 4fb3ba92..a418a5b4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "40.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" archive: dependency: transitive description: @@ -113,6 +127,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + coverage: + dependency: transitive + description: + name: coverage + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.2" cross_file: dependency: transitive description: @@ -190,13 +218,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.3" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" ffi: dependency: transitive description: @@ -263,11 +284,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -280,6 +296,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "9.2.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" http: dependency: "direct main" description: @@ -287,6 +317,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.4" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" http_parser: dependency: transitive description: @@ -336,6 +373,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" js: dependency: transitive description: @@ -350,6 +394,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.2" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" markdown: dependency: transitive description: @@ -378,6 +429,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" octo_image: dependency: transitive description: @@ -399,6 +464,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.2.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" package_info_plus: dependency: "direct main" description: @@ -525,6 +597,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" process: dependency: transitive description: @@ -532,6 +611,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" qr_code_scanner: dependency: "direct main" description: @@ -630,6 +716,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" sky_engine: dependency: transitive description: flutter @@ -642,6 +756,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.5" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.10" source_span: dependency: transitive description: @@ -705,13 +833,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.21.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.13" typed_data: dependency: transitive description: @@ -789,6 +931,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "8.3.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9031ea5d..7489e150 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,10 +38,9 @@ dependencies: url_launcher: ^6.0.9 # Open link in system browser dev_dependencies: - flutter_launcher_icons: - flutter_test: - sdk: flutter - lint: ^1.0.0 + flutter_launcher_icons: ^0.9.0 + lint: ^1.8.0 + test: ^1.21.0 flutter_icons: android: true diff --git a/test/api_test.dart b/test/api_test.dart new file mode 100644 index 00000000..36baa7fc --- /dev/null +++ b/test/api_test.dart @@ -0,0 +1,45 @@ +/* + * Unit tests for the API class + */ + +import 'package:test/test.dart'; + +import 'package:inventree/api.dart'; +import 'package:inventree/user_profile.dart'; + +void main() { + + setUp(() async { + // Ensure we have a user profile available + // This profile will match the dockerized InvenTree setup, running locally + + print("Creating user profile"); + await UserProfileDBManager().addProfile(UserProfile( + username: "testuser", + password: "testpassword""", + server: "http://localhost:12345", + selected: true, + )); + + final profiles = await UserProfileDBManager().getAllProfiles(); + + // Ensure we have one profile available + expect(profiles.length, equals(1)); + + // Select the profile + await UserProfileDBManager().selectProfile(profiles.first.key ?? 1); + + }); + + test("Select Profile", () async { + // Ensure that we can select a user profile + final prf = await UserProfileDBManager().getSelectedProfile(); + + expect(prf, isNot(null)); + + expect(prf?.username, equals("testuser")); + expect(prf?.password, equals("testpassword")); + expect(prf?.server, equals("http://localhost:12345")); + }); + +} \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart index 3c73102a..4e838ac5 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,9 +5,9 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import "package:flutter_test/flutter_test.dart"; +// import "package:flutter_test/flutter_test.dart"; void main() { - testWidgets("Counter increments smoke test", (WidgetTester tester) async { - }); + // testWidgets("Counter increments smoke test", (WidgetTester tester) async { + // }); } From d898efdf6dc7631395cd1a68be8c79c4c3370237 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 May 2022 23:35:20 +1000 Subject: [PATCH 08/38] Rename github workflow --- .github/workflows/{lint.yaml => ci.yaml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{lint.yaml => ci.yaml} (98%) diff --git a/.github/workflows/lint.yaml b/.github/workflows/ci.yaml similarity index 98% rename from .github/workflows/lint.yaml rename to .github/workflows/ci.yaml index 44b1ed3b..dbbce593 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/ci.yaml @@ -1,6 +1,6 @@ # Run flutter linting checks -name: lint +name: CI on: push: From fe3c298f860b1f93572ad7b0ab7de1fbd6054eca Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 May 2022 23:37:55 +1000 Subject: [PATCH 09/38] Workflow updates --- .github/workflows/ci.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dbbce593..12dca6b1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,12 +12,9 @@ on: jobs: - lint: + test: runs-on: ubuntu-latest - env: - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - steps: - name: Checkout code uses: actions/checkout@v2 From 96ae1be3ec2eadbe8d8f7bc04c8889820d318c16 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 May 2022 23:42:30 +1000 Subject: [PATCH 10/38] Add coveralls step --- .github/workflows/ci.yaml | 5 ++++- test/api_test.dart | 8 ++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 12dca6b1..456c715a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -35,4 +35,7 @@ jobs: - run: flutter pub get - run: cp lib/dummy_dsn.dart lib/dsn.dart - run: flutter analyze - - run: flutter test --coverage + - name: Run Unit Tests + run: | + flutter test --coverage + coveralls \ No newline at end of file diff --git a/test/api_test.dart b/test/api_test.dart index 36baa7fc..5f24f897 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -2,18 +2,14 @@ * Unit tests for the API class */ -import 'package:test/test.dart'; - -import 'package:inventree/api.dart'; -import 'package:inventree/user_profile.dart'; +import "package:test/test.dart"; +import "package:inventree/user_profile.dart"; void main() { setUp(() async { // Ensure we have a user profile available // This profile will match the dockerized InvenTree setup, running locally - - print("Creating user profile"); await UserProfileDBManager().addProfile(UserProfile( username: "testuser", password: "testpassword""", From caa10b5f8fe255429180fc3af8afaa16c45c6711 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 May 2022 23:48:58 +1000 Subject: [PATCH 11/38] Add python requirements file --- .github/workflows/ci.yaml | 1 + requirements.txt | 4 ++++ test/api_test.dart | 26 ++++++++++++++++++++------ 3 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 requirements.txt diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 456c715a..90b2df32 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,5 +37,6 @@ jobs: - run: flutter analyze - name: Run Unit Tests run: | + pip install -Ur requirements.txt flutter test --coverage coveralls \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..94f990c7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +# Python requirements for devops + +coverage==5.3 # Unit test coverage +coveralls==2.1.2 # Coveralls linking (for code coverage reporting) \ No newline at end of file diff --git a/test/api_test.dart b/test/api_test.dart index 5f24f897..1b5470b4 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -27,15 +27,29 @@ void main() { }); - test("Select Profile", () async { + // Run a set of tests for user profile functionality + group("Profile Tests", () async { + + test("Profile Name Check", () async { + bool result = false; + + result = await UserProfileDBManager().profileNameExists("doesnotexist"); + expect(result, equals(false)); + + result = await UserProfileDBManager().profileNameExists("testuser"); + expect(result, equals(true)); + }); + // Ensure that we can select a user profile - final prf = await UserProfileDBManager().getSelectedProfile(); + test("Select Profile", () async { + final prf = await UserProfileDBManager().getSelectedProfile(); - expect(prf, isNot(null)); + expect(prf, isNot(null)); - expect(prf?.username, equals("testuser")); - expect(prf?.password, equals("testpassword")); - expect(prf?.server, equals("http://localhost:12345")); + expect(prf?.username, equals("testuser")); + expect(prf?.password, equals("testpassword")); + expect(prf?.server, equals("http://localhost:12345")); + }); }); } \ No newline at end of file From 00baff7a976e332bcedfad8a039f9747b615a99a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 00:26:09 +1000 Subject: [PATCH 12/38] All tests pass now --- lib/user_profile.dart | 91 +++++++++++++++++++++++++++---------------- test/api_test.dart | 71 ++++++++++++++++++++++++++++----- 2 files changed, 119 insertions(+), 43 deletions(-) diff --git a/lib/user_profile.dart b/lib/user_profile.dart index 251aa309..507d9f07 100644 --- a/lib/user_profile.dart +++ b/lib/user_profile.dart @@ -62,68 +62,90 @@ class UserProfileDBManager { Future get _db async => InvenTreePreferencesDB.instance.database; + /* + * Check if a profile with the specified name exists in the database + */ Future profileNameExists(String name) async { - final finder = Finder(filter: Filter.equals("name", name)); + final profiles = await getAllProfiles(); - final profiles = await store.find(await _db, finder: finder); + for (var prf in profiles) { + if (name == prf.name) { + return true; + } + } - return profiles.isNotEmpty; + // No match found! + return false; } - Future addProfile(UserProfile profile) async { + /* + * Add a new UserProfile to the profiles database. + */ + Future addProfile(UserProfile profile) async { + + if (profile.name.isEmpty || profile.username.isEmpty || profile.password.isEmpty) { + print("Profile missing required values - not adding to database"); + return false; + } // Check if a profile already exists with the name final bool exists = await profileNameExists(profile.name); if (exists) { print("UserProfile '${profile.name}' already exists"); - return; + return false; } int key = await store.add(await _db, profile.toJson()) as int; - print("Added user profile <${key}> - '${profile.name}'"); - // Record the key profile.key = key; + + return true; } + /* + * Mark the particular profile as selected + */ Future selectProfile(int key) async { - /* - * Mark the particular profile as selected - */ - - final result = await store.record("selected").put(await _db, key); - - return result; + await store.record("selected").put(await _db, key); } - - Future updateProfile(UserProfile profile) async { - - if (profile.key == null) { - await addProfile(profile); - return; + + /* + * Update the selected profile in the database. + * The unique integer is used to determine if the profile already exists. + */ + Future updateProfile(UserProfile profile) async { + + // Prevent invalid profile data from being updated + if (profile.name.isEmpty || profile.username.isEmpty || profile.password.isEmpty) { + print("Profile missing required values - not updating"); + return false; } - final result = await store.record(profile.key).update(await _db, profile.toJson()); + if (profile.key == null) { + bool result = await addProfile(profile); + return result; + } - print("Updated user profile <${profile.key}> - '${profile.name}'"); + await store.record(profile.key).update(await _db, profile.toJson()); - return result; + return true; } + /* + * Remove a user profile from the database + */ Future deleteProfile(UserProfile profile) async { await store.record(profile.key).delete(await _db); - print("Deleted user profile <${profile.key}> - '${profile.name}'"); } + /* + * Return the currently selected profile. + * The key of the UserProfile should match the "selected" property + */ Future getSelectedProfile() async { - /* - * Return the currently selected profile. - * - * key should match the "selected" property - */ final selected = await store.record("selected").get(await _db); @@ -158,11 +180,12 @@ class UserProfileDBManager { if (profiles[idx].key is int) { profileList.add( - UserProfile.fromJson( - profiles[idx].key as int, - profiles[idx].value as Map, - profiles[idx].key == selected, - )); + UserProfile.fromJson( + profiles[idx].key as int, + profiles[idx].value as Map, + profiles[idx].key == selected, + ) + ); } } diff --git a/test/api_test.dart b/test/api_test.dart index 1b5470b4..928c4028 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -10,33 +10,85 @@ void main() { setUp(() async { // Ensure we have a user profile available // This profile will match the dockerized InvenTree setup, running locally - await UserProfileDBManager().addProfile(UserProfile( + + // To start with, there should not be *any* profiles available + var profiles = await UserProfileDBManager().getAllProfiles(); + + for (var prf in profiles) { + UserProfileDBManager().deleteProfile(prf); + } + + // Check that there are *no* profiles in the database + profiles = await UserProfileDBManager().getAllProfiles(); + expect(profiles.length, equals(0)); + + // Now, create one! + bool result = await UserProfileDBManager().addProfile(UserProfile( + name: "Test Profile", username: "testuser", password: "testpassword""", server: "http://localhost:12345", selected: true, )); - final profiles = await UserProfileDBManager().getAllProfiles(); + expect(result, equals(true)); // Ensure we have one profile available + // expect(profiles.length, equals(1)); + profiles = await UserProfileDBManager().getAllProfiles(); + expect(profiles.length, equals(1)); - // Select the profile - await UserProfileDBManager().selectProfile(profiles.first.key ?? 1); + int key = -1; + // Find the first available profile + for (var p in profiles) { + if (p.key != null) { + key = p.key ?? key; + break; + } + } + + // Select the profile + await UserProfileDBManager().selectProfile(key); }); // Run a set of tests for user profile functionality - group("Profile Tests", () async { + group("Profile Tests:", () { - test("Profile Name Check", () async { - bool result = false; + test("Add Invalid Profiles", () async { + // Add a profile with missing data + bool result = await UserProfileDBManager().addProfile( + UserProfile( + username: "what", + password: "why", + ) + ); - result = await UserProfileDBManager().profileNameExists("doesnotexist"); expect(result, equals(false)); - result = await UserProfileDBManager().profileNameExists("testuser"); + // Add a profile with a name that already exists + result = await UserProfileDBManager().addProfile( + UserProfile( + name: "Test Profile", + username: "xyz", + password: "hunter42", + ) + ); + + expect(result, equals(false)); + + // Check that the number of protocols available is still the same + var profiles = await UserProfileDBManager().getAllProfiles(); + + expect(profiles.length, equals(1)); + }); + + test("Profile Name Check", () async { + bool result = await UserProfileDBManager().profileNameExists("doesnotexist"); + expect(result, equals(false)); + + result = await UserProfileDBManager().profileNameExists("Test Profile"); expect(result, equals(true)); }); @@ -46,6 +98,7 @@ void main() { expect(prf, isNot(null)); + expect(prf?.name, equals("Test Profile")); expect(prf?.username, equals("testuser")); expect(prf?.password, equals("testpassword")); expect(prf?.server, equals("http://localhost:12345")); From b8d413027032a1e15d6293393a4bd2818c62f093 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 00:30:56 +1000 Subject: [PATCH 13/38] Adds tests for the "updateProfile" function --- test/api_test.dart | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/test/api_test.dart b/test/api_test.dart index 928c4028..9c28f716 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -92,16 +92,31 @@ void main() { expect(result, equals(true)); }); - // Ensure that we can select a user profile test("Select Profile", () async { + // Ensure that we can select a user profile final prf = await UserProfileDBManager().getSelectedProfile(); expect(prf, isNot(null)); - expect(prf?.name, equals("Test Profile")); - expect(prf?.username, equals("testuser")); - expect(prf?.password, equals("testpassword")); - expect(prf?.server, equals("http://localhost:12345")); + if (prf != null) { + UserProfile p = prf; + + expect(p.name, equals("Test Profile")); + expect(p.username, equals("testuser")); + expect(p.password, equals("testpassword")); + expect(p.server, equals("http://localhost:12345")); + + // Test that we can update the profile + p.name = "different name"; + + bool result = await UserProfileDBManager().updateProfile(p); + expect(result, equals(true)); + + // Trying to update with an invalid value will fail! + p.password = ""; + result = await UserProfileDBManager().updateProfile(p); + expect(result, equals(false)); + } }); }); From 80d898e212f80474d70b0d694a90aa8e78325483 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 00:39:05 +1000 Subject: [PATCH 14/38] Add github token secret --- .github/workflows/ci.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 90b2df32..184740cb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,6 +10,9 @@ on: branches: - master +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + jobs: test: From 237a7da54a293cd4d283ad9253e1c219b7e971ee Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 00:44:07 +1000 Subject: [PATCH 15/38] Add .coveragerc file --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..8c4a4b78 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +source = ./lib \ No newline at end of file From 6ef95499b78e919546707696680c13a6651aa3b8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 19:27:44 +1000 Subject: [PATCH 16/38] Add genhtml step --- .github/workflows/ci.yaml | 2 + lib/app_settings.dart | 68 ---------------------------------- lib/preferences.dart | 76 ++++++++++++++++++++++++++++---------- test/preferences_test.dart | 21 +++++++++++ 4 files changed, 80 insertions(+), 87 deletions(-) delete mode 100644 lib/app_settings.dart create mode 100644 test/preferences_test.dart diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 184740cb..dbf1d5be 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,6 +40,8 @@ jobs: - run: flutter analyze - name: Run Unit Tests run: | + apt-get install lcov pip install -Ur requirements.txt flutter test --coverage + genhtml coverage/lcov.info -o coverage/html coveralls \ No newline at end of file diff --git a/lib/app_settings.dart b/lib/app_settings.dart deleted file mode 100644 index e009b784..00000000 --- a/lib/app_settings.dart +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Class for managing app-level configuration options - */ - -import "package:sembast/sembast.dart"; -import "package:inventree/preferences.dart"; - -// Settings key values -const String INV_HOME_SHOW_SUBSCRIBED = "homeShowSubscribed"; -const String INV_HOME_SHOW_PO = "homeShowPo"; -const String INV_HOME_SHOW_MANUFACTURERS = "homeShowManufacturers"; -const String INV_HOME_SHOW_CUSTOMERS = "homeShowCustomers"; -const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers"; - -const String INV_SOUNDS_BARCODE = "barcodeSounds"; -const String INV_SOUNDS_SERVER = "serverSounds"; - -const String INV_PART_SUBCATEGORY = "partSubcategory"; - -const String INV_STOCK_SUBLOCATION = "stockSublocation"; -const String INV_STOCK_SHOW_HISTORY = "stockShowHistory"; - -const String INV_REPORT_ERRORS = "reportErrors"; - -const String INV_STRICT_HTTPS = "strictHttps"; - -class InvenTreeSettingsManager { - - factory InvenTreeSettingsManager() { - return _manager; - } - - InvenTreeSettingsManager._internal(); - - final store = StoreRef("settings"); - - Future get _db async => InvenTreePreferencesDB.instance.database; - - Future getValue(String key, dynamic backup) async { - - final value = await store.record(key).get(await _db); - - if (value == null) { - return backup; - } - - return value; - } - - // Load a boolean setting - Future getBool(String key, bool backup) async { - final dynamic value = await getValue(key, backup); - - if (value is bool) { - return value; - } else { - return backup; - } - } - - Future setValue(String key, dynamic value) async { - - await store.record(key).put(await _db, value); - } - - // Ensure we only ever create a single instance of this class - static final InvenTreeSettingsManager _manager = InvenTreeSettingsManager._internal(); -} diff --git a/lib/preferences.dart b/lib/preferences.dart index 2c52a88d..961a16bc 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -6,6 +6,26 @@ import "package:sembast/sembast_io.dart"; import "package:path/path.dart"; +// Settings key values +const String INV_HOME_SHOW_SUBSCRIBED = "homeShowSubscribed"; +const String INV_HOME_SHOW_PO = "homeShowPo"; +const String INV_HOME_SHOW_MANUFACTURERS = "homeShowManufacturers"; +const String INV_HOME_SHOW_CUSTOMERS = "homeShowCustomers"; +const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers"; + +const String INV_SOUNDS_BARCODE = "barcodeSounds"; +const String INV_SOUNDS_SERVER = "serverSounds"; + +const String INV_PART_SUBCATEGORY = "partSubcategory"; + +const String INV_STOCK_SUBLOCATION = "stockSublocation"; +const String INV_STOCK_SHOW_HISTORY = "stockShowHistory"; + +const String INV_REPORT_ERRORS = "reportErrors"; + +const String INV_STRICT_HTTPS = "strictHttps"; + + /* * Class for storing InvenTree preferences in a NoSql DB */ @@ -54,32 +74,50 @@ class InvenTreePreferencesDB { } } -class InvenTreePreferences { - factory InvenTreePreferences() { - return _api; +/* + * InvenTree setings manager class. + * Provides functions for loading and saving settings, with provision for default values + */ +class InvenTreeSettingsManager { + + factory InvenTreeSettingsManager() { + return _manager; } - InvenTreePreferences._internal(); + InvenTreeSettingsManager._internal(); - /* The following settings are not stored to persistent storage, - * instead they are only used as "session preferences". - * They are kept here as a convenience only. - */ + final store = StoreRef("settings"); - // Expand subcategory list in PartCategory view - bool expandCategoryList = false; + Future get _db async => InvenTreePreferencesDB.instance.database; - // Expand part list in PartCategory view - bool expandPartList = true; + Future getValue(String key, dynamic backup) async { - // Expand sublocation list in StockLocation view - bool expandLocationList = false; + final value = await store.record(key).get(await _db); - // Expand item list in StockLocation view - bool expandStockList = true; + if (value == null) { + return backup; + } - // Ensure we only ever create a single instance of the preferences class - static final InvenTreePreferences _api = InvenTreePreferences._internal(); + return value; + } -} \ No newline at end of file + // Load a boolean setting + Future getBool(String key, bool backup) async { + final dynamic value = await getValue(key, backup); + + if (value is bool) { + return value; + } else { + return backup; + } + } + + Future setValue(String key, dynamic value) async { + + await store.record(key).put(await _db, value); + } + + // Ensure we only ever create a single instance of this class + static final InvenTreeSettingsManager _manager = InvenTreeSettingsManager._internal(); +} diff --git a/test/preferences_test.dart b/test/preferences_test.dart new file mode 100644 index 00000000..cca80185 --- /dev/null +++ b/test/preferences_test.dart @@ -0,0 +1,21 @@ +/* + * Unit tests for the preferences manager + */ + +import "package:test/test.dart"; +import "package:inventree/preferences.dart"; + +void main() { + + setUp(() async { + + }); + + group("Settings Tests:", () { + test("Default Values", () async { + // Boolean values + expect(await InvenTreeSettingsManager().getBool("test", false), equals(false)); + expect(await InvenTreeSettingsManager().getBool("test", true), equals(true)); + }); + }); +} \ No newline at end of file From a42c1dc8863932844fbb8445f76e88ca519a2359 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 19:30:23 +1000 Subject: [PATCH 17/38] Reduce CI actions --- .github/workflows/android.yaml | 3 --- .github/workflows/ci.yaml | 2 +- .github/workflows/ios.yaml | 3 --- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml index a9d49403..4199cf28 100644 --- a/.github/workflows/android.yaml +++ b/.github/workflows/android.yaml @@ -6,9 +6,6 @@ on: push: branches: - master - pull_request: - branches: - - master jobs: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dbf1d5be..d8b97092 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,4 +44,4 @@ jobs: pip install -Ur requirements.txt flutter test --coverage genhtml coverage/lcov.info -o coverage/html - coveralls \ No newline at end of file + coveralls diff --git a/.github/workflows/ios.yaml b/.github/workflows/ios.yaml index 039a6e9a..3dc6338c 100644 --- a/.github/workflows/ios.yaml +++ b/.github/workflows/ios.yaml @@ -6,9 +6,6 @@ on: push: branches: - master - pull_request: - branches: - - master jobs: From df4845044065e984b39d29c079a50fd259eb26dd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 19:33:01 +1000 Subject: [PATCH 18/38] Use coveralls github action --- .github/workflows/ci.yaml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d8b97092..962e9ae7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -35,13 +35,14 @@ jobs: run: | cd lib/l10n python3 collect_translations.py - - run: flutter pub get - - run: cp lib/dummy_dsn.dart lib/dsn.dart - - run: flutter analyze - name: Run Unit Tests run: | - apt-get install lcov - pip install -Ur requirements.txt + cp lib/dummy_dsn.dart lib/dsn.dart + flutter pub get + flutter analyze flutter test --coverage - genhtml coverage/lcov.info -o coverage/html - coveralls + + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} From b18dd9207979f4353e5132a9c3365ad880cd6b3a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 19:44:47 +1000 Subject: [PATCH 19/38] Fixes after preferences file refactor --- lib/api.dart | 2 +- lib/helpers.dart | 3 ++- lib/inventree/sentry.dart | 2 +- lib/settings/app_settings.dart | 2 +- lib/settings/home_settings.dart | 2 +- lib/widget/dialogs.dart | 8 ++++---- lib/widget/home.dart | 2 +- lib/widget/part_list.dart | 2 +- lib/widget/stock_detail.dart | 2 +- lib/widget/stock_list.dart | 2 +- 10 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 9994b655..99d005d4 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -6,7 +6,7 @@ import "package:flutter/foundation.dart"; import "package:http/http.dart" as http; import "package:intl/intl.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:open_file/open_file.dart"; import "package:cached_network_image/cached_network_image.dart"; diff --git a/lib/helpers.dart b/lib/helpers.dart index 524cd92a..85d550ae 100644 --- a/lib/helpers.dart +++ b/lib/helpers.dart @@ -8,7 +8,8 @@ */ import "package:audioplayers/audioplayers.dart"; -import "package:inventree/app_settings.dart"; + +import "package:inventree/preferences.dart"; String simpleNumberString(double number) { // Ref: https://stackoverflow.com/questions/55152175/how-to-remove-trailing-zeros-using-dart diff --git a/lib/inventree/sentry.dart b/lib/inventree/sentry.dart index 429a50af..cd05a14f 100644 --- a/lib/inventree/sentry.dart +++ b/lib/inventree/sentry.dart @@ -1,7 +1,7 @@ import "dart:io"; import "package:device_info_plus/device_info_plus.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:package_info_plus/package_info_plus.dart"; import "package:sentry_flutter/sentry_flutter.dart"; diff --git a/lib/settings/app_settings.dart b/lib/settings/app_settings.dart index 124246d1..f322bf7c 100644 --- a/lib/settings/app_settings.dart +++ b/lib/settings/app_settings.dart @@ -3,7 +3,7 @@ import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/l10.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; class InvenTreeAppSettingsWidget extends StatefulWidget { diff --git a/lib/settings/home_settings.dart b/lib/settings/home_settings.dart index c5776c65..13e19cd2 100644 --- a/lib/settings/home_settings.dart +++ b/lib/settings/home_settings.dart @@ -5,7 +5,7 @@ import "package:inventree/l10.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; class HomeScreenSettingsWidget extends StatefulWidget { @override diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index 357983b2..e6b3bfb7 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -1,12 +1,12 @@ - -import "package:inventree/app_settings.dart"; -import "package:inventree/widget/snacks.dart"; import "package:audioplayers/audioplayers.dart"; import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; -import "package:inventree/l10.dart"; import "package:one_context/one_context.dart"; +import "package:inventree/l10.dart"; +import "package:inventree/preferences.dart"; +import "package:inventree/widget/snacks.dart"; + Future confirmationDialog(String title, String text, {IconData icon = FontAwesomeIcons.questionCircle, String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async { String _accept = acceptText ?? L10().ok; diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 64c566de..290c8666 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -6,7 +6,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/barcode.dart"; import "package:inventree/l10.dart"; import "package:inventree/settings/login.dart"; diff --git a/lib/widget/part_list.dart b/lib/widget/part_list.dart index ca4d158e..528273f7 100644 --- a/lib/widget/part_list.dart +++ b/lib/widget/part_list.dart @@ -6,7 +6,7 @@ import "package:inventree/widget/paginator.dart"; import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/api.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/l10.dart"; diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index b0b3cdab..b742cfb2 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -22,7 +22,7 @@ import "package:inventree/l10.dart"; import "package:inventree/helpers.dart"; import "package:inventree/api.dart"; import "package:inventree/api_form.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; class StockDetailWidget extends StatefulWidget { diff --git a/lib/widget/stock_list.dart b/lib/widget/stock_list.dart index 09d0dc3a..90481c78 100644 --- a/lib/widget/stock_list.dart +++ b/lib/widget/stock_list.dart @@ -5,7 +5,7 @@ import "package:inventree/inventree/stock.dart"; import "package:inventree/widget/paginator.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/l10.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/widget/stock_detail.dart"; import "package:inventree/api.dart"; From 6b0fd2a7083f63e78f4cdca16219a2fdb1918e14 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 20:03:48 +1000 Subject: [PATCH 20/38] Add script to find and test all un-touched .dart files --- .github/workflows/ci.yaml | 1 + .gitignore | 3 +++ find_dart_files.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 find_dart_files.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 962e9ae7..14f63455 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,6 +38,7 @@ jobs: - name: Run Unit Tests run: | cp lib/dummy_dsn.dart lib/dsn.dart + python3 find_dart_files.py flutter pub get flutter analyze flutter test --coverage diff --git a/.gitignore b/.gitignore index 2f258989..a9ecf32f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ coverage/* +# This file is auto-generated as part of the CI process +test/test_touch_files.dart + # Sentry API key lib/dsn.dart diff --git a/find_dart_files.py b/find_dart_files.py new file mode 100644 index 00000000..338ed89d --- /dev/null +++ b/find_dart_files.py @@ -0,0 +1,35 @@ +""" +This script recursively finds any '.dart' files in the ./lib directory, +and generates a 'test' file which includes all these files. + +This is to ensure that *all* .dart files are included in test coverage. +By default, source files which are not touched by the unit tests are not included! + +Ref: https://github.com/flutter/flutter/issues/27997#issue-410722816 +""" + +from pathlib import Path + +if __name__ == '__main__': + + dart_files = Path('lib').rglob('*.dart') + + with open("test/test_touch_files.dart", "w") as f: + + f.write("// ignore_for_file: unused_import\n\n") + + for path in dart_files: + path = str(path) + # Remove leading 'lib\' text + path = path[4:] + path = path.replace('\\', '/') + f.write(f'import "package:inventree/{path}";\n') + + f.write("\n\n") + + f.write("// DO NOT EDIT THIS FILE - it has been auto-generated by 'find_dart_files.py'\n") + f.write("// It has been created to ensure that *all* source file are included in coverage data\n") + f.write("// Reference: https://github.com/flutter/flutter/issues/27997#issue-410722816\n\n") + + f.write("// Do not actually test anything!") + f.write("void main() {}\n") From 12828e47f92048cbf2baef5485e00c9b9b1a7668 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 20:12:01 +1000 Subject: [PATCH 21/38] Had commented out the line that actually did anything... --- find_dart_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/find_dart_files.py b/find_dart_files.py index 338ed89d..614c3d60 100644 --- a/find_dart_files.py +++ b/find_dart_files.py @@ -31,5 +31,5 @@ if __name__ == '__main__': f.write("// It has been created to ensure that *all* source file are included in coverage data\n") f.write("// Reference: https://github.com/flutter/flutter/issues/27997#issue-410722816\n\n") - f.write("// Do not actually test anything!") + f.write("// Do not actually test anything!\n") f.write("void main() {}\n") From 31325f4893465414d469679e565c09caa9b3c63f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 20:24:31 +1000 Subject: [PATCH 22/38] File name is apparently important... --- .coveragerc | 2 -- .gitignore | 2 +- find_dart_files.py | 7 ++++--- 3 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 8c4a4b78..00000000 --- a/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[run] -source = ./lib \ No newline at end of file diff --git a/.gitignore b/.gitignore index a9ecf32f..07101c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ coverage/* # This file is auto-generated as part of the CI process -test/test_touch_files.dart +test/coverage_helper_test.dart # Sentry API key lib/dsn.dart diff --git a/find_dart_files.py b/find_dart_files.py index 614c3d60..ca20e011 100644 --- a/find_dart_files.py +++ b/find_dart_files.py @@ -5,7 +5,7 @@ and generates a 'test' file which includes all these files. This is to ensure that *all* .dart files are included in test coverage. By default, source files which are not touched by the unit tests are not included! -Ref: https://github.com/flutter/flutter/issues/27997#issue-410722816 +Ref: https://github.com/flutter/flutter/issues/27997 """ from pathlib import Path @@ -14,7 +14,7 @@ if __name__ == '__main__': dart_files = Path('lib').rglob('*.dart') - with open("test/test_touch_files.dart", "w") as f: + with open("test/coverage_helper_test.dart", "w") as f: f.write("// ignore_for_file: unused_import\n\n") @@ -29,7 +29,8 @@ if __name__ == '__main__': f.write("// DO NOT EDIT THIS FILE - it has been auto-generated by 'find_dart_files.py'\n") f.write("// It has been created to ensure that *all* source file are included in coverage data\n") - f.write("// Reference: https://github.com/flutter/flutter/issues/27997#issue-410722816\n\n") + + f.write('import "package:test/test.dart";\n\n'); f.write("// Do not actually test anything!\n") f.write("void main() {}\n") From ee3b7502dc821f57d45fd5c0674d59edd6b6ba06 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 20:30:00 +1000 Subject: [PATCH 23/38] Skip some files --- find_dart_files.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/find_dart_files.py b/find_dart_files.py index ca20e011..c7397c6b 100644 --- a/find_dart_files.py +++ b/find_dart_files.py @@ -18,8 +18,18 @@ if __name__ == '__main__': f.write("// ignore_for_file: unused_import\n\n") + skips = [ + 'generated', + 'l10n', + 'dummy_dsn.dart', + ] + for path in dart_files: path = str(path) + + if any([s in path for s in skips]): + continue + # Remove leading 'lib\' text path = path[4:] path = path.replace('\\', '/') From 253a75129aa8351f63e9a4d580bfcea67b6c6c2e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 20:41:03 +1000 Subject: [PATCH 24/38] Extra tests --- test/preferences_test.dart | 9 +++++++++ test/{api_test.dart => user_profile_test.dart} | 2 ++ 2 files changed, 11 insertions(+) rename test/{api_test.dart => user_profile_test.dart} (96%) diff --git a/test/preferences_test.dart b/test/preferences_test.dart index cca80185..b402b7a9 100644 --- a/test/preferences_test.dart +++ b/test/preferences_test.dart @@ -16,6 +16,15 @@ void main() { // Boolean values expect(await InvenTreeSettingsManager().getBool("test", false), equals(false)); expect(await InvenTreeSettingsManager().getBool("test", true), equals(true)); + + // String values + expect(await InvenTreeSettingsManager().getValue("test", "x"), equals("x")); + }); + + test("Set value", () async { + await InvenTreeSettingsManager().setValue("abc", "xyz"); + + expect(await InvenTreeSettingsManager().getValue("abc", "123"), equals("xyz")); }); }); } \ No newline at end of file diff --git a/test/api_test.dart b/test/user_profile_test.dart similarity index 96% rename from test/api_test.dart rename to test/user_profile_test.dart index 9c28f716..5de1043e 100644 --- a/test/api_test.dart +++ b/test/user_profile_test.dart @@ -106,6 +106,8 @@ void main() { expect(p.password, equals("testpassword")); expect(p.server, equals("http://localhost:12345")); + expect(p.toString(), equals("<${p.key}> Test Profile : http://localhost:12345 - testuser:testpassword")); + // Test that we can update the profile p.name = "different name"; From 63dd081a1c760dbaf0add841e58853cc53609749 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 20:49:32 +1000 Subject: [PATCH 25/38] Test fix? --- test/user_profile_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/user_profile_test.dart b/test/user_profile_test.dart index 5de1043e..9b672ad5 100644 --- a/test/user_profile_test.dart +++ b/test/user_profile_test.dart @@ -15,7 +15,7 @@ void main() { var profiles = await UserProfileDBManager().getAllProfiles(); for (var prf in profiles) { - UserProfileDBManager().deleteProfile(prf); + await UserProfileDBManager().deleteProfile(prf); } // Check that there are *no* profiles in the database From 62b0fcbec5b7a04b65e82ca40d1c05839db338bc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 May 2022 21:03:43 +1000 Subject: [PATCH 26/38] Start InvenTree server --- .github/workflows/ci.yaml | 29 +++++++++++++++++++++++++---- find_dart_files.py | 2 +- lib/app_colors.dart | 2 -- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 14f63455..f3ae7c75 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,5 +1,3 @@ -# Run flutter linting checks - name: CI on: @@ -12,7 +10,16 @@ on: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + INVENTREE_DB_ENGINE: django.db.backends.sqlite3 + INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3 + INVENTREE_MEDIA_ROOT: ../test_inventree_media + INVENTREE_STATIC_ROOT: ../test_inventree_static + INVENTREE_ADMIN_USER: testuser + INVENTREE_ADMIN_PASSWORD: testpassword + INVENTREE_ADMIN_EMAIL: test@test.com + INVENTREE_PYTHON_TEST_SERVER: http://localhost:12345 + INVENTREE_PYTHON_TEST_USERNAME: testuser + INVENTREE_PYTHON_TEST_PASSWORD: testpassword jobs: test: @@ -35,12 +42,26 @@ jobs: run: | cd lib/l10n python3 collect_translations.py - - name: Run Unit Tests + - name: Static Analysis Tests run: | cp lib/dummy_dsn.dart lib/dsn.dart python3 find_dart_files.py flutter pub get flutter analyze + + - name: Start InvenTree Server + run: | + sudo apt-get install python3-dev python3-pip python3-venv python3-wheel g++ + pip3 install invoke + git clone --depth 1 https://github.com/inventree/inventree ./inventree_server + cd inventree_server + invoke install + invoke migrate + invoke import-fixtures + invoke server -a 127.0.0.1:12345 & + invoke wait + - name: Unit Tests + run: | flutter test --coverage - name: Coveralls diff --git a/find_dart_files.py b/find_dart_files.py index c7397c6b..a182daaa 100644 --- a/find_dart_files.py +++ b/find_dart_files.py @@ -21,7 +21,7 @@ if __name__ == '__main__': skips = [ 'generated', 'l10n', - 'dummy_dsn.dart', + 'dsn.dart', ] for path in dart_files: diff --git a/lib/app_colors.dart b/lib/app_colors.dart index 99d81384..2f0395f6 100644 --- a/lib/app_colors.dart +++ b/lib/app_colors.dart @@ -1,5 +1,3 @@ - - import "dart:ui"; const Color COLOR_GRAY = Color.fromRGBO(50, 50, 50, 1); From e424a3cf7bc736d732f04f21514b96ffb30cf1dc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 00:02:27 +1000 Subject: [PATCH 27/38] Start of unit tests for the actual API code --- lib/api.dart | 4 ++- lib/l10.dart | 14 ++++++----- lib/widget/snacks.dart | 18 ++++++-------- pubspec.lock | 33 +++++++++++++++++++------ pubspec.yaml | 30 +++-------------------- test/api_test.dart | 55 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 103 insertions(+), 51 deletions(-) create mode 100644 test/api_test.dart diff --git a/lib/api.dart b/lib/api.dart index 99d005d4..4d42be04 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -201,6 +201,8 @@ class InvenTreeAPI { // Authentication token (initially empty, must be requested) String _token = ""; + bool get hasToken => _token.isNotEmpty; + /* * Check server connection and display messages if not connected. * Useful as a precursor check before performing operations. @@ -278,7 +280,7 @@ class InvenTreeAPI { bool _connecting = false; bool isConnected() { - return profile != null && _connected && baseUrl.isNotEmpty && _token.isNotEmpty; + return profile != null && _connected && baseUrl.isNotEmpty && hasToken; } bool isConnecting() { diff --git a/lib/l10.dart b/lib/l10.dart index ce9f3199..2b44f962 100644 --- a/lib/l10.dart +++ b/lib/l10.dart @@ -7,16 +7,18 @@ import "package:flutter/material.dart"; // Shortcut function to reduce boilerplate! I18N L10() { - BuildContext? _ctx = OneContext().context; + if (OneContext.hasContext) { + BuildContext? _ctx = OneContext().context; - if (_ctx != null) { - I18N? i18n = I18N.of(_ctx); + if (_ctx != null) { + I18N? i18n = I18N.of(_ctx); - if (i18n != null) { - return i18n; + if (i18n != null) { + return i18n; + } } } // Fallback for "null" context - return I18NEn(); + return I18NEn(); } \ No newline at end of file diff --git a/lib/widget/snacks.dart b/lib/widget/snacks.dart index e6ee8644..f578debf 100644 --- a/lib/widget/snacks.dart +++ b/lib/widget/snacks.dart @@ -1,21 +1,19 @@ - -/* - * Display a snackbar with: - * - * a) Text on the left - * b) Icon on the right - * - * | Text | - */ - import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:one_context/one_context.dart"; import "package:inventree/l10.dart"; +/* + * Display a configurable 'snackbar' at the bottom of the screen + */ void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) { + // Escape quickly if we do not have context + if (!OneContext.hasContext) { + return; + } + BuildContext? context = OneContext().context; if (context != null) { diff --git a/pubspec.lock b/pubspec.lock index a418a5b4..0f7c12f7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "40.0.0" + version: "31.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "2.8.0" archive: dependency: transitive description: @@ -113,6 +113,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" clock: dependency: transitive description: @@ -140,7 +147,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.3.2" + version: "1.0.3" cross_file: dependency: transitive description: @@ -218,6 +225,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" ffi: dependency: transitive description: @@ -284,6 +298,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -839,21 +858,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.1" + version: "1.19.5" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.8" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.13" + version: "0.4.9" typed_data: dependency: transitive description: @@ -937,7 +956,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "8.3.0" + version: "7.5.0" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7489e150..59c3e664 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,7 +40,9 @@ dependencies: dev_dependencies: flutter_launcher_icons: ^0.9.0 lint: ^1.8.0 - test: ^1.21.0 + test: ^1.19.0 + flutter_test: + sdk: flutter flutter_icons: android: true @@ -63,29 +65,3 @@ flutter: - assets/sounds/barcode_scan.mp3 - assets/sounds/barcode_error.mp3 - assets/sounds/server_error.mp3 - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/test/api_test.dart b/test/api_test.dart new file mode 100644 index 00000000..0841e7e5 --- /dev/null +++ b/test/api_test.dart @@ -0,0 +1,55 @@ +/* + * Unit tests for the InvenTree API code + */ + +import "package:test/test.dart"; + +import "package:inventree/api.dart"; +import "package:inventree/user_profile.dart"; + + + +void main() { + + setUp(() async { + + // Create and select a profile to user + await UserProfileDBManager().addProfile(UserProfile( + name: "Test Profile", + server: "http://localhost:12345", + username: "testuser", + password: "testpassword", + selected: true, + )); + + }); + + group("Login Tests:", () { + + test("Disconnected", () async { + // Test that calling disconnect() does the right thing + var api = InvenTreeAPI(); + + api.disconnectFromServer(); + + // Check expected values + expect(api.isConnected(), equals(false)); + expect(api.isConnecting(), equals(false)); + expect(api.hasToken, equals(false)); + + }); + + test("Login Success", () async { + // Test that we can login to the server successfully + var api = InvenTreeAPI(); + + // Attempt to connect + final bool result = await api.connectToServer(); + + expect(result, equals(true)); + expect(api.hasToken, equals(true)); + + expect(api.baseUrl, equals("http://localhost:12345/")); + }); + }); +} \ No newline at end of file From c453aaaf8ac090fb2c6e8587fa1edb53826d19a3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 22 May 2022 06:39:34 +1000 Subject: [PATCH 28/38] New translations app_en.arb (Hungarian) --- lib/l10n/hu_HU/app_hu_HU.arb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/l10n/hu_HU/app_hu_HU.arb b/lib/l10n/hu_HU/app_hu_HU.arb index 4075b803..da164801 100644 --- a/lib/l10n/hu_HU/app_hu_HU.arb +++ b/lib/l10n/hu_HU/app_hu_HU.arb @@ -545,6 +545,8 @@ "@search": { "description": "search" }, + "searching": "Keresés", + "@searching": {}, "searchLocation": "Hely keresése", "@searchLocation": {}, "searchParts": "Alkatrészek keresése", From f13b04d02985bf668e17fd59672ffef3a203db30 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 08:23:20 +1000 Subject: [PATCH 29/38] Refactor audio file player - Do not play if there is no context available (e.g. unit testing) --- lib/api_form.dart | 2 +- lib/barcode.dart | 69 ++++++++++++++++++++++++------------ lib/helpers.dart | 32 ++++++++--------- lib/widget/dialogs.dart | 5 ++- lib/widget/stock_detail.dart | 3 +- pubspec.yaml | 4 +-- test/api_test.dart | 29 ++++++++++++++- 7 files changed, 94 insertions(+), 50 deletions(-) diff --git a/lib/api_form.dart b/lib/api_form.dart index 07221a1e..588658bc 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -335,7 +335,7 @@ class APIFormField { controller.text = hash; data["value"] = hash; - successTone(); + barcodeSuccessTone(); showSnackIcon( L10().barcodeAssigned, diff --git a/lib/barcode.dart b/lib/barcode.dart index 8781f2d1..85702e0e 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -11,15 +11,38 @@ import "package:qr_code_scanner/qr_code_scanner.dart"; import "package:inventree/inventree/stock.dart"; import "package:inventree/inventree/part.dart"; -import "package:inventree/l10.dart"; -import "package:inventree/helpers.dart"; import "package:inventree/api.dart"; +import "package:inventree/helpers.dart"; +import "package:inventree/l10.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/widget/location_display.dart"; import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/stock_detail.dart"; +/* + * Play an audible 'success' alert to the user. + */ +Future barcodeSuccessTone() async { + + final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; + + if (en) { + playAudioFile("sounds/barcode_scan.mp3"); + } +} + +Future barcodeFailureTone() async { + + final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; + + if (en) { + playAudioFile("sounds/barcode_error.mp3"); + } +} + + class BarcodeHandler { /* * Class which "handles" a barcode, by communicating with the InvenTree server, @@ -44,7 +67,7 @@ class BarcodeHandler { // Called when the server does not know about a barcode // Override this function - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeNoMatch, @@ -55,7 +78,7 @@ class BarcodeHandler { Future onBarcodeUnhandled(BuildContext context, Map data) async { - failureTone(); + barcodeFailureTone(); // Called when the server returns an unhandled response showServerError(L10().responseUnknown, data.toString()); @@ -125,7 +148,7 @@ class BarcodeScanHandler extends BarcodeHandler { @override Future onBarcodeUnknown(BuildContext context, Map data) async { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeNoMatch, @@ -146,7 +169,7 @@ class BarcodeScanHandler extends BarcodeHandler { if (pk > 0) { - successTone(); + barcodeSuccessTone(); InvenTreeStockLocation().get(pk).then((var loc) { if (loc is InvenTreeStockLocation) { @@ -156,7 +179,7 @@ class BarcodeScanHandler extends BarcodeHandler { }); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidStockLocation, @@ -170,7 +193,7 @@ class BarcodeScanHandler extends BarcodeHandler { if (pk > 0) { - successTone(); + barcodeSuccessTone(); InvenTreeStockItem().get(pk).then((var item) { @@ -183,7 +206,7 @@ class BarcodeScanHandler extends BarcodeHandler { }); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidStockItem, @@ -196,7 +219,7 @@ class BarcodeScanHandler extends BarcodeHandler { if (pk > 0) { - successTone(); + barcodeSuccessTone(); InvenTreePart().get(pk).then((var part) { @@ -209,7 +232,7 @@ class BarcodeScanHandler extends BarcodeHandler { }); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidPart, @@ -218,7 +241,7 @@ class BarcodeScanHandler extends BarcodeHandler { } } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeUnknown, @@ -275,7 +298,7 @@ class StockItemScanIntoLocationHandler extends BarcodeHandler { if (result) { - successTone(); + barcodeSuccessTone(); Navigator.of(context).pop(); @@ -285,7 +308,7 @@ class StockItemScanIntoLocationHandler extends BarcodeHandler { ); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeScanIntoLocationFailure, @@ -294,7 +317,7 @@ class StockItemScanIntoLocationHandler extends BarcodeHandler { } } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidStockLocation, @@ -329,14 +352,14 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { if (item == null) { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidStockItem, success: false, ); } else if (item.locationId == location.pk) { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().itemInLocation, @@ -347,7 +370,7 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { if (result) { - successTone(); + barcodeSuccessTone(); showSnackIcon( L10().barcodeScanIntoLocationSuccess, @@ -355,7 +378,7 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { ); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeScanIntoLocationFailure, @@ -365,7 +388,7 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { } } else { - failureTone(); + barcodeFailureTone(); // Does not match a valid stock item! showSnackIcon( @@ -401,7 +424,7 @@ class UniqueBarcodeHandler extends BarcodeHandler { @override Future onBarcodeMatched(BuildContext context, Map data) async { - failureTone(); + barcodeFailureTone(); // If the barcode is known, we can"t assign it to the stock item! showSnackIcon( @@ -424,7 +447,7 @@ class UniqueBarcodeHandler extends BarcodeHandler { String hash = (data["hash"] ?? "") as String; if (hash.isEmpty) { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeError, @@ -432,7 +455,7 @@ class UniqueBarcodeHandler extends BarcodeHandler { ); } else { - successTone(); + barcodeSuccessTone(); // Close the barcode scanner Navigator.of(context).pop(); diff --git a/lib/helpers.dart b/lib/helpers.dart index 85d550ae..2bdbed53 100644 --- a/lib/helpers.dart +++ b/lib/helpers.dart @@ -8,8 +8,7 @@ */ import "package:audioplayers/audioplayers.dart"; - -import "package:inventree/preferences.dart"; +import "package:one_context/one_context.dart"; String simpleNumberString(double number) { // Ref: https://stackoverflow.com/questions/55152175/how-to-remove-trailing-zeros-using-dart @@ -17,22 +16,19 @@ String simpleNumberString(double number) { return number.toStringAsFixed(number.truncateToDouble() == number ? 0 : 1); } -Future successTone() async { +/* + * Play an audio file from the requested path. + * + * Note: If OneContext module fails the 'hasConext' check, + * we will not attempt to play the sound + */ +Future playAudioFile(String path) async { - final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; - - if (en) { - final player = AudioCache(); - player.play("sounds/barcode_scan.mp3"); + if (!OneContext.hasContext) { + return; } + + final player = AudioCache(); + player.play(path); + } - -Future failureTone() async { - - final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; - - if (en) { - final player = AudioCache(); - player.play("sounds/barcode_error.mp3"); - } -} \ No newline at end of file diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index e6b3bfb7..5fbe20b1 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -1,6 +1,6 @@ -import "package:audioplayers/audioplayers.dart"; import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; +import "package:inventree/helpers.dart"; import "package:one_context/one_context.dart"; import "package:inventree/l10.dart"; @@ -108,8 +108,7 @@ Future showServerError(String title, String description) async { final bool tones = await InvenTreeSettingsManager().getValue(INV_SOUNDS_SERVER, true) as bool; if (tones) { - final player = AudioCache(); - player.play("sounds/server_error.mp3"); + playAudioFile("sounds/server_error.mp3"); } showSnackIcon( diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index b742cfb2..50107797 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -19,7 +19,6 @@ import "package:inventree/widget/stock_item_history.dart"; import "package:inventree/widget/stock_item_test_results.dart"; import "package:inventree/widget/stock_notes.dart"; import "package:inventree/l10.dart"; -import "package:inventree/helpers.dart"; import "package:inventree/api.dart"; import "package:inventree/api_form.dart"; import "package:inventree/preferences.dart"; @@ -1008,7 +1007,7 @@ class _StockItemDisplayState extends RefreshableState { } ).then((result) { if (result) { - successTone(); + barcodeSuccessTone(); showSnackIcon( L10().barcodeAssigned, diff --git a/pubspec.yaml b/pubspec.yaml index 59c3e664..33932d52 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,10 +39,10 @@ dependencies: dev_dependencies: flutter_launcher_icons: ^0.9.0 - lint: ^1.8.0 - test: ^1.19.0 flutter_test: sdk: flutter + lint: ^1.8.0 + test: ^1.19.0 flutter_icons: android: true diff --git a/test/api_test.dart b/test/api_test.dart index 0841e7e5..4631c5ca 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -39,6 +39,30 @@ void main() { }); + test("Login Failure", () async { + // Tests for various types of login failures + var api = InvenTreeAPI(); + + // Incorrect server address + var profile = await UserProfileDBManager().getSelectedProfile(); + + assert(profile != null); + + if (profile != null) { + profile.server = "http://localhost:5555"; + await UserProfileDBManager().updateProfile(profile); + } + + bool result = await api.connectToServer(); + + assert(!result); + + // TODO: Test the the right 'error message' is returned + + // TODO: Test incorrect login details + + }); + test("Login Success", () async { // Test that we can login to the server successfully var api = InvenTreeAPI(); @@ -46,10 +70,13 @@ void main() { // Attempt to connect final bool result = await api.connectToServer(); + // Check expected values expect(result, equals(true)); expect(api.hasToken, equals(true)); - expect(api.baseUrl, equals("http://localhost:12345/")); + + expect(api.isConnected(), equals(true)); + expect(api.isConnecting(), equals(false)); }); }); } \ No newline at end of file From fc911ea5b5932ab8b89b6d90e964d964765b79ec Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 08:27:01 +1000 Subject: [PATCH 30/38] Server connection check passes now --- test/api_test.dart | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/test/api_test.dart b/test/api_test.dart index 4631c5ca..8a4195a2 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -10,17 +10,32 @@ import "package:inventree/user_profile.dart"; void main() { - + setUp(() async { - - // Create and select a profile to user - await UserProfileDBManager().addProfile(UserProfile( - name: "Test Profile", - server: "http://localhost:12345", - username: "testuser", - password: "testpassword", - selected: true, - )); + + if (! await UserProfileDBManager().profileNameExists("Test Profile")) { + // Create and select a profile to user + await UserProfileDBManager().addProfile(UserProfile( + name: "Test Profile", + server: "http://localhost:12345", + username: "testuser", + password: "testpassword", + selected: true, + )); + } + + var prf = await UserProfileDBManager().getSelectedProfile(); + + // Ensure that the server settings are correct by default, + // as they can get overwritten by subsequent tests + + if (prf != null) { + prf.server = "http://localhost:12345"; + prf.username = "testuser"; + prf.password = "testpassword"; + + await UserProfileDBManager().updateProfile(prf); + } }); From cdeac137bf8bebf3130607bf4fa317041ced1af7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 08:38:34 +1000 Subject: [PATCH 31/38] Improved API connection testing --- test/api_test.dart | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/test/api_test.dart b/test/api_test.dart index 8a4195a2..3ede3504 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -66,15 +66,28 @@ void main() { if (profile != null) { profile.server = "http://localhost:5555"; await UserProfileDBManager().updateProfile(profile); + + bool result = await api.connectToServer(); + assert(!result); + + // TODO: Test the the right 'error message' is returned + // TODO: The request above should throw a 'SockeException' + + // Test incorrect login details + profile.server = "http://localhost:12345"; + profile.username = "invalidusername"; + + await UserProfileDBManager().updateProfile(profile); + + await api.connectToServer(); + assert(!result); + + // TODO: Test that the connection attempt above throws an authentication error + } else { + assert(false); } - bool result = await api.connectToServer(); - assert(!result); - - // TODO: Test the the right 'error message' is returned - - // TODO: Test incorrect login details }); From 8e1804b39dbf0e5a4cd9f8c572143bf4ccc684e1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 08:48:46 +1000 Subject: [PATCH 32/38] Debug --- test/api_test.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/api_test.dart b/test/api_test.dart index 3ede3504..83ff2e37 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -15,6 +15,9 @@ void main() { if (! await UserProfileDBManager().profileNameExists("Test Profile")) { // Create and select a profile to user + + print("TEST: Creating profile for user 'testuser'"); + await UserProfileDBManager().addProfile(UserProfile( name: "Test Profile", server: "http://localhost:12345", @@ -30,6 +33,7 @@ void main() { // as they can get overwritten by subsequent tests if (prf != null) { + prf.name = "Test Profile"; prf.server = "http://localhost:12345"; prf.username = "testuser"; prf.password = "testpassword"; @@ -72,7 +76,7 @@ void main() { // TODO: Test the the right 'error message' is returned // TODO: The request above should throw a 'SockeException' - + // Test incorrect login details profile.server = "http://localhost:12345"; profile.username = "invalidusername"; From 2e86a02343091c32079566a52299a0dfb94d76fb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 09:00:09 +1000 Subject: [PATCH 33/38] more debug --- lib/user_profile.dart | 4 ++++ test/api_test.dart | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/user_profile.dart b/lib/user_profile.dart index 507d9f07..05c98a82 100644 --- a/lib/user_profile.dart +++ b/lib/user_profile.dart @@ -151,8 +151,12 @@ class UserProfileDBManager { final profiles = await store.find(await _db); + print("getSelectedProfile() - ${profiles.length} profiles available - selected = ${selected}"); + for (int idx = 0; idx < profiles.length; idx++) { + print("- Checking ${idx} - key = ${profiles[idx].key} - ${profiles[idx].value.toString()}"); + if (profiles[idx].key is int && profiles[idx].key == selected) { return UserProfile.fromJson( profiles[idx].key as int, diff --git a/test/api_test.dart b/test/api_test.dart index 83ff2e37..4553d338 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -76,7 +76,7 @@ void main() { // TODO: Test the the right 'error message' is returned // TODO: The request above should throw a 'SockeException' - + // Test incorrect login details profile.server = "http://localhost:12345"; profile.username = "invalidusername"; From 625d29fcf1ecef8b352a5cdcb2fe63d5b0243035 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 09:13:49 +1000 Subject: [PATCH 34/38] Adds debug message helper --- lib/api.dart | 1 + lib/helpers.dart | 16 +++++++++++++++- lib/user_profile.dart | 11 ++++++----- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 4d42be04..34dfb43b 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -16,6 +16,7 @@ import "package:flutter_cache_manager/flutter_cache_manager.dart"; import "package:inventree/widget/dialogs.dart"; import "package:inventree/l10.dart"; +import "package:inventree/helpers.dart"; import "package:inventree/inventree/sentry.dart"; import "package:inventree/inventree/model.dart"; import "package:inventree/user_profile.dart"; diff --git a/lib/helpers.dart b/lib/helpers.dart index 2bdbed53..24dc6e84 100644 --- a/lib/helpers.dart +++ b/lib/helpers.dart @@ -7,9 +7,23 @@ * supressing trailing zeroes */ +import "dart:io"; + import "package:audioplayers/audioplayers.dart"; import "package:one_context/one_context.dart"; + +/* + * Display a debug message if we are in testing mode, or running in debug mode + */ +void debug(dynamic msg) { + + if (Platform.environment.containsKey("FLUTTER_TEST")) { + print("DEBUG: ${msg.toString()}"); + } +} + + String simpleNumberString(double number) { // Ref: https://stackoverflow.com/questions/55152175/how-to-remove-trailing-zeros-using-dart @@ -19,7 +33,7 @@ String simpleNumberString(double number) { /* * Play an audio file from the requested path. * - * Note: If OneContext module fails the 'hasConext' check, + * Note: If OneContext module fails the 'hasContext' check, * we will not attempt to play the sound */ Future playAudioFile(String path) async { diff --git a/lib/user_profile.dart b/lib/user_profile.dart index 05c98a82..2d4a474c 100644 --- a/lib/user_profile.dart +++ b/lib/user_profile.dart @@ -1,6 +1,7 @@ import "package:sembast/sembast.dart"; +import "package:inventree/helpers.dart"; import "package:inventree/preferences.dart"; class UserProfile { @@ -85,7 +86,7 @@ class UserProfileDBManager { Future addProfile(UserProfile profile) async { if (profile.name.isEmpty || profile.username.isEmpty || profile.password.isEmpty) { - print("Profile missing required values - not adding to database"); + debug("addProfile() : Profile missing required values - not adding to database"); return false; } @@ -93,7 +94,7 @@ class UserProfileDBManager { final bool exists = await profileNameExists(profile.name); if (exists) { - print("UserProfile '${profile.name}' already exists"); + debug("addProfile() : UserProfile '${profile.name}' already exists"); return false; } @@ -120,7 +121,7 @@ class UserProfileDBManager { // Prevent invalid profile data from being updated if (profile.name.isEmpty || profile.username.isEmpty || profile.password.isEmpty) { - print("Profile missing required values - not updating"); + debug("updateProfile() : Profile missing required values - not updating"); return false; } @@ -151,11 +152,11 @@ class UserProfileDBManager { final profiles = await store.find(await _db); - print("getSelectedProfile() - ${profiles.length} profiles available - selected = ${selected}"); + debug("getSelectedProfile() : ${profiles.length} profiles available - selected = ${selected}"); for (int idx = 0; idx < profiles.length; idx++) { - print("- Checking ${idx} - key = ${profiles[idx].key} - ${profiles[idx].value.toString()}"); + debug("- Checking ${idx} - key = ${profiles[idx].key} - ${profiles[idx].value.toString()}"); if (profiles[idx].key is int && profiles[idx].key == selected) { return UserProfile.fromJson( From 4e14bd077c3dbdf5ac07e56c9bcc62d2b001e95f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 09:17:32 +1000 Subject: [PATCH 35/38] Improved debug messages --- lib/api.dart | 16 +++++++--------- lib/preferences.dart | 5 ----- lib/settings/login.dart | 1 - 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 34dfb43b..e098a523 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -341,7 +341,7 @@ class InvenTreeAPI { // Clear the list of available plugins _plugins.clear(); - print("Connecting to ${apiUrl} -> username=${username}"); + debug("Connecting to ${apiUrl} -> username=${username}"); APIResponse response; @@ -434,7 +434,7 @@ class InvenTreeAPI { // Return the received token _token = (data["token"] ?? "") as String; - print("Received token - $_token"); + debug("Received token from server"); // Request user role information (async) getUserRoles(); @@ -448,7 +448,7 @@ class InvenTreeAPI { } void disconnectFromServer() { - print("InvenTreeAPI().disconnectFromServer()"); + debug("API : disconnectFromServer()"); _connected = false; _connecting = false; @@ -504,7 +504,7 @@ class InvenTreeAPI { roles.clear(); - print("Requesting user role data"); + debug("API: Requesting user role data"); // Next we request the permissions assigned to the current user // Note: 2021-02-27 this "roles" feature for the API was just introduced. @@ -534,7 +534,7 @@ class InvenTreeAPI { return; } - print("Requesting plugin information"); + debug("API: getPluginInformation()"); // Request a list of plugins from the server final List results = await InvenTreePlugin().list(); @@ -664,7 +664,7 @@ class InvenTreeAPI { _request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale()); } on SocketException catch (error) { - print("SocketException at ${url}: ${error.toString()}"); + debug("SocketException at ${url}: ${error.toString()}"); showServerError(L10().connectionRefused, error.toString()); return; } on TimeoutException { @@ -673,7 +673,7 @@ class InvenTreeAPI { return; } on HandshakeException catch (error) { print("HandshakeException at ${url}:"); - print(error.toString()); + debug(error.toString()); showServerError(L10().serverCertificateError, error.toString()); return; } catch (error, stackTrace) { @@ -1236,8 +1236,6 @@ class InvenTreeAPI { var plugins = getPlugins(mixin: "locate"); - print("locateItemOrLocation"); - if (plugins.isEmpty) { // TODO: Error message return; diff --git a/lib/preferences.dart b/lib/preferences.dart index 961a16bc..b804cbd9 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -60,16 +60,11 @@ class InvenTreePreferencesDB { // Get a platform-specific directory where persistent app data can be stored final appDocumentDir = await getApplicationDocumentsDirectory(); - print("Documents Dir: ${appDocumentDir.toString()}"); - - print("Path: ${appDocumentDir.path}"); - // Path with the form: /platform-specific-directory/demo.db final dbPath = join(appDocumentDir.path, "InvenTreeSettings.db"); final database = await databaseFactoryIo.openDatabase(dbPath); - // Any code awaiting the Completer's future will now start executing _dbOpenCompleter.complete(database); } } diff --git a/lib/settings/login.dart b/lib/settings/login.dart index a998af41..e61c08b4 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -344,7 +344,6 @@ class _ProfileEditState extends State { Uri uri = Uri.parse(value); if (uri.hasScheme) { - print("Scheme: ${uri.scheme}"); if (!["http", "https"].contains(uri.scheme.toLowerCase())) { return L10().serverStart; } From 53b69d9623e5754200a273b7d85bee247dfb1979 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 09:29:04 +1000 Subject: [PATCH 36/38] Improvements for profile management --- lib/user_profile.dart | 35 ++++++++++++++++++++++++++++------- test/api_test.dart | 4 ++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/lib/user_profile.dart b/lib/user_profile.dart index 2d4a474c..9eff1edf 100644 --- a/lib/user_profile.dart +++ b/lib/user_profile.dart @@ -106,13 +106,6 @@ class UserProfileDBManager { return true; } - /* - * Mark the particular profile as selected - */ - Future selectProfile(int key) async { - await store.record("selected").put(await _db, key); - } - /* * Update the selected profile in the database. * The unique integer is used to determine if the profile already exists. @@ -196,4 +189,32 @@ class UserProfileDBManager { return profileList; } + + /* + * Mark the particular profile as selected + */ + Future selectProfile(int key) async { + await store.record("selected").put(await _db, key); + } + + /* + * Look-up and select a profile by name. + * Return true if the profile was selected + */ + Future selectProfileByName(String name) async { + var profiles = await getAllProfiles(); + + for (var prf in profiles) { + if (prf.name == name) { + int key = prf.key ?? -1; + + if (key >= 0) { + await selectProfile(key); + return true; + } + } + } + + return false; + } } diff --git a/test/api_test.dart b/test/api_test.dart index 4553d338..2ed41619 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -41,6 +41,10 @@ void main() { await UserProfileDBManager().updateProfile(prf); } + // Ensure the profile is selected + assert(! await UserProfileDBManager().selectProfileByName("Missing Profile")); + assert(await UserProfileDBManager().selectProfileByName("Test Profile")); + }); group("Login Tests:", () { From b98f044204b6c6c8546098c21de714a8033c5645 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 09:56:22 +1000 Subject: [PATCH 37/38] More checks --- lib/api.dart | 10 +++----- lib/inventree/stock.dart | 4 +-- lib/widget/drawer.dart | 4 +-- lib/widget/home.dart | 16 ++++++------ lib/widget/purchase_order_detail.dart | 2 +- lib/widget/stock_detail.dart | 8 +++--- test/api_test.dart | 36 ++++++++++++++++++++++----- 7 files changed, 50 insertions(+), 30 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index e098a523..52bd29a1 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -208,7 +208,7 @@ class InvenTreeAPI { * Check server connection and display messages if not connected. * Useful as a precursor check before performing operations. */ - bool checkConnection(BuildContext context) { + bool checkConnection() { // Firstly, is the server connected? if (!isConnected()) { @@ -292,14 +292,10 @@ class InvenTreeAPI { static final InvenTreeAPI _api = InvenTreeAPI._internal(); // API endpoint for receiving purchase order line items was introduced in v12 - bool supportPoReceive() { - return apiVersion >= 12; - } + bool get supportsPoReceive => apiVersion >= 12; // "Modern" API transactions were implemented in API v14 - bool supportModernStockTransactions() { - return apiVersion >= 14; - } + bool get supportsModernStockTransactions => apiVersion >= 14; /* * Connect to the remote InvenTree server: diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 3dc4f2b1..2bb741f2 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -533,7 +533,7 @@ class InvenTreeStockItem extends InvenTreeModel { Map data = {}; // Note: Format of adjustment API was updated in API v14 - if (api.supportModernStockTransactions()) { + if (api.supportsModernStockTransactions) { // Modern (> 14) API data = { "items": [ @@ -560,7 +560,7 @@ class InvenTreeStockItem extends InvenTreeModel { } // Expected API return code depends on server API version - final int expected_response = api.supportModernStockTransactions() ? 201 : 200; + final int expected_response = api.supportsModernStockTransactions ? 201 : 200; var response = await api.post( endpoint, diff --git a/lib/widget/drawer.dart b/lib/widget/drawer.dart index 85a30259..cff24de8 100644 --- a/lib/widget/drawer.dart +++ b/lib/widget/drawer.dart @@ -34,7 +34,7 @@ class InvenTreeDrawer extends StatelessWidget { void _search() { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; _closeDrawer(); @@ -51,7 +51,7 @@ class InvenTreeDrawer extends StatelessWidget { * Upon successful scan, data are passed off to be decoded. */ Future _scan() async { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; _closeDrawer(); scanQrCode(context); diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 290c8666..19f1c953 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -71,13 +71,13 @@ class _InvenTreeHomePageState extends State { UserProfile? _profile; void _scan(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; scanQrCode(context); } void _showParts(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); } @@ -87,7 +87,7 @@ class _InvenTreeHomePageState extends State { } void _showStarredParts(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push( context, @@ -100,13 +100,13 @@ class _InvenTreeHomePageState extends State { } void _showStock(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); } void _showPurchaseOrders(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push( context, @@ -118,19 +118,19 @@ class _InvenTreeHomePageState extends State { /* void _showSuppliers(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().suppliers, {"is_supplier": "true"}))); } void _showManufacturers(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().manufacturers, {"is_manufacturer": "true"}))); } void _showCustomers(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().customers, {"is_customer": "true"}))); } diff --git a/lib/widget/purchase_order_detail.dart b/lib/widget/purchase_order_detail.dart index 27486969..2abe56c6 100644 --- a/lib/widget/purchase_order_detail.dart +++ b/lib/widget/purchase_order_detail.dart @@ -247,7 +247,7 @@ class _PurchaseOrderDetailState extends RefreshableState { Future _addStockDialog() async { // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportModernStockTransactions()) { + if (InvenTreeAPI().supportsModernStockTransactions) { Map fields = { "pk": { @@ -391,7 +391,7 @@ class _StockItemDisplayState extends RefreshableState { void _removeStockDialog() { // TODO: In future, deprecate support for the older API - if (InvenTreeAPI().supportModernStockTransactions()) { + if (InvenTreeAPI().supportsModernStockTransactions) { Map fields = { "pk": { "parent": "items", @@ -463,7 +463,7 @@ class _StockItemDisplayState extends RefreshableState { Future _countStockDialog() async { // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportModernStockTransactions()) { + if (InvenTreeAPI().supportsModernStockTransactions) { Map fields = { "pk": { @@ -566,7 +566,7 @@ class _StockItemDisplayState extends RefreshableState { Future _transferStockDialog(BuildContext context) async { // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportModernStockTransactions()) { + if (InvenTreeAPI().supportsModernStockTransactions) { Map fields = { "pk": { diff --git a/test/api_test.dart b/test/api_test.dart index 2ed41619..113ab4d3 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -5,6 +5,7 @@ import "package:test/test.dart"; import "package:inventree/api.dart"; +import "package:inventree/helpers.dart"; import "package:inventree/user_profile.dart"; @@ -91,12 +92,13 @@ void main() { assert(!result); // TODO: Test that the connection attempt above throws an authentication error + + assert(!api.checkConnection()); + } else { assert(false); } - - }); test("Login Success", () async { @@ -107,12 +109,34 @@ void main() { final bool result = await api.connectToServer(); // Check expected values - expect(result, equals(true)); - expect(api.hasToken, equals(true)); + assert(result); + assert(api.hasToken); expect(api.baseUrl, equals("http://localhost:12345/")); - expect(api.isConnected(), equals(true)); - expect(api.isConnecting(), equals(false)); + assert(api.isConnected()); + assert(!api.isConnecting()); + assert(api.checkConnection()); }); + + test("Version Checks", () async { + // Test server version information + var api = InvenTreeAPI(); + + assert(await api.connectToServer()); + + // Check supported functions + assert(api.apiVersion >= 50); + assert(api.supportsSettings); + assert(api.supportsNotifications); + assert(api.supportsModernStockTransactions); + assert(api.supportsPoReceive); + + // Check available permissions + assert(api.checkPermission("part", "change")); + assert(api.checkPermission("stocklocation", "delete")); + assert(api.checkPermission("part", "weirdpermission")); + assert(api.checkPermission("blah", "bloo")); + }); + }); } \ No newline at end of file From 2cd73a98e8b7aa319a5c70ed5b57ecccdab8f536 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 10:03:56 +1000 Subject: [PATCH 38/38] Remove unused import --- test/api_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/api_test.dart b/test/api_test.dart index 113ab4d3..b46cab64 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -5,7 +5,6 @@ import "package:test/test.dart"; import "package:inventree/api.dart"; -import "package:inventree/helpers.dart"; import "package:inventree/user_profile.dart";