]> gitweb.hamatoma.de Git - flutter_bones.git/commitdiff
daily work: coverage improved
authorHamatoma <author@hamatoma.de>
Tue, 3 Nov 2020 08:14:42 +0000 (09:14 +0100)
committerHamatoma <author@hamatoma.de>
Tue, 3 Nov 2020 08:14:42 +0000 (09:14 +0100)
29 files changed:
lib/app.dart
lib/flutter_bones.dart
lib/src/helper/helper_async.dart [new file with mode: 0644]
lib/src/model/button_model.dart
lib/src/model/column_model.dart
lib/src/model/combo_base_model.dart
lib/src/model/db_reference_model.dart
lib/src/model/standard/user_model.dart
lib/src/model/text_field_model.dart
lib/src/page/configuration/configuration_list_page.dart
lib/src/page/role/role_list_page.dart
lib/src/page/user/user_list_page.dart
lib/src/persistence/persistence.dart
lib/src/persistence/persistence_cache.dart
lib/src/persistence/rest_persistence.dart
lib/src/widget/callback_controller_bones.dart
lib/src/widget/filter_set.dart
lib/src/widget/list_form.dart
lib/src/widget/page_controller_bones.dart
lib/src/widget/view.dart
lib/src/widget/widget_list.dart
test/model/db_model_test.dart
test/model/model_test.dart
test/model/standard_test.dart
test/page/application_test.dart [new file with mode: 0644]
test/persistence_cache_test.dart
test/rest_persistence_test.dart
test/tool/tool_test.dart
test/widget/widget_test.dart

index ba7415315e6208ff0ffba6b5f7f9fd363802397d..12883f7fba8780b0b8f0eba11637787a138c4dd4 100644 (file)
@@ -37,7 +37,7 @@ class BoneAppState extends State<BoneApp> {
         primarySwatch: Colors.blue,
         visualDensity: VisualDensity.adaptivePlatformDensity,
       ),
-      initialRoute: '/configuration/list',
+      initialRoute: '/user/list',
       onGenerateRoute: _getRoute,
     );
   }
index 76cf4063ca7937a72a102af21cb83d964fc6f96a..1220da33804abcdde0db069078925aeb049f42b6 100644 (file)
@@ -5,6 +5,7 @@
 export 'src/helper/settings.dart';
 export 'src/helper/string_helper.dart';
 export 'src/helper/validators.dart';
+export 'src/helper/helper_async.dart';
 export 'src/model/button_model.dart';
 export 'src/model/checkbox_model.dart';
 export 'src/model/combobox_model.dart';
