xmas-app 1.0.0.dev1__tar.gz → 1.0.0.dev3__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-1.0.0.dev3/.gitignore +17 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/PKG-INFO +78 -26
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/README.md +65 -11
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/pyproject.toml +37 -27
- xmas_app-1.0.0.dev3/version.py +88 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/association.py +16 -13
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/documents.py +57 -23
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/feature_context_menu.py +9 -2
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/form/art_attribute.py +31 -10
- xmas_app-1.0.0.dev3/xmas_app/components/form/associations.py +58 -0
- xmas_app-1.0.0.dev3/xmas_app/components/form/bindable.py +133 -0
- xmas_app-1.0.0.dev3/xmas_app/components/form/context.py +52 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/form/fields/__init__.py +1 -3
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/form/fields/association.py +10 -8
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/form/fields/basic.py +5 -4
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/form/fields/datatype.py +21 -18
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/form/fields/enumeration.py +1 -1
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/form/fields/measure.py +3 -3
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/form/fields/temporal.py +2 -1
- xmas_app-1.0.0.dev3/xmas_app/components/form/filter_bar.py +121 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/form/helpers.py +34 -3
- xmas_app-1.0.0.dev3/xmas_app/components/form/model_form.py +850 -0
- xmas_app-1.0.0.dev3/xmas_app/components/form/multi_edit.py +200 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/form/tabs.py +7 -5
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/form/validation.py +17 -4
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/planmigration.py +89 -70
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/plantree.py +320 -70
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/planvalidation.py +13 -7
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/core/auth.py +54 -58
- xmas_app-1.0.0.dev3/xmas_app/core/db.py +614 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/core/events.py +1 -0
- xmas_app-1.0.0.dev3/xmas_app/core/lifecycle.py +121 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/core/logging.py +9 -1
- xmas_app-1.0.0.dev3/xmas_app/core/middleware.py +93 -0
- xmas_app-1.0.0.dev3/xmas_app/core/settings.py +142 -0
- xmas_app-1.0.0.dev3/xmas_app/core/webchannel.py +190 -0
- xmas_app-1.0.0.dev3/xmas_app/deps/__init__.py +89 -0
- xmas_app-1.0.0.dev3/xmas_app/deps/plan.py +154 -0
- xmas_app-1.0.0.dev3/xmas_app/main.py +470 -0
- xmas_app-1.0.0.dev3/xmas_app/models/crud.py +115 -0
- xmas_app-1.0.0.dev3/xmas_app/models/editor_context.py +122 -0
- xmas_app-1.0.0.dev3/xmas_app/models/errors.py +20 -0
- xmas_app-1.0.0.dev3/xmas_app/models/identity.py +39 -0
- xmas_app-1.0.0.dev3/xmas_app/models/labels.py +7 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/models/mappings.py +1 -1
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/models/migrations/env.py +7 -4
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/models/migrations/versions/3715fd158e35_add_notify_trigger.py +1 -1
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/models/migrations/versions/53a0d4789886_docs_table.py +1 -1
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/models/migrations/versions/5bc8757a75a8_remove_unique_index_from_doc_filename.py +1 -1
- xmas_app-1.0.0.dev3/xmas_app/models/migrations/versions/813752acba84_add_authz_schema.py +176 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/models/migrations/versions/aaf3c696f708_add_mappings.py +1 -1
- xmas_app-1.0.0.dev3/xmas_app/models/orm.py +274 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/models/split.py +0 -11
- xmas_app-1.0.0.dev3/xmas_app/models/stylesheets.py +31 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/models/validation_view.py +5 -5
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/pages/base.py +8 -13
- xmas_app-1.0.0.dev3/xmas_app/pages/feature.py +425 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/pages/planmanager.py +379 -179
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/pages/plantools.py +16 -11
- xmas_app-1.0.0.dev3/xmas_app/repositories/__init__.py +1 -0
- xmas_app-1.0.0.dev3/xmas_app/repositories/authz.py +99 -0
- xmas_app-1.0.0.dev3/xmas_app/services/authz_service.py +69 -0
- xmas_app-1.0.0.dev3/xmas_app/services/broker.py +137 -0
- xmas_app-1.0.0.dev3/xmas_app/services/crud.py +331 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/services/document.py +3 -3
- xmas_app-1.0.0.dev3/xmas_app/services/labels.py +27 -0
- xmas_app-1.0.0.dev3/xmas_app/services/permissions.py +32 -0
- xmas_app-1.0.0.dev3/xmas_app/services/plan.py +35 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/services/split.py +85 -32
- xmas_app-1.0.0.dev3/xmas_app/services/stylesheets.py +59 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/util/__init__.py +8 -1
- xmas_app-1.0.0.dev3/xmas_app/util/get_models.py +64 -0
- xmas_app-1.0.0.dev3/xmas_app/util/gml.py +36 -0
- xmas_app-1.0.0.dev3/xmas_app/util/helpers.py +23 -0
- xmas_app-1.0.0.dev1/xmas_app/components/form/associations.py +0 -103
- xmas_app-1.0.0.dev1/xmas_app/components/form/bindable.py +0 -57
- xmas_app-1.0.0.dev1/xmas_app/components/form/context.py +0 -33
- xmas_app-1.0.0.dev1/xmas_app/components/form/filter_bar.py +0 -73
- xmas_app-1.0.0.dev1/xmas_app/components/form/model_form.py +0 -322
- xmas_app-1.0.0.dev1/xmas_app/core/db.py +0 -416
- xmas_app-1.0.0.dev1/xmas_app/core/lifecycle.py +0 -83
- xmas_app-1.0.0.dev1/xmas_app/core/settings.py +0 -108
- xmas_app-1.0.0.dev1/xmas_app/core/webchannel.py +0 -103
- xmas_app-1.0.0.dev1/xmas_app/deps/__init__.py +0 -24
- xmas_app-1.0.0.dev1/xmas_app/deps/version_guard.py +0 -32
- xmas_app-1.0.0.dev1/xmas_app/main.py +0 -366
- xmas_app-1.0.0.dev1/xmas_app/models/crud.py +0 -57
- xmas_app-1.0.0.dev1/xmas_app/models/orm.py +0 -136
- xmas_app-1.0.0.dev1/xmas_app/pages/feature.py +0 -147
- xmas_app-1.0.0.dev1/xmas_app/services/broker.py +0 -17
- xmas_app-1.0.0.dev1/xmas_app/services/crud.py +0 -213
- xmas_app-1.0.0.dev1/xmas_app/util/get_models.py +0 -53
- xmas_app-1.0.0.dev1/xmas_app/util/gml.py +0 -38
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/LICENSE +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/__init__.py +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/form/__init__.py +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/form/fields/codelist.py +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/label.py +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/components/statuslog.py +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/core/__init__.py +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/models/__init__.py +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/models/migrations/script.py.mako +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/models/validation_report.py +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/pages/__init__.py +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/resources/mappings.toml +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/services/__init__.py +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/services/validation.py +0 -0
- {xmas_app-1.0.0.dev1 → xmas_app-1.0.0.dev3}/xmas_app/util/client.py +0 -0
|
@@ -1,27 +1,26 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: xmas-app
|
|
3
|
-
Version: 1.0.0.
|
|
3
|
+
Version: 1.0.0-dev.3
|
|
4
4
|
Summary: The XLeitstelle model-driven application schema app.
|
|
5
|
-
|
|
5
|
+
Project-URL: Homepage, https://gitlab.opencode.de/xleitstelle/xmas-app
|
|
6
|
+
Project-URL: Issues, https://gitlab.opencode.de/xleitstelle/xmas-app/-/issues
|
|
7
|
+
Author-email: Tobias Kraft <tobias.kraft@gv.hamburg.de>
|
|
8
|
+
License-Expression: EUPL-1.2+
|
|
6
9
|
License-File: LICENSE
|
|
7
|
-
Author: Tobias Kraft
|
|
8
|
-
Author-email: tobias.kraft@gv.hamburg.de
|
|
9
|
-
Requires-Python: >=3.12,<3.14
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Framework :: Pydantic :: 2
|
|
12
12
|
Classifier: License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Requires-
|
|
16
|
-
Requires-Dist:
|
|
17
|
-
Requires-Dist:
|
|
15
|
+
Requires-Python: >=3.14
|
|
16
|
+
Requires-Dist: aiohttp>=3.13.5
|
|
17
|
+
Requires-Dist: httpx2<3,>=2.4.0
|
|
18
|
+
Requires-Dist: nicegui<4.0,>=3.12
|
|
18
19
|
Requires-Dist: pydantic-extra-types[semver]
|
|
19
|
-
Requires-Dist: pydantic-settings
|
|
20
|
+
Requires-Dist: pydantic-settings<3.0,>=2.0
|
|
20
21
|
Requires-Dist: structlog
|
|
21
|
-
Requires-Dist: tenacity
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Project-URL: Homepage, https://gitlab.opencode.de/xleitstelle/xmas-app
|
|
24
|
-
Project-URL: Issues, https://gitlab.opencode.de/xleitstelle/xmas-app/-/issues
|
|
22
|
+
Requires-Dist: tenacity<10,>=9.1.4
|
|
23
|
+
Requires-Dist: xmas-core==2.0.0rc12
|
|
25
24
|
Description-Content-Type: text/markdown
|
|
26
25
|
|
|
27
26
|
# XMAS-App
|
|
@@ -30,7 +29,7 @@ Description-Content-Type: text/markdown
|
|
|
30
29
|
[](https://gitlab.opencode.de/xleitstelle/xmas-app/-/releases)
|
|
31
30
|
|
|
32
31
|
The **X**Leistelle **m**odel-driven **a**pplication **s**chema app is a Python web application to edit and create data according to geo-spatial standards of the [XLeistelle](https://xleitstelle.de), e.g. XPlanung, XTrasse. It is based on [NiceGUI](https://nicegui.io/) for a graphical user interface
|
|
33
|
-
and [
|
|
32
|
+
and [xmas-core](https://gitlab.opencode.de/xleitstelle/xmas-core) for the data model and respective functionality.
|
|
34
33
|
|
|
35
34
|
While it could be advanced to a standalone application, its current focus is [integration in QGIS](https://gitlab.opencode.de/xleitstelle/xmas-plugin) to provide attribute forms etc.
|
|
36
35
|
|
|
@@ -70,7 +69,11 @@ pixi install
|
|
|
70
69
|
## Running
|
|
71
70
|
|
|
72
71
|
### Preconditions
|
|
73
|
-
A Postgres DB with PostGIS extension and adequate permissions for the used role.
|
|
72
|
+
A Postgres DB with PostGIS extension and adequate permissions for the used role. Missing tables are generated on initialization.
|
|
73
|
+
|
|
74
|
+
A database created by `xplan-tools` (xmas-core < 2.0) cannot be upgraded: startup reports `SchemaOutdatedError`. Start with a fresh database instead, with the PostGIS extension enabled and `PGDATABASE` pointing at it, and re-import the plans through the plan manager. The app's own tables live in the same schema as the plans, so nothing of them carries over: organisations are created again on first login, plans are attributed to whoever imports them, and attachments have to be uploaded again.
|
|
75
|
+
|
|
76
|
+
Plans written straight into the database, for example with `xmas convert`, get no authorization row: the app does not list them, and opening one gives a 404.
|
|
74
77
|
|
|
75
78
|
### Environment Configuration
|
|
76
79
|
|
|
@@ -126,18 +129,18 @@ Use the local-mode database settings in `.env` (`PGHOST=127.0.0.1`, `PGPORT=1543
|
|
|
126
129
|
|
|
127
130
|
Test data can be downloaded from https://gitlab.opencode.de/xleitstelle/xplanung/testdaten, e.g. a ZIP archive with [BP_Plans in v 6.0](https://gitlab.opencode.de/xleitstelle/xplanung/testdaten/-/archive/main/testdaten-main.zip?path=valide/6_0/bp).
|
|
128
131
|
|
|
129
|
-
To import all `.gml` files at once, extract the archive and use
|
|
132
|
+
To import all `.gml` files at once, extract the archive and use the commands below. They write the plans straight into the database, without the authorization rows the app needs, so the app does not show them; for plans to work with in the app, import them through the plan manager.
|
|
130
133
|
|
|
131
134
|
**Linux / macOS / WSL:**
|
|
132
135
|
```bash
|
|
133
136
|
for f in <path>/bp/*.gml; do
|
|
134
|
-
|
|
137
|
+
xmas convert "$f" postgresql://postgres:postgres@localhost:55432/postgres
|
|
135
138
|
done
|
|
136
139
|
```
|
|
137
140
|
|
|
138
141
|
**Windows:**
|
|
139
142
|
```cmd
|
|
140
|
-
for %f in (*.gml) do
|
|
143
|
+
for %f in (*.gml) do xmas convert "%f" postgresql://postgres:postgres@127.0.0.1:55432/postgres
|
|
141
144
|
```
|
|
142
145
|
|
|
143
146
|
## Authentication
|
|
@@ -146,24 +149,70 @@ The application supports two authentication modes, controlled by the `AUTH_MODE`
|
|
|
146
149
|
|
|
147
150
|
### Static token
|
|
148
151
|
|
|
149
|
-
|
|
152
|
+
Maps a bearer token to an identity record the operator writes. It is used for local development, for the `xmas-plugin` CI pipeline (which cannot run the interactive OAuth2/PKCE flow headlessly), and for self-hosted installations that do not run an identity provider. See [Static tokens in production](#static-tokens-in-production) before using it for the last of these.
|
|
150
153
|
|
|
151
|
-
Set `AUTH_MODE=static` in `.env`.
|
|
154
|
+
Set `AUTH_MODE=static` in `.env`.
|
|
152
155
|
|
|
153
156
|
Requests must include:
|
|
154
157
|
```
|
|
155
158
|
Authorization: Bearer <token>
|
|
156
159
|
```
|
|
157
160
|
|
|
158
|
-
|
|
161
|
+
Each token maps to a full identity record via the `AUTH_TOKENS` environment variable (JSON). The record stands in for the claims a Keycloak access token would otherwise carry:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
AUTH_TOKENS={"<generated-token>": {"user_name": "test-user", "user_id": "41a74c7b-dea8-42c2-bd15-9b88012b10d7", "org_id": "25459bec-0f6e-4f3a-a3f4-3b3e6a2a5d11", "email": "test@example.org", "role": "editor"}}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
| Field | Required | Meaning |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `user_name` | no | Human-readable label for whoever edits the env var (not consumed by the app) |
|
|
170
|
+
| `user_id` | yes | Subject identifier; stands in for the Keycloak `sub`. Any string, see below. Persisted as `created_by` |
|
|
171
|
+
| `org_id` | no* | UUID of the user's org. A `null` or absent value marks a grant-scoped external invitee |
|
|
172
|
+
| `email` | no | Display / audit only. Not used as an identity key |
|
|
173
|
+
| `role` | no | One of `admin`, `editor`, `viewer`. Absent or `null` is treated as no role, which denies writes |
|
|
174
|
+
|
|
175
|
+
> *`org_id` is optional syntactically, but `null` means the user is treated as
|
|
176
|
+
> an external invitee (grant-scoped only) and will be denied org-scoped plan
|
|
177
|
+
> access. For a normal dev user, set a real org UUID. When `AUTH_MODE=static`,
|
|
178
|
+
> `AUTH_TOKENS` is required, startup fails loudly if it is missing or malformed.
|
|
179
|
+
|
|
180
|
+
`user_id` is not required to be a UUID. A Keycloak `sub` happens to be one, but OIDC
|
|
181
|
+
defines the subject as an opaque string, so the columns storing it are `text` and any
|
|
182
|
+
stable, unique-per-deployment string works here (ADR-0005, Appendix A). Records are
|
|
183
|
+
parsed into `AuthenticatedUser`, the same type the Keycloak path produces.
|
|
159
184
|
|
|
160
|
-
|
|
185
|
+
See `.env.example` for the full format.
|
|
161
186
|
|
|
162
|
-
|
|
187
|
+
#### Static tokens in production
|
|
188
|
+
|
|
189
|
+
Static mode is supported for self-hosted installations. It is not suitable for an installation reachable from the public internet, because it lacks what that needs:
|
|
190
|
+
|
|
191
|
+
- No expiry. A token is valid until it is removed from the configuration.
|
|
192
|
+
- No revocation short of editing the configuration and restarting.
|
|
193
|
+
- No signature. The token is the secret itself, so anyone who sees it in transit, in a log, in a shell history or in a backup of `.env` holds the credential.
|
|
194
|
+
- No rate limiting. Guessing tokens is slowed by nothing but the network.
|
|
195
|
+
- Identity and role are whatever the operator wrote into `AUTH_TOKENS`.
|
|
196
|
+
|
|
197
|
+
How an installation is exposed is the operating organisation's decision, and so is acting on the recommendations below (ADR-0007). An installation that has to be reachable from the internet should use Keycloak.
|
|
198
|
+
|
|
199
|
+
- **Length.** With `APP_MODE=prod`, the default, startup fails if any token is shorter than 32 characters. Generate one with `python -c "import secrets; print(secrets.token_urlsafe(32))"`.
|
|
200
|
+
- **One token per person.** Recommended, not enforced. With one shared token, every action is attributed to that token's identity, so the record shows the organisation rather than the person.
|
|
201
|
+
- **Storage.** Treat `AUTH_TOKENS` like a password: restrict who can read `.env`, and keep tokens out of version control, tickets and chat.
|
|
202
|
+
- **One place.** Set `AUTH_TOKENS` either in the environment or in `.env`, not both. The two are merged, so a token removed from one stays valid while it is still in the other.
|
|
203
|
+
- **Rotation.** Edit `AUTH_TOKENS` and restart. That is the whole mechanism; there is no expiry and no revocation list.
|
|
204
|
+
|
|
205
|
+
At startup in prod, the application logs that static mode is active and how many tokens are configured, never the tokens themselves.
|
|
206
|
+
|
|
207
|
+
### Keycloak JWT (recommended for production)
|
|
208
|
+
|
|
209
|
+
Validates RS256-signed access tokens issued by a Keycloak realm via the JWKS endpoint. This is the mode for any installation reachable from the public internet.
|
|
210
|
+
|
|
211
|
+
Set `AUTH_MODE=keycloak` in `.env`. See `.env.example` for the required Keycloak variables (`KEYCLOAK_ISSUER`, `KEYCLOAK_AUDIENCE`, `KEYCLOAK_JWKS_URL`).
|
|
163
212
|
|
|
164
213
|
> **Note:** `KEYCLOAK_ISSUER` must match the `iss` claim in the token exactly — including scheme, host, and port. `KEYCLOAK_JWKS_URL` is the address of the Keycloak service as seen from inside the Docker network, which may differ from the issuer URL seen by clients.
|
|
165
214
|
|
|
166
|
-
The local Keycloak instance is started automatically via `compose.yaml`. A development realm (`realm-xmas-dev.json`) is imported on first start, including a test user and the required audience mapper.
|
|
215
|
+
The local Keycloak instance is started automatically via `compose.yaml`. A development realm (`realm-xmas-dev.json`) is imported on first start, including a test user, the `xmas-plugin` client for the OAuth2/PKCE flow, and the required audience mapper.
|
|
167
216
|
|
|
168
217
|
For Keycloak setup, Identity Provider federation (e.g. Microsoft Entra ID / SSO), and onboarding documentation, see [`keycloak/README.md`](keycloak/README.md).
|
|
169
218
|
|
|
@@ -185,6 +234,10 @@ Then run:
|
|
|
185
234
|
pixi run pytest tests
|
|
186
235
|
```
|
|
187
236
|
|
|
237
|
+
## Agentic Coding
|
|
238
|
+
|
|
239
|
+
See `.devcontainer/README.md` for Claude Code setup.
|
|
240
|
+
|
|
188
241
|
## Troubleshooting
|
|
189
242
|
|
|
190
243
|
### PyJWKClientConnectionError
|
|
@@ -197,4 +250,3 @@ Make sure proxy settings aren't blocking Keycloak: add `keycloak` to the `NO_PRO
|
|
|
197
250
|
The code in this repository is licensed under the [EUPL-1.2-or-later](https://joinup.ec.europa.eu/collection/eupl)
|
|
198
251
|
|
|
199
252
|
© [XLeitstelle](https://xleitstelle.de), 2025
|
|
200
|
-
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://gitlab.opencode.de/xleitstelle/xmas-app/-/releases)
|
|
5
5
|
|
|
6
6
|
The **X**Leistelle **m**odel-driven **a**pplication **s**chema app is a Python web application to edit and create data according to geo-spatial standards of the [XLeistelle](https://xleitstelle.de), e.g. XPlanung, XTrasse. It is based on [NiceGUI](https://nicegui.io/) for a graphical user interface
|
|
7
|
-
and [
|
|
7
|
+
and [xmas-core](https://gitlab.opencode.de/xleitstelle/xmas-core) for the data model and respective functionality.
|
|
8
8
|
|
|
9
9
|
While it could be advanced to a standalone application, its current focus is [integration in QGIS](https://gitlab.opencode.de/xleitstelle/xmas-plugin) to provide attribute forms etc.
|
|
10
10
|
|
|
@@ -44,7 +44,11 @@ pixi install
|
|
|
44
44
|
## Running
|
|
45
45
|
|
|
46
46
|
### Preconditions
|
|
47
|
-
A Postgres DB with PostGIS extension and adequate permissions for the used role.
|
|
47
|
+
A Postgres DB with PostGIS extension and adequate permissions for the used role. Missing tables are generated on initialization.
|
|
48
|
+
|
|
49
|
+
A database created by `xplan-tools` (xmas-core < 2.0) cannot be upgraded: startup reports `SchemaOutdatedError`. Start with a fresh database instead, with the PostGIS extension enabled and `PGDATABASE` pointing at it, and re-import the plans through the plan manager. The app's own tables live in the same schema as the plans, so nothing of them carries over: organisations are created again on first login, plans are attributed to whoever imports them, and attachments have to be uploaded again.
|
|
50
|
+
|
|
51
|
+
Plans written straight into the database, for example with `xmas convert`, get no authorization row: the app does not list them, and opening one gives a 404.
|
|
48
52
|
|
|
49
53
|
### Environment Configuration
|
|
50
54
|
|
|
@@ -100,18 +104,18 @@ Use the local-mode database settings in `.env` (`PGHOST=127.0.0.1`, `PGPORT=1543
|
|
|
100
104
|
|
|
101
105
|
Test data can be downloaded from https://gitlab.opencode.de/xleitstelle/xplanung/testdaten, e.g. a ZIP archive with [BP_Plans in v 6.0](https://gitlab.opencode.de/xleitstelle/xplanung/testdaten/-/archive/main/testdaten-main.zip?path=valide/6_0/bp).
|
|
102
106
|
|
|
103
|
-
To import all `.gml` files at once, extract the archive and use
|
|
107
|
+
To import all `.gml` files at once, extract the archive and use the commands below. They write the plans straight into the database, without the authorization rows the app needs, so the app does not show them; for plans to work with in the app, import them through the plan manager.
|
|
104
108
|
|
|
105
109
|
**Linux / macOS / WSL:**
|
|
106
110
|
```bash
|
|
107
111
|
for f in <path>/bp/*.gml; do
|
|
108
|
-
|
|
112
|
+
xmas convert "$f" postgresql://postgres:postgres@localhost:55432/postgres
|
|
109
113
|
done
|
|
110
114
|
```
|
|
111
115
|
|
|
112
116
|
**Windows:**
|
|
113
117
|
```cmd
|
|
114
|
-
for %f in (*.gml) do
|
|
118
|
+
for %f in (*.gml) do xmas convert "%f" postgresql://postgres:postgres@127.0.0.1:55432/postgres
|
|
115
119
|
```
|
|
116
120
|
|
|
117
121
|
## Authentication
|
|
@@ -120,24 +124,70 @@ The application supports two authentication modes, controlled by the `AUTH_MODE`
|
|
|
120
124
|
|
|
121
125
|
### Static token
|
|
122
126
|
|
|
123
|
-
|
|
127
|
+
Maps a bearer token to an identity record the operator writes. It is used for local development, for the `xmas-plugin` CI pipeline (which cannot run the interactive OAuth2/PKCE flow headlessly), and for self-hosted installations that do not run an identity provider. See [Static tokens in production](#static-tokens-in-production) before using it for the last of these.
|
|
124
128
|
|
|
125
|
-
Set `AUTH_MODE=static` in `.env`.
|
|
129
|
+
Set `AUTH_MODE=static` in `.env`.
|
|
126
130
|
|
|
127
131
|
Requests must include:
|
|
128
132
|
```
|
|
129
133
|
Authorization: Bearer <token>
|
|
130
134
|
```
|
|
131
135
|
|
|
132
|
-
|
|
136
|
+
Each token maps to a full identity record via the `AUTH_TOKENS` environment variable (JSON). The record stands in for the claims a Keycloak access token would otherwise carry:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
AUTH_TOKENS={"<generated-token>": {"user_name": "test-user", "user_id": "41a74c7b-dea8-42c2-bd15-9b88012b10d7", "org_id": "25459bec-0f6e-4f3a-a3f4-3b3e6a2a5d11", "email": "test@example.org", "role": "editor"}}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
| Field | Required | Meaning |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `user_name` | no | Human-readable label for whoever edits the env var (not consumed by the app) |
|
|
145
|
+
| `user_id` | yes | Subject identifier; stands in for the Keycloak `sub`. Any string, see below. Persisted as `created_by` |
|
|
146
|
+
| `org_id` | no* | UUID of the user's org. A `null` or absent value marks a grant-scoped external invitee |
|
|
147
|
+
| `email` | no | Display / audit only. Not used as an identity key |
|
|
148
|
+
| `role` | no | One of `admin`, `editor`, `viewer`. Absent or `null` is treated as no role, which denies writes |
|
|
149
|
+
|
|
150
|
+
> *`org_id` is optional syntactically, but `null` means the user is treated as
|
|
151
|
+
> an external invitee (grant-scoped only) and will be denied org-scoped plan
|
|
152
|
+
> access. For a normal dev user, set a real org UUID. When `AUTH_MODE=static`,
|
|
153
|
+
> `AUTH_TOKENS` is required, startup fails loudly if it is missing or malformed.
|
|
154
|
+
|
|
155
|
+
`user_id` is not required to be a UUID. A Keycloak `sub` happens to be one, but OIDC
|
|
156
|
+
defines the subject as an opaque string, so the columns storing it are `text` and any
|
|
157
|
+
stable, unique-per-deployment string works here (ADR-0005, Appendix A). Records are
|
|
158
|
+
parsed into `AuthenticatedUser`, the same type the Keycloak path produces.
|
|
159
|
+
|
|
160
|
+
See `.env.example` for the full format.
|
|
161
|
+
|
|
162
|
+
#### Static tokens in production
|
|
133
163
|
|
|
134
|
-
|
|
164
|
+
Static mode is supported for self-hosted installations. It is not suitable for an installation reachable from the public internet, because it lacks what that needs:
|
|
135
165
|
|
|
136
|
-
|
|
166
|
+
- No expiry. A token is valid until it is removed from the configuration.
|
|
167
|
+
- No revocation short of editing the configuration and restarting.
|
|
168
|
+
- No signature. The token is the secret itself, so anyone who sees it in transit, in a log, in a shell history or in a backup of `.env` holds the credential.
|
|
169
|
+
- No rate limiting. Guessing tokens is slowed by nothing but the network.
|
|
170
|
+
- Identity and role are whatever the operator wrote into `AUTH_TOKENS`.
|
|
171
|
+
|
|
172
|
+
How an installation is exposed is the operating organisation's decision, and so is acting on the recommendations below (ADR-0007). An installation that has to be reachable from the internet should use Keycloak.
|
|
173
|
+
|
|
174
|
+
- **Length.** With `APP_MODE=prod`, the default, startup fails if any token is shorter than 32 characters. Generate one with `python -c "import secrets; print(secrets.token_urlsafe(32))"`.
|
|
175
|
+
- **One token per person.** Recommended, not enforced. With one shared token, every action is attributed to that token's identity, so the record shows the organisation rather than the person.
|
|
176
|
+
- **Storage.** Treat `AUTH_TOKENS` like a password: restrict who can read `.env`, and keep tokens out of version control, tickets and chat.
|
|
177
|
+
- **One place.** Set `AUTH_TOKENS` either in the environment or in `.env`, not both. The two are merged, so a token removed from one stays valid while it is still in the other.
|
|
178
|
+
- **Rotation.** Edit `AUTH_TOKENS` and restart. That is the whole mechanism; there is no expiry and no revocation list.
|
|
179
|
+
|
|
180
|
+
At startup in prod, the application logs that static mode is active and how many tokens are configured, never the tokens themselves.
|
|
181
|
+
|
|
182
|
+
### Keycloak JWT (recommended for production)
|
|
183
|
+
|
|
184
|
+
Validates RS256-signed access tokens issued by a Keycloak realm via the JWKS endpoint. This is the mode for any installation reachable from the public internet.
|
|
185
|
+
|
|
186
|
+
Set `AUTH_MODE=keycloak` in `.env`. See `.env.example` for the required Keycloak variables (`KEYCLOAK_ISSUER`, `KEYCLOAK_AUDIENCE`, `KEYCLOAK_JWKS_URL`).
|
|
137
187
|
|
|
138
188
|
> **Note:** `KEYCLOAK_ISSUER` must match the `iss` claim in the token exactly — including scheme, host, and port. `KEYCLOAK_JWKS_URL` is the address of the Keycloak service as seen from inside the Docker network, which may differ from the issuer URL seen by clients.
|
|
139
189
|
|
|
140
|
-
The local Keycloak instance is started automatically via `compose.yaml`. A development realm (`realm-xmas-dev.json`) is imported on first start, including a test user and the required audience mapper.
|
|
190
|
+
The local Keycloak instance is started automatically via `compose.yaml`. A development realm (`realm-xmas-dev.json`) is imported on first start, including a test user, the `xmas-plugin` client for the OAuth2/PKCE flow, and the required audience mapper.
|
|
141
191
|
|
|
142
192
|
For Keycloak setup, Identity Provider federation (e.g. Microsoft Entra ID / SSO), and onboarding documentation, see [`keycloak/README.md`](keycloak/README.md).
|
|
143
193
|
|
|
@@ -159,6 +209,10 @@ Then run:
|
|
|
159
209
|
pixi run pytest tests
|
|
160
210
|
```
|
|
161
211
|
|
|
212
|
+
## Agentic Coding
|
|
213
|
+
|
|
214
|
+
See `.devcontainer/README.md` for Claude Code setup.
|
|
215
|
+
|
|
162
216
|
## Troubleshooting
|
|
163
217
|
|
|
164
218
|
### PyJWKClientConnectionError
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xmas-app"
|
|
3
|
-
|
|
3
|
+
dynamic = ["version"]
|
|
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 = [
|
|
@@ -11,19 +11,19 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3",
|
|
12
12
|
]
|
|
13
13
|
readme = "README.md"
|
|
14
|
-
license = "EUPL-1.2
|
|
14
|
+
license = "EUPL-1.2+"
|
|
15
15
|
license-files = ["LICENSE"]
|
|
16
16
|
dependencies = [
|
|
17
17
|
"pydantic-settings>=2.0,<3.0",
|
|
18
18
|
"pydantic-extra-types[semver]",
|
|
19
|
-
"
|
|
19
|
+
"xmas-core == 2.0.0rc12",
|
|
20
20
|
"nicegui>=3.12,<4.0",
|
|
21
21
|
"aiohttp>=3.13.5",
|
|
22
22
|
"tenacity>=9.1.4,<10",
|
|
23
23
|
"httpx2>=2.4.0,<3",
|
|
24
24
|
"structlog",
|
|
25
25
|
]
|
|
26
|
-
requires-python = ">=3.
|
|
26
|
+
requires-python = ">=3.14"
|
|
27
27
|
|
|
28
28
|
[project.urls]
|
|
29
29
|
Homepage = "https://gitlab.opencode.de/xleitstelle/xmas-app"
|
|
@@ -45,21 +45,29 @@ dev = { features = ["dev"], solve-group = "default" }
|
|
|
45
45
|
build = { features = ["build"], solve-group = "default" }
|
|
46
46
|
prod = { features = [], solve-group = "default" }
|
|
47
47
|
test = { features = ["test"], solve-group = "default" }
|
|
48
|
+
# Union of "dev" and "test", for the devcontainer: it needs both the lint/format
|
|
49
|
+
# tooling and pytest, and "default" (the same union) is off-limits there because
|
|
50
|
+
# Windows PyCharm owns that env in the shared, bind-mounted .pixi folder.
|
|
51
|
+
container = { features = ["dev", "test"], solve-group = "default" }
|
|
48
52
|
|
|
49
53
|
[tool.pixi.dependencies]
|
|
54
|
+
python = "3.14.*"
|
|
50
55
|
gdal = ">=3.13.0,<4"
|
|
51
56
|
libgdal-core = ">=3.13.0,<4"
|
|
52
|
-
pyjwt = ">=2.
|
|
53
|
-
cryptography = "
|
|
54
|
-
|
|
55
|
-
[tool.pixi.target.win.dependencies]
|
|
56
|
-
winloop = "*"
|
|
57
|
+
pyjwt = ">=2.15.1,<3"
|
|
58
|
+
cryptography = "*"
|
|
57
59
|
|
|
58
60
|
[tool.pixi.target.unix.dependencies]
|
|
59
61
|
uvloop = "*"
|
|
60
62
|
|
|
61
63
|
[tool.pytest.ini_options]
|
|
62
64
|
asyncio_mode = "auto"
|
|
65
|
+
addopts = "--tb=short"
|
|
66
|
+
timeout = 180
|
|
67
|
+
timeout_method = "thread"
|
|
68
|
+
# setup/teardown is not charged against the timeout: the session-scoped Postgres
|
|
69
|
+
# container would otherwise bill its image pull to the first integration test
|
|
70
|
+
timeout_func_only = true
|
|
63
71
|
filterwarnings = [
|
|
64
72
|
"ignore::DeprecationWarning"
|
|
65
73
|
]
|
|
@@ -69,30 +77,21 @@ omit = [
|
|
|
69
77
|
"xmas_app/models/migrations/*",
|
|
70
78
|
]
|
|
71
79
|
|
|
72
|
-
[tool.pixi.tasks
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
pixi workspace version {{version}}
|
|
78
|
-
&& git pull
|
|
79
|
-
&& pixi lock
|
|
80
|
-
&& git add pyproject.toml pixi.lock
|
|
81
|
-
&& git commit -S -m "bump to $(pixi workspace version get)"
|
|
82
|
-
&& git tag -s $(pixi workspace version get) -m "v$(pixi workspace version get)"
|
|
83
|
-
&& git push
|
|
84
|
-
&& git push --tags
|
|
85
|
-
&& pixi workspace version get
|
|
86
|
-
"""
|
|
80
|
+
[tool.pixi.tasks]
|
|
81
|
+
# tag the next release (push it yourself), parts as in version.py's `bump`: pixi run tag dev
|
|
82
|
+
tag = { cmd = "python version.py {{ parts }}", args = [
|
|
83
|
+
{ arg = "parts", default = "patch" },
|
|
84
|
+
] }
|
|
87
85
|
|
|
88
86
|
|
|
89
87
|
[dependency-groups]
|
|
90
88
|
dev = ["pre-commit>=4", "ruff>=0.8", "cyclonedx-bom>=7"]
|
|
91
|
-
build = ["
|
|
89
|
+
build = ["uv"]
|
|
92
90
|
test = [
|
|
93
91
|
"pytest",
|
|
94
92
|
"pytest-asyncio",
|
|
95
93
|
"pytest-cov",
|
|
94
|
+
"pytest-timeout",
|
|
96
95
|
"testcontainers"
|
|
97
96
|
]
|
|
98
97
|
|
|
@@ -109,5 +108,16 @@ extend-select = ["I", "D"]
|
|
|
109
108
|
convention = "google"
|
|
110
109
|
|
|
111
110
|
[build-system]
|
|
112
|
-
requires = ["
|
|
113
|
-
build-backend = "
|
|
111
|
+
requires = ["hatchling>=1.27"]
|
|
112
|
+
build-backend = "hatchling.build"
|
|
113
|
+
|
|
114
|
+
[tool.hatch.version]
|
|
115
|
+
source = "code"
|
|
116
|
+
path = "version.py"
|
|
117
|
+
|
|
118
|
+
# xmas_app has no __init__.py, so hatchling cannot find it by the project name
|
|
119
|
+
[tool.hatch.build.targets.wheel]
|
|
120
|
+
packages = ["xmas_app"]
|
|
121
|
+
|
|
122
|
+
[tool.hatch.build.targets.sdist]
|
|
123
|
+
include = ["xmas_app", "version.py"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Package version for hatchling (`[tool.hatch.version] source = "code"`).
|
|
2
|
+
|
|
3
|
+
The git tag is the only source. CI_COMMIT_TAG wins: tag pipelines and the Docker build have
|
|
4
|
+
no usable git. Otherwise `git describe`: on a tag that is the tag, past it a local version
|
|
5
|
+
`<tag>+<n>.g<sha>` (sorts after the tag, refused by PyPI); no git or no tag gives 0.0.0.dev0.
|
|
6
|
+
A tag must be semver whose prerelease is also valid PEP 440 (-dev.N/-rc.N): GitLab's
|
|
7
|
+
changelog API and the CI prerelease regexes read it as semver. alpha/beta are left out:
|
|
8
|
+
semver sorts them before dev, PEP 440 after it.
|
|
9
|
+
|
|
10
|
+
`python version.py PART...` (`pixi run tag dev`) tags the commit with the tag after
|
|
11
|
+
the last one, like `uv version --bump`, see `bump`. Here, not in the pixi task:
|
|
12
|
+
its shell carries on after a failed `$(...)`, tagging an empty name.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import subprocess # nosec B404 - runs git only
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
_TAG = re.compile(r"(\d+)\.(\d+)\.(\d+)(?:-(dev|rc)\.(\d+))?")
|
|
21
|
+
_PRE = ("dev", "rc")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _version() -> str:
|
|
25
|
+
"""The version of the checkout: CI_COMMIT_TAG, else from `git describe`."""
|
|
26
|
+
if tag := os.environ.get("CI_COMMIT_TAG"):
|
|
27
|
+
if not _TAG.fullmatch(tag):
|
|
28
|
+
raise ValueError(f"tag {tag!r} is not X.Y.Z or X.Y.Z-(dev|rc).N")
|
|
29
|
+
return tag
|
|
30
|
+
try:
|
|
31
|
+
out = subprocess.run( # nosec B603 B607 - fixed argv, git from PATH
|
|
32
|
+
["git", "describe", "--tags", "--long", "--match", "[0-9]*"],
|
|
33
|
+
capture_output=True,
|
|
34
|
+
text=True,
|
|
35
|
+
check=True,
|
|
36
|
+
cwd=os.path.dirname(os.path.abspath(__file__)),
|
|
37
|
+
).stdout.strip()
|
|
38
|
+
except OSError, subprocess.CalledProcessError:
|
|
39
|
+
return "0.0.0.dev0"
|
|
40
|
+
tag, n, sha = out.rsplit("-", 2)
|
|
41
|
+
return tag if n == "0" else f"{tag}+{n}.{sha}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def bump(tag: str, *parts: str) -> str:
|
|
45
|
+
"""The tag after `tag`, each part applied in order.
|
|
46
|
+
|
|
47
|
+
major/minor/patch raise that component and drop the prerelease, stable drops it,
|
|
48
|
+
dev/rc count up (dev.1 -> dev.2) or move forward (dev.2 -> rc.1). A prerelease of a
|
|
49
|
+
released tag needs a release part first (`major dev`: 0.18.6 -> 1.0.0-dev.1), else it
|
|
50
|
+
would sort before the tag it follows.
|
|
51
|
+
"""
|
|
52
|
+
m = _TAG.fullmatch(tag)
|
|
53
|
+
if not m:
|
|
54
|
+
raise ValueError(f"tag {tag!r} is not X.Y.Z or X.Y.Z-(dev|rc).N")
|
|
55
|
+
major, minor, patch = map(int, m.group(1, 2, 3))
|
|
56
|
+
pre, n = m[4], int(m[5] or 0)
|
|
57
|
+
released = pre is None
|
|
58
|
+
for part in parts:
|
|
59
|
+
match part:
|
|
60
|
+
case "major":
|
|
61
|
+
major, minor, patch, pre, released = major + 1, 0, 0, None, False
|
|
62
|
+
case "minor":
|
|
63
|
+
minor, patch, pre, released = minor + 1, 0, None, False
|
|
64
|
+
case "patch":
|
|
65
|
+
patch, pre, released = patch + 1, None, False
|
|
66
|
+
case "stable":
|
|
67
|
+
pre = None
|
|
68
|
+
case "dev" | "rc" if released:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"{tag} is released: bump major, minor or patch before {part}"
|
|
71
|
+
)
|
|
72
|
+
case "dev" | "rc" if part == pre:
|
|
73
|
+
n += 1
|
|
74
|
+
case "dev" | "rc" if pre is None or _PRE.index(part) > _PRE.index(pre):
|
|
75
|
+
pre, n = part, 1
|
|
76
|
+
case _:
|
|
77
|
+
raise ValueError(f"cannot bump {tag} by {part!r}")
|
|
78
|
+
return f"{major}.{minor}.{patch}" + (f"-{pre}.{n}" if pre else "")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__version__ = _version()
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
tag = bump(__version__.split("+")[0], *sys.argv[1:])
|
|
85
|
+
subprocess.run( # nosec B603 B607 - fixed argv, git from PATH
|
|
86
|
+
["git", "tag", "-s", tag, "-m", f"v{tag}"], check=True
|
|
87
|
+
)
|
|
88
|
+
print(f"tagged {tag}; release with: git push origin {tag}")
|
|
@@ -147,11 +147,13 @@ class AssociationList(ui.button):
|
|
|
147
147
|
case "existing":
|
|
148
148
|
if self.ref_objects is None:
|
|
149
149
|
self.table_existing.props("loading")
|
|
150
|
-
self.
|
|
151
|
-
self.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
async with self._form.sessionmaker.begin() as session:
|
|
151
|
+
self.ref_objects = await get_ref_objects(
|
|
152
|
+
self.refs,
|
|
153
|
+
self._form.plan_id,
|
|
154
|
+
extra_props=self._form._mappings.association_table.extra_property,
|
|
155
|
+
session=session,
|
|
156
|
+
)
|
|
155
157
|
self.table_existing.rows = self.ref_objects
|
|
156
158
|
self.table_existing.selected = self.ref_objects
|
|
157
159
|
self.table_existing.props(
|
|
@@ -161,14 +163,15 @@ class AssociationList(ui.button):
|
|
|
161
163
|
case "candidates":
|
|
162
164
|
if self.ref_candidates is None:
|
|
163
165
|
self.table_candidates.props("loading")
|
|
164
|
-
self.
|
|
165
|
-
self.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
166
|
+
async with self._form.sessionmaker.begin() as session:
|
|
167
|
+
self.ref_candidates = await get_ref_candidates(
|
|
168
|
+
self.refs,
|
|
169
|
+
self._form.plan_id,
|
|
170
|
+
self._form.model,
|
|
171
|
+
self.rel,
|
|
172
|
+
extra_props=self._form._mappings.association_table.extra_property,
|
|
173
|
+
session=session,
|
|
174
|
+
)
|
|
172
175
|
self.table_candidates.rows = self.ref_candidates
|
|
173
176
|
self.table_candidates.props(
|
|
174
177
|
f":selected-rows-label='(numberOfRows) => `${{ numberOfRows }} von {len(self.ref_candidates)} Referenzen selektiert`'"
|