xmas-app 0.13.1__py3-none-any.whl → 0.14.0__py3-none-any.whl
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/db.py +8 -122
- xmas_app/form.py +6 -5
- xmas_app/main.py +43 -273
- xmas_app/settings.py +1 -1
- {xmas_app-0.13.1.dist-info → xmas_app-0.14.0.dist-info}/METADATA +2 -2
- {xmas_app-0.13.1.dist-info → xmas_app-0.14.0.dist-info}/RECORD +9 -9
- {xmas_app-0.13.1.dist-info → xmas_app-0.14.0.dist-info}/WHEEL +0 -0
- {xmas_app-0.13.1.dist-info → xmas_app-0.14.0.dist-info}/entry_points.txt +0 -0
- {xmas_app-0.13.1.dist-info → xmas_app-0.14.0.dist-info}/licenses/LICENSE +0 -0
xmas_app/db.py
CHANGED
|
@@ -1,77 +1,15 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
3
|
from nicegui.observables import ObservableSet
|
|
4
|
-
from sqlalchemy import select
|
|
5
|
-
from sqlalchemy.
|
|
6
|
-
from sqlalchemy.orm import aliased, selectinload
|
|
7
|
-
from xplan_tools.interface.db import DBRepository
|
|
4
|
+
from sqlalchemy import select
|
|
5
|
+
from sqlalchemy.orm import selectinload
|
|
8
6
|
from xplan_tools.model import model_factory
|
|
9
7
|
from xplan_tools.model.base import BaseFeature
|
|
10
8
|
from xplan_tools.model.orm import Feature, Refs
|
|
11
9
|
|
|
12
10
|
from xmas_app.settings import get_mappings, get_settings
|
|
13
11
|
|
|
14
|
-
logger = logging.getLogger(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def get_db_feature_ids(
|
|
18
|
-
repo: DBRepository,
|
|
19
|
-
typenames: str | list | None = None,
|
|
20
|
-
featuretype_regex: str | None = None,
|
|
21
|
-
value_prop: str | None = None,
|
|
22
|
-
) -> dict:
|
|
23
|
-
"""
|
|
24
|
-
Query the database for features matching the provided type(s) or regex, and
|
|
25
|
-
return a mapping from feature IDs to label (from `value_prop`) or a fallback string.
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
repo (DBRepository): The database repository instance.
|
|
29
|
-
typenames (str | list | None): Single typename or list of typenames to filter on.
|
|
30
|
-
featuretype_regex (str | None): Optional regex to match featuretype.
|
|
31
|
-
value_prop (str | None): The property to use for the label in the result dict.
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
dict: Mapping from stringified feature ID to label or fallback.
|
|
35
|
-
"""
|
|
36
|
-
logger.info("Entered db.get_db_feature_ids().")
|
|
37
|
-
results = {}
|
|
38
|
-
try:
|
|
39
|
-
with repo.Session() as session:
|
|
40
|
-
stmt = select(Feature)
|
|
41
|
-
if typenames:
|
|
42
|
-
names_list = [typenames] if isinstance(typenames, str) else typenames
|
|
43
|
-
stmt = stmt.where(Feature.featuretype.in_(names_list))
|
|
44
|
-
logger.info(f"Filtering for typenames: {names_list}")
|
|
45
|
-
if featuretype_regex:
|
|
46
|
-
stmt = stmt.where(Feature.featuretype.regexp_match(featuretype_regex))
|
|
47
|
-
logger.info(f"Filtering with regex: {featuretype_regex}")
|
|
48
|
-
try:
|
|
49
|
-
db_result = session.execute(stmt)
|
|
50
|
-
features = db_result.unique().scalars().all()
|
|
51
|
-
logger.info(f"Found {len(features)} feature(s) in DB.")
|
|
52
|
-
except Exception as db_ex:
|
|
53
|
-
logger.error(f"Database query failed: {db_ex}", exc_info=True)
|
|
54
|
-
return {}
|
|
55
|
-
|
|
56
|
-
for feature in features:
|
|
57
|
-
label = None
|
|
58
|
-
try:
|
|
59
|
-
label = (
|
|
60
|
-
feature.properties.get(value_prop, None) if value_prop else None
|
|
61
|
-
)
|
|
62
|
-
except Exception as prop_ex:
|
|
63
|
-
logger.warning(
|
|
64
|
-
f"Error accessing properties of feature ID {feature.id}: {prop_ex}",
|
|
65
|
-
exc_info=True,
|
|
66
|
-
)
|
|
67
|
-
key = str(feature.id)
|
|
68
|
-
value = label if label else f"{feature.featuretype}::{feature.id}"
|
|
69
|
-
results[key] = value
|
|
70
|
-
|
|
71
|
-
except Exception as ex:
|
|
72
|
-
logger.error(f"Failed to get feature IDs: {ex}", exc_info=True)
|
|
73
|
-
raise
|
|
74
|
-
return results
|
|
12
|
+
logger = logging.getLogger("xmas_app")
|
|
75
13
|
|
|
76
14
|
|
|
77
15
|
def get_db_plans() -> list[dict]:
|
|
@@ -113,34 +51,11 @@ def get_db_plans() -> list[dict]:
|
|
|
113
51
|
return results
|
|
114
52
|
|
|
115
53
|
|
|
116
|
-
def delete_db_association(base_id: str, related_id: str, repo: DBRepository):
|
|
117
|
-
try:
|
|
118
|
-
with repo.Session() as session:
|
|
119
|
-
stmt = text("""
|
|
120
|
-
DELETE FROM public.refs
|
|
121
|
-
WHERE base_id = :base_id AND related_id = :related_id
|
|
122
|
-
""")
|
|
123
|
-
try:
|
|
124
|
-
result = session.execute(
|
|
125
|
-
stmt, {"base_id": base_id, "related_id": related_id}
|
|
126
|
-
)
|
|
127
|
-
session.commit()
|
|
128
|
-
logger.info(
|
|
129
|
-
f"Deleted {result.rowcount} record(s) from public.refs with base_id={base_id} and related_id={related_id}"
|
|
130
|
-
)
|
|
131
|
-
return {"deleted_rows": result.rowcount}
|
|
132
|
-
except SQLAlchemyError as db_ex:
|
|
133
|
-
session.rollback()
|
|
134
|
-
logger.error(f"Database delete failed: {db_ex}", exc_info=True)
|
|
135
|
-
return {"error": str(db_ex)}
|
|
136
|
-
except Exception as ex:
|
|
137
|
-
logger.error(f"Failed to delete association: {ex}", exc_info=True)
|
|
138
|
-
raise
|
|
139
|
-
|
|
140
|
-
|
|
141
54
|
def get_nodes(
|
|
142
55
|
id: str, features_set: ObservableSet
|
|
143
56
|
) -> tuple[dict, str, str, str] | None:
|
|
57
|
+
"""Build tree nodes for ui.tree element."""
|
|
58
|
+
|
|
144
59
|
def build_node(feature: Feature, path: str) -> dict:
|
|
145
60
|
features_set.add(feature.id)
|
|
146
61
|
node = {
|
|
@@ -280,38 +195,9 @@ def get_ref_candidates(
|
|
|
280
195
|
rel_inv = model.get_property_info(rel)["assoc_info"]["reverse"]
|
|
281
196
|
|
|
282
197
|
with get_settings().repo.Session() as session:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
IndirectFeature = aliased(Feature, name="indirect_feature")
|
|
287
|
-
|
|
288
|
-
RefPlanDirect = aliased(Refs, name="ref_plan_direct")
|
|
289
|
-
RefDirectIndirect = aliased(Refs, name="ref_direct_indirect")
|
|
290
|
-
direct = (
|
|
291
|
-
select(DirectFeature)
|
|
292
|
-
.select_from(Plan)
|
|
293
|
-
.join(RefPlanDirect, RefPlanDirect.base_id == Plan.id)
|
|
294
|
-
.join(DirectFeature, DirectFeature.id == RefPlanDirect.related_id)
|
|
295
|
-
.where(Plan.id == plan_id)
|
|
296
|
-
.where(DirectFeature.featuretype.in_(featuretypes))
|
|
297
|
-
.where(DirectFeature.id.not_in(refs))
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
indirect = (
|
|
301
|
-
select(IndirectFeature)
|
|
302
|
-
.select_from(Plan)
|
|
303
|
-
.join(RefPlanDirect, RefPlanDirect.base_id == Plan.id)
|
|
304
|
-
.join(DirectFeature, DirectFeature.id == RefPlanDirect.related_id)
|
|
305
|
-
.join(RefDirectIndirect, RefDirectIndirect.base_id == DirectFeature.id)
|
|
306
|
-
.join(IndirectFeature, IndirectFeature.id == RefDirectIndirect.related_id)
|
|
307
|
-
.where(Plan.id == plan_id)
|
|
308
|
-
.where(IndirectFeature.featuretype.in_(featuretypes))
|
|
309
|
-
.where(IndirectFeature.id.not_in(refs))
|
|
310
|
-
)
|
|
311
|
-
stmt = direct.union(indirect)
|
|
312
|
-
|
|
313
|
-
candidates = (
|
|
314
|
-
session.execute(select(Feature).from_statement(stmt)).scalars().all()
|
|
198
|
+
plan = session.get(Feature, plan_id)
|
|
199
|
+
candidates = plan.related_features(
|
|
200
|
+
session, featuretypes=featuretypes, exclude_ids=refs
|
|
315
201
|
)
|
|
316
202
|
return [
|
|
317
203
|
_feature_to_table_row(feature)
|
xmas_app/form.py
CHANGED
|
@@ -1112,6 +1112,11 @@ class ModelForm:
|
|
|
1112
1112
|
)
|
|
1113
1113
|
.classes("w-full q-pa-none q-gutter-none")
|
|
1114
1114
|
.props("square filled dense")
|
|
1115
|
+
.bind_value(
|
|
1116
|
+
bind_obj,
|
|
1117
|
+
key,
|
|
1118
|
+
backward=lambda x: None if not x else x,
|
|
1119
|
+
)
|
|
1115
1120
|
)
|
|
1116
1121
|
targets = await run.io_bound(
|
|
1117
1122
|
self._get_association_targets,
|
|
@@ -1132,11 +1137,6 @@ class ModelForm:
|
|
|
1132
1137
|
input.set_value(parent_id)
|
|
1133
1138
|
elif len((keys := list(targets.keys()))) == 1:
|
|
1134
1139
|
input.set_value(keys[0])
|
|
1135
|
-
input.bind_value(
|
|
1136
|
-
bind_obj,
|
|
1137
|
-
key,
|
|
1138
|
-
backward=lambda x: None if not x else x,
|
|
1139
|
-
)
|
|
1140
1140
|
input.bind_enabled_from(
|
|
1141
1141
|
self,
|
|
1142
1142
|
"editable",
|
|
@@ -1222,3 +1222,4 @@ class ModelForm:
|
|
|
1222
1222
|
feature[model_geom] = geom
|
|
1223
1223
|
self.feature = self._get_bindable_object(self.model)(**feature)
|
|
1224
1224
|
await self._build_model_form(self.model, self.feature, main_form, preview)
|
|
1225
|
+
self._propagate_data()
|
xmas_app/main.py
CHANGED
|
@@ -6,10 +6,11 @@ import logging
|
|
|
6
6
|
import re
|
|
7
7
|
from contextlib import asynccontextmanager
|
|
8
8
|
from functools import partial
|
|
9
|
+
from importlib import metadata
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from tempfile import NamedTemporaryFile, gettempdir
|
|
11
|
-
from typing import
|
|
12
|
-
from uuid import
|
|
12
|
+
from typing import Literal
|
|
13
|
+
from uuid import UUID
|
|
13
14
|
|
|
14
15
|
import pydantic
|
|
15
16
|
import pydantic_core
|
|
@@ -17,7 +18,6 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
|
17
18
|
from nicegui import app, run, ui
|
|
18
19
|
from nicegui.events import ClickEventArguments, ValueChangeEventArguments
|
|
19
20
|
from nicegui.observables import ObservableSet
|
|
20
|
-
from pydantic import ValidationError
|
|
21
21
|
from starlette.applications import Starlette
|
|
22
22
|
from xplan_tools.interface import repo_factory
|
|
23
23
|
from xplan_tools.interface.db import DBRepository
|
|
@@ -30,7 +30,7 @@ from xplan_tools.util import (
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
from xmas_app.components.buttons import FeatureInteractionButton
|
|
33
|
-
from xmas_app.db import
|
|
33
|
+
from xmas_app.db import get_db_plans, get_nodes
|
|
34
34
|
from xmas_app.deps.version_guard import enforce_plugin_version
|
|
35
35
|
from xmas_app.form import ModelForm
|
|
36
36
|
from xmas_app.models.crud import InsertPayload, UpdatePayload
|
|
@@ -39,6 +39,8 @@ from xmas_app.services import crud
|
|
|
39
39
|
from xmas_app.settings import get_appschema, get_settings
|
|
40
40
|
from xmas_app.split_service import PlanSplitService, SplitValidationError
|
|
41
41
|
|
|
42
|
+
__version__ = metadata.version("xmas_app")
|
|
43
|
+
|
|
42
44
|
|
|
43
45
|
def _resolve_log_dir() -> Path:
|
|
44
46
|
if get_settings().app_mode == "prod":
|
|
@@ -108,42 +110,19 @@ async def get_model_for_select(
|
|
|
108
110
|
return False
|
|
109
111
|
return True
|
|
110
112
|
|
|
113
|
+
options = [
|
|
114
|
+
model.get_name()
|
|
115
|
+
for _, model in inspect.getmembers(
|
|
116
|
+
importlib.import_module(
|
|
117
|
+
f"xplan_tools.model.appschema.{appschema + appschema_version.replace('.', '')}"
|
|
118
|
+
),
|
|
119
|
+
filter_members,
|
|
120
|
+
)
|
|
121
|
+
]
|
|
111
122
|
# update the input element options
|
|
112
|
-
model_select.set_options(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for _, model in inspect.getmembers(
|
|
116
|
-
importlib.import_module(
|
|
117
|
-
f"xplan_tools.model.appschema.{appschema + appschema_version.replace('.', '')}"
|
|
118
|
-
),
|
|
119
|
-
filter_members,
|
|
120
|
-
)
|
|
121
|
-
]
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
@ui.page("/", dependencies=[Depends(enforce_plugin_version)])
|
|
126
|
-
def index():
|
|
127
|
-
"""
|
|
128
|
-
Render the main index page of the XPlan-GUI application.
|
|
129
|
-
Args:
|
|
130
|
-
request (Request): The incoming HTTP request object (FastAPI).
|
|
131
|
-
"""
|
|
132
|
-
# print(request.headers)
|
|
133
|
-
ui.colors(primary="rgb(157 157 156)", secondary="rgb(57 92 127)")
|
|
134
|
-
with ui.header():
|
|
135
|
-
ui.label("XPlan-GUI")
|
|
136
|
-
ui.button(
|
|
137
|
-
"Neues Objekt", on_click=lambda: ui.navigate.to(f"/feature/{str(uuid4())}")
|
|
138
|
-
)
|
|
139
|
-
ui.select(
|
|
140
|
-
dict(
|
|
141
|
-
sorted(get_db_feature_ids(get_settings().repo).items(), key=lambda x: x[1])
|
|
142
|
-
),
|
|
143
|
-
label="Feature auswählen",
|
|
144
|
-
with_input=True,
|
|
145
|
-
on_change=lambda x: ui.navigate.to(f"/feature/{x.value}"),
|
|
146
|
-
)
|
|
123
|
+
model_select.set_options(options)
|
|
124
|
+
if len(options) == 1:
|
|
125
|
+
model_select.set_value(options[0])
|
|
147
126
|
|
|
148
127
|
|
|
149
128
|
@ui.page(
|
|
@@ -153,15 +132,21 @@ def index():
|
|
|
153
132
|
)
|
|
154
133
|
async def plan_tree(
|
|
155
134
|
request: Request,
|
|
156
|
-
id:
|
|
135
|
+
id: UUID,
|
|
157
136
|
):
|
|
137
|
+
id = str(id)
|
|
138
|
+
|
|
158
139
|
async def update_nodes():
|
|
159
|
-
result = await get_nodes
|
|
140
|
+
result = await run.io_bound(get_nodes, id, app.storage.client["features"])
|
|
160
141
|
if result:
|
|
161
142
|
nodes, *_ = result
|
|
162
|
-
|
|
143
|
+
app.storage.client["nodes"] = nodes
|
|
163
144
|
tree.update()
|
|
164
145
|
|
|
146
|
+
ui.colors(primary="rgb(157 157 156)", secondary="rgb(57 92 127)")
|
|
147
|
+
ui.button.default_props("flat square no-caps")
|
|
148
|
+
ui.button_group.default_props("flat square no-caps")
|
|
149
|
+
|
|
165
150
|
app.storage.client["user_agent"] = request.headers.get("user-agent", None)
|
|
166
151
|
qgis = app.storage.client["user_agent"].startswith(get_settings().qgis_plugin_name)
|
|
167
152
|
if qgis:
|
|
@@ -182,21 +167,6 @@ async def plan_tree(
|
|
|
182
167
|
ui.separator()
|
|
183
168
|
action_dialog = ui.dialog().on("hide", lambda e: e.sender.clear())
|
|
184
169
|
|
|
185
|
-
ui.colors(primary="rgb(157 157 156)", secondary="rgb(57 92 127)")
|
|
186
|
-
ui.button.default_props("flat square no-caps")
|
|
187
|
-
ui.button_group.default_props("flat square no-caps")
|
|
188
|
-
|
|
189
|
-
# # --- Create the dialog placeholder once ---
|
|
190
|
-
# with ui.dialog() as confirm_removeAssociation_dialog, ui.card():
|
|
191
|
-
# removeAssociation_label = (
|
|
192
|
-
# ui.label()
|
|
193
|
-
# ) # placeholder text we can update dynamically
|
|
194
|
-
# with ui.row().classes("justify-end w-full"):
|
|
195
|
-
# cancel_removeAssociation_button = ui.button(
|
|
196
|
-
# "Abbrechen", on_click=confirm_removeAssociation_dialog.close
|
|
197
|
-
# )
|
|
198
|
-
# removeAssociation_confirm_button = ui.button("Aufheben", color="red")
|
|
199
|
-
|
|
200
170
|
# --- Create the dialog placeholder once ---
|
|
201
171
|
with ui.dialog() as confirm_deleteObject_dialog, ui.card(align_items="stretch"):
|
|
202
172
|
label = ui.label(
|
|
@@ -212,16 +182,6 @@ async def plan_tree(
|
|
|
212
182
|
confirm_button.on("click", partial(delete_feature, target))
|
|
213
183
|
confirm_deleteObject_dialog.open()
|
|
214
184
|
|
|
215
|
-
# # --- Function to open the dialog dynamically ---
|
|
216
|
-
# def confirm_delete_association(origin, target):
|
|
217
|
-
# removeAssociation_label.text = f"Möchten Sie diese Beziehung wirklich aufheben?"
|
|
218
|
-
# removeAssociation_confirm_button.on(
|
|
219
|
-
# "click",
|
|
220
|
-
# lambda: remove_association(origin, target),
|
|
221
|
-
# confirm_removeAssociation_dialog.close(),
|
|
222
|
-
# )
|
|
223
|
-
# confirm_removeAssociation_dialog.open()
|
|
224
|
-
|
|
225
185
|
async def show_menu(e: ValueChangeEventArguments):
|
|
226
186
|
action_dialog.clear()
|
|
227
187
|
if e.value:
|
|
@@ -377,9 +337,6 @@ async def plan_tree(
|
|
|
377
337
|
def update_notification(e):
|
|
378
338
|
notification.message = f"{len(e.sender)} Features geladen"
|
|
379
339
|
|
|
380
|
-
if not id:
|
|
381
|
-
return
|
|
382
|
-
|
|
383
340
|
await ui.context.client.connected()
|
|
384
341
|
logger.debug("Feature called by ID:")
|
|
385
342
|
logger.debug(id)
|
|
@@ -398,8 +355,9 @@ async def plan_tree(
|
|
|
398
355
|
nodes, featuretype, id, appschema = result
|
|
399
356
|
else:
|
|
400
357
|
return
|
|
358
|
+
app.storage.client["nodes"] = nodes
|
|
401
359
|
tree = ui.tree(
|
|
402
|
-
nodes,
|
|
360
|
+
app.storage.client["nodes"],
|
|
403
361
|
on_select=show_menu,
|
|
404
362
|
tick_strategy=None,
|
|
405
363
|
).props("accordion no-transition")
|
|
@@ -760,13 +718,13 @@ async def plans(
|
|
|
760
718
|
@ui.page("/feature/{id}", dependencies=[Depends(enforce_plugin_version)])
|
|
761
719
|
async def feature(
|
|
762
720
|
request: Request,
|
|
763
|
-
id:
|
|
764
|
-
planId:
|
|
765
|
-
parentId:
|
|
721
|
+
id: UUID,
|
|
722
|
+
planId: UUID | None = None,
|
|
723
|
+
parentId: UUID | None = None,
|
|
766
724
|
featureType: str | None = None,
|
|
767
725
|
featuretypeRegex: str | None = None,
|
|
768
|
-
appschema: str
|
|
769
|
-
version: str
|
|
726
|
+
appschema: str = get_settings().appschema,
|
|
727
|
+
version: str = get_settings().appschema_version,
|
|
770
728
|
):
|
|
771
729
|
"""
|
|
772
730
|
Render and manage a single feature view for the XPlan-GUI application.
|
|
@@ -797,8 +755,7 @@ async def feature(
|
|
|
797
755
|
# form.feature.update(data)
|
|
798
756
|
# form._get_art_options()
|
|
799
757
|
|
|
800
|
-
|
|
801
|
-
version = version or get_settings().appschema_version
|
|
758
|
+
id = str(id)
|
|
802
759
|
|
|
803
760
|
async def get_qgis_feature():
|
|
804
761
|
return await ui.run_javascript(
|
|
@@ -810,20 +767,6 @@ async def feature(
|
|
|
810
767
|
timeout=3,
|
|
811
768
|
)
|
|
812
769
|
|
|
813
|
-
def validate_feature():
|
|
814
|
-
form: ModelForm = app.storage.client["form"]
|
|
815
|
-
if model_instance := form.model_instance:
|
|
816
|
-
feature_data = model_instance.model_dump(
|
|
817
|
-
mode="json",
|
|
818
|
-
exclude_none=True,
|
|
819
|
-
exclude={form.model.get_geom_field()},
|
|
820
|
-
) | {"featuretype": form.model.get_name()}
|
|
821
|
-
ui.run_javascript(f"""new QWebChannel(qt.webChannelTransport, function (channel) {{
|
|
822
|
-
channel.objects.handler.receive_feature({feature_data})
|
|
823
|
-
}});""")
|
|
824
|
-
else:
|
|
825
|
-
form.radio_filter.set_value("mandatory")
|
|
826
|
-
|
|
827
770
|
async def add_form(
|
|
828
771
|
feature_type: str,
|
|
829
772
|
feature: dict,
|
|
@@ -885,15 +828,14 @@ async def feature(
|
|
|
885
828
|
|
|
886
829
|
ui.colors(primary="rgb(157 157 156)", secondary="rgb(57 92 127)")
|
|
887
830
|
app.storage.client["form"] = None
|
|
888
|
-
app.storage.client["plan_id"] = planId
|
|
889
|
-
app.storage.client["parent_id"] = parentId
|
|
831
|
+
app.storage.client["plan_id"] = str(planId) if planId else None
|
|
832
|
+
app.storage.client["parent_id"] = str(parentId) if parentId else None
|
|
890
833
|
app.storage.client["user_agent"] = request.headers.get("user-agent", None)
|
|
891
834
|
|
|
892
835
|
qgis = app.storage.client["user_agent"].startswith(get_settings().qgis_plugin_name)
|
|
893
836
|
|
|
894
837
|
if qgis:
|
|
895
838
|
ui.add_head_html('<script src="qrc:///qtwebchannel/qwebchannel.js"></script>')
|
|
896
|
-
ui.on("validateFeature", validate_feature)
|
|
897
839
|
|
|
898
840
|
try:
|
|
899
841
|
model = get_settings().repo.get(id)
|
|
@@ -982,194 +924,22 @@ async def feature(
|
|
|
982
924
|
await add_form(feature_type, feature)
|
|
983
925
|
|
|
984
926
|
|
|
985
|
-
@ui.page("/feature/{id}/associations", dependencies=[Depends(enforce_plugin_version)])
|
|
986
|
-
def get_associations(
|
|
987
|
-
request: Request,
|
|
988
|
-
id: str,
|
|
989
|
-
# wkbType: str | None = None,
|
|
990
|
-
# planId: str | None = None,
|
|
991
|
-
# parentId: str | None = None,
|
|
992
|
-
# editable: bool = True,
|
|
993
|
-
# featureType: str | None = None,
|
|
994
|
-
# featuretypeRegex: str | None = None,
|
|
995
|
-
appschema: str = get_settings().appschema,
|
|
996
|
-
version: str = get_settings().appschema_version,
|
|
997
|
-
):
|
|
998
|
-
"""
|
|
999
|
-
Render the associations view for a given feature in the XPlan‑GUI application.
|
|
1000
|
-
|
|
1001
|
-
Args:
|
|
1002
|
-
request (Request): Incoming FastAPI HTTP request object.
|
|
1003
|
-
id (str): Identifier or UUID of the feature whose associations will be displayed.
|
|
1004
|
-
appschema (str): Application schema name (defaults from settings).
|
|
1005
|
-
version (str): Schema version string (defaults from settings).
|
|
1006
|
-
"""
|
|
1007
|
-
|
|
1008
|
-
def transfer_feature_data():
|
|
1009
|
-
ui.run_javascript(f"""new QWebChannel(qt.webChannelTransport, function (channel) {{
|
|
1010
|
-
channel.objects.handler.handle_feature_creation({feature_data})
|
|
1011
|
-
}});""")
|
|
1012
|
-
|
|
1013
|
-
def handle_assoc_select(e: ValueChangeEventArguments):
|
|
1014
|
-
stepper.next()
|
|
1015
|
-
feature_data["source"] = {
|
|
1016
|
-
"property": e.value,
|
|
1017
|
-
"value": feature[e.value],
|
|
1018
|
-
"list": model.get_property_info(e.value)["list"],
|
|
1019
|
-
}
|
|
1020
|
-
prop_info = model.get_property_info(e.value)
|
|
1021
|
-
types = prop_info["typename"]
|
|
1022
|
-
type_select.set_options(types if isinstance(types, list) else [types])
|
|
1023
|
-
if isinstance(types, str):
|
|
1024
|
-
type_select.set_value(types)
|
|
1025
|
-
|
|
1026
|
-
def handle_geom_select(geom_regex: str):
|
|
1027
|
-
feature_data["geom_regex"] = geom_regex
|
|
1028
|
-
transfer_feature_data()
|
|
1029
|
-
|
|
1030
|
-
def handle_type_select(e: ValueChangeEventArguments):
|
|
1031
|
-
featuretype = e.value
|
|
1032
|
-
feature_data["featuretype"] = featuretype
|
|
1033
|
-
source_prop = prop_select.value
|
|
1034
|
-
target_model = model_factory(featuretype, version, appschema)
|
|
1035
|
-
if (assoc_info := model.get_property_info(source_prop)["assoc_info"]) and (
|
|
1036
|
-
target_prop := assoc_info["reverse"]
|
|
1037
|
-
):
|
|
1038
|
-
target_prop_info = target_model.get_property_info(target_prop)
|
|
1039
|
-
feature_data[target_prop] = [id] if target_prop_info["list"] else id
|
|
1040
|
-
geom_types = target_model.get_geom_types()
|
|
1041
|
-
if not geom_types:
|
|
1042
|
-
transfer_feature_data()
|
|
1043
|
-
else:
|
|
1044
|
-
geom_required = not target_model.get_property_info(
|
|
1045
|
-
target_model.get_geom_field()
|
|
1046
|
-
)["nullable"]
|
|
1047
|
-
type_regex_mapping = {
|
|
1048
|
-
geom_type.get_name(): geom_type.model_fields["wkt"].metadata[0].pattern
|
|
1049
|
-
for geom_type in geom_types
|
|
1050
|
-
}
|
|
1051
|
-
if not geom_required:
|
|
1052
|
-
type_regex_mapping["Keine Geometrie"] = "NOGEOMETRY"
|
|
1053
|
-
if len(geom_types) == 1 and geom_required:
|
|
1054
|
-
feature_data["geom_regex"] = list(type_regex_mapping.values())[0]
|
|
1055
|
-
transfer_feature_data()
|
|
1056
|
-
else:
|
|
1057
|
-
with stepper:
|
|
1058
|
-
with ui.step("Geometrieart auswählen") as geom_step:
|
|
1059
|
-
ui.label("Geometrieart des neuen Features auswählen")
|
|
1060
|
-
ui.radio(
|
|
1061
|
-
{
|
|
1062
|
-
geom_type: geom_type.replace("PolygonObject", "Fläche")
|
|
1063
|
-
.replace("PointObject", "Punkt")
|
|
1064
|
-
.replace("LineObject", "Linie")
|
|
1065
|
-
for geom_type in type_regex_mapping.keys()
|
|
1066
|
-
},
|
|
1067
|
-
on_change=lambda e: handle_geom_select(
|
|
1068
|
-
type_regex_mapping[e.value]
|
|
1069
|
-
),
|
|
1070
|
-
).props("inline")
|
|
1071
|
-
stepper.set_value(geom_step)
|
|
1072
|
-
|
|
1073
|
-
ui.colors(primary="rgb(157 157 156)", secondary="rgb(57 92 127)")
|
|
1074
|
-
ui.button.default_props("flat square no-caps")
|
|
1075
|
-
ui.dropdown_button.default_props("flat square no-caps")
|
|
1076
|
-
ui.select.default_props("square filled dense").default_classes("w-full")
|
|
1077
|
-
|
|
1078
|
-
app.storage.client["user_agent"] = request.headers.get("user-agent", None)
|
|
1079
|
-
|
|
1080
|
-
qgis = app.storage.client["user_agent"].startswith(get_settings().qgis_plugin_name)
|
|
1081
|
-
|
|
1082
|
-
feature_data = {}
|
|
1083
|
-
|
|
1084
|
-
if qgis:
|
|
1085
|
-
ui.add_head_html('<script src="qrc:///qtwebchannel/qwebchannel.js"></script>')
|
|
1086
|
-
|
|
1087
|
-
try:
|
|
1088
|
-
model = get_settings().repo.get(id)
|
|
1089
|
-
# feature_type = model.get_name()
|
|
1090
|
-
feature = model.model_dump(mode="json")
|
|
1091
|
-
except ValueError:
|
|
1092
|
-
ui.label("Feature nicht gefunden")
|
|
1093
|
-
return
|
|
1094
|
-
assocs = model.get_associations()
|
|
1095
|
-
if not assocs:
|
|
1096
|
-
ui.label("Feature verfügt über keine Assoziationen")
|
|
1097
|
-
return
|
|
1098
|
-
with ui.stepper().props("vertical flat").classes("w-full") as stepper:
|
|
1099
|
-
with ui.step("Attribut auswählen"):
|
|
1100
|
-
ui.label(
|
|
1101
|
-
"Assoziationsattribut, für das ein neues Feature erzeugt werden soll, auswählen"
|
|
1102
|
-
)
|
|
1103
|
-
with ui.stepper_navigation():
|
|
1104
|
-
prop_select = ui.select(
|
|
1105
|
-
assocs, on_change=handle_assoc_select, with_input=True
|
|
1106
|
-
)
|
|
1107
|
-
with ui.step("Featuretype auswählen"):
|
|
1108
|
-
ui.label("Featuretype des neuen Features auswählen")
|
|
1109
|
-
with ui.stepper_navigation():
|
|
1110
|
-
type_select = ui.select(
|
|
1111
|
-
[], on_change=handle_type_select, with_input=True
|
|
1112
|
-
)
|
|
1113
|
-
# with ui.step("Bake"):
|
|
1114
|
-
# ui.label("Bake for 20 minutes")
|
|
1115
|
-
# with ui.stepper_navigation():
|
|
1116
|
-
# ui.button("Done", on_click=lambda: ui.notify("Yay!", type="positive"))
|
|
1117
|
-
# ui.button("Back", on_click=stepper.previous).props("flat")
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
927
|
@app.get("/health_check")
|
|
1121
|
-
def health_check() ->
|
|
1122
|
-
"""
|
|
1123
|
-
Health check endpoint to verify that the service is running.
|
|
928
|
+
def health_check() -> dict:
|
|
929
|
+
"""Health check endpoint to verify that the service is running.
|
|
1124
930
|
|
|
1125
931
|
Returns:
|
|
1126
|
-
|
|
1127
|
-
"""
|
|
1128
|
-
return "OK"
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
# TODO replace with QWebChannel funcionality
|
|
1132
|
-
@app.post("/validate/{featuretype}")
|
|
1133
|
-
def validate_featuretype(
|
|
1134
|
-
featuretype: str,
|
|
1135
|
-
featureData: dict[str, Any],
|
|
1136
|
-
appschema: str = get_settings().appschema,
|
|
1137
|
-
version: str = get_settings().appschema_version,
|
|
1138
|
-
) -> str:
|
|
1139
|
-
"""
|
|
1140
|
-
Validate feature data against the model.
|
|
1141
|
-
|
|
1142
|
-
Args:
|
|
1143
|
-
featuretype (str): The type of the feature to validate (path parameter).
|
|
1144
|
-
featureData (dict[str, Any]): Raw feature data to be validated.
|
|
1145
|
-
appschema (str): Application schema name (default from settings).
|
|
1146
|
-
version (str): Schema version string (default from settings).
|
|
932
|
+
dict: A simple dictionary with the app's name and version.
|
|
1147
933
|
"""
|
|
1148
|
-
|
|
1149
|
-
featureData.pop("featuretype", None)
|
|
1150
|
-
try:
|
|
1151
|
-
model = model_factory(featuretype, version, appschema)
|
|
1152
|
-
if (geom_field := model.get_geom_field()) and (
|
|
1153
|
-
geom := featureData.pop("geometry", None)
|
|
1154
|
-
):
|
|
1155
|
-
featureData[geom_field] = geom
|
|
1156
|
-
model.model_validate(featureData)
|
|
1157
|
-
except ValidationError as e:
|
|
1158
|
-
raise HTTPException(
|
|
1159
|
-
status_code=400,
|
|
1160
|
-
detail={
|
|
1161
|
-
"errors": e.errors(),
|
|
1162
|
-
},
|
|
1163
|
-
)
|
|
1164
|
-
return "OK"
|
|
934
|
+
return {"name": "XMAS-App", "version": __version__}
|
|
1165
935
|
|
|
1166
936
|
|
|
1167
937
|
@app.post(
|
|
1168
938
|
"/insert-features", status_code=201, dependencies=[Depends(enforce_plugin_version)]
|
|
1169
939
|
)
|
|
1170
|
-
async def insert_features(payload: InsertPayload, planId:
|
|
940
|
+
async def insert_features(payload: InsertPayload, planId: UUID):
|
|
1171
941
|
"""Insert a number of features."""
|
|
1172
|
-
await crud.create(payload, planId)
|
|
942
|
+
await crud.create(payload, str(planId))
|
|
1173
943
|
|
|
1174
944
|
|
|
1175
945
|
@app.post("/update-features", dependencies=[Depends(enforce_plugin_version)])
|
xmas_app/settings.py
CHANGED
|
@@ -63,7 +63,7 @@ class Settings(BaseSettings):
|
|
|
63
63
|
"https://registry.gdi-de.org/codelist/de.xleitstelle.xplanung"
|
|
64
64
|
)
|
|
65
65
|
qgis_plugin_name: str = "XMAS-Plugin"
|
|
66
|
-
qgis_plugin_min_version: _VersionPydanticAnnotation = Version(major=0, minor=
|
|
66
|
+
qgis_plugin_min_version: _VersionPydanticAnnotation = Version(major=0, minor=16)
|
|
67
67
|
mappings_toml: str = str(Path(__file__).parent / "resources/mappings.toml")
|
|
68
68
|
|
|
69
69
|
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xmas-app
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: The XLeitstelle model-driven application schema app.
|
|
5
5
|
License: EUPL-1.2-or-later
|
|
6
6
|
License-File: LICENSE
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
15
15
|
Requires-Dist: nicegui (>=2.20,<3.0)
|
|
16
16
|
Requires-Dist: pydantic-extra-types[semver]
|
|
17
17
|
Requires-Dist: pydantic-settings (>=2.0,<3.0)
|
|
18
|
-
Requires-Dist: xplan-tools (>=1.
|
|
18
|
+
Requires-Dist: xplan-tools (>=1.12.1,<1.13.0)
|
|
19
19
|
Project-URL: Homepage, https://gitlab.opencode.de/xleitstelle/xmas-app
|
|
20
20
|
Project-URL: Issues, https://gitlab.opencode.de/xleitstelle/xmas-app/-/issues
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
@@ -2,11 +2,11 @@ xmas_app/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
|
2
2
|
xmas_app/components/association.py,sha256=xZUP-_S8IOmWEx_vWf5eipDjGWOSELdc_TEov8x-R9w,10270
|
|
3
3
|
xmas_app/components/buttons.py,sha256=siwpqDFKcXxEMdaOglzgYeeuWKbiVLYW8BHi0Wx0HGk,2255
|
|
4
4
|
xmas_app/components/label.py,sha256=WkqcHK4sGRFENiR_afrrkCDMGq_AZAT63zbN1GSpgv0,424
|
|
5
|
-
xmas_app/db.py,sha256=
|
|
5
|
+
xmas_app/db.py,sha256=J-D7FjobO3x8wvBfJdN9ZXfztLmaoN9ZopTNDflWZPk,7235
|
|
6
6
|
xmas_app/db_uow.py,sha256=usvqo1bO4dQnMbiub5PyQAAgu8Af-HYAJTsFNYS3Le0,761
|
|
7
7
|
xmas_app/deps/version_guard.py,sha256=ZcTtReXLsT-_dpQyELkajU2HbLvqIJgeU16pMHFAMyc,928
|
|
8
|
-
xmas_app/form.py,sha256=
|
|
9
|
-
xmas_app/main.py,sha256=
|
|
8
|
+
xmas_app/form.py,sha256=D1GQy77QBqmFUjHtSzEuUqwLvSxRq0kLWddx0jPXoRk,59039
|
|
9
|
+
xmas_app/main.py,sha256=Eqfh3k2B5R45hMbVcTBYfsqgQeVyeGYxLxE4QUcvb4E,43121
|
|
10
10
|
xmas_app/models/crud.py,sha256=Rtz-v2FpMM4AcTc_wcye_XjZtssTydgtSQcKuIK1yiM,418
|
|
11
11
|
xmas_app/models/mappings.py,sha256=SwCbo_TQKiTDUy5-xkNEEIB3J7VFh-VsDbR2_0wyv0Q,213
|
|
12
12
|
xmas_app/pygeoapi/config.yaml,sha256=lmb08zXk8-dB0sh_4EQ1enVBIqgBI-v6QcoIiHyH3N8,7992
|
|
@@ -15,12 +15,12 @@ xmas_app/pygeoapi/provider.py,sha256=uKHK_Hsqale1C2pNRKYpyka9GnNHYvGBmbzfcSi05NE
|
|
|
15
15
|
xmas_app/resources/mappings.toml,sha256=VbDrooIdNmJOGLJtdg1Kh1bo_-DyUT7JL7aQMrBhRMU,97
|
|
16
16
|
xmas_app/schema.py,sha256=3P-i14caMqydGsxNw6SOAcanEg1Qr4qxqciAow1aCUM,1507
|
|
17
17
|
xmas_app/services/crud.py,sha256=lxH9kFIuOjaM1gEuWb9fbZ4a_hD2PI1_gjUwnfChD5g,4305
|
|
18
|
-
xmas_app/settings.py,sha256=
|
|
18
|
+
xmas_app/settings.py,sha256=Fb7KRnOSND1n9IhRwgnOt5txwOCpgaUSsFBlwNhrtgg,3419
|
|
19
19
|
xmas_app/split_service.py,sha256=Ag_Ix2vMgT04OsQ0DIGK7WFKQlbktmmTqWlESO2ZgVs,26701
|
|
20
20
|
xmas_app/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
xmas_app/util/codelist.py,sha256=dUn7mOgTmV9Zi8A9iePyOyByMIe0Cf3ERn-n4eHrzM8,1961
|
|
22
|
-
xmas_app-0.
|
|
23
|
-
xmas_app-0.
|
|
24
|
-
xmas_app-0.
|
|
25
|
-
xmas_app-0.
|
|
26
|
-
xmas_app-0.
|
|
22
|
+
xmas_app-0.14.0.dist-info/METADATA,sha256=7kqqKNFmy5lhqdfyo85IvLd6h1SVJJVJ9QbtTAkVkVs,5323
|
|
23
|
+
xmas_app-0.14.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
24
|
+
xmas_app-0.14.0.dist-info/entry_points.txt,sha256=uve2b0hgPbVfiD047-hOI6_dD97ZytvCPWk7HsN9Xng,53
|
|
25
|
+
xmas_app-0.14.0.dist-info/licenses/LICENSE,sha256=b8nnCcy_4Nd_v_okJ6mDKCvi64jkexzbSfIag7TR5mU,13827
|
|
26
|
+
xmas_app-0.14.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|