]> gitweb.hamatoma.de Git - flutter_bones.git/commitdiff
daily work: user+configuration+role work, new: menu
authorHamatoma <author@hamatoma.de>
Sun, 8 Nov 2020 14:52:05 +0000 (15:52 +0100)
committerHamatoma <author@hamatoma.de>
Tue, 10 Nov 2020 23:06:04 +0000 (00:06 +0100)
62 files changed:
CreateModule
data/ddl/menu.sql [new file with mode: 0644]
data/ddl/user.sql
data/rest/configuration.yaml
data/rest/custom.user.yaml [new file with mode: 0644]
data/rest/menu.yaml [new file with mode: 0644]
data/rest/role.yaml
data/rest/user.yaml
lib/app.dart
lib/flutter_bones.dart
lib/src/helper/string_helper.dart
lib/src/model/button_model.dart
lib/src/model/checkbox_model.dart
lib/src/model/column_model.dart
lib/src/model/combobox_model.dart
lib/src/model/db_reference_model.dart
lib/src/model/field_model.dart
lib/src/model/model_types.dart
lib/src/model/module_model.dart
lib/src/model/page_model.dart
lib/src/model/standard/menu_model.dart [new file with mode: 0644]
lib/src/model/standard/standard_modules.dart
lib/src/model/standard/user_model.dart
lib/src/model/text_field_model.dart
lib/src/model/widget_model.dart
lib/src/page/application_data.dart
lib/src/page/configuration/configuration_change_page.dart
lib/src/page/configuration/configuration_controller.dart
lib/src/page/configuration/configuration_create_page.dart
lib/src/page/configuration/configuration_list_page.dart
lib/src/page/login_page.dart [deleted file]
lib/src/page/login_page.dart.01 [new file with mode: 0644]
lib/src/page/menu/menu_change_page.dart [new file with mode: 0644]
lib/src/page/menu/menu_controller.dart [new file with mode: 0644]
lib/src/page/menu/menu_create_page.dart [new file with mode: 0644]
lib/src/page/menu/menu_list_page.dart [new file with mode: 0644]
lib/src/page/role/role_change_page.dart
lib/src/page/role/role_controller.dart
lib/src/page/role/role_create_page.dart
lib/src/page/role/role_list_page.dart
lib/src/page/user/hash.dart [new file with mode: 0644]
lib/src/page/user/user_change_page.dart
lib/src/page/user/user_controller.dart
lib/src/page/user/user_create_page.dart
lib/src/page/user/user_list_page.dart
lib/src/page/user/user_login_page.dart [new file with mode: 0644]
lib/src/page/user/user_password_page.dart [new file with mode: 0644]
lib/src/private/bdrawer.dart
lib/src/widget/callback_controller_bones.dart [deleted file]
lib/src/widget/dropdown_button_form_bone.dart
lib/src/widget/edit_form.dart
lib/src/widget/filter_set.dart
lib/src/widget/page_controller_bones.dart
lib/src/widget/raised_button_bone.dart
lib/src/widget/text_form_field_bone.dart
lib/src/widget/view.dart
lib/src/widget/widget_list.dart
lib/src/widget/widget_validators.dart [new file with mode: 0644]
model_tool/Export [new file with mode: 0755]
pubspec.yaml
test/model/standard_test.dart
test/widget/widget_validators_test.dart [new file with mode: 0644]

index baf291ff3a290ede25501ee81cdcf1d3ec301ad6..d513e73ba439b8b5619ad28302beb0ccac114b1f 100755 (executable)
@@ -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 (file)
index 0000000..44cc7f3
--- /dev/null
@@ -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)
+);
index 84441b664798c1886e35598040196416377825a1..a32e844492b319302320bf99ff7b30341d656867 100644 (file)
@@ -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,
index 98f5ae0bc1c0bd0c508de01afc21ad383a362114..6fd0ce367fa53f2c52bb8fb2930ec1f930caa408 100644 (file)
@@ -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 (file)
index 0000000..37243eb
--- /dev/null
@@ -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 (file)
index 0000000..54966a8
--- /dev/null
@@ -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;"
index c621943b8c629d36a7da8977fdf0f8356b2c5128..f59f8a29d50588b7eb5bab58d463d25c553a4d0f 100644 (file)
@@ -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)
index c85fad982d413eef30f83273413864f230f016e9..f99d0acb12f62fc2afda717ed5c574600ddcff3b 100644 (file)
@@ -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);"
index b40f1234ff9469f74fdc24ac351c8c18e11c6051..d79e985add9288445892dff73076fccba27fe732 100644 (file)
@@ -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<BoneApp> {
         primarySwatch: Colors.blue,
         visualDensity: VisualDensity.adaptivePlatformDensity,
       ),
