From 33bb6148de9f6fe9411c81313406dfb9174e05da Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 9 Feb 2021 21:38:50 +1100 Subject: [PATCH] Cleanup for API error handling Ref: https://stackoverflow.com/questions/54617432/looking-up-a-deactivated-widgets-ancestor-is-unsafe --- lib/api.dart | 75 +++++++++-------- lib/barcode.dart | 13 +-- lib/inventree/model.dart | 139 +++++++++++++------------------ lib/inventree/part.dart | 11 ++- lib/inventree/stock.dart | 4 +- lib/l10n | 2 +- lib/settings/login.dart | 9 +- lib/widget/category_display.dart | 2 +- lib/widget/dialogs.dart | 67 +++++++++------ lib/widget/home.dart | 5 +- lib/widget/part_detail.dart | 2 +- 11 files changed, 162 insertions(+), 167 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index f50f65e9..55e2b6b5 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -68,15 +68,6 @@ class InvenTreeAPI { static const _URL_GET_TOKEN = "user/token/"; static const _URL_GET_VERSION = ""; - Future showServerError(BuildContext context, String description) async { - showErrorDialog( - context, - I18N.of(context).serverError, - description, - icon: FontAwesomeIcons.server - ); - } - // Base URL for InvenTree API e.g. http://192.168.120.10:8000 String _BASE_URL = ""; @@ -209,18 +200,18 @@ class InvenTreeAPI { print("Connecting to ${apiUrl} -> ${username}:${password}"); - var response = await get("").timeout(Duration(seconds: 5)).catchError((error) { + var response = await get("").timeout(Duration(seconds: 10)).catchError((error) { print("Error connecting to server: ${error.toString()}"); if (error is SocketException) { - print("Error: socket exception: ${error.toString()}"); - // TODO - Display error dialog!! - //showServerError(context, "Connection Refused"); + showServerError( + context, + I18N.of(context).connectionRefused, + error.toString()); return null; } else if (error is TimeoutException) { - // TODO - Display timeout dialog here - //showTimeoutDialog(context); + showTimeoutError(context); return null; } else { // Unknown error type - re-throw the error and Sentry will catch it @@ -236,8 +227,9 @@ class InvenTreeAPI { if (response.statusCode != 200) { // Any status code other than 200! + showStatusCodeError(context, response.statusCode); + // TODO: Interpret the error codes and show custom message? - await showServerError(context, "Invalid response code: ${response.statusCode.toString()}"); return false; } @@ -246,29 +238,30 @@ class InvenTreeAPI { print("Response from server: $data"); // We expect certain response from the server - if (!data.containsKey("server") || !data.containsKey("version")) { + if (!data.containsKey("server") || !data.containsKey("version") || !data.containsKey("instance")) { + + showServerError( + context, + "Missing Data", + "Server response missing required fields" + ); - await showServerError(context, "Server response missing required fields"); return false; } - print("Server: " + data["server"]); - print("Version: " + data["version"]); - + // Record server information _version = data["version"]; - - if (!_checkServerVersion(_version)) { - await showServerError(context, "Server version is too old.\n\nServer Version: ${_version}\n\nRequired version: ${_requiredVersionString}"); - return false; - } - - // Record the instance name of the server instance = data['instance'] ?? ''; - // Request token from the server if we do not already have one - if (false && _token.isNotEmpty) { - print("Already have token - $_token"); - return true; + // Check that the remote server version is *new* enough + if (!_checkServerVersion(_version)) { + showServerError( + context, + "Old Server Version", + "\n\nServer Version: ${_version}\n\nRequired version: ${_requiredVersionString}" + ); + + return false; } // Clear the existing token value @@ -277,24 +270,33 @@ class InvenTreeAPI { print("Requesting token from server"); response = await get(_URL_GET_TOKEN).timeout(Duration(seconds: 10)).catchError((error) { + print("Error requesting token:"); print(error); - return null; + }); if (response == null) { - await showServerError(context, "Error requesting access token"); + showServerError( + context, "Token Error", "Error requesting access token from server" + ); + return false; } if (response.statusCode != 200) { - await showServerError(context, "Invalid status code: ${response.statusCode.toString()}"); + showStatusCodeError(context, response.statusCode); return false; } else { var data = json.decode(response.body); if (!data.containsKey("token")) { - await showServerError(context, "No token provided in response"); + showServerError( + context, + "Missing Token", + "Access token missing from response" + ); + return false; } @@ -304,6 +306,7 @@ class InvenTreeAPI { _connected = true; + // Ok, probably pretty good... return true; }; } diff --git a/lib/barcode.dart b/lib/barcode.dart index 67dfdf09..a58c37b3 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -66,14 +66,11 @@ class BarcodeHandler { ); } - Future processBarcode(BuildContext context, QRViewController _controller, String barcode, {String url = "barcode/", bool show_dialog = false}) { + Future processBarcode(BuildContext context, QRViewController _controller, String barcode, {String url = "barcode/"}) { this._context = context; this._controller = _controller; print("Scanned barcode data: ${barcode}"); - if (show_dialog) { - showProgressDialog(context, "Scanning", "Sending barcode data to server"); - } // Send barcode request to server InvenTreeAPI().post( @@ -83,10 +80,6 @@ class BarcodeHandler { } ).then((var response) { - if (show_dialog) { - hideProgressDialog(context); - } - if (response.statusCode != 200) { showErrorDialog( context, @@ -119,10 +112,6 @@ class BarcodeHandler { Duration(seconds: 5) ).catchError((error) { - if (show_dialog) { - hideProgressDialog(context); - } - showErrorDialog( context, "Error", diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 96abf38c..9c0358df 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -1,11 +1,11 @@ import 'dart:async'; +import 'dart:io'; import 'package:InvenTree/api.dart'; import 'package:InvenTree/widget/dialogs.dart'; import 'package:flutter/cupertino.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'dart:convert'; @@ -108,22 +108,21 @@ class InvenTreeModel { /* * Reload this object, by requesting data from the server */ - Future reload(BuildContext context, {bool dialog = false}) async { - - if (dialog) { - showProgressDialog(context, I18N.of(context).refreshing, "Refreshing data for ${NAME}"); - } + Future reload(BuildContext context) async { var response = await api.get(url, params: defaultGetFilters()) .timeout(Duration(seconds: 10)) .catchError((e) { - if (dialog) { - hideProgressDialog(context); + if (e is SocketException) { + showServerError( + context, + I18N.of(context).connectionRefused, + e.toString() + ); } - - if (e is TimeoutException) { - showTimeoutDialog(context); + else if (e is TimeoutException) { + showTimeoutError(context); } else { // Re-throw the error (Sentry will catch) throw e; @@ -136,17 +135,8 @@ class InvenTreeModel { return false; } - if (dialog) { - hideProgressDialog(context); - } - if (response.statusCode != 200) { - showErrorDialog( - context, - I18N.of(context).serverError, - "${I18N.of(context).statusCode}: ${response.statusCode}" - ); - + showStatusCodeError(context, response.statusCode); print("Error retrieving data"); return false; } @@ -159,7 +149,7 @@ class InvenTreeModel { } // POST data to update the model - Future update(BuildContext context, {Map values, bool show_dialog = false}) async { + Future update(BuildContext context, {Map values}) async { var addr = path.join(URL, pk.toString()); @@ -167,20 +157,18 @@ class InvenTreeModel { addr += "/"; } - if (show_dialog) { - showProgressDialog(context, "Updating ${NAME}", "Sending data to server"); - } - var response = await api.patch(addr, body: values) .timeout(Duration(seconds: 10)) .catchError((e) { - if (show_dialog) { - hideProgressDialog(context); - } - - if (e is TimeoutException) { - showTimeoutDialog(context); + if (e is SocketException) { + showServerError( + context, + I18N.of(context).connectionRefused, + e.toString() + ); + } else if (e is TimeoutException) { + showTimeoutError(context); } else { // Re-throw the error, let Sentry report it throw e; @@ -191,12 +179,8 @@ class InvenTreeModel { if (response == null) return false; - if (show_dialog) { - hideProgressDialog(context); - } - if (response.statusCode != 200) { - showErrorDialog(context, I18N.of(context).serverError, "${I18N.of(context).statusCode}: ${response.statusCode}"); + showStatusCodeError(context, response.statusCode); return false; } @@ -205,7 +189,7 @@ class InvenTreeModel { } // Return the detail view for the associated pk - Future get(BuildContext context, int pk, {Map filters, bool dialog = false}) async { + Future get(BuildContext context, int pk, {Map filters}) async { // TODO - Add "timeout" // TODO - Add error catching @@ -227,20 +211,15 @@ class InvenTreeModel { print("GET: $addr ${params.toString()}"); - if (dialog) { - showProgressDialog(context, I18N.of(context).requestingData, "Requesting ${NAME} data from server"); - } - var response = await api.get(addr, params: params) .timeout(Duration(seconds: 10)) .catchError((e) { - if (dialog) { - hideProgressDialog(context); + if (e is SocketException) { + showServerError(context, I18N.of(context).connectionRefused, e.toString()); } - - if (e is TimeoutException) { - showTimeoutDialog(context); + else if (e is TimeoutException) { + showTimeoutError(context); } else { // Re-throw the error (handled by Sentry) throw e; @@ -252,12 +231,8 @@ class InvenTreeModel { return null; } - if (dialog) { - hideProgressDialog(context); - } - if (response.statusCode != 200) { - showErrorDialog(context, I18N.of(context).serverError, "${I18N.of(context).statusCode}: ${response.statusCode}"); + showStatusCodeError(context, response.statusCode); return null; } @@ -280,15 +255,24 @@ class InvenTreeModel { InvenTreeModel _model; - await api.post(URL, body: data) - .timeout(Duration(seconds: 5)) - .catchError((e) { + await api.post(URL, body: data).timeout(Duration(seconds: 10)).catchError((e) { + print("Error during CREATE"); print(e.toString()); - showErrorDialog( - context, - I18N.of(context).serverError, - e.toString() - ); + + if (e is SocketException) { + showServerError( + context, + I18N.of(context).connectionRefused, + e.toString() + ); + } + else if (e is TimeoutException) { + showTimeoutError(context); + } else { + // Re-throw the error (Sentry will catch) + throw e; + } + return null; }) .then((http.Response response) { @@ -297,11 +281,7 @@ class InvenTreeModel { var decoded = json.decode(response.body); _model = createFromJson(decoded); } else { - showErrorDialog( - context, - I18N.of(context).serverError, - "${I18N.of(context).statusCode}: ${response.statusCode}" - ); + showStatusCodeError(context, response.statusCode); } }); @@ -309,7 +289,7 @@ class InvenTreeModel { } // Return list of objects from the database, with optional filters - Future> list(BuildContext context, {Map filters, bool dialog=false}) async { + Future> list(BuildContext context, {Map filters}) async { if (filters == null) { filters = {}; @@ -328,20 +308,19 @@ class InvenTreeModel { // TODO - Add "timeout" // TODO - Add error catching - if (dialog) { - showProgressDialog(context, I18N.of(context).requestingData, "Requesting ${NAME} data from server"); - } - var response = await api.get(URL, params:params) .timeout(Duration(seconds: 10)) .catchError((e) { - if (dialog) { - hideProgressDialog(context); + if (e is SocketException) { + showServerError( + context, + I18N.of(context).connectionRefused, + e.toString() + ); } - - if (e is TimeoutException) { - showTimeoutDialog(context); + else if (e is TimeoutException) { + showTimeoutError(context); } else { // Re-throw the error throw e; @@ -354,15 +333,13 @@ class InvenTreeModel { return null; } - if (dialog) { - hideProgressDialog(context); - } - // A list of "InvenTreeModel" items List results = new List(); if (response.statusCode != 200) { - print("Error retreiving data"); + showStatusCodeError(context, response.statusCode); + + // Return empty list return results; } @@ -420,8 +397,6 @@ class InvenTreeModel { return true; } - - } diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index ac5c469c..c136a41b 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -161,7 +161,6 @@ class InvenTreePart extends InvenTreeModel { "part": "${pk}", "in_stock": "true", }, - dialog: showDialog, ).then((var items) { stockItems.clear(); @@ -186,7 +185,6 @@ class InvenTreePart extends InvenTreeModel { filters: { "part": "${pk}", }, - dialog: showDialog, ).then((var templates) { testingTemplates.clear(); @@ -205,6 +203,15 @@ class InvenTreePart extends InvenTreeModel { // Get the stock count for this Part double get inStock => double.tryParse(jsondata['in_stock'].toString() ?? '0'); + String get inStockString { + + if (inStock == inStock.toInt()) { + return inStock.toInt().toString(); + } else { + return inStock.toString(); + } + } + // Get the number of units being build for this Part double get building => double.tryParse(jsondata['building'].toString() ?? '0'); diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 60f2ee50..494b3aea 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -97,7 +97,6 @@ class InvenTreeStockItem extends InvenTreeModel { filters: { "part": "${partId}", }, - dialog: showDialog, ).then((var templates) { testTemplates.clear(); @@ -113,7 +112,7 @@ class InvenTreeStockItem extends InvenTreeModel { int get testResultCount => testResults.length; - Future getTestResults(BuildContext context, {bool showDialog=false}) async { + Future getTestResults(BuildContext context) async { await InvenTreeStockItemTestResult().list( context, @@ -121,7 +120,6 @@ class InvenTreeStockItem extends InvenTreeModel { "stock_item": "${pk}", "user_detail": "true", }, - dialog: showDialog, ).then((var results) { testResults.clear(); diff --git a/lib/l10n b/lib/l10n index 79b2c87e..90f3bbf1 160000 --- a/lib/l10n +++ b/lib/l10n @@ -1 +1 @@ -Subproject commit 79b2c87e9611abbae7a7251ac68cbfed475f7699 +Subproject commit 90f3bbf1fae86efd0bb0686bef12452a09507669 diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 00032824..d0695020 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -19,6 +19,8 @@ class InvenTreeLoginSettingsWidget extends StatefulWidget { class _InvenTreeLoginSettingsState extends State { + final GlobalKey<_InvenTreeLoginSettingsState> _loginKey = GlobalKey<_InvenTreeLoginSettingsState>(); + final GlobalKey _formKey = new GlobalKey(); final GlobalKey _addProfileKey = new GlobalKey(); @@ -176,7 +178,7 @@ class _InvenTreeLoginSettingsState extends State { _reload(); // Attempt server login (this will load the newly selected profile - InvenTreeAPI().connectToServer(context).then((result) { + InvenTreeAPI().connectToServer(_loginKey.currentContext).then((result) { _reload(); }); @@ -209,7 +211,7 @@ class _InvenTreeLoginSettingsState extends State { if (InvenTreeAPI().isConnected() && profile.key == InvenTreeAPI().profile.key) { // Attempt server login (this will load the newly selected profile - InvenTreeAPI().connectToServer(context).then((result) { + InvenTreeAPI().connectToServer(_loginKey.currentContext).then((result) { _reload(); }); } @@ -262,8 +264,6 @@ class _InvenTreeLoginSettingsState extends State { final Size screenSize = MediaQuery.of(context).size; - print("Building!"); - List children = []; if (profiles != null && profiles.length > 0) { @@ -333,6 +333,7 @@ class _InvenTreeLoginSettingsState extends State { } return Scaffold( + key: _loginKey, appBar: AppBar( title: Text(I18N.of(context).profileSelect), ), diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index ca0e4776..2a6c6722 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -268,7 +268,7 @@ class PartList extends StatelessWidget { return ListTile( title: Text("${part.name}"), subtitle: Text("${part.description}"), - trailing: Text("${part.inStock}"), + trailing: Text("${part.inStockString}"), leading: InvenTreeAPI().getImage( part.thumbnail, width: 40, diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index 828a4476..803476ab 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; Future confirmationDialog(BuildContext context, String title, String text, {String acceptText, String rejectText, Function onAccept, Function onReject}) async { @@ -92,37 +93,55 @@ Future showErrorDialog(BuildContext context, String title, String descript showDialog( context: context, - child: SimpleDialog( - title: ListTile( - title: Text(error), - leading: FaIcon(icon), - ), - children: [ - ListTile( - title: Text(title), - subtitle: Text(description) - ) - ] - ) - ).then((value) { - if (onDismissed != null) { - onDismissed(); - } - }); + builder: (dialogContext) { + return SimpleDialog( + title: ListTile( + title: Text(error), + leading: FaIcon(icon), + ), + children: [ + ListTile( + title: Text(title), + subtitle: Text(description) + ) + ] + ); + }).then((value) { + if (onDismissed != null) { + onDismissed(); + } + }); } -void showTimeoutDialog(BuildContext context) { - /* - Show a server timeout dialog - */ +Future showServerError(BuildContext context, String title, String description) async { - showErrorDialog( + if (title == null || title.isEmpty) { + title = I18N.of(context).serverError; + } + + await showErrorDialog( context, - I18N.of(context).timeout, - I18N.of(context).noResponse + title, + description, + error: I18N.of(context).serverError, + icon: FontAwesomeIcons.server ); } +Future showStatusCodeError(BuildContext context, int status, {int expected = 200}) async { + + await showServerError( + context, + "Invalid Response Code", + "Server responded with status code ${status}" + ); +} + +Future showTimeoutError(BuildContext context) async { + + await showServerError(context, I18N.of(context).timeout, I18N.of(context).noResponse); +} + void showProgressDialog(BuildContext context, String title, String description) { showDialog( diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 44ae938b..3955668e 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -27,6 +27,8 @@ class InvenTreeHomePage extends StatefulWidget { class _InvenTreeHomePageState extends State { + final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>(); + _InvenTreeHomePageState() : super() { // Initially load the profile and attempt server connection @@ -115,7 +117,7 @@ class _InvenTreeHomePageState extends State { if (!InvenTreeAPI().isConnected() && !InvenTreeAPI().isConnecting()) { // Attempt server connection - InvenTreeAPI().connectToServer(_context).then((result) { + InvenTreeAPI().connectToServer(_homeKey.currentContext).then((result) { setState(() {}); }); } @@ -198,6 +200,7 @@ class _InvenTreeHomePageState extends State { // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( + key: _homeKey, appBar: AppBar( title: Text(I18N.of(context).appTitle), actions: [ diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index a0930752..64cedbe9 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -217,7 +217,7 @@ class _PartDisplayState extends RefreshableState { ListTile( title: Text(I18N.of(context).stock), leading: FaIcon(FontAwesomeIcons.boxes), - trailing: Text("${part.inStock}"), + trailing: Text("${part.inStockString}"), onTap: () { _showStock(context); },