xmas-app 0.15.1__tar.gz → 0.15.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {xmas_app-0.15.1 → xmas_app-0.15.2}/PKG-INFO +1 -1
  2. {xmas_app-0.15.1 → xmas_app-0.15.2}/pyproject.toml +1 -1
  3. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/form.py +200 -60
  4. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/pages/planmanager.py +9 -0
  5. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/util/db.py +1 -1
  6. {xmas_app-0.15.1 → xmas_app-0.15.2}/LICENSE +0 -0
  7. {xmas_app-0.15.1 → xmas_app-0.15.2}/README.md +0 -0
  8. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/components/__init__.py +0 -0
  9. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/components/association.py +0 -0
  10. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/components/buttons.py +0 -0
  11. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/components/label.py +0 -0
  12. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/deps/version_guard.py +0 -0
  13. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/main.py +0 -0
  14. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/models/__init__.py +0 -0
  15. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/models/crud.py +0 -0
  16. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/models/mappings.py +0 -0
  17. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/models/split.py +0 -0
  18. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/pages/__init__.py +0 -0
  19. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/pages/base.py +0 -0
  20. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/pygeoapi/config.yaml +0 -0
  21. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/pygeoapi/openapi.yaml +0 -0
  22. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/pygeoapi/provider.py +0 -0
  23. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/resources/mappings.toml +0 -0
  24. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/services/__init__.py +0 -0
  25. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/services/crud.py +0 -0
  26. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/services/split.py +0 -0
  27. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/settings.py +0 -0
  28. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/util/__init__.py +0 -0
  29. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/util/codelist.py +0 -0
  30. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/util/db_uow.py +0 -0
  31. {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/util/get_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xmas-app
3
- Version: 0.15.1
3
+ Version: 0.15.2
4
4
  Summary: The XLeitstelle model-driven application schema app.
5
5
  License: EUPL-1.2-or-later
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "xmas-app"
3
- version = "0.15.1"
3
+ version = "0.15.2"
4
4
  description = "The XLeitstelle model-driven application schema app."
5
5
  authors = [{ name = "Tobias Kraft", email = "tobias.kraft@gv.hamburg.de" }]
6
6
  classifiers = [
@@ -1,9 +1,10 @@
1
+ import logging
1
2
  import re
2
3
  from datetime import date
3
4
  from functools import partial
4
5
 
5
6
  import orjson
6
- from nicegui import ElementFilter, app, run, ui
7
+ from nicegui import ElementFilter, app, events, run, ui
7
8
  from nicegui.binding import BindableProperty
8
9
  from pydantic import AnyUrl, ValidationError
9
10
  from sqlalchemy import select
@@ -17,6 +18,8 @@ from xmas_app.components.label import LabelWithDescription
17
18
  from xmas_app.settings import get_settings
18
19
  from xmas_app.util.codelist import get_codelist_options
19
20
 
21
+ logger = logging.getLogger("xmas_app")
22
+
20
23
 
21
24
  class ModelForm:
22
25
  """A dynamic form builder for XPlan features using NiceGUI.
@@ -50,6 +53,35 @@ class ModelForm:
50
53
  self.header = header
51
54
  self.rendered = False
52
55
  self.editable = False
56
+ self.validation_errors = []
57
+ self.validation_notification = None
58
+ ui.on("showValidationDetails", self._show_validation_details)
59
+
60
+ async def _show_validation_details(self, _: events.GenericEventArguments):
61
+ def _go_to_property_row(key: str):
62
+ validation_dialog.close()
63
+ for element in ElementFilter(marker=["property_row", key]).not_within(
64
+ marker="sub_form"
65
+ ):
66
+ ui.navigate.to(element)
67
+ break
68
+
69
+ if self.parent:
70
+ with self.parent, ui.dialog() as validation_dialog, ui.card():
71
+ ui.label("Validierungsfehler").classes("text-bold")
72
+ with ui.list():
73
+ for error in self.validation_errors:
74
+ name = error["loc"][0]
75
+ with ui.item(
76
+ on_click=lambda key=name: _go_to_property_row(key)
77
+ ).tooltip(f"Klicken um zum Attribut {name!r} zu springen"):
78
+ with ui.item_section().props("avatar"):
79
+ ui.icon("error", color="negative")
80
+ with ui.item_section():
81
+ ui.item_label(name)
82
+ ui.item_label(error["msg"]).props("caption")
83
+ await validation_dialog
84
+ validation_dialog.delete()
53
85
 
54
86
  async def _filter_properties(self):
55
87
  """Filter property rows in the UI based on the selected filter option."""
@@ -231,17 +263,35 @@ class ModelForm:
231
263
  ):
232
264
  setattr(feature, geom_field, geom)
233
265
  try:
234
- return self.model.model_validate(feature)
235
- except ValidationError:
236
- if not getattr(self, "validation_error_notification", False):
237
- self.validation_error_notification = True
238
- ui.notification(
239
- "Fehlende Pflichtattribute",
266
+ model_instance = self.model.model_validate(feature)
267
+ if self.validation_notification:
268
+ self.validation_notification.dismiss()
269
+ return model_instance
270
+ except ValidationError as e:
271
+ logger.debug(
272
+ "validation error for feature %r with id %r:\n %s",
273
+ self.featuretype,
274
+ self.feature.id,
275
+ "\n ".join(str(error) for error in e.errors()),
276
+ )
277
+ self.validation_errors = e.errors()
278
+ if not self.validation_notification:
279
+ self.validation_notification = ui.notification(
280
+ "Validierungsfehler",
240
281
  type="negative",
241
282
  position="bottom",
242
- on_dismiss=lambda: setattr(
243
- self, "validation_error_notification", False
244
- ),
283
+ timeout=None,
284
+ on_dismiss=lambda: setattr(self, "validation_notification", None),
285
+ actions=[
286
+ {
287
+ "icon": "description",
288
+ ":handler": "() => {emitEvent('showValidationDetails')}",
289
+ "noDismiss": True,
290
+ },
291
+ {
292
+ "icon": "close",
293
+ },
294
+ ],
245
295
  )
246
296
 
247
297
  async def _set_stylesheetId_and_schriftinhalt(self) -> None:
@@ -723,7 +773,10 @@ class ModelForm:
723
773
  case "BasicType":
724
774
  match prop_info["typename"]:
725
775
  case "CharacterString":
726
- if key == "art":
776
+ if (
777
+ key == "art"
778
+ and "stylesheetId" in model.model_fields.keys()
779
+ ):
727
780
  options = {}
728
781
  if existing := getattr(bind_obj, key, None):
729
782
  options.update(
@@ -1019,40 +1072,115 @@ class ModelForm:
1019
1072
  case "Temporal":
1020
1073
  match prop_info["typename"]:
1021
1074
  case "Date":
1075
+ property_row.tooltip("Eingabe über Kalender")
1022
1076
  with (
1023
1077
  ui.input(
1024
- prop_info["typename"],
1025
- validation={
1026
- "ungültiges Datum": self.validate_date
1027
- }
1078
+ label=prop_info["typename"],
1079
+ validation=None
1028
1080
  if prop_info["nullable"]
1029
1081
  else {
1030
1082
  "Pflichtfeld": lambda x: x,
1031
- "ungültiges Datum": self.validate_date,
1032
1083
  },
1033
1084
  )
1034
- .bind_value(
1085
+ .bind_value_from(
1035
1086
  bind_obj,
1036
1087
  key,
1037
- backward=lambda x: None if not x else x,
1088
+ backward=lambda x: ", ".join(x)
1089
+ if isinstance(x, list)
1090
+ else x,
1038
1091
  )
1039
1092
  .classes("w-full q-pa-none q-gutter-none")
1093
+ .style("pointer-events: none;")
1040
1094
  .props(
1041
- "stack-label square filled dense mask='####-##-##'"
1042
- ) as date
1095
+ """
1096
+ stack-label
1097
+ square
1098
+ dense
1099
+ filled
1100
+ """
1101
+ ) as date_viewer
1043
1102
  ):
1044
- with ui.menu().props("no-parent-event") as menu:
1045
- ui.date(on_change=menu.close).bind_value(
1046
- date
1047
- ).bind_enabled_from(self, "editable")
1048
- with date.add_slot("append"):
1103
+ with ui.dialog() as menu:
1104
+ date_picker = (
1105
+ ui.date()
1106
+ .bind_value(
1107
+ bind_obj,
1108
+ key,
1109
+ forward=lambda x: sorted(x)
1110
+ if isinstance(x, list)
1111
+ else x or None,
1112
+ )
1113
+ .bind_enabled_from(self, "editable")
1114
+ .classes("w-96")
1115
+ .props(
1116
+ f"""
1117
+ title='{key}'
1118
+ :locale='{
1119
+ orjson.dumps(
1120
+ {
1121
+ "daysShort": [
1122
+ "So",
1123
+ "Mo",
1124
+ "Di",
1125
+ "Mi",
1126
+ "Do",
1127
+ "Fr",
1128
+ "Sa",
1129
+ ],
1130
+ "months": [
1131
+ "Januar",
1132
+ "Februar",
1133
+ "März",
1134
+ "April",
1135
+ "Mai",
1136
+ "Juni",
1137
+ "Juli",
1138
+ "August",
1139
+ "September",
1140
+ "Oktober",
1141
+ "November",
1142
+ "Dezember",
1143
+ ],
1144
+ "monthsShort": [
1145
+ "Jan",
1146
+ "Feb",
1147
+ "Mär",
1148
+ "Apr",
1149
+ "Mai",
1150
+ "Jun",
1151
+ "Jul",
1152
+ "Aug",
1153
+ "Sep",
1154
+ "Okt",
1155
+ "Nov",
1156
+ "Dez",
1157
+ ],
1158
+ }
1159
+ ).decode()
1160
+ }'
1161
+ """
1162
+ )
1163
+ )
1164
+ if prop_info["list"]:
1165
+ date_picker.props(
1166
+ "multiple subtitle='Liste von Datumsangaben'"
1167
+ )
1168
+ else:
1169
+ date_picker.props(
1170
+ "subtitle='Datumsangabe'"
1171
+ )
1172
+ date_picker.on_value_change(menu.close)
1173
+ with date_viewer.add_slot("append"):
1049
1174
  ui.icon("edit_calendar").on(
1050
1175
  "click", menu.open
1051
- ).classes("cursor-pointer")
1052
- date.bind_enabled_from(
1176
+ ).classes("cursor-pointer").style(
1177
+ "pointer-events: auto;"
1178
+ )
1179
+ date_viewer.bind_enabled_from(
1053
1180
  self,
1054
1181
  "editable",
1055
- backward=lambda v, input=date: input.props(
1182
+ backward=lambda v,
1183
+ input=date_viewer: input.props(
1056
1184
  **{
1057
1185
  "add"
1058
1186
  if not v
@@ -1060,41 +1188,53 @@ class ModelForm:
1060
1188
  }
1061
1189
  ),
1062
1190
  )
1063
- date.validate()
1191
+ date_viewer.validate()
1064
1192
  case "TM_Duration":
1065
- input = (
1066
- ui.number(
1067
- prop_info["typename"],
1068
- value=default,
1069
- suffix="Tage",
1070
- step=1,
1071
- precision=0,
1072
- format="%d",
1073
- validation=None
1074
- if prop_info["nullable"]
1075
- else {
1076
- "Pflichtfeld": lambda x: x is not None
1077
- },
1078
- )
1079
- .bind_value(
1080
- bind_obj,
1081
- key,
1082
- forward=lambda x: x * 86400 if x else None,
1083
- backward=lambda x: x / 86400 if x else None,
1193
+ if not prop_info["list"]:
1194
+ input = (
1195
+ ui.number(
1196
+ prop_info["typename"],
1197
+ value=default,
1198
+ suffix="Tage",
1199
+ step=1,
1200
+ precision=0,
1201
+ format="%d",
1202
+ validation=None
1203
+ if prop_info["nullable"]
1204
+ else {
1205
+ "Pflichtfeld": lambda x: x
1206
+ is not None
1207
+ },
1208
+ )
1209
+ .bind_value(
1210
+ bind_obj,
1211
+ key,
1212
+ forward=lambda x: x * 86400
1213
+ if x
1214
+ else None,
1215
+ backward=lambda x: x / 86400
1216
+ if x
1217
+ else None,
1218
+ )
1219
+ .props(
1220
+ "stack-label clearable square filled dense"
1221
+ )
1222
+ .classes("w-full q-pa-none q-gutter-none")
1084
1223
  )
1085
- .props(
1086
- "stack-label clearable square filled dense"
1224
+ input.bind_enabled_from(
1225
+ self,
1226
+ "editable",
1227
+ backward=lambda v, input=input: input.props(
1228
+ **{
1229
+ "add"
1230
+ if not v
1231
+ else "remove": "readonly"
1232
+ }
1233
+ ),
1087
1234
  )
1088
- .classes("w-full q-pa-none q-gutter-none")
1089
- )
1090
- input.bind_enabled_from(
1091
- self,
1092
- "editable",
1093
- backward=lambda v, input=input: input.props(
1094
- **{"add" if not v else "remove": "readonly"}
1095
- ),
1096
- )
1097
- input.validate()
1235
+ input.validate()
1236
+ else:
1237
+ ui.label("TODO")
1098
1238
  case _:
1099
1239
  ui.label("TODO Temporal")
1100
1240
  case "DataType":
@@ -341,6 +341,13 @@ class PlanManagerPage(BasePage):
341
341
  "align": "left",
342
342
  "sortable": True,
343
343
  },
344
+ {
345
+ "name": "name",
346
+ "label": "Name",
347
+ "field": "name",
348
+ "align": "left",
349
+ "sortable": True,
350
+ },
344
351
  ]
345
352
  table = (
346
353
  ui.table(
@@ -357,6 +364,8 @@ class PlanManagerPage(BasePage):
357
364
  dense
358
365
  virtual-scroll
359
366
  bordered
367
+ visible-columns=['label','featuretype','appschema','updated']
368
+ :filter-method="(rows, terms) => rows.filter(row => ['featuretype','appschema','updated','name'].some(key => String(row[key] ?? '').toLowerCase().includes((terms || '').toLowerCase())))"
360
369
  """
361
370
  )
362
371
  .classes("w-full h-full")
@@ -54,7 +54,7 @@ def get_db_plans() -> list[dict]:
54
54
  plan_data = {
55
55
  "id": str(feature.id),
56
56
  "name": plan_name,
57
- "label": f"{plan_name[:15]}{'...' if len(plan_name) > 15 else ''}",
57
+ "label": f"{plan_name[:20]}{'...' if len(plan_name) > 20 else ''}",
58
58
  "featuretype": feature.featuretype,
59
59
  "appschema": f"{feature.appschema.upper()} {feature.version}",
60
60
  "updated": f"{updated_local.strftime('%d.%m.%Y %H:%M')}",
File without changes
File without changes
File without changes