-      initialRoute: '/user/list',
+      initialRoute: '/menu/list',
       //initialRoute: '/async',
       onGenerateRoute: _getRoute,
     );
@@ -73,8 +75,15 @@ Route<dynamic> _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) {
index 1220da33804abcdde0db069078925aeb049f42b6..f2760bfec9a7c2bd2eb0a17062078b00b333cf32 100644 (file)
@@ -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';
index ba9224e30057003e943e8f3ad116040a43007df3..484b30874eb9799e917ea8f0fb38bb1207a2de91 100644 (file)
@@ -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:
index d2a5a29a09b17afc3c87682d35ae4b0e352cef2f..600f2c0f01dda2065c60176d60d98755411886cb 100644 (file)
@@ -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<String, dynamic> map;
   List<String> 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<ButtonModelType>('buttonType', map, ButtonModelType.values);
index 79c4c16bd564d4ce0c0b712a6ffae0cc935e83b5..dd6f281e7cd148ffc4b7dd8ab385cd22ab68911d 100644 (file)
@@ -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]
index 69ecb4c511d1720d6728ee97252638e696919f30..eb57c90f016fe2667e3911359996c68170172e69 100644 (file)
@@ -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 =
index da2a9483a3054ee58d4cb6852147f328adeb9480..23aa10849067dd71547f765b3b0260b5d5ed6ea3 100644 (file)
@@ -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();
   }
 }
index 2032c709c87651fb05913fc3e634d570462769dd..76a7efbc6e17af0d87dea1e394683b6a2fccb3e5 100644 (file)
@@ -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();
   }
 }
index 6e2a19e5d0a6abdf885aa6cd9849631f4a3ceaeb..db484f85844f443be99841eb39d3a29bafd97986 100644 (file)
@@ -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 = <FieldValidator>[];
+
+  /// 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 = <String, dynamic>{};
+
+  /// a model specific error message:
+  /// key: validator name, e.g. 'regExpr'
+  /// value: the error message
+  final messageOfValidator = <String, String>{};
   final Map<String, dynamic> 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>('filterType', map, FilterType.values);
     options = parseOptions('options', map);
+    validatorsString = parseString('validators', map);
+    parseValidatorsText(parseString('validatorsText', map));
     dataType = parseEnum<DataType>('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 {}
index c7f29ad126b9ad3871a6d3fdc55dcb42e2f98b96..437b98af5271147adb734359c0ac89eda6516417 100644 (file)
@@ -18,3 +18,4 @@ enum FilterType {
   pattern,
 }
 enum WaitState { undef, initial, waiting, ready }
+typedef VoidCallbackBones = void Function();
index 3bfd13e64d71703a635848271cc71241520607c5..a23e9a8ee57cc97e33e1c1b3f4202c169aecf78a 100644 (file)
@@ -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<String, dynamic> map;
   String name;
   List<String> 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);
   }
 
index eeb141654c3c94f8c86e645924f35e48bf0d2ac3..956bd338ef55d2d8d95ea7bf5ea3112258637eda 100644 (file)
@@ -21,6 +21,7 @@ class PageModel extends ModelBase {
   final fields = <FieldModel>[];
   final buttons = <ButtonModel>[];
   final widgets = <WidgetModel>[];
+  String sql;
   List<String> tableTitles;
   List<String> 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<PageModelType>(
         '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 (file)
index 0000000..21b321b
--- /dev/null
@@ -0,0 +1,98 @@
+import 'package:dart_bones/dart_bones.dart';
+
+import '../module_model.dart';
+
+class MenuModel extends ModuleModel {
+  static final mapMenu = <String, dynamic>{
+    '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;
+}
index 5aa6f0ac287b659c1f752a377c46b85200e35b52..efd121767c439a2fc9f0a2b14280352b14f4fce1 100644 (file)
@@ -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<String> standardModules() => ['configuration', 'role', 'user'];
+List<String> standardModules() => ['configuration', 'menu', 'role', 'user'];
index 01bdc64ad75403cc6e2e5bffad869aee58f8e73f..3b04a9411c2aac75212b3e1de7f4ae86aa144409 100644 (file)
@@ -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",
+              },
+            ]
+          }
+        ]
+      },
     ]
   };
 
index 3c623dbff99b562fddcee3de6179ad6833b2da1e..627bd380ef2ee7f9238ff15de1e61fb0baad37de 100644 (file)
@@ -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();
   }
 }
index c4fcce772f399fa8ac5a757c11d930381cce8020..3db5f1113d85cac17610585e3e396715566f0207 100644 (file)
@@ -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;
 
