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.
- {xmas_app-0.15.1 → xmas_app-0.15.2}/PKG-INFO +1 -1
- {xmas_app-0.15.1 → xmas_app-0.15.2}/pyproject.toml +1 -1
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/form.py +200 -60
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/pages/planmanager.py +9 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/util/db.py +1 -1
- {xmas_app-0.15.1 → xmas_app-0.15.2}/LICENSE +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/README.md +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/components/__init__.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/components/association.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/components/buttons.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/components/label.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/deps/version_guard.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/main.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/models/__init__.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/models/crud.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/models/mappings.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/models/split.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/pages/__init__.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/pages/base.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/pygeoapi/config.yaml +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/pygeoapi/openapi.yaml +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/pygeoapi/provider.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/resources/mappings.toml +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/services/__init__.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/services/crud.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/services/split.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/settings.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/util/__init__.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/util/codelist.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/util/db_uow.py +0 -0
- {xmas_app-0.15.1 → xmas_app-0.15.2}/xmas_app/util/get_models.py +0 -0
|
@@ -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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
|
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
|
-
.
|
|
1085
|
+
.bind_value_from(
|
|
1035
1086
|
bind_obj,
|
|
1036
1087
|
key,
|
|
1037
|
-
backward=lambda 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
|
-
"
|
|
1042
|
-
|
|
1095
|
+
"""
|
|
1096
|
+
stack-label
|
|
1097
|
+
square
|
|
1098
|
+
dense
|
|
1099
|
+
filled
|
|
1100
|
+
"""
|
|
1101
|
+
) as date_viewer
|
|
1043
1102
|
):
|
|
1044
|
-
with ui.
|
|
1045
|
-
|
|
1046
|
-
date
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1191
|
+
date_viewer.validate()
|
|
1064
1192
|
case "TM_Duration":
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
-
.
|
|
1086
|
-
|
|
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
|
-
.
|
|
1089
|
-
|
|
1090
|
-
|
|
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[:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|