diff --git a/lib/src/helper/helper_async.dart b/lib/src/helper/helper_async.dart
new file mode 100644 (file)
index 0000000..00df9e2
--- /dev/null
@@ -0,0 +1,5 @@
+/// Waits for a given amount of [millisec] and call then a [callback] function (if given).
+Future wait({int millisec = 50, Function callback}) {
+  callback ??= () => null;
+  return Future.delayed(Duration(milliseconds: millisec), callback());
+}
index 84bd202d40c933786689bae3d88fbbcb385130d3..d2a5a29a09b17afc3c87682d35ae4b0e352cef2f 100644 (file)
@@ -44,7 +44,7 @@ class ButtonModel extends WidgetModel {
 
   /// Returns the name including the names of the parent
   @override
-  String fullName() => '${section.name}.$name';
+  String fullName() => '${section?.name}.$name';
 
   /// Parses the map and stores the data in the instance.
   void parse() {
index 7ab3ba84accc30fc02c17167a145645628fa07fc..b9946776f120e1c127f0b8a108f0e4ee6c05c92c 100644 (file)
@@ -86,7 +86,7 @@ class ColumnModel extends ComboBaseModel {
     if (foreignKey != null &&
         regExprForeignKey.firstMatch(foreignKey) == null) {
       logger.error(
-          'invalid foreign key: $foreignKey expected: "<table>.<primary> <colName>" e.g. "role.role_id role_name"');
+          'invalid foreign key in ${fullName()}: $foreignKey expected: "<table>.<primary> <colName>" e.g. "role.role_id role_name"');
     }
     checkOptionsByRegExpr(options, regExprOptions);
     if (options.contains('primary')) {
index 5a11f57a67f872ffa9d754a197d0c60c10a37ab7..264644adc9d2b105935c13b9dfb8430d14d2b428 100644 (file)
@@ -9,12 +9,13 @@ import 'widget_model.dart';
 /// A base class for combobox items like ComboboxModel, ColumnModel and DbReferenceModel.
 abstract class ComboBaseModel extends FieldModel {
   static final regExprListDbOption =
-      RegExp(r'^\w+\.\w+;\w+ \w+(?:;(:? ?:\w+=[^ ]*?)+)?$');
+      RegExp(r'^\w+\.\w+\.\w+;\w+ \w+(?:;( ?:\w+=[^ ])*)?$');
   static final regExprListConfiguration = RegExp(r'^scope:\w+$');
   List<String> texts;
   List values;
   String listOption;
   ComboboxListType listType;
+  ComboboxData data;
 
   ComboBaseModel(SectionModel section, PageModel page, Map map,
       WidgetModelType widgetType, BaseLogger logger)
@@ -38,20 +39,6 @@ abstract class ComboBaseModel extends FieldModel {
       : super.direct(section, page, map, widgetType, name, label, toolTip,
             dataType, options, logger);
 
-/*
-      SectionModel section,
-      PageModel page,
-      this.map,
-      WidgetModelType fieldModelType,
-      String name,
-      String label,
-      String toolTip,
-      DataType dataType,
-      List<String> options,
-      BaseLogger logger,
-
- */
-
   /// Parses the map and stores the data in the instance.
   void parse() {
     super.parse();
@@ -69,7 +56,9 @@ abstract class ComboBaseModel extends FieldModel {
         if (texts.isEmpty) {
           logger.error('missing attribute "texts" in ${fullName()}');
           listType = ComboboxListType.undef;
-        } else if (values.isNotEmpty && texts.length != values.length) {
+        } else if (values != null &&
+            values.isNotEmpty &&
+            texts.length != values.length) {
           logger.error(
               'different sizes of "Texts" and "values": ${texts.length}/${values.length}');
           listType = ComboboxListType.undef;
@@ -85,12 +74,40 @@ abstract class ComboBaseModel extends FieldModel {
       case ComboboxListType.dbColumn:
         if (regExprListDbOption.firstMatch(listOption) == null) {
           logger.error(
-              'wrong syntax in "listOption". Example: "role.list;role_name role_id;:role_name=%"');
+              'wrong syntax in "listOption". Example: "all.role.list;role_name role_id;:role_name=%"');
           listType = ComboboxListType.undef;
         }
         break;
     }
   }
+
+  /// Returns a [ComboboxData] related to the [dataType] with given [texts]
+  /// and [values].
+  static ComboboxData createByType(
+      DataType dataType, List<String> texts, Iterable values) {
+    ComboboxData rc;
+    switch (dataType) {
+      case DataType.bool:
+        rc = ComboboxData<bool>(texts, values);
+        break;
+      case DataType.currency:
+      case DataType.int:
+      case DataType.reference:
+        rc = ComboboxData<int>(texts, values);
+        break;
+      case DataType.date:
+      case DataType.dateTime:
+        rc = ComboboxData<DateTime>(texts, values);
+        break;
+      case DataType.float:
+        rc = ComboboxData<double>(texts, values);
+        break;
+      case DataType.string:
+        rc = ComboboxData<String>(texts, values);
+        break;
+    }
+    return rc;
+  }
 }
 
 class ComboboxData<T> {
index d138346d2193625c2b0e2cc02d6a8ee566d22e5e..aa74170859726075e8b4aa3ba753d5f5e73b8ee8 100644 (file)
@@ -55,11 +55,11 @@ class DbReferenceModel extends ComboBaseModel {
 
   /// Parses the map and stores the data in the instance.
   void parse() {
-    super.parse();
     checkSuperfluousAttributes(
         map,
-        'column label maxSize listOption listType name options rows toolTip texts values'
+        'column filterType label maxSize listOption listType name options rows toolTip texts widgetType values'
             .split(' '));
+    super.parse();
     final columnName = parseString('column', map, required: true);
     column = page.module.getColumn(columnName, this);
     maxSize = parseInt('maxSize', map, required: false);
index 097e2a0623d0cf67bcf98bd99bfa4779dc57a069..9fcb1c9adf289caa6d91dc89e4481676a6e50d68 100644 (file)
@@ -3,7 +3,7 @@ import 'package:dart_bones/dart_bones.dart';
 import '../module_model.dart';
 
 class UserModel extends ModuleModel {
-  static final model = <String, dynamic>{
+  static final mapUser = <String, dynamic>{
     "module": "user",
     "tables": [
       {
@@ -48,11 +48,11 @@ class UserModel extends ModuleModel {
             'dataType': 'reference',
             'label': 'Role',
             'foreignKey': 'role.role_id role_name',
-            'listType': 'explicite',
-            'texts': ';-;Administrator;Benutzer;Gast;Verwalter',
-            'values': ';;1;2;3;4',
-            //"listType": "dbColumn",
-            //"listOption": "role.list;role_name role_id;:role_name=%",
+            //'listType': 'explicite',
+            //'texts': ';-;Administrator;Benutzer;Gast;Verwalter',
+            //'values': ';;1;2;3;4',
+            "listType": "dbColumn",
+            "listOption": "all.role.list;role_name role_id;:role_name=%",
           },
         ]
       },
@@ -97,21 +97,20 @@ class UserModel extends ModuleModel {
             "sectionType": "filterPanel",
             "children": [
               {
-                "widgetType": "textField",
+                "widgetType": "dbReference",
                 "filterType": "pattern",
                 "name": "user_name",
-                "label": "Name",
+                "column": "user_name",
+                "toolTip":
+                    "Filter bezüglich des Names der anzuzeigenden Einträge: Joker '*' (beliebiger String) ist erlaubt."
               },
               {
-                "widgetType": "combobox",
-                "filterType": "equals",
+                "widgetType": "dbReference",
                 "name": "user_role",
-                "label": "Rolle",
-                "listType": "explicite",
-                "texts": ";Alle Rollen;Administrator;Benutzer;Gast;Verwalter",
-                "values": ";;1;2;3;4"
-                //"listType": "dbColumn",
-                //"listOption": "role.list;role_name role_id;:role_name=%",
+                "filterType": "equals",
+                "column": "user_role",
+                "toolTip":
+                    "Filter bezüglich der Rolle der anzuzeigenden Einträge. '-' bedeutet keine Einschränkung"
               }
             ]
           }
@@ -120,7 +119,7 @@ class UserModel extends ModuleModel {
     ]
   };
 
-  UserModel(BaseLogger logger) : super(model, logger);
+  UserModel(BaseLogger logger) : super(mapUser, logger);
 
   /// Returns the name including the names of the parent
   @override
index 01a157a9c10f39b146dabb43f9327a47cd5aa57a..d893f31bd761157b7a2fc05ee9d878620aca191c 100644 (file)
@@ -14,12 +14,23 @@ class TextFieldModel extends FieldModel {
   int rows;
   var value;
 
-  final Map<String, dynamic> map;
-
   TextFieldModel(
-      SectionModel section, PageModel page, this.map, BaseLogger logger)
+      SectionModel section, PageModel page, Map map, BaseLogger logger)
       : super(section, page, map, WidgetModelType.textField, logger);
 
+  TextFieldModel.direct(
+      SectionModel section,
+      PageModel page,
+      Map map,
+      String name,
+      String label,
+      String toolTip,
+      DataType dataType,
+      List<String> options,
+      BaseLogger logger)
+      : super.direct(section, page, null, WidgetModelType.textField, name,
+            label, toolTip, dataType, options, logger);
+
   /// Dumps the internal structure into a [stringBuffer]
   StringBuffer dump(StringBuffer stringBuffer) {
     stringBuffer
index abb29253d04c1e9b72607b4ba66fac3ff406ce96..12fbb28b28fa05695fc19b81fc19bec4eeb513c1 100644 (file)
@@ -1,7 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_bones/src/widget/callback_controller_bones.dart';
 
-import '../../widget/filter_set.dart';
+import '../../widget/widget_list.dart';
 import '../../widget/list_form.dart';
 import '../application_data.dart';
 import 'configuration_controller.dart';
@@ -29,7 +29,7 @@ class ConfigurationListPageState extends State<ConfigurationListPage> {
       GlobalKey<FormState>(debugLabel: 'configuration_list');
   Iterable<dynamic> rowsDeprecated;
   ConfigurationController controller;
-  FilterSet filters;
+  WidgetList filters;
 
   ConfigurationListPageState(this.applicationData);
 
index e685ec9cfbe21fad12b5fe88570b3335425ac277..09e73e0e4d64f690ce4b7e3dddf87902a0d9c503 100644 (file)
@@ -1,7 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_bones/src/widget/callback_controller_bones.dart';
 
-import '../../widget/filter_set.dart';
+import '../../widget/widget_list.dart';
 import '../../widget/list_form.dart';
 import '../application_data.dart';
 import 'role_controller.dart';
@@ -29,7 +29,7 @@ class RoleListPageState extends State<RoleListPage> {
       GlobalKey<FormState>(debugLabel: 'role_list');
   Iterable<dynamic> rowsDeprecated;
   RoleController controller;
-  FilterSet filters;
+  WidgetList filters;
 
   RoleListPageState(this.applicationData);
 
index 394c87a1a3d643903c2d42a9f919d7efbaf7810a..84832de36b159286f6dea9197e3cd44f0c137e04 100644 (file)
@@ -1,7 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_bones/src/widget/callback_controller_bones.dart';
 
-import '../../widget/filter_set.dart';
+import '../../widget/widget_list.dart';
 import '../../widget/list_form.dart';
 import '../application_data.dart';
 import 'user_controller.dart';
@@ -29,38 +29,40 @@ class UserListPageState extends State<UserListPage> {
       GlobalKey<FormState>(debugLabel: 'user_list');
   Iterable<dynamic> rowsDeprecated;
   UserController controller;
-  FilterSet filters;
+  WidgetList filters;
 
   UserListPageState(this.applicationData);
 
+  @override
+  void initState() {
+    super.initState();
+    controller =
+        UserController(_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.widgetList
+        .waitForCompletion(controller)
+        .then((result) => setState(() => null));
+  }
+
   @override
   Widget build(BuildContext context) {
-    if (controller == null) {
-      controller =
-          UserController(_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');
     return Scaffold(
       appBar: applicationData.appBarBuilder('Benutzer'),
       drawer: applicationData.drawerBuilder(context),
index cd2839ef59af873cf24e0e67ee0bf9e0d6be282c..58f71d76ba3c1649ad6f9d195a9f39840b012074 100644 (file)
@@ -29,9 +29,16 @@ abstract class Persistence {
 
   /// Returns a record with primary key [id] of the [module] with the
   /// SQL statement [sqlName].
-  Future<Map<String, dynamic>> record(
+  Future<Map<String, dynamic>> recordById(
       {@required String module, String sqlName, @required int id});
 
+  /// Returns a record specified by one or more [parameters] of the [module] with the
+  /// SQL statement [sqlName].
+  Future<Map<String, dynamic>> recordByParameter(
+      {@required String module,
+      @required String sqlName,
+      @required Map<String, dynamic> parameters});
+
   /// Updates a record of [module] with the [data] using the SQL statement [sqlName].
   Future update(
       {@required String module,
index d3e299ea6042d34fbdd94908b5c9087d70e03016..b78c0a1e88b5a42080258ea0d094c350164da88b 100644 (file)
@@ -1,27 +1,40 @@
 import 'package:dart_bones/dart_bones.dart';
 import 'package:flutter_bones/flutter_bones.dart';
 import 'package:flutter_bones/src/model/combo_base_model.dart';
-import 'package:meta/meta.dart';
-import 'persistence.dart';
+
+/// Specifies an cache entry.
+class CacheEntry {
+  final String key;
+  final CacheEntryType entryType;
+  final List list;
+  bool ready;
+  bool oneTime;
+  CacheEntry(this.key, this.entryType, this.list,
+      {this.ready = false, this.oneTime = false});
+}
+
+enum CacheEntryType {
+  combobox,
+  record,
+  list,
+}
 
 /// A cache for objects coming from a persistence layer.
 /// Uses the least recently used algorithm to avoid overflow.
 class PersistenceCache {
   static PersistenceCache _instance;
-  //static final regExpCombobox = RegExp(r'^\w\.\w+\.\w+;\w+ \w+;( ?:\w+=\S*)*$');
   static final regExpCombobox =
       RegExp(r'^\w+\.\w+\.\w+;\w+ \w+;( ?:\w+=\S*)*$');
+  static final regExpRecord = RegExp(r'^\w+\.\w+\.\w+;( ?:\w+=\S*)+$');
+  static int nextRecordNo = 0;
   final leastReasentlyUsed = <CacheEntry>[];
   final map = <String, CacheEntry>{};
   final maxEntries;
-  factory PersistenceCache() {
-    return _instance;
-  }
   final ApplicationData application;
   BaseLogger logger;
 
-  PersistenceCache.internal(this.application, this.maxEntries) {
-    this.logger = this.application.logger;
+  factory PersistenceCache() {
+    return _instance;
   }
 
   /// True constructor (of the singleton instance).
@@ -34,14 +47,27 @@ class PersistenceCache {
     return _instance;
   }
 
+  PersistenceCache.internal(this.application, this.maxEntries) {
+    this.logger = this.application.logger;
+  }
+
+  /// Returns a unique key for using record().
+  String buildRecordKeyPrefix() {
+    final rc = 'key${++nextRecordNo}';
+    return rc;
+  }
+
   /// Returns the data (text, values) of a combobox specified by [key].
   /// If the data are unavailable (e.g. a asynchronous request is running) the
   /// result is null.
-  /// [key] has the format:
+  /// The format of [key]:
   /// "<id>.<module>.<sqlName>;<colText> <colValue>;<param1> <param2>..."
   /// e.g. "x99.role.list;role_name role_id;:role_name=% :role_active=T"
   /// If [hasUndef] is true the first combobox list entry is '-' with the value null.
-  ComboboxData combobox(String key, {bool hasUndef = false}) {
+  /// If [oneTime] is true the result is only used one time, the LRU algoritm is
+  /// not used.
+  ComboboxData combobox(String key,
+      {bool hasUndef = false, bool oneTime = false}) {
     ComboboxData rc;
     if (regExpCombobox.firstMatch(key) == null) {
       logger.error('wrong key syntax: $key');
@@ -60,10 +86,14 @@ class PersistenceCache {
         final keyValue = element.split('=');
         params2[keyValue[0]] = keyValue[1];
       });
-      final entry = CacheEntry(key, CacheEntryType.combobox, [
-        hasUndef ? ['-'] : <String>[],
-        hasUndef ? <dynamic>[null] : <dynamic>[]
-      ]);
+      final entry = CacheEntry(
+          key,
+          CacheEntryType.combobox,
+          [
+            hasUndef ? ['-'] : <String>[],
+            hasUndef ? <dynamic>[null] : <dynamic>[]
+          ],
+          oneTime: oneTime);
       insert(entry);
       application.persistence
           .list(
@@ -82,38 +112,83 @@ class PersistenceCache {
     return rc;
   }
 
+  /// Deletes an entry given by [key] from the cache.
+  /// Returns true on success.
+  bool deleteEntry(String key) {
+    bool rc = map.containsKey(key);
+    if (rc) {
+      leastReasentlyUsed.remove(map[key]);
+      map.remove(key);
+    }
+    return rc;
+  }
+
   /// Inserts an [cacheEntry] into the cache.
   void insert(CacheEntry cacheEntry) {
     if (leastReasentlyUsed.length >= maxEntries) {
       map.remove(leastReasentlyUsed[0].key);
       leastReasentlyUsed.removeAt(0);
     }
-    leastReasentlyUsed.add(cacheEntry);
+    final offset = maxEntries > 50 ? 25 : maxEntries / 2;
+    if (cacheEntry.oneTime && leastReasentlyUsed.length > offset) {
+      leastReasentlyUsed.insert(offset, cacheEntry);
+    } else {
+      leastReasentlyUsed.add(cacheEntry);
+    }
     map[cacheEntry.key] = cacheEntry;
   }
 
+  /// Returns database record specified by [key].
+  /// If the data are unavailable (e.g. a asynchronous request is running) the
+  /// result is null, otherwise the result may be an empty list (record not found).
+  /// The format of [key]:
+  /// "<id>.<module>.<sqlName>;<param1> <param2>..."
+  /// e.g. "x99.role.by_name:role_name=eve :excluded=22"
+  /// If [oneTime] is true the result is only used one time, the LRU algoritm is
+  /// not used.
+  Map record(String key, {bool oneTime = true}) {
+    Map rc;
+    if (regExpRecord.firstMatch(key) == null) {
+      logger.error('wrong key syntax: $key');
+    } else if (map.containsKey(key)) {
+      final entry = updateLRU(key);
+      if (entry.ready) {
+        rc = entry.list[0];
+      }
+    } else {
+      final parts = key.split(';');
+      final idModuleName = parts[0].split('.');
+      final params = parts[1].split(' ');
+      final params2 = <String, dynamic>{};
+      params.forEach((element) {
+        final keyValue = element.split('=');
+        params2[keyValue[0]] = keyValue[1];
+      });
+      final entry =
+          CacheEntry(key, CacheEntryType.record, <Map>[null], oneTime: oneTime);
+      insert(entry);
+      application.persistence
+          .recordByParameter(
+              module: idModuleName[1],
+              sqlName: idModuleName[2],
+              parameters: params2)
+          .then((row) {
+        entry.list[0] = row;
+        entry.ready = true;
+      });
+    }
+    return rc;
+  }
+
   /// Moves the entry specified by [key] at the top of the Least Recently Used
   /// stack.
   /// Returns the entry specified by [key].
   CacheEntry updateLRU(String key) {
     final entry = map[key];
-    leastReasentlyUsed.remove(entry);
-    leastReasentlyUsed.add(entry);
+    if (!entry.oneTime) {
+      leastReasentlyUsed.remove(entry);
+      leastReasentlyUsed.add(entry);
+    }
     return entry;
   }
 }
-
-/// Specifies an cache entry.
-class CacheEntry {
-  final String key;
-  final CacheEntryType entryType;
-  final List list;
-  bool ready;
-  CacheEntry(this.key, this.entryType, this.list, {this.ready = false});
-}
-
-enum CacheEntryType {
-  combobox,
-  record,
-  list,
-}
index d6da8904f08ca79737464959b89a9dad590a52ee..70d5300b6452efd10b5ece98a3f75335b5a0c3cf 100644 (file)
@@ -63,9 +63,9 @@ class RestPersistence extends Persistence {
 
   @override
   Future customQuery(
-      {String module,
-      String sqlName,
-      String sqlType,
+      {@required String module,
+      @required String sqlName,
+      @required String sqlType,
       Map<String, dynamic> params}) async {
     var rc;
     assert(['list', 'record', 'update'].contains(sqlType));
@@ -81,14 +81,17 @@ class RestPersistence extends Persistence {
   }
 
   @override
-  Future delete({String module, String sqlName, int id}) async {
+  Future delete(
+      {@required String module, String sqlName, @required int id}) async {
     sqlName ??= 'delete';
     await runRequest(module, sqlName, 'delete', body: '{":${module}_id":$id}');
   }
 
   @override
   Future<int> insert(
-      {String module, String sqlName, Map<String, dynamic> data}) async {
+      {@required String module,
+      String sqlName,
+      @required Map<String, dynamic> data}) async {
     sqlName ??= 'insert';
     var rc = 0;
     final data2 = data == null ? '{}' : convert.jsonEncode(data);
@@ -101,7 +104,8 @@ class RestPersistence extends Persistence {
   }
 
   @override
-  Future<dynamic> list({String module, String sqlName, Map params}) async {
+  Future<dynamic> list(
+      {@required String module, String sqlName, Map params}) async {
     sqlName ??= 'list';
     Iterable<dynamic> rc;
     final body = params == null ? '{}' : convert.jsonEncode(params);
@@ -116,8 +120,8 @@ class RestPersistence extends Persistence {
   }
 
   @override
-  Future<Map<String, dynamic>> record(
-      {String module, String sqlName, int id}) async {
+  Future<Map<String, dynamic>> recordById(
+      {@required String module, String sqlName, @required int id}) async {
     sqlName ??= 'record';
     Map rc;
     final answer = await runRequest(module, sqlName, 'record',
@@ -128,6 +132,21 @@ class RestPersistence extends Persistence {
     return rc;
   }
 
+  @override
+  Future<Map<String, dynamic>> recordByParameter(
+      {@required String module,
+      @required String sqlName,
+      @required Map<String, dynamic> parameters}) async {
+    sqlName ??= 'record';
+    Map rc;
+    final answer = await runRequest(module, sqlName, 'record',
+        body: convert.jsonEncode(parameters));
+    if (answer.isNotEmpty) {
+      rc = convert.jsonDecode(answer);
+    }
+    return rc;
+  }
+
   /// Handles a HTTP request with a single HTTP connection.
   /// [module]: the module which implies the requested table.
   /// [sqlName]: the name of the SQL statement.
@@ -153,7 +172,9 @@ class RestPersistence extends Persistence {
 
   @override
   Future update(
-      {String module, String sqlName, Map<String, dynamic> data}) async {
+      {@required String module,
+      String sqlName,
+      @required Map<String, dynamic> data}) async {
     sqlName ??= 'update';
     final data2 = data == null ? '{}' : convert.jsonEncode(data);
     await runRequest(module, sqlName, 'update', body: data2);
index 6d41598e0303e824337bc7cbe88a5d82bfbd29a8..300d0e6a54cadf30d1bbf80cb37ea630a0a02e23 100644 (file)
@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 
+import '../model/model_types.dart';
 import '../model/combobox_model.dart';
 import '../model/combo_base_model.dart';
 import '../model/field_model.dart';
@@ -18,7 +19,7 @@ abstract class CallbackControllerBones {
   void buildRows();
 
   /// Returns the [ComboboxData] (texts and values) of the [ComboboxModel] named [name].
-  ComboboxData comboboxData<T>(String name);
+  ComboboxData comboboxData(String name, DataType dataType);
 
   /// Frees all resources.
   void dispose();
index 55851f4aa622a92f963695fc40c3e9a6636a9674..3857700837db9dca96a6b3a32d6d5d5e571b77a9 100644 (file)
@@ -2,19 +2,18 @@ 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/dropdown_button_form_bone.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
 class FilterItem {
   final String name;
   final String label;
   final FilterType filterType;
   final String toolTip;
-  final FilterPredicate filterPredicate;
   var value;
 
   FilterItem({
@@ -22,45 +21,10 @@ class FilterItem {
     this.filterType,
     this.toolTip,
     this.name,
-    this.filterPredicate,
   });
-
-  bool isValid(Map<String, dynamic> row) {
-    bool rc = true;
-    if (filterPredicate != null) {
-      rc = filterPredicate(row);
-    } else {
-      final current = row[name].toString();
-      final value2 = value ?? '';
-      switch (filterType) {
-        case FilterType.pattern:
-          rc = value2 == ''
-              ? true
-              : (value2.startsWith('*')
-                  ? current.contains(value2.replaceAll('*', ''))
-                  : current.startsWith(value2));
-          break;
-        case FilterType.dateFrom:
-          // TODO: Handle this case.
-          break;
-        case FilterType.dateTil:
-          // TODO: Handle this case.
-          break;
-        case FilterType.dateTimeFrom:
-          // TODO: Handle this case.
-          break;
-        case FilterType.dateTimeTil:
-          // TODO: Handle this case.
-          break;
-        default:
-          rc = true;
-          break;
-      }
-    }
-    return rc;
-  }
 }
 
+@deprecated
 class FilterSet {
   var filters = <FilterItem>[];
   final BaseLogger logger;
@@ -100,17 +64,5 @@ class FilterSet {
     return rc;
   }
 
-  /// Tests whether the [row] belongs to the result.
-  bool isValid(Map<String, dynamic> row) {
-    var rc = true;
-    for (var filter in filters) {
-      if (!filter.isValid(row)) {
-        rc = false;
-        break;
-      }
-    }
-    return rc;
-  }
-
   dynamic valueOf(String name) => byName(name).value;
 }
index 9a2f7540c83472fc6357ee3a635c294d4da6e40f..7b9ce3091f128451136503363f479f6106bf5700 100644 (file)
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bones/src/widget/page_controller_bones.dart';
 
 import '../helper/string_helper.dart';
-import 'filter_set.dart';
+import 'widget_list.dart';
 import 'page_controller_bones.dart';
 import 'view.dart';
 
@@ -87,7 +87,7 @@ class ListForm {
   /// If [errorMessage] is not null this message will be shown.
   static Form listForm(
       {@required Key key,
-      @required FilterSet filters,
+      @required WidgetList filters,
       @required List<Widget> buttons,
       @required List<Widget> titles,
       @required List<String> columnNames,
@@ -99,8 +99,9 @@ class ListForm {
       String customString}) {
     final padding =
         configuration.asFloat('form.card.padding', defaultValue: 16.0);
+
     final widgets = <Widget>[
-      ...filters.getWidgets(),
+      ...filters.widgets,
       SizedBox(
           height: configuration.asFloat('form.gap.field_button.height',
               defaultValue: 16.0)),
index e76fdfcec5ce96a6794f477604f07a9c839c7c8e..48a13b2cb2c577465f14151ab177463da468fe98 100644 (file)
@@ -1,15 +1,15 @@
 import 'package:flutter/material.dart';
+import 'package:flutter_bones/flutter_bones.dart';
 
 import '../helper/string_helper.dart';
-import '../model/model_types.dart';
 import '../model/column_model.dart';
 import '../model/combo_base_model.dart';
-import '../model/page_model.dart';
-import '../model/module_model.dart';
 import '../model/field_model.dart';
+import '../model/model_types.dart';
+import '../model/module_model.dart';
+import '../model/page_model.dart';
 import '../page/application_data.dart';
 import 'callback_controller_bones.dart';
-import 'filter_set.dart';
 import 'view.dart';
 import 'widget_list.dart';
 
@@ -77,13 +77,55 @@ class PageControllerBones implements CallbackControllerBones {
     widgetList.clear();
     page.fields.forEach((model) {
       final value = initialRow == null ? null : initialRow[model.name];
+      completeModelByPersistence(model);
       widgetList.addWidget(model.name,
           View(moduleModel.logger).modelToWidget(model, this, value));
     });
   }
 
+  /// Completes database based components to the model, e.g. the list for
+  /// comboboxes.
+  void completeModelByPersistence(WidgetModel model) {
+    if (model is ComboBaseModel) {
+      if (model.data == null) {
+        if (model.listType == ComboboxListType.explicite) {
+          model.data = ComboBaseModel.createByType(
+              model.dataType, model.texts, model.values);
+          model.data.waitState = WaitState.ready;
+        } else {
+          model.data = comboboxData(model.name, model.dataType);
+        }
+      }
+    }
+  }
+
   @override
-  ComboboxData comboboxData<T>(String name) {
+  ComboboxData comboboxData(String name, DataType dataType) {
+    ComboboxData rc;
+    switch (dataType) {
+      case DataType.bool:
+        rc = comboboxDataByType<bool>(name);
+        break;
+      case DataType.currency:
+      case DataType.int:
+      case DataType.reference:
+        rc = comboboxDataByType<int>(name);
+        break;
+      case DataType.date:
+      case DataType.dateTime:
+        rc = comboboxDataByType<DateTime>(name);
+        break;
+      case DataType.float:
+        rc = comboboxDataByType<double>(name);
+        break;
+      case DataType.string:
+        comboboxDataByType<String>(name);
+        break;
+    }
+    return rc;
+  }
+
+  ComboboxData<T> comboboxDataByType<T>(String name) {
     ComboboxData rc = comboboxDataMap.containsKey(name)
         ? comboboxDataMap[name]
         : ComboboxData<T>([], []);
@@ -101,7 +143,7 @@ class PageControllerBones implements CallbackControllerBones {
             comboboxDataMap[name] = rc;
             break;
           case ComboboxListType.dbColumn:
-            comboboxDataDb(model, rc);
+            comboboxDataDb<T>(model, rc);
             break;
           case ComboboxListType.configuration:
             break;
@@ -111,7 +153,7 @@ class PageControllerBones implements CallbackControllerBones {
     return rc;
   }
 
-  void comboboxDataDb(ComboBaseModel model, ComboboxData data) {
+  void comboboxDataDb<T>(ComboBaseModel model, ComboboxData<T> data) {
     // example: role.list;role_displayname role_id;:role_name=% :excluded=0'
     final parts = model.listOption.split(';');
     final moduleName = parts[0].split('.');
@@ -148,7 +190,7 @@ class PageControllerBones implements CallbackControllerBones {
   /// Gets the data from the database using the [primaryId].
   fetchData(int primaryId) async {
     applicationData.persistence
-        .record(module: moduleModel.name, id: primaryId)
+        .recordById(module: moduleModel.name, id: primaryId)
         .then((row) {
       page.fields.forEach((model) {
         model.value = row[model.name];
@@ -159,18 +201,19 @@ class PageControllerBones implements CallbackControllerBones {
     });
   }
 
-  FilterSet filterSet({@required String pageName}) {
-    final rc = FilterSet(globalKey, parent, this, moduleModel.logger);
+  /// Returns a [WidgetList] filled with widgets
+  WidgetList filterSet({@required String pageName}) {
+    final rc = WidgetList('${page.fullName()}.widgets', moduleModel.logger);
     moduleModel
         .pageByName(pageName)
         .fields
         .where((element) => element.filterType != null)
         .forEach((element) {
-      rc.add(FilterItem(
-          label: element.label,
-          filterType: element.filterType,
-          toolTip: element.toolTip,
-          name: element.name));
+      Widget widget = View().modelToWidget(element, this);
+      if (element.widgetModelType == WidgetModelType.combobox) {
+        rc.waitCandidates.add(element);
+      }
+      rc.addWidget(element.name, widget);
     });
     return rc;
   }
@@ -352,7 +395,7 @@ class PageControllerBones implements CallbackControllerBones {
     ColumnModel primary = moduleModel.mainTable().primary;
     final id = row[primary.name];
     applicationData.persistence
-        .record(module: moduleModel.name, id: id)
+        .recordById(module: moduleModel.name, id: id)
         .then((row) => startChange(id, row));
   }
 
index aad66b23d31789e8f6838ca4278a01a5899617ad..bd650d98dd657ebbab0a7fa4ad2b7cb8edd17a95 100644 (file)
@@ -74,10 +74,38 @@ class View {
     return rc;
   }
 
-  /// Creates a combobox via the [controller].
-  Widget combobox<T>(
+  /// Creates a combobox via the [controller] with an [initialValue].
+  Widget combobox(
       FieldModel model, CallbackControllerBones controller, initialValue) {
-    ComboboxData<T> comboboxData = controller.comboboxData(model.name);
+    Widget rc;
+    switch (model.dataType) {
+      case DataType.bool:
+        rc = comboboxByType<bool>(model, controller, initialValue);
+        break;
+      case DataType.currency:
+      case DataType.int:
+      case DataType.reference:
+        rc = comboboxByType<int>(model, controller, initialValue);
+        break;
+      case DataType.date:
+      case DataType.dateTime:
+        rc = comboboxByType<DateTime>(model, controller, initialValue);
+        break;
+      case DataType.float:
+        rc = comboboxByType<double>(model, controller, initialValue);
+        break;
+      case DataType.string:
+        comboboxByType<String>(model, controller, initialValue);
+        break;
+    }
+    return rc;
+  }
+
+  /// Creates a combobox via the [controller] depending on the type <T>
+  /// with an [initialValue].
+  Widget comboboxByType<T>(
+      FieldModel model, CallbackControllerBones controller, initialValue) {
+    ComboboxData<T> comboboxData = (model as ComboBaseModel).data;
     final items = <DropdownMenuItem<T>>[];
     for (var ix = 0; ix < comboboxData.texts.length; ix++) {
       items.add(DropdownMenuItem(
@@ -95,6 +123,8 @@ class View {
     return rc;
   }
 
+  /// Creates a widget related to a [model] of type [DbReferenceModel]
+  /// via the [controller] with an [initialValue].
   Widget dbReference(DbReferenceModel model, CallbackControllerBones controller,
       dynamic initialValue) {
     var rc;
index 9fb72affc2d89bb8674a70d261157f07dc1cc5a9..fd861eec108e7808f46b91cb04a4c956f43b80c5 100644 (file)
@@ -1,6 +1,7 @@
 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 '../helper/string_helper.dart';
 import '../model/model_types.dart';
@@ -15,6 +16,9 @@ class WidgetList {
   final widgetMap = <String, Widget>{};
   final widgets = <Widget>[];
 
+  /// Data that will be filled asynchronously.
+  final waitCandidates = <FieldModel>[];
+
   WidgetList(this.name, this.logger);
 
   /// Tests whether the widget list is empty.
@@ -93,4 +97,30 @@ class WidgetList {
     widgets.clear();
     widgetMap.clear();
   }
+
+  /// Checks in a loop all open requests for completion.
+  /// If
+  Future waitForCompletion(PageControllerBones controller) async {
+    var again = true;
+    ComboboxData data;
+    // 30 sec:
+    int maxCount = 50 * 30;
+    while (again && maxCount > 0) {
+      again = false;
+      for (var model in waitCandidates) {
+        if (model is ComboBaseModel) {
+          data = controller.comboboxData(model.listOption, model.dataType);
+          if (data != null && data.waitState == WaitState.ready) {
+            model.data = data;
+            waitCandidates.remove(model);
+          } else {
+            again = true;
+          }
+        }
+      }
+      if (again) {
+        await wait(millisec: 20);
+      }
+    }
+  }
 }
index 443628105db670dfd30b65549c52af75ae46793c..e90c6530b56b97b268b82bfde1ab9e686dd71d63 100644 (file)
@@ -2,12 +2,14 @@ import 'dart:convert';
 
 import 'package:dart_bones/dart_bones.dart';
 import 'package:flutter_bones/flutter_bones.dart';
+import 'package:flutter_bones/src/model/combo_base_model.dart';
 import 'package:test/test.dart';
 
 void main() {
   final logger = MemoryLogger(LEVEL_FINE);
   group('module', () {
     test('module', () {
+      WidgetModel.lastId = 0;
       logger.clear();
       final module = Demo1(cloneOfMap(userModel), logger);
       module.parse();
@@ -20,7 +22,7 @@ void main() {
       expect(table.fullName(), equals('demo1.user'));
       expect(table.widgetName(), equals('user'));
       final dump = module.dump(StringBuffer()).toString();
-      expect(dump, '''= module demo1: options: 
+      expect(dump, equals('''= module demo1: options: 
 == table user: options: 
     column user_id: DataType.int "Id" options: primary notnull unique
     column user_name: DataType.string "User" options: unique
@@ -42,7 +44,7 @@ void main() {
     textField role: options: 
     button buttonStore: text: options: null 
     ] # create.simpleForm1
-''');
+'''));
       final userField = table.columnByName('user_id');
       expect(userField, isNotNull);
       expect(userField.widgetName(), 'user_id');
@@ -53,6 +55,135 @@ void main() {
       expect(userRef.widgetName(), equals('user'));
     });
   });
+  test('combobase-basic', () {
+    var data = ComboBaseModel.createByType(DataType.bool, ['x'], <bool>[true]);
+    expect(data, isNotNull);
+    data = ComboBaseModel.createByType(DataType.int, ['x'], <int>[3]);
+    expect(data, isNotNull);
+    data = ComboBaseModel.createByType(DataType.float, ['x'], <double>[3.9]);
+    expect(data, isNotNull);
+    data = ComboBaseModel.createByType(
+        DataType.date, ['x'], <DateTime>[DateTime(2020, 1, 1)]);
+    expect(data, isNotNull);
+    data = ComboBaseModel.createByType(DataType.string, ['x'], <String>["Hi"]);
+    expect(data, isNotNull);
+  });
+  test('combobase', () {
+    WidgetModel.lastId = 0;
+    logger.clear();
+    final map = <String, dynamic>{
+      'module': 'demo1',
+      'tables': [
+        {
+          'table': 'user',
+          'columns': [
+            {
+              'column': 'user_id',
+              'dataType': 'int',
+              'label': 'Id',
+              'options': 'primary',
+              'listType': 'explicite',
+              'texts': ';a;b',
+              'values': ';22;33'
+            },
+          ],
+        },
+      ],
+    };
+    final module = ModuleModel(map, logger);
+    module.parse();
+    final errors = logger.errors;
+    expect(errors.length, equals(1));
+  });
+  group('errors', () {
+    test('errors-combobase', () {
+      WidgetModel.lastId = 0;
+      logger.clear();
+      final map = <String, dynamic>{
+        'module': 'demo1',
+        'tables': [
+          {
+            'table': 'user',
+            'columns': [
+              {
+                'column': 'user_id',
+                'dataType': 'int',
+                'label': 'Id',
+                'options': 'primary',
+                'listType': 'undef',
+              },
+            ],
+          },
+        ],
+      };
+      final module = ModuleModel(map, logger);
+      module.parse();
+      final errors = logger.errors;
+      expect(errors.length, equals(2));
+      expect(
+          errors.contains(
+              'wrong value "undef" in attribute "listType" in user.user_id'),
+          isTrue);
+    });
+    test('errors-missing-texts', () {
+      WidgetModel.lastId = 0;
+      logger.clear();
+      final map = <String, dynamic>{
+        'module': 'demo1',
+        'tables': [
+          {
+            'table': 'user',
+            'columns': [
+              {
+                'column': 'user_id',
+                'dataType': 'int',
+                'label': 'Id',
+                'listType': 'explicite',
+              },
+            ],
+          },
+        ],
+      };
+      final module = ModuleModel(map, logger);
+      module.parse();
+      final errors = logger.errors;
+      expect(errors.length, equals(2));
+      expect(
+          errors.contains(
+              'missing attribute "texts" in user.user_id'),
+          isTrue);
+    });
+    test('errors-#texts!=#values', () {
+      WidgetModel.lastId = 0;
+      logger.clear();
+      final map = <String, dynamic>{
+        'module': 'demo1',
+        'tables': [
+          {
+            'table': 'user',
+            'columns': [
+              {
+                'column': 'user_id',
+                'dataType': 'int',
+                'label': 'Id',
+                'listType': 'explicite',
+                'texts': ';a',
+                'values': ';1;2',
+              },
+            ],
+          },
+        ],
+      };
+      final module = ModuleModel(map, logger);
+      module.parse();
+      final errors = logger.errors;
+      expect(errors.length, equals(2));
+      expect(
+          errors.contains(
+              'different sizes of "Texts" and "values": 1/2'),
+          isTrue);
+    });
+  });
 }
 
 final userModel = <String, dynamic>{
@@ -78,7 +209,7 @@ final userModel = <String, dynamic>{
           'column': 'user_role',
           'dataType': 'reference',
           'label': 'Role',
-          'foreignKey': 'role.role_id',
+          'foreignKey': 'role.role_id role_name',
           'widgetType': 'combobox',
         },
       ]
index a2c5cc39bd8393059a6edc3e2095f731143354c9..e4fdbd2ac11103120731fedc31bb7a86317eca6c 100644 (file)
@@ -8,6 +8,7 @@ void main() {
   final logger = MemoryLogger(LEVEL_FINE);
   group('module', () {
     test('module', () {
+      WidgetModel.lastId = 0;
       logger.clear();
       final module = Demo1(cloneOfMap(userModel), logger);
       module.parse();
@@ -17,9 +18,9 @@ void main() {
       expect(page?.fullName(), equals('demo1.create'));
       expect(module.fullName(), equals('demo1'));
       final dump = module.dump(StringBuffer()).toString();
-      expect(dump, '''= module demo1: options: 
+      expect(dump, equals('''= module demo1: options: 
 == table user: options: 
-    column user_id: DataType.int "Id" options: primary notnull unique
+    column user_id: DataType.int "Id" options: primary notnull unique readonly
     column user_name: DataType.string "User" options: unique notnull
     column user_role: DataType.reference "Role" options: 
     column user_createdat: DataType.dateTime "Erzeugt" options: hidden null
@@ -33,9 +34,9 @@ void main() {
     ] # create.simpleForm1
 == page change: PageModelType.change options: 
     = section simpleForm1: SectionModelType.simpleForm options:  [
-    allDbFields 12 options: 
+    allDbFields 13 options: 
     ] # change.simpleForm1
-''');
+'''));
       final userField = page.fieldByName('user');
       expect(userField, isNotNull);
       expect(userField.widgetName(), 'user');
@@ -49,6 +50,204 @@ void main() {
       expect(button.section, equals(userField.section));
       expect(button.fullName(), 'simpleForm1.buttonStore');
       expect(button.widgetName(), 'buttonStore');
+      final all = module.pageByName('change').getWidgets(
+              (item) => item.widgetModelType == WidgetModelType.allDbFields);
+      expect(all.length, equals(1));
+      final widget = all[0];
+      final name = widget.fullName() + ' ' + widget.widgetName();
+      expect(name.contains('allDbFields'), isTrue);
+    });
+  });
+  group('page-errors', () {
+    test('add-button', () {
+      WidgetModel.lastId = 0;
+      logger.clear();
+      final map = <String, dynamic>{
+        'module': 'demo1',
+        'tables': [
+          {
+            'table': 'user',
+            'columns': [
+              {
+                'column': 'user_id',
+                'dataType': 'int',
+                'label': 'Id',
+                'options': 'primary',
+              },
+            ]
+          },
+        ],
+        'pages': [
+          {
+            "page": "list",
+            "pageType": "list",
+            "tableColumns": "user_id",
+            "tableTitles": ";Id;Name",
+            "sections": [
+              {
+                "sectionType": "filterPanel",
+                "children": [
+                  {
+                    "widgetType": "dbReference",
+                    "filterType": "pattern",
+                    "name": "user_name",
+                    "column": "user_name",
+                  },
+                ],
+              },
+            ],
+          },
+        ],
+      };
+      final module = ModuleModel(map, logger);
+      module.parse();
+      final page = module.pageByName('list');
+      page.addButton(ButtonModel.direct(
+          null,
+          page,
+          'a',
+          'b',
+          ButtonModelType.store,
+          [],
+          null,
+          logger));
+      page.addButton(ButtonModel.direct(
+          null,
+          page,
+          'a',
+          'b',
+          ButtonModelType.store,
+          [],
+          null,
+          logger));
+      page.buttonByName('unknown');
+      page.addField(TextFieldModel.direct(
+          null,
+          page,
+          null,
+          'x',
+          'y',
+          'z',
+          DataType.int,
+          <String>[],
+          logger));
+      page.addField(TextFieldModel.direct(
+          null,
+          page,
+          null,
+          'x',
+          'y',
+          'z',
+          DataType.int,
+          <String>[],
+          logger));
+      page.buttonByName('unknown');
+      page.fieldByName('nothing');
+      final errors = logger.errors;
+      expect(errors.length, equals(7));
+      expect(errors.contains('missing column user_name in table demo1.user'),
+          isTrue);
+      expect(
+          errors.contains('different sizes of tableTitles/tableColumns: 2/1'),
+          isTrue);
+      expect(errors.contains('button null.a already defined: null.a'),
+          isTrue);
+      expect(errors.contains('missing button unknown in page demo1.list'),
+          isTrue);
+      expect(errors.contains('missing field nothing in page demo1.list'),
+          isTrue);
+      expect(errors.contains('field list.x already defined: list.x'),
+          isTrue);
+    });
+    test('missing-section', () {
+      WidgetModel.lastId = 0;
+      logger.clear();
+      final map = <String, dynamic>{
+        'module': 'demo1',
+        'pages': [
+          {
+            "page": "list",
+            "pageType": "list",
+          },
+        ],
+      };
+      final module = ModuleModel(map, logger);
+      module.parse();
+      final page = module.pageByName('list');
+      expect(page, isNotNull);
+      final errors = logger.errors;
+      expect(errors.length, equals(1));
+      expect(errors.contains('missing sections in page demo1.list'),
+          isTrue);
+    });
+    test('wrong-section', () {
+      WidgetModel.lastId = 0;
+      logger.clear();
+      final map = <String, dynamic>{
+        'module': 'demo1',
+        'pages': [
+          {
+            "page": "list",
+            "pageType": "list",
+            "sections": "wrong"
+          },
+        ],
+      };
+      final module = ModuleModel(map, logger);
+      module.parse();
+      final page = module.pageByName('list');
+      expect(page, isNotNull);
+      final errors = logger.errors;
+      expect(errors.length, equals(1));
+      expect(errors.contains('"sections" is not an array in demo1.list: wrong'),
+          isTrue);
+    });
+    test('tableTitles not in list', () {
+      WidgetModel.lastId = 0;
+      logger.clear();
+      final map = <String, dynamic>{
+        'module': 'demo1',
+        'pages': [
+          {
+            "page": "list",
+            "pageType": "change",
+            "tableTitles": ";a;b",
+          },
+        ],
+      };
+      final module = ModuleModel(map, logger);
+      module.parse();
+      final page = module.pageByName('list');
+      final errors = logger.errors;
+      expect(errors.length, equals(2));
+      expect(errors.contains(
+          'tableTitles and tableColumns are only meaningful in list pages: demo1.list'),
+          isTrue);
+      expect(page.fullName() + page.widgetName(), equals('demo1.listlist'));
+    });
+    test('curious section', () {
+      WidgetModel.lastId = 0;
+      logger.clear();
+      final map = <String, dynamic>{
+        'module': 'demo1',
+        'pages': [
+          {
+            "page": "list",
+            "pageType": "change",
+            "sections": [
+              [],
+            ]
+          },
+        ],
+      };
+      final module = ModuleModel(map, logger);
+      module.parse();
+      final page = module.pageByName('list');
+      final errors = logger.errors;
+      expect(errors.length, equals(1));
+      expect(errors.contains('curious item in section list of demo1.list: []'),
+          isTrue);
+      expect(page.fullName() + page.widgetName(), equals('demo1.listlist'));
     });
   });
   group('ModelBase', () {
@@ -68,12 +267,13 @@ void main() {
       var errors = logger.errors;
       expect(errors.length, equals(1));
       expect(logger.contains('blub'), isTrue);
-      logger.clearErrors();
+      logger.clear();
       field['options'] = 3;
       module = Demo1(map, logger);
+      logger.log('wrong option type: 3');
       module.parse();
       errors = logger.errors;
-      expect(errors.length, equals(1));
+      expect(errors.length, greaterThan(0));
       expect(logger.contains('wrong datatype'), isTrue);
       // ===
       logger.clear();
@@ -83,7 +283,7 @@ void main() {
       module = Demo1(map, logger);
       module.parse();
       errors = logger.errors;
-      expect(errors.length, equals(3));
+      expect(errors.length, greaterThan(2));
       expect(logger.contains('unknown attribute "newfeature"'), isTrue);
       expect(
           logger.contains(
@@ -94,19 +294,20 @@ void main() {
       // ===
       logger.clear();
       field.removeWhere(
-          (key, value) => key == 'newfeature' || key == 'dataType');
+              (key, value) => key == 'newfeature' || key == 'dataType');
       map['pages'][0].remove('pageType');
       module = Demo1(map, logger);
       module.parse();
       map['pages'][0]['pageType'] = 'simpleForm';
       errors = logger.errors;
-      expect(errors.length, equals(2));
+      expect(errors.length, greaterThan(1));
       expect(logger.contains('missing attribute \"pageType\" in demo1.create'),
           isTrue);
     });
   });
   group('CheckboxModel', () {
     test('basic', () {
+      WidgetModel.lastId = 0;
       logger.clear();
       final map = cloneOfMap(userModel);
       final field = <String, dynamic>{
@@ -130,6 +331,7 @@ void main() {
       expect(checkbox.value, isTrue);
     });
     test('errors', () {
+      WidgetModel.lastId = 0;
       logger.clear();
       final map = cloneOfMap(userModel);
       final field = <String, dynamic>{
@@ -149,7 +351,7 @@ void main() {
       final dump = module.dump(StringBuffer()).toString();
       expect(dump, equals('''= module demo1: options: 
 == table user: options: 
-    column user_id: DataType.int "Id" options: primary notnull unique
+    column user_id: DataType.int "Id" options: primary notnull unique readonly
     column user_name: DataType.string "User" options: unique notnull
     column user_role: DataType.reference "Role" options: 
     column user_createdat: DataType.dateTime "Erzeugt" options: hidden null
@@ -163,12 +365,13 @@ void main() {
     ] # create.simpleForm1
 == page change: PageModelType.change options: 
     = section simpleForm1: SectionModelType.simpleForm options:  [
-    allDbFields 102 options: 
+    allDbFields 13 options: 
     ] # change.simpleForm1
 '''));
     });
   });
   group('ComboboxModel', () {
+    WidgetModel.lastId = 0;
     logger.clear();
     test('basic', () {
       final map = cloneOfMap(userModel);
@@ -192,7 +395,7 @@ void main() {
       final dump = module.dump(StringBuffer()).toString();
       expect(dump, equals('''= module demo1: options: 
 == table user: options: 
-    column user_id: DataType.int "Id" options: primary notnull unique
+    column user_id: DataType.int "Id" options: primary notnull unique readonly
     column user_name: DataType.string "User" options: unique notnull
     column user_role: DataType.reference "Role" options: 
     column user_createdat: DataType.dateTime "Erzeugt" options: hidden null
@@ -206,13 +409,14 @@ void main() {
     ] # create.simpleForm1
 == page change: PageModelType.change options: 
     = section simpleForm1: SectionModelType.simpleForm options:  [
-    allDbFields 117 options: 
+    allDbFields 30 options: 
     ] # change.simpleForm1
 '''));
     });
   });
-  group('allDbFields', (){
-    test('allDbFields', (){
+  group('allDbFields', () {
+    test('allDbFields', () {
+      WidgetModel.lastId = 0;
       logger.clear();
       final map = cloneOfMap(userModel);
       var module = Demo1(map, logger);
@@ -229,6 +433,7 @@ void main() {
   });
   group('Non field widgets', () {
     test('basic', () {
+      WidgetModel.lastId = 0;
       logger.clear();
       final map = cloneOfMap(userModel);
       final list = [
@@ -252,13 +457,148 @@ void main() {
             ' ' + element.widgetName() + '/' + element.fullName();
       });
       expect(names.contains('null'), isFalse);
-      final nonFieldWidgets = page.getWidgets((item) => [
+      final nonFieldWidgets = page.getWidgets((item) =>
+          [
             WidgetModelType.text,
             WidgetModelType.emptyLine
           ].contains(item.widgetModelType));
       expect(nonFieldWidgets.length, equals(2));
     });
   });
+  group('section-errors', () {
+    test('missing children', () {
+      WidgetModel.lastId = 0;
+      logger.clear();
+      final map = <String, dynamic>{
+        'module': 'demo1',
+        'pages': [
+          {
+            "page": "list",
+            "pageType": "list",
+            "sections": [
+              {
+                "sectionType": "filterPanel",
+              },
+            ],
+          },
+        ],
+      };
+      final module = ModuleModel(map, logger);
+      module.parse();
+      final errors = logger.errors;
+      expect(errors.length, equals(1));
+      expect(errors.contains('missing children in list.filterPanel1'),
+          isTrue);
+    });
+    test('wrong children', () {
+      WidgetModel.lastId = 0;
+      logger.clear();
+      final map = <String, dynamic>{
+        'module': 'demo1',
+        'pages': [
+          {
+            "page": "list",
+            "pageType": "list",
+            "sections": [
+              {
+                "sectionType": "filterPanel",
+                "children": "a"
+              },
+            ],
+          },
+        ],
+      };
+      final module = ModuleModel(map, logger);
+      module.parse();
+      final errors = logger.errors;
+      expect(errors.length, equals(1));
+      expect(errors.contains('"children" is not a list in list.filterPanel1: a'),
+          isTrue);
+    });
+    test('not a map in children', () {
+      WidgetModel.lastId = 0;
+      logger.clear();
+      final map = <String, dynamic>{
+        'module': 'demo1',
+        'pages': [
+          {
+            "page": "list",
+            "pageType": "list",
+            "sections": [
+              {
+                "sectionType": "filterPanel",
+                "children": [
+                  []
+                ]
+              },
+            ],
+          },
+        ],
+      };
+      final module = ModuleModel(map, logger);
+      module.parse();
+      final errors = logger.errors;
+      expect(errors.length, equals(1));
+      expect(errors.contains('child 1 of "children" is not a map in list.filterPanel1: []'),
+          isTrue);
+    });
+    test('missing type in child', () {
+      WidgetModel.lastId = 0;
+      logger.clear();
+      final map = <String, dynamic>{
+        'module': 'demo1',
+        'pages': [
+          {
+            "page": "list",
+            "pageType": "list",
+            "sections": [
+              {
+                "sectionType": "filterPanel",
+                "children": [
+                  {},
+                ]
+              },
+            ],
+          },
+        ],
+      };
+      final module = ModuleModel(map, logger);
+      module.parse();
+      final errors = logger.errors;
+      expect(errors.length, equals(1));
+      expect(errors.contains('child 1 of "children" does not have "widgetType" in list.filterPanel1: {}'),
+          isTrue);
+    });
+    test('unknown type in child', () {
+      WidgetModel.lastId = 0;
+      logger.clear();
+      final map = <String, dynamic>{
+        'module': 'demo1',
+        'pages': [
+          {
+            "page": "list",
+            "pageType": "list",
+            "sections": [
+              {
+                "sectionType": "filterPanel",
+                "children": [
+                  {
+                    "widgetType": ""
+                  },
+                ]
+              },
+            ],
+          },
+        ],
+      };
+      final module = ModuleModel(map, logger);
+      module.parse();
+      final errors = logger.errors;
+      expect(errors.length, equals(1));
+      expect(errors.contains('Section: unknown "widgetType"  in list.filterPanel1'),
+          isTrue);
+    });
+  });
 }
 
 final userModel = <String, dynamic>{
@@ -284,7 +624,7 @@ final userModel = <String, dynamic>{
           'column': 'user_role',
           'dataType': 'reference',
           'label': 'Role',
-          'foreignKey': 'role.role_id',
+          'foreignKey': 'role.role_id role_name',
           'widgetType': 'combobox',
         },
       ]
index e0cd2159805b0b7dd2a85793b7ca0692e23fc785..7e076695d31c8f46f1aa9bfc79afbcd23c97b638 100644 (file)
@@ -6,6 +6,7 @@ void main() {
   final logger = MemoryLogger(LEVEL_FINE);
   group('module', () {
     test('role', () {
+      WidgetModel.lastId = 0;
       logger.clear();
       final module = RoleModel(logger);
       module.parse();
@@ -16,7 +17,7 @@ void main() {
       final dump = module.dump(StringBuffer()).toString();
       expect(dump, '''= module role: options: 
 == table role: options: 
-    column role_id: DataType.int "Id" options: primary notnull unique
+    column role_id: DataType.int "Id" options: primary notnull unique readonly
     column role_name: DataType.string "Rolle" options: unique notnull
     column role_priority: DataType.int "Priorität" options: 
     column role_active: DataType.bool "Aktiv" options: 
@@ -30,7 +31,7 @@ void main() {
     ] # create.simpleForm1
 == page change: PageModelType.change options: 
     = section simpleForm1: SectionModelType.simpleForm options:  [
-    allDbFields 13 options: 
+    allDbFields 16 options: 
     ] # change.simpleForm1
 == page list: PageModelType.list options: 
     = section filterPanel1: SectionModelType.filterPanel options:  [
@@ -39,6 +40,7 @@ void main() {
 ''');
     });
     test('user', () {
+      WidgetModel.lastId = 0;
       logger.clear();
       final module = UserModel(logger);
       module.parse();
@@ -47,13 +49,13 @@ void main() {
       final errors = logger.errors;
       expect(errors.length, equals(0));
       final dump = module.dump(StringBuffer()).toString();
-      expect(dump, '''= module user: options: 
+      expect(dump, equals('''= module user: options: 
 == table user: options: 
-    column user_id: DataType.int "Id" options: primary notnull unique
+    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 "User" options: password
+    column user_password: DataType.string "Passwort" options: password
     column user_role: DataType.reference "Role" options: 
     column user_createdat: DataType.dateTime "Erzeugt" options: hidden null
     column user_createdby: DataType.string "Erzeugt von" options: hidden
@@ -61,20 +63,21 @@ void main() {
     column user_changedby: DataType.string "Geändert von" options: hidden
 == page create: PageModelType.create options: 
     = section simpleForm1: SectionModelType.simpleForm options:  [
-    allDbFields 31 options: 
+    allDbFields 12 options: 
     ] # create.simpleForm1
 == page change: PageModelType.change options: 
     = section simpleForm1: SectionModelType.simpleForm options:  [
-    allDbFields 34 options: 
+    allDbFields 20 options: 
     ] # change.simpleForm1
 == page list: PageModelType.list options: 
     = section filterPanel1: SectionModelType.filterPanel options:  [
     textField user_name: options: 
-    combobox user_role: texts:  options: 
+    textField user_role: options: 
     ] # list.filterPanel1
-''');
+'''));
     });
     test('configuration', () {
+      WidgetModel.lastId = 0;
       logger.clear();
       final module = ConfigurationModel(logger);
       module.parse();
@@ -83,10 +86,10 @@ void main() {
       final errors = logger.errors;
       expect(errors.length, equals(0));
       final dump = module.dump(StringBuffer()).toString();
-      expect(dump, startsWith('''= module configuration: options: 
+      expect(dump, equals('''= module configuration: options: 
 == table configuration: options: 
-    column configuration_id: DataType.int "Id" options: primary notnull unique
-    column configuration_scope: DataType.string "Bereich" options: unique notnull
+    column configuration_id: DataType.int "Id" options: primary notnull unique readonly
+    column configuration_scope: DataType.string "Bereich" options: notnull
     column configuration_property: DataType.string "Eigenschaft" options: 
     column configuration_order: DataType.int "Reihe" options: 
     column configuration_type: DataType.string "Datentyp" options: 
@@ -98,17 +101,18 @@ void main() {
     column configuration_changedby: DataType.string "Geändert von" options: hidden
 == page create: PageModelType.create options: 
     = section simpleForm1: SectionModelType.simpleForm options:  [
-    allDbFields'''));
-      /*
-      expect(dump.contains('''text: options:
+    allDbFields 13 options: 
     ] # create.simpleForm1
 == page change: PageModelType.change options: 
     = section simpleForm1: SectionModelType.simpleForm options:  [
-    allDbFields '''), isTrue);
-      */
-      expect(dump,
-          contains('combobox configuration_scope: texts:  options: undef'));
-      expect(dump, contains('textField configuration_name: options:'));
+    allDbFields 22 options: 
+    ] # change.simpleForm1
+== page list: PageModelType.list options: 
+    = section filterPanel1: SectionModelType.filterPanel options:  [
+    textField configuration_scope: options: 
+    textField configuration_property: options: 
+    ] # list.filterPanel1
+'''));
     });
   });
 }
diff --git a/test/page/application_test.dart b/test/page/application_test.dart
new file mode 100644 (file)
index 0000000..02bdac1
--- /dev/null
@@ -0,0 +1,38 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:flutter_bones/src/widget/page_controller_bones.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() async {
+  final logger = MemoryLogger(LEVEL_FINE);
+  RestPersistence persistence;
+  BaseConfiguration configuration;
+  PageControllerBones pageControllerBones;
+  ApplicationData applicationData;
+  setUpAll(() {
+    configuration = BaseConfiguration({
+      'client': {
+        'host': 'localhost',
+        'port': 58011,
+        'schema': 'http',
+        'application': 'unittest',
+        'version': '1.0.0',
+      }
+    }, logger);
+    persistence = RestPersistence.fromConfig(configuration, logger);
+    pageControllerBones = PageControllerBones(
+        null, null, UserModel(logger), 'list', null, applicationData);
+    applicationData =
+          ApplicationData(configuration, null, null, persistence, logger);
+  });
+  group('Basics', () {
+    test('basic', () async {
+      expect(applicationData, isNotNull);
+      expect(pageControllerBones, isNotNull);
+      applicationData.pushCaller(pageControllerBones);
+      applicationData.popCaller();
+      applicationData.setLastErrorMessage('list', 'True error');
+      expect(applicationData.lastErrorMessage('list'), 'True error');
+    });
+  });
+}
index 04881671107c41abd5ff2e48bc2b20d5a46865bf..69e017e20761f059877ab15f0ff62cbac0161523 100644 (file)
@@ -29,6 +29,7 @@ void main() async {
   });
   group('basics', () {
     test('combobox', () async {
+      logger.clear();
       final cache2 = PersistenceCache();
       expect(cache2, isNotNull);
       const key = 'id1.role.list;role_name role_id;:role_name=%';
@@ -37,9 +38,9 @@ void main() async {
       var data2 = cache.combobox(key2, hasUndef: false);
       expect(data, isNull);
       expect(data2, isNull);
-      await sleep(200);
+      await wait(millisec: 200);
       data = cache.combobox(key);
-      await sleep(200);
+      await wait(millisec: 200);
       data2 = cache.combobox(key2);
       expect(logger.errors.length, equals(0));
       expect(data, isNotNull);
@@ -52,9 +53,11 @@ void main() async {
       expect(cache.leastReasentlyUsed.length, equals(2));
       const key3 = 'id3.role.list;role_name role_id;:role_name=%';
       var data3 = cache.combobox(key3, hasUndef: false);
+      expect(data3, isNull);
       expect(cache.leastReasentlyUsed.length, equals(2));
       expect(cache.map.containsKey(key), isFalse);
       expect(cache.map.containsKey(key2), isTrue);
+      expect(cache.deleteEntry(key3), isTrue);
     });
     test('error', () {
       logger.clear();
@@ -63,9 +66,25 @@ void main() async {
       expect(logger.errors.length, equals(1));
       expect(logger.contains('wrong key syntax: id1.role.list;role_name+role_id;:role_name=%'), isTrue);
     });
+    test('record', () async {
+      logger.clear();
+      final cache2 = PersistenceCache();
+      expect(cache2, isNotNull);
+      const key = 'id1.role.by_role_name;:role_name=Administrator :excluded=0';
+      const key2 = 'id2.role.by_role_name;:role_name=Admistrator :excluded=1';
+      var data = cache.record(key, oneTime: true);
+      var data2 = cache.record(key2, oneTime: false);
+      expect(data, isNull);
+      expect(data2, isNull);
+      await wait(millisec: 200);
+      data = cache.record(key);
+      data2 = cache.record(key2);
+      await wait(millisec: 200);
+      expect(logger.errors.length, equals(0));
+      expect(data, isNotNull);
+      expect(data.containsKey('role_id'), isTrue);
+      expect(data2.isEmpty, isTrue);
+    });
   });
 }
 
-Future sleep(int millisec) {
-  return Future.delayed(Duration(milliseconds: millisec), () => null);
-}
index 2e35b4bbbe1c83853320ae83286ff12a103267fb..7a35bebe35447f23b14193810f06126107a8b41b 100644 (file)
@@ -40,9 +40,19 @@ void main() async {
       expect(list.length, greaterThanOrEqualTo(4));
       expect(list[0].containsKey('role_id'), isTrue);
     });
-    test('record', () async {
+    test('recordById', () async {
       final rest = RestPersistence();
-      final record = await rest.record(module: 'role', id: 2);
+      final record = await rest.recordById(module: 'role', id: 2);
+      expect(record, isNotNull);
+      expect(record.length, greaterThan(4));
+      expect(record.containsKey('role_id'), isTrue);
+    });
+    test('recordByName', () async {
+      final rest = RestPersistence();
+      final record = await rest.recordByParameter(
+          module: 'role',
+          sqlName: 'by_role_name',
+          parameters: {':role_name': 'Administrator', ':excluded': 0});
       expect(record, isNotNull);
       expect(record.length, greaterThan(4));
       expect(record.containsKey('role_id'), isTrue);
@@ -73,7 +83,7 @@ void main() async {
         ':role_active': 'T',
         ':role_changedby': 'eve'
       });
-      final answer = rest.record(module: 'role', id: id);
+      final answer = rest.recordById(module: 'role', id: id);
       logger.log('log: $answer');
       await rest.delete(module: 'role', id: id);
     });
index 55254ba8acab3b38a54d3a35f1d65feaa566b5b2..790b331856d1224d9e65f73f1807457c99fbd230 100644 (file)
@@ -61,7 +61,7 @@ modules:
         sql: "SELECT * from role WHERE role_name=:role_name&&role_id!=:excluded;"
       - name: list
         type: list
-        sql: "SELECT * from role
+        sql: "SELECT t0.* from role t0
           WHERE role_name like :role_name;"
 '''));
     });
index d67b471f6ccf92f28050f28423d7d84b0713dbd0..2c32b0b624589d960915799e32da6b0879e97b52 100644 (file)
@@ -10,14 +10,25 @@ import 'package:flutter/src/foundation/diagnostics.dart';
 import 'package:flutter/src/widgets/framework.dart' as x;
 import 'package:flutter_bones/flutter_bones.dart';
 import 'package:flutter_bones/src/widget/widget_helper.dart';
-import 'package:flutter_bones/src/private/bsettings.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('WidgetHelper', () {
     test('toolTip', () {
       final widget = Text('Hi');
@@ -29,9 +40,8 @@ void main() {
   });
   group('ModuleController', () {
     test('basic', () {
-      ApplicationData appData = ApplicationData(
-          BaseConfiguration({}, logger), (title) => null, (context) => null,
-      BSettings.lastInstance.persistence, logger);
+      ApplicationData appData = ApplicationData(BaseConfiguration({}, logger),
+          (title) => null, (context) => null, persistence, logger);
       final role = RoleCreatePage(appData);
       if (appData.lastModuleState == null) {
         role.createState();