index 9dda8ed4dd5795527100123c9c91d5fa8e3af87a..3db0d3e7287c318aa020d193d2a772cceea9ac1b 100644 (file)
@@ -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();
index a711d8cab2d9e94370496ef87493d69b34be45b5..865e1c2552058c692034f9a6f717c625fd250f80 100644 (file)
@@ -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,
index 76c7e894b610dd80143093ed5fcf946937ba7c9f..cc579c2cc7ecb86301363d67accd37afdde4e40e 100644 (file)
@@ -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 {
index 23a7383c1a4824ae680306041420ab9b2027d420..a6f8797c925c47130e8c19ec9fa015902fca92ce 100644 (file)
@@ -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 {
index 12fbb28b28fa05695fc19b81fc19bec4eeb513c1..dcc9c709f60a95caf222fdddfc0c961c7abb9572 100644 (file)
@@ -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<ConfigurationListPage> {
       GlobalKey<FormState>(debugLabel: 'configuration_list');
   Iterable<dynamic> 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<ConfigurationListPage> {
             ),
           ]),
         ],
-        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
deleted file mode 100644 (file)
index e0367fa..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:dart_bones/dart_bones.dart';
-import 'package:flutter_bones/flutter_bones.dart';
-
-class LoginPage extends StatefulWidget {
-  final ApplicationData pageData;
-  LoginPage(this.pageData, {Key key}) : super(key: key);
-
-  @override
-  LoginPageState createState() {
-    // LoginPageState.setPageData(pageData);
-    final rc = LoginPageState(pageData);
-
-    return rc;
-  }
-}
-
-class LoginPageState extends State<LoginPage> {
-  LoginPageState(this.pageData);
-
-  final ApplicationData pageData;
-
-  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
-  static User currentUser = User();
-
-  @override
-  Widget build(BuildContext context) {
-    final user = LoginUser();
-    return Scaffold(
-        appBar: pageData.appBarBuilder('login'),
-        drawer: pageData.drawerBuilder(context),
-        body: SimpleForm.simpleForm(
-          key: _formKey,
-          configuration: pageData.configuration,
-          fields: <Widget>[
-            Text('Bitte loggen Sie sich ein.'),
-            TextFormField(
-              validator: checkNotEmpty,
-              decoration: InputDecoration(labelText: 'Name'),
-              onSaved: (input) => user.name = input,
-            ),
-            TextFormField(
-              validator: (input) =>
-                  Validation.isEmail(input) || Validation.isPhoneNumber(input)
-                      ? null
-                      : 'keine Emailadresse und keine Telefonnummer: $input',
-              decoration: InputDecoration(labelText: 'Passwort'),
-              onSaved: (input) => user.password = input,
-              obscureText: true,
-            ),
-          ],
-          buttons: <Widget>[
-            RaisedButton(
-              onPressed: () => login(context),
-              child: Text('Anmelden'),
-            ),
-          ],
-        ));
-  }
-
-  void login(context) async {
-    if (_formKey.currentState.validate()) {
-      _formKey.currentState.save();
-      //@ToDo: store in database
-    }
-  }
-}
-
-class LoginUser {
-  String name;
-  String password;
-}
diff --git a/lib/src/page/login_page.dart.01 b/lib/src/page/login_page.dart.01
new file mode 100644 (file)
index 0000000..e0367fa
--- /dev/null
@@ -0,0 +1,72 @@
+import 'package:flutter/material.dart';
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+
+class LoginPage extends StatefulWidget {
+  final ApplicationData pageData;
+  LoginPage(this.pageData, {Key key}) : super(key: key);
+
+  @override
+  LoginPageState createState() {
+    // LoginPageState.setPageData(pageData);
+    final rc = LoginPageState(pageData);
+
+    return rc;
+  }
+}
+
+class LoginPageState extends State<LoginPage> {
+  LoginPageState(this.pageData);
+
+  final ApplicationData pageData;
+
+  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
+  static User currentUser = User();
+
+  @override
+  Widget build(BuildContext context) {
+    final user = LoginUser();
+    return Scaffold(
+        appBar: pageData.appBarBuilder('login'),
+        drawer: pageData.drawerBuilder(context),
+        body: SimpleForm.simpleForm(
+          key: _formKey,
+          configuration: pageData.configuration,
+          fields: <Widget>[
+            Text('Bitte loggen Sie sich ein.'),
+            TextFormField(
+              validator: checkNotEmpty,
+              decoration: InputDecoration(labelText: 'Name'),
+              onSaved: (input) => user.name = input,
+            ),
+            TextFormField(
+              validator: (input) =>
+                  Validation.isEmail(input) || Validation.isPhoneNumber(input)
+                      ? null
+                      : 'keine Emailadresse und keine Telefonnummer: $input',
+              decoration: InputDecoration(labelText: 'Passwort'),
+              onSaved: (input) => user.password = input,
+              obscureText: true,
+            ),
+          ],
+          buttons: <Widget>[
+            RaisedButton(
+              onPressed: () => login(context),
+              child: Text('Anmelden'),
+            ),
+          ],
+        ));
+  }
+
+  void login(context) async {
+    if (_formKey.currentState.validate()) {
+      _formKey.currentState.save();
+      //@ToDo: store in database
+    }
+  }
+}
+
+class LoginUser {
+  String name;
+  String password;
+}
diff --git a/lib/src/page/menu/menu_change_page.dart b/lib/src/page/menu/menu_change_page.dart
new file mode 100644 (file)
index 0000000..5127b13
--- /dev/null
@@ -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<MenuChangePage> {
+  final ApplicationData applicationData;
+  final int primaryId;
+  final Map initialRow;
+  final GlobalKey<FormState> _formKey =
+      GlobalKey<FormState>(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 (file)
index 0000000..581143a
--- /dev/null
@@ -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<FormState> formKey, State<StatefulWidget> 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 (file)
index 0000000..d7acb01
--- /dev/null
@@ -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<MenuCreatePage> {
+  final ApplicationData applicationData;
+
+  final GlobalKey<FormState> _formKey =
+      GlobalKey<FormState>(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 (file)
index 0000000..5a0b1a7
--- /dev/null
@@ -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<MenuListPage> {
+  final ApplicationData applicationData;
+
+  final GlobalKey<FormState> _formKey =
+      GlobalKey<FormState>(debugLabel: 'menu_list');
+  Iterable<dynamic> 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: <Widget>[
+          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()),
+      ),
+    );
+  }
+}
index 4a48fdae706f01356668d13f3f97070ffa035a79..3368f049d2f7a55bc6cb3bb348570e548285d42a 100644 (file)
@@ -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,
index 18f7dbfefa5ebe219d4f11413947af0747224157..13a7406b73f8266f3ea73a1bf2623b5152d630ad 100644 (file)
@@ -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 {
index 875bea9c9158fd34b7007c9c6d09ead1907c69c5..dc670762be49564e198a64a00c55e5236f375511 100644 (file)
@@ -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 {
index 09e73e0e4d64f690ce4b7e3dddf87902a0d9c503..b373ba4bfb7514e9e2f4e5033957d2b823587155 100644 (file)
@@ -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<RoleListPage> {
       GlobalKey<FormState>(debugLabel: 'role_list');
   Iterable<dynamic> 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<RoleListPage> {
             ),
           ]),
         ],
-        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 (file)
index 0000000..a12ad4f
--- /dev/null
@@ -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();
+}
index 0e9bf2d789bc8b904a8304e7b2decc0d1375b846..61471aa3939b035e61dd71a9fc2ed55ae15253dc 100644 (file)
@@ -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<UserChangePage> {
         }
       });
       controller.initialize();
