From 904a334f3e5c3bf9005ff0d7de38645f9e46591c Mon Sep 17 00:00:00 2001 From: Hamatoma Date: Sun, 8 Nov 2020 15:52:05 +0100 Subject: [PATCH] daily work: user+configuration+role work, new: menu --- CreateModule | 3 +- data/ddl/menu.sql | 11 + data/ddl/user.sql | 4 +- data/rest/configuration.yaml | 4 +- data/rest/custom.user.yaml | 18 ++ data/rest/menu.yaml | 31 ++ data/rest/role.yaml | 4 +- data/rest/user.yaml | 12 +- lib/app.dart | 15 +- lib/flutter_bones.dart | 3 +- lib/src/helper/string_helper.dart | 7 +- lib/src/model/button_model.dart | 10 +- lib/src/model/checkbox_model.dart | 1 + lib/src/model/column_model.dart | 6 +- lib/src/model/combobox_model.dart | 5 +- lib/src/model/db_reference_model.dart | 5 + lib/src/model/field_model.dart | 183 ++++++++++++ lib/src/model/model_types.dart | 1 + lib/src/model/module_model.dart | 38 ++- lib/src/model/page_model.dart | 36 ++- lib/src/model/standard/menu_model.dart | 98 ++++++ lib/src/model/standard/standard_modules.dart | 6 +- lib/src/model/standard/user_model.dart | 72 ++++- lib/src/model/text_field_model.dart | 5 +- lib/src/model/widget_model.dart | 2 +- lib/src/page/application_data.dart | 19 +- .../configuration_change_page.dart | 6 +- .../configuration_controller.dart | 3 +- .../configuration_create_page.dart | 3 +- .../configuration_list_page.dart | 59 ++-- .../{login_page.dart => login_page.dart.01} | 0 lib/src/page/menu/menu_change_page.dart | 79 +++++ lib/src/page/menu/menu_controller.dart | 26 ++ lib/src/page/menu/menu_create_page.dart | 51 ++++ lib/src/page/menu/menu_list_page.dart | 94 ++++++ lib/src/page/role/role_change_page.dart | 6 +- lib/src/page/role/role_controller.dart | 3 +- lib/src/page/role/role_create_page.dart | 3 +- lib/src/page/role/role_list_page.dart | 59 ++-- lib/src/page/user/hash.dart | 8 + lib/src/page/user/user_change_page.dart | 23 +- lib/src/page/user/user_controller.dart | 3 +- lib/src/page/user/user_create_page.dart | 3 +- lib/src/page/user/user_list_page.dart | 2 +- lib/src/page/user/user_login_page.dart | 112 +++++++ lib/src/page/user/user_password_page.dart | 110 +++++++ lib/src/private/bdrawer.dart | 43 ++- lib/src/widget/callback_controller_bones.dart | 104 ------- lib/src/widget/dropdown_button_form_bone.dart | 4 +- lib/src/widget/edit_form.dart | 18 +- lib/src/widget/filter_set.dart | 19 +- lib/src/widget/page_controller_bones.dart | 219 +++++++------- lib/src/widget/raised_button_bone.dart | 4 +- lib/src/widget/text_form_field_bone.dart | 24 +- lib/src/widget/view.dart | 40 ++- lib/src/widget/widget_list.dart | 2 +- lib/src/widget/widget_validators.dart | 281 ++++++++++++++++++ model_tool/Export | 3 + pubspec.yaml | 2 +- test/model/standard_test.dart | 61 +++- test/widget/widget_validators_test.dart | 227 ++++++++++++++ 61 files changed, 1889 insertions(+), 414 deletions(-) create mode 100644 data/ddl/menu.sql create mode 100644 data/rest/custom.user.yaml create mode 100644 data/rest/menu.yaml create mode 100644 lib/src/model/standard/menu_model.dart rename lib/src/page/{login_page.dart => login_page.dart.01} (100%) create mode 100644 lib/src/page/menu/menu_change_page.dart create mode 100644 lib/src/page/menu/menu_controller.dart create mode 100644 lib/src/page/menu/menu_create_page.dart create mode 100644 lib/src/page/menu/menu_list_page.dart create mode 100644 lib/src/page/user/hash.dart create mode 100644 lib/src/page/user/user_login_page.dart create mode 100644 lib/src/page/user/user_password_page.dart delete mode 100644 lib/src/widget/callback_controller_bones.dart create mode 100644 lib/src/widget/widget_validators.dart create mode 100755 model_tool/Export create mode 100644 test/widget/widget_validators_test.dart diff --git a/CreateModule b/CreateModule index baf291f..d513e73 100755 --- a/CreateModule +++ b/CreateModule @@ -2,6 +2,7 @@ MODULE=$1 if [ -z "$MODULE" ]; then echo "Missing module name" + echo "example: $0 menu" else cd lib/src/page if [ -d $MODULE ]; then @@ -18,4 +19,4 @@ else cd $MODULE perl -pi -e"s/role/$MODULE/g;s/Role/$MODULE2/g;" *.dart rename -v "s/role(.*)/${MODULE}\$1/;" *.dart -fi \ No newline at end of file +fi diff --git a/data/ddl/menu.sql b/data/ddl/menu.sql new file mode 100644 index 0000000..44cc7f3 --- /dev/null +++ b/data/ddl/menu.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS menu; +CREATE TABLE menu ( + menu_id INT(10) UNSIGNED NOT NULL UNIQUE AUTO_INCREMENT, + menu_name VARCHAR(64) UNIQUE NOT NULL, + menu_icon INT(10) UNSIGNED, + menu_createdat TIMESTAMP NULL, + menu_createdby VARCHAR(16), + menu_changedat TIMESTAMP NULL, + menu_changedby VARCHAR(16), + PRIMARY KEY(menu_id) +); diff --git a/data/ddl/user.sql b/data/ddl/user.sql index 84441b6..a32e844 100644 --- a/data/ddl/user.sql +++ b/data/ddl/user.sql @@ -2,8 +2,8 @@ DROP TABLE IF EXISTS user; CREATE TABLE user ( user_id INT(10) UNSIGNED NOT NULL UNIQUE AUTO_INCREMENT, user_name VARCHAR(64) UNIQUE NOT NULL, - user_displayname VARCHAR(32) UNIQUE, - user_email VARCHAR(128) UNIQUE, + user_displayname VARCHAR(32) UNIQUE NOT NULL, + user_email VARCHAR(128) UNIQUE NOT NULL, user_password VARCHAR(128), user_role INT(10) UNSIGNED, user_createdat TIMESTAMP NULL, diff --git a/data/rest/configuration.yaml b/data/rest/configuration.yaml index 98f5ae0..6fd0ce3 100644 --- a/data/rest/configuration.yaml +++ b/data/rest/configuration.yaml @@ -1,11 +1,11 @@ --- # configuration of the bones backend for configuration: -created: 2020.10.29 22:35:03 +created: 2020.11.10 23:47:43 author: flutter_bones.module_model.exportSqlBackend() version: 1.0.0 modules: - module: configuration - list: + sqlInfos: - name: insert type: insert sql: "INSERT INTO configuration(configuration_scope,configuration_property,configuration_order,configuration_type,configuration_value,configuration_description,configuration_createdat,configuration_createdby) diff --git a/data/rest/custom.user.yaml b/data/rest/custom.user.yaml new file mode 100644 index 0000000..37243eb --- /dev/null +++ b/data/rest/custom.user.yaml @@ -0,0 +1,18 @@ +--- +# configuration of the bones backend for user: +created: 2020.11.09 07:25 +author: flutter_bones.module_model.exportSqlBackend() +version: 1.0.0 +modules: + - module: user + sqlInfos: + - name: update_pw + type: update + sql: "UPDATE user SET + user_password=:user_password,user_changedat=NOW(),user_changedby=:user_changedby + WHERE user_id=:user_id;" + - name: login + type: record + sql: "SELECT u.*, r.* FROM user u + LEFT JOIN role r ON u.user_role=r.role_id + WHERE (user_name=:user or user_email=:user);" diff --git a/data/rest/menu.yaml b/data/rest/menu.yaml new file mode 100644 index 0000000..54966a8 --- /dev/null +++ b/data/rest/menu.yaml @@ -0,0 +1,31 @@ +--- +# configuration of the bones backend for menu: +created: 2020.11.10 23:47:43 +author: flutter_bones.module_model.exportSqlBackend() +version: 1.0.0 +modules: + - module: menu + sqlInfos: + - name: insert + type: insert + sql: "INSERT INTO menu(menu_name,menu_icon,menu_createdat,menu_createdby) + VALUES(:menu_name,:menu_icon,NOW(),:menu_createdby);" + - name: update + type: update + sql: "UPDATE menu SET + menu_name=:menu_name,menu_icon=:menu_icon,menu_changedat=NOW(),menu_changedby=:menu_changedby + WHERE menu_id=:menu_id;" + - name: delete + type: delete + sql: "DELETE from menu WHERE menu_id=:menu_id;" + - name: record + type: record + sql: "SELECT * from menu WHERE menu_id=:menu_id;" + - name: by_menu_name + type: record + sql: "SELECT * from menu WHERE menu_name=:menu_name&&menu_id!=:excluded;" + - name: list + type: list + sql: "SELECT t0.*,t1.configuration_property as menu_icon from menu t0 + left join configuration t1 on t1.configuration_id=t0.menu_icon + WHERE menu_name like :menu_name;" diff --git a/data/rest/role.yaml b/data/rest/role.yaml index c621943..f59f8a2 100644 --- a/data/rest/role.yaml +++ b/data/rest/role.yaml @@ -1,11 +1,11 @@ --- # configuration of the bones backend for role: -created: 2020.10.29 22:35:03 +created: 2020.11.10 23:47:43 author: flutter_bones.module_model.exportSqlBackend() version: 1.0.0 modules: - module: role - list: + sqlInfos: - name: insert type: insert sql: "INSERT INTO role(role_name,role_priority,role_active,role_createdat,role_createdby) diff --git a/data/rest/user.yaml b/data/rest/user.yaml index c85fad9..f99d0ac 100644 --- a/data/rest/user.yaml +++ b/data/rest/user.yaml @@ -1,19 +1,19 @@ --- # configuration of the bones backend for user: -created: 2020.10.29 22:35:03 +created: 2020.11.10 23:47:43 author: flutter_bones.module_model.exportSqlBackend() version: 1.0.0 modules: - module: user - list: + sqlInfos: - name: insert type: insert - sql: "INSERT INTO user(user_name,user_displayname,user_email,user_password,user_role,user_createdat,user_createdby) - VALUES(:user_name,:user_displayname,:user_email,:user_password,:user_role,NOW(),:user_createdby);" + sql: "INSERT INTO user(user_name,user_displayname,user_email,user_role,user_createdat,user_createdby) + VALUES(:user_name,:user_displayname,:user_email,:user_role,NOW(),:user_createdby);" - name: update type: update sql: "UPDATE user SET - user_name=:user_name,user_displayname=:user_displayname,user_email=:user_email,user_password=:user_password,user_role=:user_role,user_changedat=NOW(),user_changedby=:user_changedby + user_name=:user_name,user_displayname=:user_displayname,user_email=:user_email,user_role=:user_role,user_changedat=NOW(),user_changedby=:user_changedby WHERE user_id=:user_id;" - name: delete type: delete @@ -34,4 +34,4 @@ modules: type: list sql: "SELECT t0.*,t1.role_name as user_role from user t0 left join role t1 on t1.role_id=t0.user_role - WHERE user_name like :user_name AND (:user_role IS NULL OR t0.user_role=:user_role);" + WHERE user_name like :user_name AND (:user_role IS NULL OR user_role=:user_role);" diff --git a/lib/app.dart b/lib/app.dart index b40f123..d79e985 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -4,11 +4,13 @@ import 'package:flutter/material.dart'; import 'src/helper/settings.dart'; import 'src/page/demo_page.dart'; import 'src/page/async_example_page.dart'; -import 'src/page/login_page.dart'; import 'src/page/role/role_create_page.dart'; import 'src/page/role/role_list_page.dart'; import 'src/page/user/user_create_page.dart'; import 'src/page/user/user_list_page.dart'; +import 'src/page/user/user_login_page.dart'; +import 'src/page/menu/menu_list_page.dart'; +import 'src/page/menu/menu_create_page.dart'; import 'src/page/configuration/configuration_create_page.dart'; import 'src/page/configuration/configuration_list_page.dart'; import 'src/private/bsettings.dart'; @@ -38,7 +40,7 @@ class BoneAppState extends State { primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), - initialRoute: '/user/list', + initialRoute: '/menu/list', //initialRoute: '/async', onGenerateRoute: _getRoute, ); @@ -73,8 +75,15 @@ Route _getRoute(RouteSettings settings) { case '/configuration/create': page = ConfigurationCreatePage(BSettings.lastInstance.pageData); break; + case '/menu/list': + page = MenuCreatePage(BSettings.lastInstance.pageData); + break; + case '/menu/create': + page = MenuListPage(BSettings.lastInstance.pageData); + break; + case '/user/login': default: - page = LoginPage(BSettings.lastInstance.pageData); + page = UserLoginPage(BSettings.lastInstance.pageData); break; } if (page != null) { diff --git a/lib/flutter_bones.dart b/lib/flutter_bones.dart index 1220da3..f2760bf 100644 --- a/lib/flutter_bones.dart +++ b/lib/flutter_bones.dart @@ -20,10 +20,11 @@ export 'src/model/standard/configuration_model.dart'; export 'src/model/standard/role_model.dart'; export 'src/model/standard/standard_modules.dart'; export 'src/model/standard/user_model.dart'; +export 'src/model/standard/menu_model.dart'; export 'src/model/text_field_model.dart'; export 'src/model/text_model.dart'; export 'src/model/widget_model.dart'; -export 'src/page/login_page.dart'; +export 'src/page/login_page.dart.01'; export 'src/page/application_data.dart'; export 'src/page/role/role_create_page.dart'; export 'src/page/user/user_create_page.dart'; diff --git a/lib/src/helper/string_helper.dart b/lib/src/helper/string_helper.dart index ba9224e..484b308 100644 --- a/lib/src/helper/string_helper.dart +++ b/lib/src/helper/string_helper.dart @@ -85,6 +85,7 @@ class StringHelper { } /// Converts a string to a given [dataType]. + /// Returns null on error. static dynamic fromString(String value, DataType dataType) { dynamic rc; if (dataType == DataType.string) { @@ -109,7 +110,11 @@ class StringHelper { break; case DataType.date: case DataType.dateTime: - rc = StringUtils.stringToDateTime(value); + try { + rc = StringUtils.stringToDateTime(value); + } on FormatException { + rc = null; + } break; case DataType.int: case DataType.reference: diff --git a/lib/src/model/button_model.dart b/lib/src/model/button_model.dart index d2a5a29..600f2c0 100644 --- a/lib/src/model/button_model.dart +++ b/lib/src/model/button_model.dart @@ -1,4 +1,5 @@ import 'package:dart_bones/dart_bones.dart'; +import 'model_types.dart'; import 'widget_model.dart'; import 'section_model.dart'; import 'page_model.dart'; @@ -6,12 +7,13 @@ import 'page_model.dart'; /// Describes a button widget. class ButtonModel extends WidgetModel { static final regExprOptions = RegExp(r'^(undef)$'); - String text; + String label; String name; String toolTip; final Map map; List options; ButtonModelType buttonModelType; + VoidCallbackBones onPressed; /// Constructs an instance by parsing the map for some properties. ButtonModel(SectionModel section, PageModel page, this.map, BaseLogger logger) @@ -29,7 +31,7 @@ class ButtonModel extends WidgetModel { BaseLogger logger) : super(section, page, WidgetModelType.button, logger) { this.name = name; - this.text = text; + this.label = text; this.toolTip = toolTip; this.options = options; this.buttonModelType = buttonModelType; @@ -38,7 +40,7 @@ class ButtonModel extends WidgetModel { /// Dumps a the instance into a [stringBuffer] StringBuffer dump(StringBuffer stringBuffer) { stringBuffer.write( - ' button $name: text: options: $text ${listToString(options)}\n'); + ' button $name: text: options: $label ${listToString(options)}\n'); return stringBuffer; } @@ -51,7 +53,7 @@ class ButtonModel extends WidgetModel { name = parseString('name', map, required: true); checkSuperfluousAttributes(map, 'buttonType label name options text toolTip widgetType'.split(' ')); - text = parseString('text', map); + label = parseString('label', map, required: true); toolTip = parseString('toolTip', map); buttonModelType = parseEnum('buttonType', map, ButtonModelType.values); diff --git a/lib/src/model/checkbox_model.dart b/lib/src/model/checkbox_model.dart index 79c4c16..dd6f281 100644 --- a/lib/src/model/checkbox_model.dart +++ b/lib/src/model/checkbox_model.dart @@ -22,6 +22,7 @@ class CheckboxModel extends FieldModel { map, 'name label options toolTip widgetType'.split(' ')); options = parseOptions('options', map); checkOptionsByRegExpr(options, regExprOptions); + parseFinish(); } /// Dumps the instance into a [StringBuffer] diff --git a/lib/src/model/column_model.dart b/lib/src/model/column_model.dart index 69ecb4c..eb57c90 100644 --- a/lib/src/model/column_model.dart +++ b/lib/src/model/column_model.dart @@ -1,16 +1,16 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:meta/meta.dart'; +import 'combo_base_model.dart'; import 'model_base.dart'; import 'model_types.dart'; import 'table_model.dart'; import 'widget_model.dart'; -import 'combo_base_model.dart'; /// Describes a column of a database table. class ColumnModel extends ComboBaseModel { static final regExprOptions = RegExp( - r'^(undef|readonly|disabled|doStore|hidden|null|notnull|password|primary|required|unique)$'); + r'^(disabled|doStore|hidden|null|notnull|password|primary|readonly|required|undef|unique)$'); static final regExprListDbOption = RegExp(r'^\w+\.\w+;\w+ \w+(?:;(:? ?:\w+=[^ ]*?)+)?$'); static final regExprForeignKey = RegExp(r'^\w+\.\w+ \w+$'); @@ -75,7 +75,7 @@ class ColumnModel extends ComboBaseModel { name = parseString('column', map, required: true); checkSuperfluousAttributes( map, - 'column dataType defaultValue foreignKey label listOption listType options rows size texts tooTip values widgetType' + 'column dataType defaultValue foreignKey label listOption listType options rows size texts tooTip validators validatorsText values widgetType' .split(' ')); super.parse(); dataType = diff --git a/lib/src/model/combobox_model.dart b/lib/src/model/combobox_model.dart index da2a948..23aa108 100644 --- a/lib/src/model/combobox_model.dart +++ b/lib/src/model/combobox_model.dart @@ -7,7 +7,7 @@ import 'widget_model.dart'; /// Describes a combobox widget. class ComboboxModel extends ComboBaseModel { - static final regExprOptions = RegExp(r'^(readonly|disabled|required|undef)$'); + static final regExprOptions = RegExp(r'^(disabled|readonly|required|undef)$'); ComboboxModel( SectionModel section, PageModel page, Map map, BaseLogger logger) @@ -24,9 +24,10 @@ class ComboboxModel extends ComboBaseModel { void parse() { checkSuperfluousAttributes( map, - 'dataType defaultValue filterType label listOption listType name options texts toolTip values widgetType' + 'dataType defaultValue filterType label listOption listType name options texts toolTip values validators validatorsText widgetType' .split(' ')); super.parse(); checkOptionsByRegExpr(options, regExprOptions); + parseFinish(); } } diff --git a/lib/src/model/db_reference_model.dart b/lib/src/model/db_reference_model.dart index 2032c70..76a7efb 100644 --- a/lib/src/model/db_reference_model.dart +++ b/lib/src/model/db_reference_model.dart @@ -41,9 +41,13 @@ class DbReferenceModel extends ComboBaseModel { this.column = column; maxSize = column.size; rows = column.rows; + messageOfValidator.addAll(column.messageOfValidator); + validatorsString = column.validatorsString; + parameterOfValidator.addAll(column.parameterOfValidator); if (column.hasOption('primary') && !column.hasOption('readonly')) { options.add('readonly'); } + parseFinish(); } /// Dumps the internal structure into a [stringBuffer] @@ -76,5 +80,6 @@ class DbReferenceModel extends ComboBaseModel { listType ??= column.listType; checkOptionsByRegExpr(options, regExprOptions); options += column.options; + parseFinish(); } } diff --git a/lib/src/model/field_model.dart b/lib/src/model/field_model.dart index 6e2a19e..db484f8 100644 --- a/lib/src/model/field_model.dart +++ b/lib/src/model/field_model.dart @@ -6,8 +6,21 @@ import 'page_model.dart'; import 'section_model.dart'; import 'widget_model.dart'; +typedef FieldValidator = String Function( + String input, FieldModel model, ValidatorController controller); + /// Base class of widgets with user interaction like text field, combobox, checkbox. abstract class FieldModel extends WidgetModel { + static const reValidatorString = r'[a-z][a-zA-Z]+(=\S+)?'; + static const reValidatorsString = + '^($reValidatorString)( ($reValidatorString))*\$'; + static final regExpValidators = RegExp(reValidatorsString); + + /// Note: changes of reValidatorNames needs changes in checkValidator() + static const reValidatorNames = + 'dateTime|email|int|maxDate|maxDateTime|maxInt|minDate|minDateTime|minInt|regExpr|required|unique'; + static const reValidatorTextItem = '^($reValidatorNames)\$'; + static final regExprValidatorText = RegExp(reValidatorTextItem); String name; String label; String toolTip; @@ -17,6 +30,21 @@ abstract class FieldModel extends WidgetModel { dynamic _value; dynamic defaultValue; + /// list of validator functions: + final validators = []; + + /// definitions of the validators, e.g. "minInt=1 maxInt=99" + String validatorsString; + + /// options of the validator functions: + /// key: validator name, e.g. "minInt" + /// value: option, e.g. 1 + final parameterOfValidator = {}; + + /// a model specific error message: + /// key: validator name, e.g. 'regExpr' + /// value: the error message + final messageOfValidator = {}; final Map map; FieldModel(SectionModel section, PageModel page, this.map, @@ -50,6 +78,102 @@ abstract class FieldModel extends WidgetModel { } } + /// Tests the validity of a validator term. + /// Note: a new name in checkValidator() needs a change of reValidatorNames + void checkValidator(String element) { + final ix = element.indexOf('='); + String name, argument; + if (ix < 0) { + name = element; + } else { + name = element.substring(0, ix); + argument = element.substring(ix + 1); + } + switch (name) { + case 'date': + case 'dateTime': + case 'email': + case 'int': + case 'required': + case 'unique': + if (argument != null) { + logger.error( + '${fullName()}: argument not allowed for $name: $argument'); + } + break; + case 'equals': + FieldModel otherField; + if (argument == null) { + logger.error('missing reference field in ${fullName()}.equals'); + } else if ((otherField = page.fieldByName(argument, required: false)) == + null) { + logger.error('unknown field $argument in ${fullName()}.equals'); + } else { + parameterOfValidator[name] = + otherField.widgetName() + ':' + otherField.label; + } + break; + case 'minInt': + case 'maxInt': + int intValue; + if (argument == null || + (intValue = StringUtils.asInt(argument)) == null) { + logger.error('${fullName()}: not an integer in $name=$argument'); + } else { + parameterOfValidator[name] = intValue; + } + break; + case 'minDate': + case 'maxDate': + try { + DateTime dateValue; + if (argument == null || + argument.contains(':') || + (dateValue = StringUtils.stringToDateTime(argument)) == null) { + throw ArgumentError(''); + } + parameterOfValidator[name] = dateValue; + } on ArgumentError { + logger.error('${fullName()}: invalid date for $name'); + } + break; + case 'minDateTime': + case 'maxDateTime': + try { + DateTime dateValue; + if (argument == null || + (dateValue = StringUtils.stringToDateTime(argument)) == null) { + throw ArgumentError(''); + } + parameterOfValidator[name] = dateValue; + } on ArgumentError { + logger.error('${fullName()}: invalid timestamp for $name'); + } + break; + case 'regExpr': + if (argument == null) { + logger.error('${fullName()}: missing regular expression in $name'); + } else { + if (RegExp('^i?/').firstMatch(argument) == null) { + logger.error('${fullName()}: missing "i/" or "/" in $name'); + } else { + try { + final caseSensitive = !argument.startsWith('i'); + final regExp = RegExp(argument.substring(caseSensitive ? 1 : 2), + caseSensitive: caseSensitive); + parameterOfValidator[name] = regExp; + } on FormatException catch (exc) { + logger.error('${fullName()}: error in $name: $exc'); + } + } + } + break; + default: + logger.error('unknown validator name $name in ${fullName()}'); + break; + } + } + /// Returns the name including the names of the parent @override String fullName() => '${page.name}.$name'; @@ -72,6 +196,8 @@ abstract class FieldModel extends WidgetModel { toolTip = parseString('toolTip', map, required: false); filterType = parseEnum('filterType', map, FilterType.values); options = parseOptions('options', map); + validatorsString = parseString('validators', map); + parseValidatorsText(parseString('validatorsText', map)); dataType = parseEnum('dataType', map, DataType.values); if (dataType == null) { switch (widgetModelType) { @@ -88,6 +214,61 @@ abstract class FieldModel extends WidgetModel { value is String ? StringHelper.fromString(value, dataType) : value; } + /// Finishes the parse process for [FieldModel]s. + /// Must be called at the end of parse() of the parent, e.g. [TextFieldModel]. + void parseFinish() { + if (validatorsString != null && + !validatorsString.startsWith('required') && + validatorsString.contains('required')) { + validatorsString = validatorsString + .replaceAll('required ', '') + .replaceAll('required', ''); + if (validatorsString.isEmpty) { + validatorsString = 'required'; + } else { + validatorsString = 'required ' + validatorsString; + } + } + if (hasOption('required') || hasOption('notnull')) { + if (validatorsString == null) { + validatorsString = 'required'; + } else if (!validatorsString.contains('required')) { + validatorsString = 'required ' + validatorsString; + } + } + if (validatorsString != null) { + validatorsString.split(' ').forEach((element) => checkValidator(element)); + } + } + + /// Splits [text] into [messageOfValidator]. + void parseValidatorsText(String text) { + if (text != null) { + text.split('|').forEach((element) { + final ix = element.indexOf('='); + if (ix < 0) { + logger.error('missing "=": $element in ${fullName()}.validatorsText'); + } else if (ix == 0) { + logger.error( + '"=" at first position: $element in ${fullName()}.validatorsText'); + } else { + final name = element.substring(0, ix); + if (regExprValidatorText.firstMatch(name) == null) { + logger.error('unknown "$name" in ${fullName()}.validatorsText'); + } else { + messageOfValidator[name] = element.substring(ix + 1); + } + } + }); + } + } + + /// Returns null or the option of the validator named [name], e.g. "minInt" + dynamic validatorParameter(String name) => + parameterOfValidator.containsKey(name) + ? parameterOfValidator[name] + : null; + /// Gets the [value] from a [row] like a db record: /// Key of the entry in [row] is the [name]. void valueFromRow(Map row) { @@ -99,3 +280,5 @@ abstract class FieldModel extends WidgetModel { @override String widgetName() => name; } + +abstract class ValidatorController {} diff --git a/lib/src/model/model_types.dart b/lib/src/model/model_types.dart index c7f29ad..437b98a 100644 --- a/lib/src/model/model_types.dart +++ b/lib/src/model/model_types.dart @@ -18,3 +18,4 @@ enum FilterType { pattern, } enum WaitState { undef, initial, waiting, ready } +typedef VoidCallbackBones = void Function(); diff --git a/lib/src/model/module_model.dart b/lib/src/model/module_model.dart index 3bfd13e..a23e9a8 100644 --- a/lib/src/model/module_model.dart +++ b/lib/src/model/module_model.dart @@ -1,20 +1,21 @@ import 'package:dart_bones/dart_bones.dart'; -import 'column_model.dart'; -import 'table_model.dart'; import 'package:meta/meta.dart'; -import 'model_base.dart'; -import 'page_model.dart'; + +import '../helper/string_helper.dart'; +import 'column_model.dart'; import 'field_model.dart'; +import 'model_base.dart'; import 'model_types.dart'; +import 'page_model.dart'; +import 'table_model.dart'; import 'widget_model.dart'; -import '../helper/string_helper.dart'; /// Describes a module. /// A module realizes a model of an entitiy which is normally related to one /// database table. /// The module administrate some pages to display that entity. class ModuleModel extends ModelBase { - static final regExprOptions = RegExp(r'^(unknown)$'); + static final regExprOptions = RegExp(r'^(noPages)$'); final Map map; String name; List options; @@ -191,7 +192,7 @@ author: flutter_bones.module_model.exportSqlBackend() version: 1.0.0 modules: - module: ${table.name} - list: + sqlInfos: - name: insert type: insert '''); @@ -318,6 +319,9 @@ modules: return rc; } + /// Tests whether an options named 'name' is set. + bool hasOption(String name) => options != null && options.contains(name); + /// Returns the main table of the module. /// This is the first defined table. TableModel mainTable() { @@ -339,18 +343,20 @@ modules: if (map.containsKey('tables')) { TableModel.parseList(this, map['tables'], logger); } - if (!map.containsKey('pages')) { - logger.error('Module $name: missing pages'); - } else { - final item = map['pages']; - if (!ModelBase.isList(item)) { - logger.error( - 'curious item in page list of ${fullName()}: ${StringUtils.limitString("$item", 80)}'); + options = parseOptions('options', map); + if (!hasOption('noPages')) { + if (!map.containsKey('pages')) { + logger.error('Module $name: missing pages'); } else { - PageModel.parseList(this, item, logger); + final item = map['pages']; + if (!ModelBase.isList(item)) { + logger.error( + 'curious item in page list of ${fullName()}: ${StringUtils.limitString("$item", 80)}'); + } else { + PageModel.parseList(this, item, logger); + } } } - options = parseOptions('options', map); checkOptionsByRegExpr(options, regExprOptions); } diff --git a/lib/src/model/page_model.dart b/lib/src/model/page_model.dart index eeb1416..956bd33 100644 --- a/lib/src/model/page_model.dart +++ b/lib/src/model/page_model.dart @@ -21,6 +21,7 @@ class PageModel extends ModelBase { final fields = []; final buttons = []; final widgets = []; + String sql; List tableTitles; List tableColumns; @@ -108,8 +109,10 @@ class PageModel extends ModelBase { /// Parses the map and stores the data in the instance. void parse() { name = parseString('page', map, required: true); - checkSuperfluousAttributes(map, - 'options page pageType sections tableColumns tableTitles'.split(' ')); + checkSuperfluousAttributes( + map, + 'options page pageType sections sql tableColumns tableTitles' + .split(' ')); pageModelType = parseEnum( 'pageType', map, PageModelType.values, required: true); @@ -125,6 +128,7 @@ class PageModel extends ModelBase { SectionModel.parseSections(this, null, item, logger); } } + sql = parseString('sql', map); tableTitles = parseStringList('tableTitles', map); tableColumns = parseString('tableColumns', map)?.split(' '); if (pageModelType != PageModelType.list && @@ -140,12 +144,29 @@ class PageModel extends ModelBase { 'different sizes of tableTitles/tableColumns: ${tableTitles.length}/${tableColumns.length}'); } - if (!options.contains('noAutoButton') && - buttonByName('search', required: false) == null) { + if (!options.contains('noAutoButton')) { final section = null; - final button = ButtonModel.direct(section, this, 'search', 'Suchen', - ButtonModelType.search, [], null, logger); - addButton(button); + switch (pageModelType) { + case PageModelType.list: + if (buttonByName('search', required: false) == null) { + addButton(ButtonModel.direct(section, this, 'search', 'Suchen', + ButtonModelType.search, [], null, logger)); + } + break; + case PageModelType.create: + case PageModelType.change: + if (buttonByName('cancel', required: false) == null) { + addButton(ButtonModel.direct(section, this, 'cancel', 'Abbruch', + ButtonModelType.cancel, [], null, logger)); + } + if (buttonByName('store', required: false) == null) { + addButton(ButtonModel.direct(section, this, 'store', 'Speichern', + ButtonModelType.store, [], null, logger)); + } + break; + default: + break; + } } checkOptionsByRegExpr(options, regExprOptions); } @@ -163,6 +184,7 @@ class PageModel extends ModelBase { 'curious item in section list of ${module.fullName()}: ${StringUtils.limitString("$item", 80)}'); } else { final page = PageModel(module, item, logger); + // we need a name before adding to module page.parse(); module.addPage(page); } diff --git a/lib/src/model/standard/menu_model.dart b/lib/src/model/standard/menu_model.dart new file mode 100644 index 0000000..21b321b --- /dev/null +++ b/lib/src/model/standard/menu_model.dart @@ -0,0 +1,98 @@ +import 'package:dart_bones/dart_bones.dart'; + +import '../module_model.dart'; + +class MenuModel extends ModuleModel { + static final mapMenu = { + 'module': 'menu', + 'tables': [ + { + 'table': 'menu', + 'columns': [ + { + 'column': 'menu_id', + 'dataType': 'int', + 'label': 'Id', + 'options': 'primary', + }, + { + 'column': 'menu_name', + 'dataType': 'string', + 'label': 'Name', + 'size': 64, + 'options': 'unique notnull', + }, + { + 'column': 'menu_icon', + 'dataType': 'reference', + 'label': 'Bild', + 'foreignKey': + 'configuration.configuration_id configuration_property', + 'listType': 'configuration', + 'listOption': 'scope:icons', + }, + ] + }, + ], + 'pages': [ + { + 'page': 'create', + 'pageType': 'create', + 'sections': [ + { + 'sectionType': 'simpleForm', + 'children': [ + { + 'widgetType': 'allDbFields', + } + ] + } + ] + }, + { + 'page': 'change', + 'pageType': 'change', + 'sections': [ + { + 'sectionType': 'simpleForm', + 'children': [ + { + 'widgetType': 'allDbFields', + }, + ] + } + ] + }, + { + 'page': 'list', + 'pageType': 'list', + 'tableColumns': 'menu_id menu_name menu_icon', + 'tableTitles': ';Id;Name;Bild', + 'sections': [ + { + 'sectionType': 'filterPanel', + 'children': [ + { + 'widgetType': 'dbReference', + 'filterType': 'pattern', + 'name': 'menu_name', + 'column': 'menu_name', + 'toolTip': + "Filter bezüglich des Namens der anzuzeigenden Einträge: Joker '*' (beliebiger String) ist erlaubt." + }, + ] + } + ] + }, + ] + }; + + MenuModel(BaseLogger logger) : super(mapMenu, logger); + + /// Returns the name including the names of the parent + @override + String fullName() => name; + + @override + String widgetName() => name; +} diff --git a/lib/src/model/standard/standard_modules.dart b/lib/src/model/standard/standard_modules.dart index 5aa6f0a..efd1217 100644 --- a/lib/src/model/standard/standard_modules.dart +++ b/lib/src/model/standard/standard_modules.dart @@ -2,6 +2,7 @@ import 'package:dart_bones/dart_bones.dart'; import '../module_model.dart'; import 'role_model.dart'; import 'user_model.dart'; +import 'menu_model.dart'; import 'configuration_model.dart'; /// Returns an instance of a standard module given by [name]. @@ -17,6 +18,9 @@ ModuleModel standardModule(String name, BaseLogger logger) { case 'configuration': rc = ConfigurationModel(logger); break; + case 'menu': + rc = MenuModel(logger); + break; default: logger.error('unknown standard module: $name'); break; @@ -25,4 +29,4 @@ ModuleModel standardModule(String name, BaseLogger logger) { } /// Returns the names of the standard modules. -List standardModules() => ['configuration', 'role', 'user']; +List standardModules() => ['configuration', 'menu', 'role', 'user']; diff --git a/lib/src/model/standard/user_model.dart b/lib/src/model/standard/user_model.dart index 01bdc64..3b04a94 100644 --- a/lib/src/model/standard/user_model.dart +++ b/lib/src/model/standard/user_model.dart @@ -27,21 +27,22 @@ class UserModel extends ModuleModel { 'dataType': 'string', 'label': 'Anzeigename', 'size': 32, - 'options': 'unique', + 'options': 'unique notnull', }, { 'column': 'user_email', 'dataType': 'string', 'label': 'EMail', 'size': 128, - 'options': 'unique', + 'options': 'unique notnull', + 'validators': 'email', }, { 'column': 'user_password', 'dataType': 'string', 'label': 'Passwort', 'size': 128, - 'options': 'password', + 'options': 'password hidden', }, { 'column': 'user_role', @@ -80,7 +81,39 @@ class UserModel extends ModuleModel { "children": [ { "widgetType": "allDbFields", - } + }, + { + "widgetType": "button", + "name": "set_password", + "label": "Passwort ändern", + "buttonType": "custom", + }, + ] + } + ] + }, + { + "page": "password", + "pageType": "change", + "sql": "update_pw", + "sections": [ + { + "sectionType": "simpleForm", + "children": [ + { + "widgetType": "textField", + "name": "user_password", + "label": "Passwort", + "options": "password", + "validators": "required", + }, + { + "widgetType": "textField", + "name": "repetition", + "label": "Wiederholung", + "options": "password", + "validators": "required equals=user_password" + }, ] } ] @@ -115,6 +148,37 @@ class UserModel extends ModuleModel { } ] }, + { + "page": "login", + "pageType": "change", + "options": "noAutoButton", + "sections": [ + { + "sectionType": "simpleForm", + "children": [ + { + "widgetType": "textField", + "name": "user", + "label": "Benutzer oder EMail", + "validators": "required", + }, + { + "widgetType": "textField", + "name": "password", + "label": "Password", + "options": "password", + "validators": "required", + }, + { + "widgetType": "button", + "buttonType": "custom", + "name": "login", + "label": "Anmelden", + }, + ] + } + ] + }, ] }; diff --git a/lib/src/model/text_field_model.dart b/lib/src/model/text_field_model.dart index 3c623db..627bd38 100644 --- a/lib/src/model/text_field_model.dart +++ b/lib/src/model/text_field_model.dart @@ -9,7 +9,7 @@ import 'widget_model.dart'; /// Describes a form text field widget. class TextFieldModel extends FieldModel { static final regExprOptions = - RegExp(r'^(readonly|disabled|password|required|unique)$'); + RegExp(r'^(disabled|password|readonly|required|unique)$'); int maxSize; int rows; var value; @@ -44,7 +44,7 @@ class TextFieldModel extends FieldModel { super.parse(); checkSuperfluousAttributes( map, - 'dataType filterType label maxSize name options rows toolTip value widgetType' + 'dataType filterType label maxSize name options rows toolTip validators validatorsText value widgetType' .split(' ')); maxSize = parseInt('maxSize', map, required: false); rows = parseInt('rows', map, required: false); @@ -59,5 +59,6 @@ class TextFieldModel extends FieldModel { } options = parseOptions('options', map); checkOptionsByRegExpr(options, regExprOptions); + parseFinish(); } } diff --git a/lib/src/model/widget_model.dart b/lib/src/model/widget_model.dart index c4fcce7..3db5f11 100644 --- a/lib/src/model/widget_model.dart +++ b/lib/src/model/widget_model.dart @@ -9,7 +9,7 @@ import 'combo_base_model.dart'; abstract class WidgetModel extends ModelBase { static int lastId = 0; final SectionModel section; - final PageModel page; + PageModel page; final WidgetModelType widgetModelType; int id; diff --git a/lib/src/page/application_data.dart b/lib/src/page/application_data.dart index 9dda8ed..3db0d3e 100644 --- a/lib/src/page/application_data.dart +++ b/lib/src/page/application_data.dart @@ -1,10 +1,9 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:flutter/material.dart'; -import '../widget/callback_controller_bones.dart'; -import '../widget/page_controller_bones.dart'; -import '../persistence/persistence_cache.dart'; import '../persistence/persistence.dart'; +import '../persistence/persistence_cache.dart'; +import '../widget/page_controller_bones.dart'; /// Data class for storing parameter to build a page. class ApplicationData { @@ -16,8 +15,11 @@ class ApplicationData { final Drawer Function(dynamic context) drawerBuilder; final Persistence persistence; PersistenceCache persistenceCache; - String currentUser; + int currentUserId; + String currentUserName; int currentRoleId; + String currentRoleName; + int currentRolePriority; /// for unittests: dynamic lastModuleState; @@ -34,8 +36,11 @@ class ApplicationData { /// [drawerBuilder] is a factory of a function returning a Drawer which handles the "Hamburger menu" ApplicationData(this.configuration, this.appBarBuilder, this.drawerBuilder, this.persistence, this.logger) { - currentUser = 'Gast'; - currentRoleId = 100; + currentUserId = 0; + currentUserName = 'Gast'; + currentRoleId = 0; + currentRolePriority = 100; + currentRoleName = 'Gast'; persistenceCache = PersistenceCache(persistence, logger); } @@ -48,10 +53,12 @@ class ApplicationData { } } + /// Stacks the caller of the current page. void pushCaller(PageControllerBones controller) { callerStack.add(controller); } + /// Removes the caller of the current page from the stack's top. void popCaller() { if (callerStack.isNotEmpty) { callerStack.removeLast(); diff --git a/lib/src/page/configuration/configuration_change_page.dart b/lib/src/page/configuration/configuration_change_page.dart index a711d8c..865e1c2 100644 --- a/lib/src/page/configuration/configuration_change_page.dart +++ b/lib/src/page/configuration/configuration_change_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/flutter_bones.dart'; -import 'package:flutter_bones/src/widget/callback_controller_bones.dart'; +import '../../helper/settings.dart'; import '../../widget/edit_form.dart'; +import '../../widget/page_controller_bones.dart'; +import '../application_data.dart'; import 'configuration_controller.dart'; class ConfigurationChangePage extends StatefulWidget { @@ -10,6 +11,7 @@ class ConfigurationChangePage extends StatefulWidget { final Map initialRow; final logger = Settings().logger; final int primaryId; + //ConfigurationChangePageState lastState; ConfigurationChangePage(this.primaryId, this.applicationData, this.initialRow, diff --git a/lib/src/page/configuration/configuration_controller.dart b/lib/src/page/configuration/configuration_controller.dart index 76c7e89..cc579c2 100644 --- a/lib/src/page/configuration/configuration_controller.dart +++ b/lib/src/page/configuration/configuration_controller.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/flutter_bones.dart'; +import '../../helper/settings.dart'; import '../../model/standard/configuration_model.dart'; import '../../widget/page_controller_bones.dart'; +import '../application_data.dart'; import 'configuration_change_page.dart'; class ConfigurationController extends PageControllerBones { diff --git a/lib/src/page/configuration/configuration_create_page.dart b/lib/src/page/configuration/configuration_create_page.dart index 23a7383..a6f8797 100644 --- a/lib/src/page/configuration/configuration_create_page.dart +++ b/lib/src/page/configuration/configuration_create_page.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/flutter_bones.dart'; +import '../../helper/settings.dart'; import '../../widget/edit_form.dart'; +import '../application_data.dart'; import 'configuration_controller.dart'; class ConfigurationCreatePage extends StatefulWidget { diff --git a/lib/src/page/configuration/configuration_list_page.dart b/lib/src/page/configuration/configuration_list_page.dart index 12fbb28..dcc9c70 100644 --- a/lib/src/page/configuration/configuration_list_page.dart +++ b/lib/src/page/configuration/configuration_list_page.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/src/widget/callback_controller_bones.dart'; -import '../../widget/widget_list.dart'; import '../../widget/list_form.dart'; +import '../../widget/page_controller_bones.dart'; import '../application_data.dart'; import 'configuration_controller.dart'; @@ -29,38 +28,38 @@ class ConfigurationListPageState extends State { GlobalKey(debugLabel: 'configuration_list'); Iterable rowsDeprecated; ConfigurationController controller; - WidgetList filters; ConfigurationListPageState(this.applicationData); + @override + void initState() { + super.initState(); + controller = ConfigurationController( + _formKey, this, 'list', context, applicationData, redrawCallback: + (RedrawReason reason, + {String customString, RedrawCallbackFunctionSimple callback}) { + switch (reason) { + case RedrawReason.fetchList: + controller.buildRows(); + break; + case RedrawReason.callback: + callback(RedrawReason.custom, customString); + setState(() {}); + break; + default: + setState(() {}); + break; + } + }); + controller.initialize(); + controller.buildWidgetList(); + controller.buildRows(); + controller.completeAsync(); + } + @override Widget build(BuildContext context) { - if (controller == null) { - controller = ConfigurationController( - _formKey, this, 'list', context, applicationData, redrawCallback: - (RedrawReason reason, - {String customString, - RedrawCallbackFunctionSimple callback}) { - switch (reason) { - case RedrawReason.fetchList: - controller.buildRows(); - break; - case RedrawReason.callback: - callback(RedrawReason.custom, customString); - setState(() {}); - break; - default: - setState(() {}); - break; - } - }); - controller.initialize(); - controller.buildWidgetList(); - controller.buildRows(); - } else { - controller = controller; - } - filters ??= controller.filterSet(pageName: 'list'); + controller.buildHandler(context); return Scaffold( appBar: applicationData.appBarBuilder('Konfigurationen'), drawer: applicationData.drawerBuilder(context), @@ -86,7 +85,7 @@ class ConfigurationListPageState extends State { ), ]), ], - filters: filters, + filters: controller.widgetList, errorMessage: applicationData.lastErrorMessage(controller.page.fullName()), ), diff --git a/lib/src/page/login_page.dart b/lib/src/page/login_page.dart.01 similarity index 100% rename from lib/src/page/login_page.dart rename to lib/src/page/login_page.dart.01 diff --git a/lib/src/page/menu/menu_change_page.dart b/lib/src/page/menu/menu_change_page.dart new file mode 100644 index 0000000..5127b13 --- /dev/null +++ b/lib/src/page/menu/menu_change_page.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +import '../../helper/settings.dart'; +import '../../widget/edit_form.dart'; +import '../../widget/page_controller_bones.dart'; +import '../application_data.dart'; +import 'menu_controller.dart'; + +class MenuChangePage extends StatefulWidget { + final ApplicationData applicationData; + final Map initialRow; + final logger = Settings().logger; + final int primaryId; + + //MenuChangePageState lastState; + + MenuChangePage(this.primaryId, this.applicationData, this.initialRow, + {Key key}) + : super(key: key); + + @override + MenuChangePageState createState() { + final rc = MenuChangePageState(primaryId, applicationData, initialRow); + + /// for unittests: + applicationData.lastModuleState = rc; + return rc; + } +} + +class MenuChangePageState extends State { + final ApplicationData applicationData; + final int primaryId; + final Map initialRow; + final GlobalKey _formKey = + GlobalKey(debugLabel: 'menu_change'); + + MenuController controller; + + MenuChangePageState(this.primaryId, this.applicationData, this.initialRow); + + @override + Widget build(BuildContext context) { + if (controller == null) { + controller = + MenuController(_formKey, this, 'change', context, applicationData, + redrawCallback: (RedrawReason reason, + {String customString, + RedrawCallbackFunctionSimple callback}) { + switch (reason) { + case RedrawReason.callback: + callback(RedrawReason.custom, customString); + setState(() {}); + break; + default: + setState(() {}); + break; + } + }); + controller.initialize(); + } + // controller.buildWidgetList() is called in editForm + return Scaffold( + appBar: applicationData.appBarBuilder('Startmenü ändern'), + drawer: applicationData.drawerBuilder(context), + body: EditForm.editForm( + key: _formKey, + pageController: controller, + configuration: applicationData.configuration, + primaryId: primaryId, + initialRow: initialRow, + )); + } + + void dispose() { + controller.dispose(); + super.dispose(); + } +} diff --git a/lib/src/page/menu/menu_controller.dart b/lib/src/page/menu/menu_controller.dart new file mode 100644 index 0000000..581143a --- /dev/null +++ b/lib/src/page/menu/menu_controller.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import '../../helper/settings.dart'; +import '../../model/standard/menu_model.dart'; +import '../../widget/page_controller_bones.dart'; +import '../application_data.dart'; +import 'menu_change_page.dart'; + +class MenuController extends PageControllerBones { + /// Controller for a page named [pageName]. + MenuController(GlobalKey formKey, State parent, + String pageName, BuildContext context, ApplicationData applicationData, + {Function redrawCallback}) + : super(formKey, parent, MenuModel(Settings().logger), pageName, context, + applicationData, redrawCallback) { + moduleModel.parse(); + } + @override + void startChange(int id, Map row) { + applicationData.pushCaller(this); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MenuChangePage(id, applicationData, row))); + } +} diff --git a/lib/src/page/menu/menu_create_page.dart b/lib/src/page/menu/menu_create_page.dart new file mode 100644 index 0000000..d7acb01 --- /dev/null +++ b/lib/src/page/menu/menu_create_page.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import '../../helper/settings.dart'; +import '../../widget/edit_form.dart'; +import '../application_data.dart'; +import 'menu_controller.dart'; + +class MenuCreatePage extends StatefulWidget { + final ApplicationData applicationData; + final logger = Settings().logger; + + MenuCreatePage(this.applicationData, {Key key}) : super(key: key); + + @override + MenuCreatePageState createState() { + final rc = MenuCreatePageState(applicationData); + + /// for unittests: + applicationData.lastModuleState = rc; + return rc; + } +} + +class MenuCreatePageState extends State { + final ApplicationData applicationData; + + final GlobalKey _formKey = + GlobalKey(debugLabel: 'menu_create'); + + MenuController controller; + + MenuCreatePageState(this.applicationData); + + @override + Widget build(BuildContext context) { + if (controller == null) { + controller = + MenuController(_formKey, this, 'create', context, applicationData); + controller.initialize(); + } + controller.buildWidgetList(); + return Scaffold( + appBar: applicationData.appBarBuilder('Neues Startmenü'), + drawer: applicationData.drawerBuilder(context), + body: EditForm.editForm( + key: _formKey, + pageController: controller, + configuration: applicationData.configuration, + )); + } +} diff --git a/lib/src/page/menu/menu_list_page.dart b/lib/src/page/menu/menu_list_page.dart new file mode 100644 index 0000000..5a0b1a7 --- /dev/null +++ b/lib/src/page/menu/menu_list_page.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; + +import '../../widget/list_form.dart'; +import '../../widget/page_controller_bones.dart'; +import '../application_data.dart'; +import 'menu_controller.dart'; + +class MenuListPage extends StatefulWidget { + final ApplicationData applicationData; + + MenuListPage(this.applicationData, {Key key}) : super(key: key); + + @override + MenuListPageState createState() { + // MenuListPageState.setPageData(pageData); + final rc = MenuListPageState(applicationData); + + /// for unittests: + applicationData.lastModuleState = rc; + return rc; + } +} + +class MenuListPageState extends State { + final ApplicationData applicationData; + + final GlobalKey _formKey = + GlobalKey(debugLabel: 'menu_list'); + Iterable rowsDeprecated; + MenuController controller; + + MenuListPageState(this.applicationData); + + @override + void initState() { + super.initState(); + controller = + MenuController(_formKey, this, 'list', context, applicationData, + redrawCallback: (RedrawReason reason, + {String customString, RedrawCallbackFunctionSimple callback}) { + switch (reason) { + case RedrawReason.fetchList: + controller.buildRows(); + break; + case RedrawReason.callback: + callback(RedrawReason.custom, customString); + setState(() {}); + break; + default: + setState(() {}); + break; + } + }); + controller.initialize(); + controller.buildWidgetList(); + controller.buildRows(); + controller.completeAsync(); + } + + @override + Widget build(BuildContext context) { + controller.buildHandler(context); + return Scaffold( + appBar: applicationData.appBarBuilder('Startmenüs'), + drawer: applicationData.drawerBuilder(context), + body: ListForm.listForm( + key: _formKey, + configuration: applicationData.configuration, + titles: ListForm.stringsToTitles(controller.page.tableTitles), + columnNames: controller.page.tableColumns ?? [], + rows: controller.listRows ?? [], + showEditIcon: true, + pageController: controller, + buttons: [ + ButtonBar(alignment: MainAxisAlignment.center, children: [ + controller.searchButton(), + RaisedButton( + child: Text('Neues Startmenü'), + onPressed: () { + Navigator.pushNamed(context, '/menu/create'); + + /// Force the redraw on return: + controller.listRows = null; + }, + ), + ]), + ], + filters: controller.widgetList, + errorMessage: + applicationData.lastErrorMessage(controller.page.fullName()), + ), + ); + } +} diff --git a/lib/src/page/role/role_change_page.dart b/lib/src/page/role/role_change_page.dart index 4a48fda..3368f04 100644 --- a/lib/src/page/role/role_change_page.dart +++ b/lib/src/page/role/role_change_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/flutter_bones.dart'; -import 'package:flutter_bones/src/widget/callback_controller_bones.dart'; +import '../../helper/settings.dart'; import '../../widget/edit_form.dart'; +import '../../widget/page_controller_bones.dart'; +import '../application_data.dart'; import 'role_controller.dart'; class RoleChangePage extends StatefulWidget { @@ -10,6 +11,7 @@ class RoleChangePage extends StatefulWidget { final Map initialRow; final logger = Settings().logger; final int primaryId; + //RoleChangePageState lastState; RoleChangePage(this.primaryId, this.applicationData, this.initialRow, diff --git a/lib/src/page/role/role_controller.dart b/lib/src/page/role/role_controller.dart index 18f7dbf..13a7406 100644 --- a/lib/src/page/role/role_controller.dart +++ b/lib/src/page/role/role_controller.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/flutter_bones.dart'; +import '../../helper/settings.dart'; import '../../model/standard/role_model.dart'; import '../../widget/page_controller_bones.dart'; +import '../application_data.dart'; import 'role_change_page.dart'; class RoleController extends PageControllerBones { diff --git a/lib/src/page/role/role_create_page.dart b/lib/src/page/role/role_create_page.dart index 875bea9..dc67076 100644 --- a/lib/src/page/role/role_create_page.dart +++ b/lib/src/page/role/role_create_page.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/flutter_bones.dart'; +import '../../helper/settings.dart'; import '../../widget/edit_form.dart'; +import '../application_data.dart'; import 'role_controller.dart'; class RoleCreatePage extends StatefulWidget { diff --git a/lib/src/page/role/role_list_page.dart b/lib/src/page/role/role_list_page.dart index 09e73e0..b373ba4 100644 --- a/lib/src/page/role/role_list_page.dart +++ b/lib/src/page/role/role_list_page.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/src/widget/callback_controller_bones.dart'; -import '../../widget/widget_list.dart'; import '../../widget/list_form.dart'; +import '../../widget/page_controller_bones.dart'; import '../application_data.dart'; import 'role_controller.dart'; @@ -29,38 +28,38 @@ class RoleListPageState extends State { GlobalKey(debugLabel: 'role_list'); Iterable rowsDeprecated; RoleController controller; - WidgetList filters; RoleListPageState(this.applicationData); + @override + void initState() { + super.initState(); + controller = + RoleController(_formKey, this, 'list', context, applicationData, + redrawCallback: (RedrawReason reason, + {String customString, RedrawCallbackFunctionSimple callback}) { + switch (reason) { + case RedrawReason.fetchList: + controller.buildRows(); + break; + case RedrawReason.callback: + callback(RedrawReason.custom, customString); + setState(() {}); + break; + default: + setState(() {}); + break; + } + }); + controller.initialize(); + controller.buildWidgetList(); + controller.buildRows(); + controller.completeAsync(); + } + @override Widget build(BuildContext context) { - if (controller == null) { - controller = - RoleController(_formKey, this, 'list', context, applicationData, - redrawCallback: (RedrawReason reason, - {String customString, - RedrawCallbackFunctionSimple callback}) { - switch (reason) { - case RedrawReason.fetchList: - controller.buildRows(); - break; - case RedrawReason.callback: - callback(RedrawReason.custom, customString); - setState(() {}); - break; - default: - setState(() {}); - break; - } - }); - controller.initialize(); - controller.buildWidgetList(); - controller.buildRows(); - } else { - controller = controller; - } - filters ??= controller.filterSet(pageName: 'list'); + controller.buildHandler(context); return Scaffold( appBar: applicationData.appBarBuilder('Rollen'), drawer: applicationData.drawerBuilder(context), @@ -86,7 +85,7 @@ class RoleListPageState extends State { ), ]), ], - filters: filters, + filters: controller.widgetList, errorMessage: applicationData.lastErrorMessage(controller.page.fullName()), ), diff --git a/lib/src/page/user/hash.dart b/lib/src/page/user/hash.dart new file mode 100644 index 0000000..a12ad4f --- /dev/null +++ b/lib/src/page/user/hash.dart @@ -0,0 +1,8 @@ +import 'package:crypto/crypto.dart'; +import 'dart:convert'; + +String hashPassword(String user, String password) { + var bytes = utf8.encode(user + " + " + password); // data being hashed + var digest = sha1.convert(bytes); + return digest.toString(); +} diff --git a/lib/src/page/user/user_change_page.dart b/lib/src/page/user/user_change_page.dart index 0e9bf2d..61471aa 100644 --- a/lib/src/page/user/user_change_page.dart +++ b/lib/src/page/user/user_change_page.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/flutter_bones.dart'; -import 'package:flutter_bones/src/widget/callback_controller_bones.dart'; - +import '../../helper/settings.dart'; +import '../../model/button_model.dart'; import '../../widget/edit_form.dart'; +import '../../widget/page_controller_bones.dart'; +import '../application_data.dart'; import 'user_controller.dart'; +import 'user_password_page.dart'; class UserChangePage extends StatefulWidget { final ApplicationData applicationData; final Map initialRow; final logger = Settings().logger; final int primaryId; + //UserChangePageState lastState; UserChangePage(this.primaryId, this.applicationData, this.initialRow, @@ -56,6 +59,7 @@ class UserChangePageState extends State { } }); controller.initialize(); + customize(); } // controller.buildWidgetList() is called in editForm return Scaffold( @@ -70,6 +74,19 @@ class UserChangePageState extends State { )); } + void customize() { + ButtonModel button = controller.page.buttonByName('set_password'); + button?.onPressed = () { + String userName = controller.page.fieldByName('user_name').value; + applicationData.pushCaller(controller); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => UserPasswordPage( + primaryId, userName, applicationData, null))); + }; + } + void dispose() { controller.dispose(); super.dispose(); diff --git a/lib/src/page/user/user_controller.dart b/lib/src/page/user/user_controller.dart index 6c69c68..30d72db 100644 --- a/lib/src/page/user/user_controller.dart +++ b/lib/src/page/user/user_controller.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/flutter_bones.dart'; +import '../../helper/settings.dart'; import '../../model/standard/user_model.dart'; import '../../widget/page_controller_bones.dart'; +import '../application_data.dart'; import 'user_change_page.dart'; class UserController extends PageControllerBones { diff --git a/lib/src/page/user/user_create_page.dart b/lib/src/page/user/user_create_page.dart index 0837ca8..c4d34f9 100644 --- a/lib/src/page/user/user_create_page.dart +++ b/lib/src/page/user/user_create_page.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/flutter_bones.dart'; +import '../../helper/settings.dart'; import '../../widget/edit_form.dart'; +import '../application_data.dart'; import 'user_controller.dart'; class UserCreatePage extends StatefulWidget { diff --git a/lib/src/page/user/user_list_page.dart b/lib/src/page/user/user_list_page.dart index 863fc82..0cdc378 100644 --- a/lib/src/page/user/user_list_page.dart +++ b/lib/src/page/user/user_list_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/src/widget/callback_controller_bones.dart'; import '../../widget/list_form.dart'; +import '../../widget/page_controller_bones.dart'; import '../application_data.dart'; import 'user_controller.dart'; diff --git a/lib/src/page/user/user_login_page.dart b/lib/src/page/user/user_login_page.dart new file mode 100644 index 0000000..f451d2d --- /dev/null +++ b/lib/src/page/user/user_login_page.dart @@ -0,0 +1,112 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../../helper/settings.dart'; +import '../../model/button_model.dart'; +import '../../widget/edit_form.dart'; +import '../../widget/page_controller_bones.dart'; +import '../application_data.dart'; +import 'user_controller.dart'; +import 'hash.dart'; + +class UserLoginPage extends StatefulWidget { + final ApplicationData applicationData; + final logger = Settings().logger; + + //UserLoginPageState lastState; + + UserLoginPage(this.applicationData, {Key key}) : super(key: key); + + @override + UserLoginPageState createState() { + final rc = UserLoginPageState(applicationData); + + /// for unittests: + applicationData.lastModuleState = rc; + return rc; + } +} + +class UserLoginPageState extends State { + final ApplicationData applicationData; + final GlobalKey _formKey = + GlobalKey(debugLabel: 'user.login'); + + UserController controller; + + UserLoginPageState(this.applicationData); + + @override + Widget build(BuildContext context) { + if (controller == null) { + controller = + UserController(_formKey, this, 'login', context, applicationData, + redrawCallback: (RedrawReason reason, + {String customString, + RedrawCallbackFunctionSimple callback}) { + switch (reason) { + case RedrawReason.callback: + callback(RedrawReason.custom, customString); + setState(() {}); + break; + default: + setState(() {}); + break; + } + }); + controller.initialize(); + customize(); + } + // controller.buildWidgetList() is called in editForm + return Scaffold( + appBar: applicationData.appBarBuilder('Passwort ändern'), + drawer: applicationData.drawerBuilder(context), + body: EditForm.editForm( + key: _formKey, + pageController: controller, + configuration: applicationData.configuration, + primaryId: 0, + )); + } + + void customize() { + ButtonModel button = controller.page.buttonByName('login'); + button?.onPressed = () { + if (_formKey.currentState.validate()) { + _formKey.currentState.save(); + final user = controller.page.fieldByName('user').value; + + final params = { + ':user': user, + }; + applicationData.persistence + .recordByParameter( + module: controller.moduleModel.name, + sqlName: 'login', + parameters: params) + .then((row) { + if (row != null && row.containsKey('user_name')) { + final user = row['user_name']; + final code = row['user_password']; + final code2 = hashPassword( + user, controller.page.fieldByName('password').value); + if (code == code2) { + applicationData.currentUserId = row['user_id']; + applicationData.currentUserName = row['user_displayname']; + applicationData.currentRoleId = row['user_role']; + applicationData.currentRoleName = row['role_name']; + applicationData.currentRolePriority = row['role_priority']; + applicationData.pushCaller(controller); + Navigator.pushNamed(context, '/user/list'); + } + } + }); + } + }; + } + + void dispose() { + controller.dispose(); + super.dispose(); + } +} diff --git a/lib/src/page/user/user_password_page.dart b/lib/src/page/user/user_password_page.dart new file mode 100644 index 0000000..4e24cb7 --- /dev/null +++ b/lib/src/page/user/user_password_page.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; + +import '../../helper/settings.dart'; +import '../../model/button_model.dart'; +import '../../widget/edit_form.dart'; +import '../../widget/page_controller_bones.dart'; +import '../application_data.dart'; +import 'user_controller.dart'; +import 'hash.dart'; + +class UserPasswordPage extends StatefulWidget { + final ApplicationData applicationData; + final Map initialRow; + final logger = Settings().logger; + final int primaryId; + final String userName; + + //UserPasswordPageState lastState; + + UserPasswordPage( + this.primaryId, this.userName, this.applicationData, this.initialRow, + {Key key}) + : super(key: key); + + @override + UserPasswordPageState createState() { + final rc = UserPasswordPageState(primaryId, this.userName, applicationData); + + /// for unittests: + applicationData.lastModuleState = rc; + return rc; + } +} + +class UserPasswordPageState extends State { + final ApplicationData applicationData; + final int primaryId; + final String userName; + final GlobalKey _formKey = + GlobalKey(debugLabel: 'user.password'); + + UserController controller; + + UserPasswordPageState(this.primaryId, this.userName, this.applicationData); + + @override + Widget build(BuildContext context) { + if (controller == null) { + controller = + UserController(_formKey, this, 'password', context, applicationData, + redrawCallback: (RedrawReason reason, + {String customString, + RedrawCallbackFunctionSimple callback}) { + switch (reason) { + case RedrawReason.callback: + callback(RedrawReason.custom, customString); + setState(() {}); + break; + default: + setState(() {}); + break; + } + }); + controller.initialize(); + customize(); + } + // controller.buildWidgetList() is called in editForm + return Scaffold( + appBar: applicationData.appBarBuilder('Passwort ändern'), + drawer: applicationData.drawerBuilder(context), + body: EditForm.editForm( + key: _formKey, + pageController: controller, + configuration: applicationData.configuration, + primaryId: primaryId, + )); + } + + void customize() { + ButtonModel button = controller.page.buttonByName('store'); + button?.onPressed = () { + if (_formKey.currentState.validate()) { + _formKey.currentState.save(); + final application = controller.applicationData; + final code = hashPassword( + userName, controller.page.fieldByName('user_password').value); + final params = { + ':user_id': primaryId, + ':user_password': code, + ':user_changedby': application.currentUserName + }; + applicationData.persistence + .update( + module: controller.moduleModel.name, + sqlName: 'update_pw', + data: params) + .then((value) { + applicationData.callerRedraw(RedrawReason.fetchList); + applicationData.popCaller(); + Navigator.pop(controller.getContext()); + }); + } + }; + } + + void dispose() { + controller.dispose(); + super.dispose(); + } +} diff --git a/lib/src/private/bdrawer.dart b/lib/src/private/bdrawer.dart index d25571d..7b3dd6b 100644 --- a/lib/src/private/bdrawer.dart +++ b/lib/src/private/bdrawer.dart @@ -2,9 +2,10 @@ import 'package:flutter/material.dart'; import '../page/configuration/configuration_list_page.dart'; import '../page/demo_page.dart'; -import '../page/login_page.dart'; import '../page/role/role_list_page.dart'; import '../page/user/user_list_page.dart'; +import '../page/user/user_login_page.dart'; +import '../page/application_data.dart'; import 'bsettings.dart'; class MenuItem { @@ -14,18 +15,48 @@ class MenuItem { MenuItem(this.title, this.page, this.icon); + static StatefulWidget pageByName( + String name, ApplicationData applicationData) { + var rc; + switch (name) { + case 'user.login': + rc = UserLoginPage(applicationData); + break; + case 'user.list': + rc = UserListPage(applicationData); + break; + case 'role.list': + rc = RoleListPage(applicationData); + break; + case 'configuration.list': + rc = RoleListPage(applicationData); + break; + case 'menu.list': + rc = RoleListPage(applicationData); + break; + case 'demo': + rc = DemoPage(applicationData); + break; + } + return rc; + } + static List menuItems() { final settings = BSettings.lastInstance; return [ - MenuItem('Login', () => LoginPage(settings.pageData), + MenuItem('Login', () => pageByName('user.login', settings.pageData), + Icons.account_box_outlined), + MenuItem('Rollen', () => pageByName('role.list', settings.pageData), Icons.account_box_outlined), - MenuItem('Rollen', () => RoleListPage(settings.pageData), + MenuItem('Benutzer', () => pageByName('user.list', settings.pageData), Icons.account_box_outlined), - MenuItem('Benutzer', () => UserListPage(settings.pageData), + MenuItem( + 'Konfiguration', + () => pageByName('configuration.list', settings.pageData), Icons.account_box_outlined), - MenuItem('Konfiguration', () => ConfigurationListPage(settings.pageData), + MenuItem('Startmenü', () => pageByName('menu.list', settings.pageData), Icons.account_box_outlined), - MenuItem('Demo', () => DemoPage(settings.pageData), + MenuItem('Demo', () => pageByName('demo', settings.pageData), Icons.account_box_outlined), ]; } diff --git a/lib/src/widget/callback_controller_bones.dart b/lib/src/widget/callback_controller_bones.dart deleted file mode 100644 index 3090924..0000000 --- a/lib/src/widget/callback_controller_bones.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../model/combobox_model.dart'; -import '../model/combo_base_model.dart'; -import '../model/field_model.dart'; -import '../page/application_data.dart'; - -typedef RedrawCallbackFunctionSimple = Function( - RedrawReason reason, String customString); -typedef RedrawCallbackFunction = Function(RedrawReason reason, - {String customString, RedrawCallbackFunctionSimple callback}); - -/// Interface for a callback controller for flutter_bones specific widgets. -/// flutter_bones specific widgets: [CheckboxListTileBone], -/// [DropDownButtonFormBone], [RaisedButtonBone], [TextFormFieldBone] -abstract class CallbackControllerBones { - /// Retrieves the rows shown in the list page. - void buildRows(); - - /// Returns the [ComboboxData] (texts and values) of the [ComboboxModel] named [name]. - ComboboxData comboboxData(String name); - - /// Frees all resources. - void dispose(); - - /// Returns the container for application specific information. - ApplicationData getApplicationData(); - - /// Returns the [BuildContext] instance of the page. - BuildContext getContext(); - - /// Returns the [model] named [name]. - FieldModel getModel(String name); - - ValueChanged getOnChanged( - String customString, CallbackControllerBones controller); - ValueChanged getOnChangedCheckbox( - String customString, CallbackControllerBones controller); - - ValueChanged getOnChangedCombobox( - String customString, CallbackControllerBones controller); - - VoidCallback getOnEditingComplete( - String customString, CallbackControllerBones controller); - - ValueChanged getOnFieldSubmitted( - String customString, CallbackControllerBones controller); - - ValueChanged getOnHighlightChanged( - String customString, CallbackControllerBones controller); - - VoidCallback getOnLongPressed( - String customString, CallbackControllerBones controller); - - VoidCallback getOnPressed( - String customString, CallbackControllerBones controller); - - FormFieldSetter getOnSaved( - String customString, CallbackControllerBones controller); - - DropdownButtonBuilder getOnSelectedItemBuilder( - String customString, CallbackControllerBones controller); - - GestureTapCallback getOnTap( - String customString, CallbackControllerBones controller); - - FormFieldValidator getOnValidator( - String customString, CallbackControllerBones controller); - - FormFieldValidator getValidator( - String customString, CallbackControllerBones controller); - - /// Handles the tap event in the table of the list page. - void onEditTap( - String customString, CallbackControllerBones controller, Map row); - - /// Rebuilds the view of the page. - /// [reason] and [customString] will be forwarded to the callback function. - void redraw(RedrawReason reason, - {String customString, RedrawCallbackFunctionSimple callback}); - - /// Returns a standard search button for the list page. - Widget searchButton(); - - /// Starts the change page to edit the record with primary key [id]. - /// [row] contains the db record of [id]. - void startChange(int id, Map row); - - /// Returns the [TextEditingController] instance of a [TextFormField] assigned - /// to a model named [name]. - TextEditingController textController(String name); - - /// Returns the field value of [FieldModel] named [fieldName] or null if not found. - dynamic valueOf(String fieldName); -} - -enum RedrawReason { - custom, - callback, - fetchList, - fetchRecord, - redraw, - setError, -} diff --git a/lib/src/widget/dropdown_button_form_bone.dart b/lib/src/widget/dropdown_button_form_bone.dart index 54cd8f7..a6ae03c 100644 --- a/lib/src/widget/dropdown_button_form_bone.dart +++ b/lib/src/widget/dropdown_button_form_bone.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import 'callback_controller_bones.dart'; +import 'page_controller_bones.dart'; /// Implements a [DropdownButtonFormField] with "outsourced" callbacks: /// [customString] a string mostly used for a name needed in the [customController] /// [callbackController] handles the callback methods. class DropdownButtonFormBone extends DropdownButtonFormField { final String customString; - final CallbackControllerBones callbackController; + final PageControllerBones callbackController; DropdownButtonFormBone( this.customString, diff --git a/lib/src/widget/edit_form.dart b/lib/src/widget/edit_form.dart index 428ef51..90275e4 100644 --- a/lib/src/widget/edit_form.dart +++ b/lib/src/widget/edit_form.dart @@ -2,7 +2,7 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:flutter/material.dart'; import 'page_controller_bones.dart'; -import 'raised_button_bone.dart'; +import 'view.dart'; typedef Function OnEditTap(Map row, int index); @@ -23,6 +23,9 @@ class EditForm { configuration.asFloat('form.card.padding', defaultValue: 16.0); pageController.buildWidgetList(initialRow); final widgets = pageController.getWidgets(); + final view = View(); + final buttons = + view.modelsToWidgets(pageController.page.buttons, pageController); return Form( key: key, child: Card( @@ -37,18 +40,7 @@ class EditForm { 'form.gap.field_button.height', defaultValue: 16.0)), ButtonBar( - children: [ - FlatButton( - child: Text('Abbruch'), - onPressed: pageController.getOnPressed( - 'cancel', pageController), - ), - RaisedButtonBone( - 'store', - pageController, - child: Text('Speichern'), - ), - ], + children: buttons, ), ], ))), diff --git a/lib/src/widget/filter_set.dart b/lib/src/widget/filter_set.dart index 3857700..7553751 100644 --- a/lib/src/widget/filter_set.dart +++ b/lib/src/widget/filter_set.dart @@ -1,11 +1,8 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bones/flutter_bones.dart'; -import 'package:flutter_bones/src/model/combo_base_model.dart'; import 'package:flutter_bones/src/widget/page_controller_bones.dart'; -import 'text_form_field_bone.dart'; - typedef FilterPredicate = bool Function(Map row); @deprecated @@ -46,20 +43,8 @@ class FilterSet { List getWidgets() { final rc = filters.map((filter) { final model = pageController.page.fieldByName(filter.name); - if (model is ComboBaseModel) { - Widget rc3 = View().combobox(model, pageController, null); - return rc3; - } else { - Widget rc2 = TextFormFieldBone( - filter.name, - pageController, - decoration: InputDecoration(labelText: filter.label), - ); - if (filter.toolTip != null) { - rc2 = Tooltip(message: filter.toolTip, child: rc2); - } - return rc2; - } + final widget = View().modelToWidget(model, pageController); + return widget; }).toList(); return rc; } diff --git a/lib/src/widget/page_controller_bones.dart b/lib/src/widget/page_controller_bones.dart index bdb87f4..3c84110 100644 --- a/lib/src/widget/page_controller_bones.dart +++ b/lib/src/widget/page_controller_bones.dart @@ -13,12 +13,16 @@ import '../model/module_model.dart'; import '../model/page_model.dart'; import '../model/widget_model.dart'; import '../page/application_data.dart'; -import 'callback_controller_bones.dart'; import 'view.dart'; import 'widget_list.dart'; +import 'widget_validators.dart'; -// This interface allows the generic handling of the edit form by a model driven module. -class PageControllerBones implements CallbackControllerBones { +typedef RedrawCallbackFunction = Function(RedrawReason reason, + {String customString, RedrawCallbackFunctionSimple callback}); +typedef RedrawCallbackFunctionSimple = Function( + RedrawReason reason, String customString); + +class PageControllerBones implements ValidatorController { final ModuleModel moduleModel; String primaryColumn; WidgetList widgetList; @@ -58,8 +62,8 @@ class PageControllerBones implements CallbackControllerBones { } } - @override - buildRows() { + /// Retrieves the rows shown in the list page. + void buildRows() { final persistence = applicationData.persistence; final params = buildSqlParams() ?? {}; persistence.list(module: moduleModel.name, params: params).then((list) { @@ -110,6 +114,7 @@ class PageControllerBones implements CallbackControllerBones { completeModels(model); widgetList.addWidget(model.name, view.modelToWidget(model, this, value)); }); + page.fields.forEach((element) => prepareModel(element)); } @deprecated @@ -219,7 +224,7 @@ class PageControllerBones implements CallbackControllerBones { return completer.future; } - @override + /// Frees all resources. void dispose() { textControllers.values.forEach((controller) => controller.dispose()); textControllers.clear(); @@ -257,17 +262,17 @@ class PageControllerBones implements CallbackControllerBones { return rc; } - @override + /// Returns the container for application specific information. ApplicationData getApplicationData() { return applicationData; } - @override + /// Returns the [BuildContext] instance of the page. BuildContext getContext() { return context; } - @override + /// Returns the [model] named [name]. FieldModel getModel(String name) { final rc = moduleModel.pageByName(pageName)?.fieldByName(name); return rc; @@ -275,20 +280,15 @@ class PageControllerBones implements CallbackControllerBones { ModuleModel getModuleModel() => moduleModel; - @override - getOnChanged(String customString, CallbackControllerBones controller) { + getOnChanged(String customString, PageControllerBones controller) { return null; } - @override - getOnChangedCheckbox( - String customString, CallbackControllerBones controller) { + getOnChangedCheckbox(String customString, PageControllerBones controller) { return null; } - @override - getOnChangedCombobox( - String customString, CallbackControllerBones controller) { + getOnChangedCombobox(String customString, PageControllerBones controller) { var rc; rc = (value) { final model = page.fieldByName(customString); @@ -297,84 +297,85 @@ class PageControllerBones implements CallbackControllerBones { return rc; } - @override - getOnEditingComplete( - String customString, CallbackControllerBones controller) { + getOnEditingComplete(String customString, PageControllerBones controller) { return null; } - @override - getOnFieldSubmitted(String customString, CallbackControllerBones controller) { + getOnFieldSubmitted(String customString, PageControllerBones controller) { return null; } - @override - getOnHighlightChanged( - String customString, CallbackControllerBones controller) { + getOnHighlightChanged(String customString, PageControllerBones controller) { return null; } - @override - getOnLongPressed(String customString, CallbackControllerBones controller) { + getOnLongPressed(String customString, PageControllerBones controller) { return null; } - @override - getOnPressed(String customString, CallbackControllerBones controller) { - var rc; - if (customString == 'search') { - rc = () { - if (globalKey.currentState.validate()) { - globalKey.currentState.save(); - listRows = null; - buildRows(); - // controller.fetchListRows(); - } - }; - } else if (customString == 'store') { - rc = () { - if (globalKey.currentState.validate()) { - globalKey.currentState.save(); - final params = widgetList.buildSqlParams(this); - PageModelType pageType = - moduleModel.pageByName(pageName).pageModelType; - switch (pageType) { - case PageModelType.create: - applicationData.persistence - .insert(module: moduleModel.name, data: params) - .then((id) { - applicationData.callerRedraw(RedrawReason.fetchList); - applicationData.popCaller(); - Navigator.pop(controller.getContext()); - }); - break; - case PageModelType.change: - applicationData.persistence - .update(module: moduleModel.name, data: params) - .then((value) { - applicationData.callerRedraw(RedrawReason.fetchList); - applicationData.popCaller(); - Navigator.pop(controller.getContext()); - }); - break; - default: - moduleModel.logger - .error('unexpected pageType $pageType for $customString'); - break; - } - } - }; - } else if (customString == 'cancel') { - rc = () { - applicationData.popCaller(); - Navigator.pop(controller.getContext()); - }; + getOnPressed(String customString, PageControllerBones controller) { + var rc = page.buttonByName(customString)?.onPressed; + if (rc == null) { + switch (customString) { + case 'search': + rc = () { + if (globalKey.currentState.validate()) { + globalKey.currentState.save(); + listRows = null; + buildRows(); + // controller.fetchListRows(); + } + }; + break; + case 'store': + rc = () { + if (globalKey.currentState.validate()) { + globalKey.currentState.save(); + final params = widgetList.buildSqlParams(this); + PageModelType pageType = + moduleModel.pageByName(pageName).pageModelType; + switch (pageType) { + case PageModelType.create: + applicationData.persistence + .insert(module: moduleModel.name, data: params) + .then((id) { + applicationData.callerRedraw(RedrawReason.fetchList); + applicationData.popCaller(); + Navigator.pop(controller.getContext()); + }); + break; + case PageModelType.change: + applicationData.persistence + .update(module: moduleModel.name, data: params) + .then((value) { + applicationData.callerRedraw(RedrawReason.fetchList); + applicationData.popCaller(); + Navigator.pop(controller.getContext()); + }); + break; + default: + moduleModel.logger + .error('unexpected pageType $pageType for $customString'); + break; + } + } + }; + break; + case 'cancel': + rc = () { + applicationData.popCaller(); + Navigator.pop(controller.getContext()); + }; + break; + case 'custom': + default: + break; + } } return rc; } - @override - getOnSaved(String customString, CallbackControllerBones controller) { + getOnSaved(String customString, PageControllerBones controller) { final rc = (input) { FieldModel model = page.fieldByName(customString); model.value = StringHelper.fromString(input, model.dataType); @@ -382,25 +383,33 @@ class PageControllerBones implements CallbackControllerBones { return rc; } - @override getOnSelectedItemBuilder( - String customString, CallbackControllerBones controller) { + String customString, PageControllerBones controller) { return null; } - @override - getOnTap(String customString, CallbackControllerBones controller) { + getOnTap(String customString, PageControllerBones controller) { return null; } - @override - getOnValidator(String customString, CallbackControllerBones controller) { - return null; - } - - @override - getValidator(String customString, CallbackControllerBones controller) { - return null; + /// Returns a callback used as validator. + /// [customString]: name of the model (of the current page). + getValidator(String customString, PageControllerBones controller) { + var rc; + final model = page.fieldByName(customString); + if (model.validators.isNotEmpty) { + rc = (String input) { + String rc2; + for (var validator in model.validators) { + rc2 = validator(input, model, controller); + if (rc2 != null) { + break; + } + } + return rc2; + }; + } + return rc; } /// Returns the widgets with at least the input fields of the form defined @@ -429,8 +438,7 @@ class PageControllerBones implements CallbackControllerBones { }); } - @override - onEditTap(String customString, CallbackControllerBones controller, Map row) { + onEditTap(String customString, PageControllerBones controller, Map row) { ColumnModel primary = moduleModel.mainTable().primary; final id = row[primary.name]; applicationData.persistence @@ -438,7 +446,8 @@ class PageControllerBones implements CallbackControllerBones { .then((row) => startChange(id, row)); } - @override + /// Rebuilds the view of the page. + /// [reason] and [customString] will be forwarded to the callback function. void redraw(RedrawReason reason, {String customString, RedrawCallbackFunctionSimple callback}) { if (redrawCallback == null) { @@ -458,22 +467,22 @@ class PageControllerBones implements CallbackControllerBones { Navigator.pushNamed(context, route); } - @override + /// Returns a standard search button for the list page. Widget searchButton() { final rc = View(moduleModel.logger) .modelToWidget(page.buttonByName('search'), this); return rc; } - @override + /// Starts the change page to edit the record with primary key [id]. + /// [row] contains the db record of [id]. void startChange(int id, Map row) { moduleModel.logger.error( 'missing override of startChange() in ${moduleModel.fullName()}'); } - /// Returns the [TextEditingController] of a [TextFormField] assigned to - /// a model named [name]. - @override + /// Returns the [TextEditingController] instance of a [TextFormField] assigned + /// to a model named [name]. TextEditingController textController(String name) { if (!textControllers.containsKey(name)) { textControllers[name] = TextEditingController(); @@ -481,9 +490,19 @@ class PageControllerBones implements CallbackControllerBones { return textControllers[name]; } - @override + /// Returns the field value of [FieldModel] named [fieldName] or null if not found. dynamic valueOf(String fieldName) { final rc = page.fieldByName(fieldName)?.value; return rc; } } + +// This interface allows the generic handling of the edit form by a model driven module. +enum RedrawReason { + custom, + callback, + fetchList, + fetchRecord, + redraw, + setError, +} diff --git a/lib/src/widget/raised_button_bone.dart b/lib/src/widget/raised_button_bone.dart index 6875e3c..fc33c88 100644 --- a/lib/src/widget/raised_button_bone.dart +++ b/lib/src/widget/raised_button_bone.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import 'callback_controller_bones.dart'; +import '../widget/page_controller_bones.dart'; /// Implements a raised button with two additional properties: /// [customString] a string often used for a name needed in a callback method /// [customObject] an object known by the controller, often used in callback methods like onPressed class RaisedButtonBone extends RaisedButton { final String customString; - final CallbackControllerBones callbackController; + final PageControllerBones callbackController; RaisedButtonBone(this.customString, this.callbackController, {ButtonTextTheme textTheme, diff --git a/lib/src/widget/text_form_field_bone.dart b/lib/src/widget/text_form_field_bone.dart index af89457..da91f2a 100644 --- a/lib/src/widget/text_form_field_bone.dart +++ b/lib/src/widget/text_form_field_bone.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'callback_controller_bones.dart'; +import '../widget/page_controller_bones.dart'; /// Interface for a [TextFormField] specific callback controller. abstract class TextFormCallbackController { @@ -29,7 +29,7 @@ abstract class TextFormCallbackController { /// [callbackController] handles the callback methods. class TextFormFieldBone extends TextFormField { final String customString; - final CallbackControllerBones callbackController; + final PageControllerBones callbackController; TextFormFieldBone( this.customString, @@ -63,7 +63,7 @@ class TextFormFieldBone extends TextFormField { bool expands = false, int maxLength, List inputFormatters, - bool enabled, + bool enabled = true, double cursorWidth = 2.0, double cursorHeight, Radius cursorRadius, @@ -77,7 +77,7 @@ class TextFormFieldBone extends TextFormField { AutovalidateMode autovalidateMode, }) : super( controller: controller, - initialValue: initialValue, + initialValue: controller != null ? null : initialValue, focusNode: focusNode, decoration: decoration, keyboardType: keyboardType, @@ -103,17 +103,23 @@ class TextFormFieldBone extends TextFormField { minLines: minLines, expands: expands, maxLength: maxLength, - onChanged: - callbackController.getOnChanged(customString, callbackController), - onTap: callbackController.getOnTap(customString, callbackController), + onChanged: readOnly + ? null + : callbackController.getOnChanged( + customString, callbackController), + onTap: readOnly + ? null + : callbackController.getOnTap(customString, callbackController), onEditingComplete: callbackController.getOnEditingComplete( customString, callbackController), onFieldSubmitted: callbackController.getOnFieldSubmitted( customString, callbackController), onSaved: callbackController.getOnSaved(customString, callbackController), - validator: - callbackController.getValidator(customString, callbackController), + validator: readOnly + ? null + : callbackController.getValidator( + customString, callbackController), inputFormatters: inputFormatters, enabled: enabled, cursorWidth: cursorWidth, diff --git a/lib/src/widget/view.dart b/lib/src/widget/view.dart index f6e1324..9b56fd1 100644 --- a/lib/src/widget/view.dart +++ b/lib/src/widget/view.dart @@ -3,18 +3,18 @@ import 'package:flutter/material.dart'; import '../helper/settings.dart'; import '../helper/string_helper.dart'; +import '../model/button_model.dart'; +import '../model/combo_base_model.dart'; import '../model/db_reference_model.dart'; -import '../model/model_types.dart'; import '../model/empty_line_model.dart'; import '../model/field_model.dart'; +import '../model/model_types.dart'; import '../model/section_model.dart'; import '../model/text_model.dart'; import '../model/widget_model.dart'; -import '../model/button_model.dart'; -import '../model/combo_base_model.dart'; -import 'page_controller_bones.dart'; import 'checkbox_list_tile_bone.dart'; import 'dropdown_button_form_bone.dart'; +import 'page_controller_bones.dart'; import 'raised_button_bone.dart'; import 'text_form_field_bone.dart'; @@ -39,11 +39,18 @@ class View { /// Creates a button from the [controller]. Widget button(ButtonModel model, PageControllerBones controller) { Widget rc; - rc = RaisedButtonBone( - model.name, - controller, - child: Text(model.text), - ); + if (model.buttonModelType == ButtonModelType.cancel) { + rc = FlatButton( + child: Text(model.label ?? 'Cancel'), + onPressed: controller.getOnPressed('cancel', controller), + ); + } else { + rc = RaisedButtonBone( + model.name, + controller, + child: Text(model.label ?? 'Save'), + ); + } if (model.toolTip != null) { rc = Tooltip(message: model.toolTip, child: rc); } @@ -302,16 +309,23 @@ class View { /// Creates a form text field from the [model]. Widget textField( FieldModel model, PageControllerBones controller, initialValue) { - final value = - initialValue == null ? null : StringHelper.asString(initialValue); + final readOnly = model.hasOption('readonly'); + final value = StringHelper.asString( + initialValue ?? model.value ?? model.defaultValue); final textController = controller.textController(model.name); - textController.text = value; + if (textController != null) { + textController.text = value; + initialValue = null; + } var rc = toolTip( TextFormFieldBone( model.name, controller, //validator: model.validator, + initialValue: value, controller: textController, - readOnly: model.hasOption('readonly'), + // ToDo: readonly doesn't work: workaround with "disable" + enabled: !model.hasOption('disabled') && !readOnly, + readOnly: readOnly, decoration: InputDecoration(labelText: model.label), obscureText: model.hasOption('password'), ), diff --git a/lib/src/widget/widget_list.dart b/lib/src/widget/widget_list.dart index 897787b..469f5f6 100644 --- a/lib/src/widget/widget_list.dart +++ b/lib/src/widget/widget_list.dart @@ -77,7 +77,7 @@ class WidgetList { final name = controller.moduleModel.mainTable().name + (isCreatePage ? '_createdby' : '_changedby'); if (!rc.containsKey(name)) { - rc[':$name'] = controller.getApplicationData().currentUser; + rc[':$name'] = controller.getApplicationData().currentUserName; } return rc; } diff --git a/lib/src/widget/widget_validators.dart b/lib/src/widget/widget_validators.dart new file mode 100644 index 0000000..c9389d9 --- /dev/null +++ b/lib/src/widget/widget_validators.dart @@ -0,0 +1,281 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:flutter_bones/src/widget/page_controller_bones.dart'; + +/// Converts the [model.validatorsString] into entries of [model.validators]. +/// Note: This method is not part of [FieldModel] because the model don't know +/// [PageControllerBones]. +void prepareModel(FieldModel model) { + var validator; + if (model.validatorsString != null) { + model.validatorsString.split(' ').forEach((element) { + final nameOption = element.split('='); + switch (nameOption[0]) { + case 'date': + validator = validateDate; + break; + case 'dateTime': + validator = validateDateTime; + break; + case 'email': + validator = validateEmail; + break; + case 'equals': + validator = validateEquals; + break; + case 'int': + validator = validateInt; + break; + case 'maxDate': + validator = validateMaxDate; + break; + case 'maxDateTime': + validator = validateMaxDateTime; + break; + case 'maxInt': + validator = validateMaxInt; + break; + case 'minDate': + validator = validateMinDate; + break; + case 'minDateTime': + validator = validateMinDateTime; + break; + case 'minInt': + validator = validateMinInt; + break; + case 'regExpr': + validator = validateRegExpr; + break; + case 'required': + validator = validateRequired; + break; + case 'unique': + default: + model.logger.error('prepareModel(): not implemented: $element'); + break; + } + if (validator != null) { + model.validators.add(validator); + } + }); + } +} + +/// Overwrites a model specific error message. +/// [defaultMessage]: null or the standard error message. +/// [name]: the name of the validator, e.g. "regExpr" +/// [model]: the model containing the model specific message. +/// Returns null if defaultMessage is null. Otherwise the model specific error +/// message is returned or [defaultMessage] if there is no model specific error. +String updateMessage(String defaultMessage, String name, FieldModel model) { + String rc = defaultMessage; + if (rc != null && model.messageOfValidator.containsKey(name)) { + rc = model.messageOfValidator[name]; + } + return rc; +} + +/// Validates whether [input] is a valid date. +/// Return null on success or error message otherwise. +String validateDate( + String input, FieldModel model, ValidatorController controller) { + String rc; + try { + StringUtils.stringToDateTime(input); + if (input.contains(':')) { + rc = "Datum mit Uhrzeit ist nicht erlaubt, nur Datum eingeben"; + } + } on ArgumentError { + rc = 'kein Datum erkannt: $input'; + } + return updateMessage(rc, 'date', model); +} + +/// Validates whether [input] is a valid date time. +/// Return null on success or error message otherwise. +String validateDateTime( + String input, FieldModel model, ValidatorController controller) { + String rc; + try { + StringUtils.stringToDateTime(input); + } on ArgumentError { + rc = 'keinen Zeitpunkt erkannt: $input'; + } + return updateMessage(rc, 'dateTime', model); +} + +/// Validates whether [input] is a valid email address. +/// Return null on success or error message otherwise. +String validateEmail( + String input, FieldModel model, ValidatorController controller) { + String rc = checkEmail(input); + return rc; +} + +/// Validates whether [input] is a valid email address. +/// Return null on success or error message otherwise. +String validateInt( + String input, FieldModel model, ValidatorController controller) { + String rc = checkInt(input); + return updateMessage(rc, 'int', model); +} + +/// Validates whether [input] is a valid date and tests whether [input] is +/// before or equals a given limit. +/// Return null on success or error message otherwise. +String validateMaxDate( + String input, FieldModel model, ValidatorController controller) { + String rc; + try { + final date = StringUtils.stringToDateTime(input); + final limit = model.validatorParameter('maxDate'); + if (date.isAfter(limit)) { + rc = + 'Datum zu jung: $input > ${StringHelper.asDatabaseString(limit, DataType.date)}'; + } + } on ArgumentError { + rc = 'kein Datum erkannt: $input'; + } + return updateMessage(rc, 'maxDate', model); +} + +/// Validates whether [input] is a valid date time and tests whether [input] is +/// before or equals a given limit. +/// Return null on success or error message otherwise. +String validateMaxDateTime( + String input, FieldModel model, ValidatorController controller) { + String rc; + try { + final date = StringUtils.stringToDateTime(input); + final limit = model.validatorParameter('maxDateTime'); + if (date.isAfter(limit)) { + rc = + 'Zeitpunkt zu jung: $input > ${StringHelper.asDatabaseString(limit, DataType.dateTime)}'; + } + } on ArgumentError { + rc = 'keinen Zeitpunkt erkannt: $input'; + } + return updateMessage(rc, 'maxDateTime', model); +} + +/// Validates whether [input] is a valid date time and tests whether [input] is +/// before or equals a given limit. +/// Return null on success or error message otherwise. +String validateMaxInt( + String input, FieldModel model, ValidatorController controller) { + String rc; + final current = StringUtils.asInt(input); + if (current == null) { + rc = 'keine Ganzzahl erkannt: $input'; + } else { + final limit = model.validatorParameter('maxInt'); + if (current > limit) { + rc = 'Zahl zu groß: $input > $limit'; + } + } + return updateMessage(rc, 'maxInt', model); +} + +/// Validates whether [input] is a valid date and tests whether [input] is +/// before or equals a given limit. +/// Return null on success or error message otherwise. +String validateMinDate( + String input, FieldModel model, ValidatorController controller) { + String rc; + try { + final date = StringUtils.stringToDateTime(input); + final limit = model.validatorParameter('minDate'); + if (date.isBefore(limit)) { + rc = + 'Datum zu alt: $input > ${StringHelper.asDatabaseString(limit, DataType.date)}'; + } + } on ArgumentError { + rc = 'kein Datum erkannt: $input'; + } + return updateMessage(rc, 'minDate', model); +} + +/// Validates whether [input] is a valid date time and tests whether [input] is +/// before or equals a given limit. +/// Return null on success or error message otherwise. +String validateMinDateTime( + String input, FieldModel model, ValidatorController controller) { + String rc; + try { + final date = StringUtils.stringToDateTime(input); + final limit = model.validatorParameter('minDateTime'); + if (date.isBefore(limit)) { + rc = + 'Zeitpunkt zu alt: $input > ${StringHelper.asDatabaseString(limit, DataType.dateTime)}'; + } + } on ArgumentError { + rc = 'keinen Zeitpunkt erkannt: $input'; + } + return updateMessage(rc, 'minDateTime', model); +} + +/// Validates whether [input] is a valid date time and tests whether [input] is +/// before or equals a given limit. +/// Return null on success or error message otherwise. +String validateMinInt( + String input, FieldModel model, ValidatorController controller) { + String rc; + final current = StringUtils.asInt(input); + if (current == null) { + rc = 'keine Ganzzahl erkannt: $input'; + } else { + final limit = model.validatorParameter('minInt'); + if (current < limit) { + rc = 'Ganzzahl zu klein: $input < $limit'; + } + } + return updateMessage(rc, 'minInt', model); +} + +/// Validates whether [input] is equals to another field content. +/// Return null on success or error message otherwise. +String validateEquals( + String input, FieldModel model, ValidatorController controller) { + String rc; + PageControllerBones controller2 = controller; + List nameLabel = model.validatorParameter('equals')?.split(':'); + final name = nameLabel == null ? null : nameLabel[0]; + final label = nameLabel == null ? 'vorigem Feld' : nameLabel[1]; + final value = controller2.textControllers.containsKey(name) + ? controller2.textControllers[name].value.text + : null; + if (input != value) { + rc = 'Eingaben von ${model?.label} und $label stimmen nicht überein'; + } + return updateMessage(rc, 'equals', model); +} + +/// Validates whether [input] is a valid date. +/// Return null on success or error message otherwise. +String validateRegExpr( + String input, FieldModel model, ValidatorController controller) { + String rc; + if (input.isNotEmpty) { + try { + RegExp regEx = model.validatorParameter('regExpr'); + if (regEx?.firstMatch(input) == null) { + rc = 'Unzulässige Eingabe "$input" in ${model.label}'; + } + } on FormatException catch (exc) { + rc = '${model.fullName()}: invalid regexpr: $exc'; + } + } + return updateMessage(rc, 'regExpr', model); +} + +/// Validates whether [input] is a valid date. +/// Return null on success or error message otherwise. +String validateRequired( + String input, FieldModel model, ValidatorController controller) { + String rc; + if (input.isEmpty) { + rc = 'Bitte Feld ${model.label} ausfüllen'; + } + return updateMessage(rc, 'required', model); +} diff --git a/model_tool/Export b/model_tool/Export new file mode 100755 index 0000000..c5e1fee --- /dev/null +++ b/model_tool/Export @@ -0,0 +1,3 @@ +#! /bin/bash +dart lib/main.dart + diff --git a/pubspec.yaml b/pubspec.yaml index b7e6ee7..6cb3154 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: flutter_localizations: sdk: flutter dart_bones: "^0.4.5" - + crypto: ^2.1.5 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. diff --git a/test/model/standard_test.dart b/test/model/standard_test.dart index 7e07669..4aa7bef 100644 --- a/test/model/standard_test.dart +++ b/test/model/standard_test.dart @@ -31,7 +31,7 @@ void main() { ] # create.simpleForm1 == page change: PageModelType.change options: = section simpleForm1: SectionModelType.simpleForm options: [ - allDbFields 16 options: + allDbFields 17 options: ] # change.simpleForm1 == page list: PageModelType.list options: = section filterPanel1: SectionModelType.filterPanel options: [ @@ -53,10 +53,10 @@ void main() { == table user: options: column user_id: DataType.int "Id" options: primary notnull unique readonly column user_name: DataType.string "User" options: unique notnull - column user_displayname: DataType.string "Anzeigename" options: unique - column user_email: DataType.string "EMail" options: unique - column user_password: DataType.string "Passwort" options: password - column user_role: DataType.reference "Role" options: + column user_displayname: DataType.string "Anzeigename" options: unique notnull + column user_email: DataType.string "EMail" options: unique notnull + column user_password: DataType.string "Passwort" options: password hidden + column user_role: DataType.reference "Rolle" options: undef column user_createdat: DataType.dateTime "Erzeugt" options: hidden null column user_createdby: DataType.string "Erzeugt von" options: hidden column user_changedat: DataType.dateTime "Geändert" options: hidden null @@ -68,12 +68,24 @@ void main() { == page change: PageModelType.change options: = section simpleForm1: SectionModelType.simpleForm options: [ allDbFields 20 options: + button set_password: text: options: Passwort ändern ] # change.simpleForm1 +== page password: PageModelType.change options: + = section simpleForm1: SectionModelType.simpleForm options: [ + textField user_password: options: password + textField repetition: options: password + ] # password.simpleForm1 == page list: PageModelType.list options: = section filterPanel1: SectionModelType.filterPanel options: [ - textField user_name: options: - textField user_role: options: + textField user_name: options: unique notnull + textField user_role: options: undef ] # list.filterPanel1 +== page login: PageModelType.change options: noAutoButton + = section simpleForm1: SectionModelType.simpleForm options: [ + textField user: options: + textField password: options: password + button login: text: options: Anmelden + ] # login.simpleForm1 ''')); }); test('configuration', () { @@ -105,13 +117,46 @@ void main() { ] # create.simpleForm1 == page change: PageModelType.change options: = section simpleForm1: SectionModelType.simpleForm options: [ - allDbFields 22 options: + allDbFields 23 options: ] # change.simpleForm1 == page list: PageModelType.list options: = section filterPanel1: SectionModelType.filterPanel options: [ textField configuration_scope: options: textField configuration_property: options: ] # list.filterPanel1 +''')); + }); + test('menu', () { + WidgetModel.lastId = 0; + logger.clear(); + final module = MenuModel(logger); + module.parse(); + expect(module.fullName(), equals('menu')); + expect(module.widgetName(), equals('menu')); + final errors = logger.errors; + expect(errors.length, equals(0)); + final dump = module.dump(StringBuffer()).toString(); + expect(dump, equals('''= module menu: options: +== table menu: options: + column menu_id: DataType.int "Id" options: primary notnull unique readonly + column menu_name: DataType.string "Name" options: unique notnull + column menu_icon: DataType.reference "Bild" options: + column menu_createdat: DataType.dateTime "Erzeugt" options: hidden null + column menu_createdby: DataType.string "Erzeugt von" options: hidden + column menu_changedat: DataType.dateTime "Geändert" options: hidden null + column menu_changedby: DataType.string "Geändert von" options: hidden +== page create: PageModelType.create options: + = section simpleForm1: SectionModelType.simpleForm options: [ + allDbFields 9 options: + ] # create.simpleForm1 +== page change: PageModelType.change options: + = section simpleForm1: SectionModelType.simpleForm options: [ + allDbFields 15 options: + ] # change.simpleForm1 +== page list: PageModelType.list options: + = section filterPanel1: SectionModelType.filterPanel options: [ + textField menu_name: options: unique notnull + ] # list.filterPanel1 ''')); }); }); diff --git a/test/widget/widget_validators_test.dart b/test/widget/widget_validators_test.dart new file mode 100644 index 0000000..a233e29 --- /dev/null +++ b/test/widget/widget_validators_test.dart @@ -0,0 +1,227 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// 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:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:flutter_bones/src/widget/widget_validators.dart'; +import 'package:test/test.dart'; + +void main() { + final logger = MemoryLogger(); + final widgetConfiguration = BaseConfiguration({}, logger); + Settings(logger: logger, widgetConfiguration: widgetConfiguration); + //Persistence persistence; + setUpAll(() { + // final configuration = BaseConfiguration({ + // 'client': { + // 'host': 'localhost', + // 'port': 58011, + // 'schema': 'http', + // 'application': 'unittest', + // 'version': '1.0.0', + // } + // }, logger); + //persistence = RestPersistence.fromConfig(configuration, logger); + }); + group('independent', () { + test('int', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'options': 'noPages', + 'tables': [ + { + 'table': 'test', + 'columns': [ + { + 'column': 'test_int', + 'dataType': 'int', + 'validators': 'int minInt=3 maxInt=10', + }, + ] + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final table = module.tableByName('test'); + expect(table, isNotNull); + final model = table.columnByName('test_int'); + model.parseFinish(); + expect(model, isNotNull); + expect(logger.errors.length, 0); + prepareModel(model); + expect(model.validators.length, 3); + expect(model.validators[0]('33', model, null), isNull); + expect(model.validators[0]('blub', model, null), + equals('Not an integer: blub')); + expect(model.validators[1]('4', model, null), isNull); + expect(model.validators[1]('blub', model, null), + equals('keine Ganzzahl erkannt: blub')); + expect(model.validators[1]('2', model, null), + equals('Ganzzahl zu klein: 2 < 3')); + expect(model.validators[2]('4', model, null), isNull); + expect(model.validators[2]('blub', model, null), + equals('keine Ganzzahl erkannt: blub')); + expect(model.validators[2]('11', model, null), + equals('Zahl zu groß: 11 > 10')); + }); + test('date', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'options': 'noPages', + 'tables': [ + { + 'table': 'test', + 'columns': [ + { + 'column': 'test_date', + 'dataType': 'date', + 'validators': 'date minDate=2020.2.1 maxDate=2020.10.3', + }, + ] + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final table = module.tableByName('test'); + expect(table, isNotNull); + final model = table.columnByName('test_date'); + expect(model, isNotNull); + model.parseFinish(); + expect(logger.errors.length, 0); + prepareModel(model); + expect(model.validators.length, 3); + expect(model.validators[0]('2020.2.2', model, null), isNull); + expect(model.validators[0]('blub', model, null), + equals('kein Datum erkannt: blub')); + expect(model.validators[1]('2020.2.2', model, null), isNull); + expect(model.validators[1]('blub', model, null), + equals('kein Datum erkannt: blub')); + expect(model.validators[1]('2020.1.1', model, null), + equals('Datum zu alt: 2020.1.1 > 2020-02-01')); + expect(model.validators[2]('2020.2.2', model, null), isNull); + expect(model.validators[2]('blub', model, null), + equals('kein Datum erkannt: blub')); + expect(model.validators[2]('2020.11.1', model, null), + equals('Datum zu jung: 2020.11.1 > 2020-10-03')); + }); + test('dateTime', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'options': 'noPages', + 'tables': [ + { + 'table': 'test', + 'columns': [ + { + 'column': 'test_date', + 'dataType': 'dateTime', + 'validators': + 'dateTime minDateTime=2020.2.1-11:03 maxDateTime=2020.10.3-10:44', + }, + ] + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final table = module.tableByName('test'); + expect(table, isNotNull); + final model = table.columnByName('test_date'); + expect(model, isNotNull); + expect(logger.errors.length, 0); + prepareModel(model); + model.parseFinish(); + expect(model.validators.length, 3); + expect(model.validators[0]('2020.2.2-10:33', model, null), isNull); + expect(model.validators[0]('blub', model, null), + equals('keinen Zeitpunkt erkannt: blub')); + expect(model.validators[1]('2020.2.2-02:44', model, null), isNull); + expect(model.validators[1]('blub', model, null), + equals('keinen Zeitpunkt erkannt: blub')); + expect(model.validators[1]('2020.1.1-3:44', model, null), + equals('Zeitpunkt zu alt: 2020.1.1-3:44 > 2020-02-01 11:03:00')); + expect(model.validators[2]('2020.2.2-23:59', model, null), isNull); + expect(model.validators[2]('blub', model, null), + equals('keinen Zeitpunkt erkannt: blub')); + expect(model.validators[2]('2020.11.1-00:22', model, null), + equals('Zeitpunkt zu jung: 2020.11.1-00:22 > 2020-10-03 10:44:00')); + }); + test('string', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'options': 'noPages', + 'tables': [ + { + 'table': 'test', + 'columns': [ + { + 'column': 'test', + 'dataType': 'string', + 'label': 'str', + 'size': '1', + 'validators': r'email required regExpr=i/^[a-z]$', + }, + { + 'column': 'test2', + 'dataType': 'string', + 'size': '1', + 'validators': r'required equals=test regExpr=/^[a-z]$', + 'validatorsText': 'required=missing|regExpr=No Name', + }, + ] + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final table = module.tableByName('test'); + expect(table, isNotNull); + var model = table.columnByName('test'); + final page = PageModel(module, null, logger); + page.addField(model); + model.page = page; + expect(model, isNotNull); + model.parseFinish(); + model.value = 'B'; + expect(logger.errors.length, 0); + prepareModel(model); + expect(model.validators.length, 3); + expect(model.validators[0]('x', model, null), isNull); + expect(model.validators[0]('', model, null), + equals('Bitte Feld str ausfüllen')); + expect(model.validators[1]('a@br.de', model, null), isNull); + expect(model.validators[1]('a.br.de', model, null), + equals('Not an email address: a.br.de Example: joe@example.com')); + expect(model.validators[2]('A', model, null), isNull); + expect(model.validators[2]('ab', model, null), + 'Unzulässige Eingabe "ab" in str'); + expect(model.validators[2]('5', model, null), + 'Unzulässige Eingabe "5" in str'); + model = table.columnByName('test2'); + expect(model, isNotNull); + page.addField(model); + model.page = page; + model.parseFinish(); + prepareModel(model); + expect(model, isNotNull); + expect(model.validators.length, 3); + expect(model.validators[0]('', model, null), equals('missing')); + expect(model.validators[1]('B', model, null), isNull); + expect(model.validators[1]('A', model, null), equals('Eingaben von null und str stimmen nicht überein')); + expect(model.validators[2]('A', model, null), equals('No Name')); + }); + }); +} -- 2.39.5