+      customize();
     }
     // controller.buildWidgetList() is called in editForm
     return Scaffold(
@@ -70,6 +74,19 @@ class UserChangePageState extends State<UserChangePage> {
         ));
   }
 
+  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();
index 6c69c68b7a4c74f4655846d7ae87951d4311edee..30d72db9c0f500bfee96b3462879029d03f55690 100644 (file)
@@ -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 {
index 0837ca8ef921e677379801adf3765a67209d1844..c4d34f96e7eacddc163c4f2afe372d110271fb58 100644 (file)
@@ -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 {
index 863fc82713e7b2c0d389a57503a8bd74bdf708b6..0cdc3783ac696f2984c6d6a232ee233a7dc880f0 100644 (file)
@@ -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 (file)
index 0000000..f451d2d
--- /dev/null
@@ -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<UserLoginPage> {
+  final ApplicationData applicationData;
+  final GlobalKey<FormState> _formKey =
+      GlobalKey<FormState>(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 (file)
index 0000000..4e24cb7
--- /dev/null
@@ -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<UserPasswordPage> {
+  final ApplicationData applicationData;
+  final int primaryId;
+  final String userName;
+  final GlobalKey<FormState> _formKey =
+      GlobalKey<FormState>(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();
+  }
+}
index d25571da43ff94cc48f8ca7831d2a5fcb9e6315c..7b3dd6b7e6ca0ddf7e547234f2b492666965d5f0 100644 (file)
@@ -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<MenuItem> menuItems() {
     final settings = BSettings.lastInstance;
     return <MenuItem>[
-      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 (file)
index 3090924..0000000
+++ /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<String> getOnChanged(
-      String customString, CallbackControllerBones controller);
-  ValueChanged<bool> getOnChangedCheckbox(
-      String customString, CallbackControllerBones controller);
-
-  ValueChanged<T> getOnChangedCombobox<T>(
-      String customString, CallbackControllerBones controller);
-
-  VoidCallback getOnEditingComplete(
-      String customString, CallbackControllerBones controller);
-
-  ValueChanged<String> getOnFieldSubmitted(
-      String customString, CallbackControllerBones controller);
-
-  ValueChanged<bool> getOnHighlightChanged(
-      String customString, CallbackControllerBones controller);
-
-  VoidCallback getOnLongPressed(
-      String customString, CallbackControllerBones controller);
-
-  VoidCallback getOnPressed(
-      String customString, CallbackControllerBones controller);
-
-  FormFieldSetter<String> getOnSaved(
-      String customString, CallbackControllerBones controller);
-
-  DropdownButtonBuilder getOnSelectedItemBuilder(
-      String customString, CallbackControllerBones controller);
-
-  GestureTapCallback getOnTap(
-      String customString, CallbackControllerBones controller);
-
-  FormFieldValidator getOnValidator(
-      String customString, CallbackControllerBones controller);
-
-  FormFieldValidator<String> 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,
-}
index 54cd8f79bed3a147c0d5f91580bf2678710665f8..a6ae03c27228d6a2de357d7247963ede202993fb 100644 (file)
@@ -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<T> extends DropdownButtonFormField<T> {
   final String customString;
-  final CallbackControllerBones callbackController;
+  final PageControllerBones callbackController;
 
   DropdownButtonFormBone(
     this.customString,
index 428ef510d87b049b076ea6bb3a741a1b463d0ad5..90275e460a436c6df13db5d7bcce4a83e0e198fe 100644 (file)
@@ -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<String, dynamic> 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: <Widget>[
-                      FlatButton(
-                        child: Text('Abbruch'),
-                        onPressed: pageController.getOnPressed(
-                            'cancel', pageController),
-                      ),
-                      RaisedButtonBone(
-                        'store',
-                        pageController,
-                        child: Text('Speichern'),
-                      ),
-                    ],
+                    children: buttons,
                   ),
                 ],
               ))),
index 3857700837db9dca96a6b3a32d6d5d5e571b77a9..755375174bbb14baa0119de5dea79a6e7908eeac 100644 (file)
@@ -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<String, dynamic> row);
 
 @deprecated
@@ -46,20 +43,8 @@ class FilterSet {
   List<Widget> 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;
   }
index bdb87f4f2ece39a455a276d8282d508289428e63..3c841105cea7620358d6c8b7c4560c6faccf422e 100644 (file)
@@ -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<T>(
-      String customString, CallbackControllerBones controller) {
+  getOnChangedCombobox<T>(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,
+}
index 6875e3c895784707a4f960f1c3496428e2595fdc..fc33c88ef691704fb9718b81f5d0aa0d8097faff 100644 (file)
@@ -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,
index af89457b3fff8280be19a33f001306bbac1c9076..da91f2a46b3e8b7d51cab57783b590a93485522e 100644 (file)
@@ -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<TextInputFormatter> 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,
index f6e13240fa0f990e0debcfa41e18a0cdec918ba9..9b56fd1ff0a5b3356807449182a70d4cbfd97ea6 100644 (file)
@@ -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'),
         ),
index 897787be72b9cdf7c0b3c62ee6ec5a2406d4166a..469f5f6264e43231687642b2589a8e1c4b61fbd5 100644 (file)
@@ -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 (file)
index 0000000..c9389d9
--- /dev/null
@@ -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<String> 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 (executable)
index 0000000..c5e1fee
--- /dev/null
@@ -0,0 +1,3 @@
+#! /bin/bash
+dart lib/main.dart
+
index b7e6ee7799c0ad81d2ba05b54771bd949df759e1..6cb3154b800650b96dc0dd44d2d4d6fcf2009af2 100644 (file)
@@ -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.
index 7e076695d31c8f46f1aa9bfc79afbcd23c97b638..4aa7bef52105936469f7c7beae8c9cf0dad2b61a 100644 (file)
@@ -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 (file)
index 0000000..a233e29
--- /dev/null
@@ -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 = <String, dynamic>{
+        '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 = <String, dynamic>{
+        '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 = <String, dynamic>{
+        '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 = <String, dynamic>{
+        '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'));
+    });
+  });
+}