Coverage for gws-app / gws / plugin / alkis / action.py: 0%
479 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-03 10:12 +0100
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-03 10:12 +0100
1"""Backend for the Flurstückssuche (cadaster parlcels search) form."""
3from typing import Optional, cast
5import os
6import re
8import gws
9import gws.base.action
10import gws.base.feature
11import gws.base.model
12import gws.base.printer
13import gws.base.shape
14import gws.base.storage
15import gws.config.util
16import gws.lib.datetimex
17import gws.lib.sa as sa
19from .data import index, exporter
20from .data import types as dt
22gws.ext.new.action('alkis')
25class EigentuemerConfig(gws.ConfigWithAccess):
26 """Access to the Eigentümer (owner) information"""
28 controlMode: bool = False
29 """Restricted mode enabled."""
30 controlRules: Optional[list[str]]
31 """Regular expression for the restricted input control."""
32 logTable: str = ''
33 """Data access protocol table name."""
36class EigentuemerOptions(gws.Node):
37 controlMode: bool
38 controlRules: list[str]
39 logTableName: str
40 logTable: Optional[sa.Table]
42 def configure(self):
43 self.controlMode = self.cfg('controlMode')
44 self.controlRules = self.cfg('controlRules', default=[])
45 self.logTableName = self.cfg('logTable')
46 self.logTable = None
49class BuchungConfig(gws.ConfigWithAccess):
50 """Access to the Grundbuch (register) information"""
52 pass
55class BuchungOptions(gws.Node):
56 pass
59class GemarkungListMode(gws.Enum):
60 """Gemarkung (Administrative Unit) list mode."""
62 none = 'none'
63 """Do not show the list."""
64 plain = 'plain'
65 """Show only gemarkung."""
66 combined = 'combined'
67 """Show gemarkung(gemeinde)."""
68 tree = 'tree'
69 """A tree with level 1 = gemeinde and level 2 = gemarkung."""
72class StrasseListMode(gws.Enum):
73 """Strasse (street) list entry format."""
75 plain = 'plain'
76 """Just strasse."""
77 withGemeinde = 'withGemeinde'
78 """Strasse (gemeinde)."""
79 withGemarkung = 'withGemarkung'
80 """Strasse (gemarkung)."""
81 withGemeindeIfRepeated = 'withGemeindeIfRepeated'
82 """Strasse (gemeinde), when needed for disambiguation."""
83 withGemarkungIfRepeated = 'withGemarkungIfRepeated'
84 """Strasse (gemarkung), when needed for disambiguation."""
87class Ui(gws.Config):
88 """Flurstückssuche UI configuration."""
90 useExport: bool = False
91 """Export function enabled."""
92 useSelect: bool = False
93 """Select mode enabled."""
94 usePick: bool = False
95 """Pick mode enabled."""
96 useHistory: bool = False
97 """History controls enabled."""
98 searchSelection: bool = False
99 """Search in selection enabled."""
100 searchSpatial: bool = False
101 """Spatial search enabled."""
102 gemarkungListMode: GemarkungListMode = GemarkungListMode.combined
103 """Gemarkung list mode."""
104 strasseListMode: StrasseListMode = StrasseListMode.plain
105 """Strasse list entry format."""
106 autoSpatialSearch: bool = False
107 """Activate spatial search after submit."""
110class Config(gws.ConfigWithAccess):
111 """Flurstückssuche action configuration."""
113 dbUid: str = ''
114 """Database provider ID."""
115 crs: gws.CrsName
116 """CRS for the ALKIS data."""
117 dataSchema: str = 'public'
118 """Schema where ALKIS tables are stored."""
119 indexSchema: str = 'gws8'
120 """Schema to store GWS internal indexes."""
121 excludeGemarkung: Optional[list[str]]
122 """Gemarkung (Administrative Unit) IDs to exclude from indexing."""
123 gemarkungFilter: Optional[list[str]]
124 """Restrict search to Gemarkung (Administrative Unit) IDs."""
126 eigentuemer: Optional[EigentuemerConfig]
127 """Access to the Eigentümer (owner) information."""
128 buchung: Optional[BuchungConfig]
129 """Access to the Grundbuch (register) information."""
130 limit: int = 100
131 """Search results limit."""
132 templates: Optional[list[gws.ext.config.template]]
133 """Templates for Flurstueck details."""
134 printers: Optional[list[gws.base.printer.Config]]
135 """Print configurations."""
136 ui: Optional[Ui]
137 """Ui options."""
139 strasseSearchOptions: Optional[gws.TextSearchOptions]
140 """Search options for street names."""
141 nameSearchOptions: Optional[gws.TextSearchOptions]
142 """Search options for person names."""
143 buchungsblattSearchOptions: Optional[gws.TextSearchOptions]
144 """Search options for book and page numbers."""
146 storage: Optional[gws.base.storage.Config]
147 """Storage configuration."""
149 exporters: Optional[list[exporter.Config]]
150 """Export configurations."""
153##
156class Props(gws.base.action.Props):
157 exporters: list[list[str]]
158 limit: int
159 printer: Optional[gws.base.printer.Props]
160 ui: Ui
161 storage: Optional[gws.base.storage.Props]
162 strasseSearchOptions: Optional[gws.TextSearchOptions]
163 nameSearchOptions: Optional[gws.TextSearchOptions]
164 buchungsblattSearchOptions: Optional[gws.TextSearchOptions]
165 withBuchung: bool
166 withEigentuemer: bool
167 withEigentuemerControl: bool
168 withFlurnummer: bool
171##
174class GetToponymsRequest(gws.Request):
175 pass
178class GetToponymsResponse(gws.Response):
179 gemeinde: list[list[str]]
180 gemarkung: list[list[str]]
181 strasse: list[list[str]]
184class FindFlurstueckRequest(gws.Request):
185 flurnummer: Optional[str]
186 flurstuecksfolge: Optional[str]
187 zaehler: Optional[str]
188 nenner: Optional[str]
189 fsnummer: Optional[str]
191 flaecheBis: Optional[float]
192 flaecheVon: Optional[float]
194 gemarkung: Optional[str]
195 gemarkungCode: Optional[str]
196 gemeinde: Optional[str]
197 gemeindeCode: Optional[str]
198 kreis: Optional[str]
199 kreisCode: Optional[str]
200 land: Optional[str]
201 landCode: Optional[str]
202 regierungsbezirk: Optional[str]
203 regierungsbezirkCode: Optional[str]
205 strasse: Optional[str]
206 hausnummer: Optional[str]
208 bblatt: Optional[str]
210 personName: Optional[str]
211 personVorname: Optional[str]
213 combinedFlurstueckCode: Optional[str]
215 shapes: Optional[list[gws.base.shape.Props]]
217 uids: Optional[list[str]]
219 crs: Optional[gws.CrsName]
220 eigentuemerControlInput: Optional[str]
221 limit: Optional[int]
223 wantEigentuemer: Optional[bool]
224 wantHistorySearch: Optional[bool]
225 wantHistoryDisplay: Optional[bool]
227 displayThemes: Optional[list[dt.DisplayTheme]]
230class FindFlurstueckResponse(gws.Response):
231 features: list[gws.FeatureProps]
232 total: int
235class FindFlurstueckResult(gws.Data):
236 flurstueckList: list[dt.Flurstueck]
237 total: int
238 query: dt.FlurstueckQuery
241class FindAdresseRequest(gws.Request):
242 crs: Optional[gws.CrsName]
244 gemarkung: Optional[str]
245 gemarkungCode: Optional[str]
246 gemeinde: Optional[str]
247 gemeindeCode: Optional[str]
248 kreis: Optional[str]
249 kreisCode: Optional[str]
250 land: Optional[str]
251 landCode: Optional[str]
252 regierungsbezirk: Optional[str]
253 regierungsbezirkCode: Optional[str]
255 strasse: Optional[str]
256 hausnummer: Optional[str]
257 bisHausnummer: Optional[str]
258 hausnummerNotNull: Optional[bool]
260 wantHistorySearch: Optional[bool]
262 combinedAdresseCode: Optional[str]
265class FindAdresseResponse(gws.Response):
266 features: list[gws.FeatureProps]
267 total: int
270class PrintFlurstueckRequest(gws.Request):
271 findRequest: FindFlurstueckRequest
272 printRequest: gws.PrintRequest
273 featureStyle: gws.StyleProps
276class ExportFlurstueckRequest(gws.Request):
277 findRequest: FindFlurstueckRequest
278 exporterUid: str
279 eigentuemerControlInput: Optional[str]
282class ExportFlurstueckResponse(gws.Response):
283 content: str
284 mime: str
287##
290_dir = os.path.dirname(__file__)
292_DEFAULT_TEMPLATES = [
293 gws.Config(
294 subject='flurstueck.title',
295 type='html',
296 path=f'{_dir}/templates/title.cx.html',
297 ),
298 gws.Config(
299 subject='flurstueck.teaser',
300 type='html',
301 path=f'{_dir}/templates/title.cx.html',
302 ),
303 gws.Config(
304 subject='flurstueck.label',
305 type='html',
306 path=f'{_dir}/templates/title.cx.html',
307 ),
308 gws.Config(
309 subject='flurstueck.description',
310 type='html',
311 path=f'{_dir}/templates/description.cx.html',
312 ),
313 gws.Config(
314 subject='adresse.title',
315 type='html',
316 path=f'{_dir}/templates/adresse_title.cx.html',
317 ),
318 gws.Config(
319 subject='adresse.teaser',
320 type='html',
321 path=f'{_dir}/templates/adresse_title.cx.html',
322 ),
323 gws.Config(
324 subject='adresse.label',
325 type='html',
326 path=f'{_dir}/templates/adresse_title.cx.html',
327 ),
328]
330_DEFAULT_PRINTER = gws.Config(
331 uid='gws.plugin.alkis.default_printer',
332 template=gws.Config(
333 type='html',
334 path=f'{_dir}/templates/print.cx.html',
335 ),
336 qualityLevels=[{'dpi': 72}],
337)
340##
343class Model(gws.base.model.default_model.Object):
344 def configure(self):
345 self.uidName = 'uid'
346 self.geometryName = 'geometry'
347 self.loadingStrategy = gws.FeatureLoadingStrategy.all
350class Object(gws.base.action.Object):
351 db: gws.DatabaseProvider
353 ix: index.Object
354 ixStatus: dt.IndexStatus
356 buchung: BuchungOptions
357 eigentuemer: EigentuemerOptions
359 dataSchema: str
360 indexSchema: str
362 model: gws.Model
363 ui: Ui
364 limit: int
366 templates: list[gws.Template]
367 printers: list[gws.Printer]
369 exporters: list[exporter.Object]
371 strasseSearchOptions: gws.TextSearchOptions
372 nameSearchOptions: gws.TextSearchOptions
373 buchungsblattSearchOptions: gws.TextSearchOptions
375 storage: Optional[gws.base.storage.Object]
377 def configure(self):
378 gws.config.util.configure_database_provider_for(self, ext_type='postgres')
380 self.dataSchema = self.cfg('dataSchema')
381 self.indexSchema = self.cfg('indexSchema')
383 self.ix = self.root.create(
384 index.Object,
385 _defaultDb=self.db,
386 crs=self.cfg('crs'),
387 schema=self.indexSchema,
388 excludeGemarkung=self.cfg('excludeGemarkung'),
389 gemarkungFilter=self.cfg('gemarkungFilter'),
390 uid=f'gws.plugin.alkis.data.index.{self.indexSchema}',
391 )
393 self.limit = self.cfg('limit')
394 self.model = self.create_child(Model)
395 self.ui = self.cfg(
396 'ui',
397 default=Ui(
398 gemarkungListMode=GemarkungListMode.combined,
399 strasseListMode=StrasseListMode.plain,
400 ),
401 )
403 p = self.cfg('templates', default=[]) + _DEFAULT_TEMPLATES
404 self.templates = [self.create_child(gws.ext.object.template, c) for c in p]
406 self.printers = self.create_children(gws.ext.object.printer, self.cfg('printers'))
407 self.printers.append(self.create_child(gws.ext.object.printer, _DEFAULT_PRINTER))
409 d = gws.TextSearchOptions(type='exact')
410 self.strasseSearchOptions = self.cfg('strasseSearchOptions', default=d)
411 self.nameSearchOptions = self.cfg('nameSearchOptions', default=d)
412 self.buchungsblattSearchOptions = self.cfg('buchungsblattSearchOptions', default=d)
414 deny_all = gws.Config(access='deny all')
416 self.buchung = self.create_child(BuchungOptions, self.cfg('buchung', default=deny_all))
418 self.eigentuemer = self.create_child(EigentuemerOptions, self.cfg('eigentuemer', default=deny_all))
419 if self.eigentuemer.logTableName:
420 self.eigentuemer.logTable = self.ix.db.table(self.eigentuemer.logTableName)
422 self.storage = self.create_child_if_configured(gws.base.storage.Object, self.cfg('storage'), categoryName='Alkis')
424 self.exporters = []
425 p = self.cfg('exporters')
426 if p:
427 self.exporters = self.create_children(exporter.Object, p)
428 elif self.ui.useExport:
429 self.exporters.append(self.create_child(exporter.Object))
431 def activate(self):
432 def _load():
433 s = self.ix.status()
434 if s.missing:
435 gws.log.warning(f'ALKIS: index not found in schema {self.indexSchema}')
436 return s
438 self.ixStatus = gws.u.get_server_global(f'gws.plugin.alkis.action.ixStatus.{self.indexSchema}', _load)
440 def props(self, user):
441 if not self.ixStatus.basic:
442 return None
444 ps: Props = cast(
445 Props,
446 gws.u.merge(
447 super().props(user),
448 limit=self.limit,
449 printer=gws.u.first(p for p in self.printers if user.can_use(p)),
450 ui=self.ui,
451 storage=self.storage,
452 withBuchung=(self.ixStatus.buchung and user.can_read(self.buchung)),
453 withEigentuemer=(self.ixStatus.eigentuemer and user.can_read(self.eigentuemer)),
454 withEigentuemerControl=(self.ixStatus.eigentuemer and user.can_read(self.eigentuemer) and self.eigentuemer.controlMode),
455 ),
456 )
458 ps.exporters = []
459 for exp in self.exporters:
460 if not user.can_use(exp):
461 continue
462 if exp.withEigentuemer and not ps.withEigentuemer:
463 continue
464 if exp.withBuchung and not ps.withBuchung:
465 continue
466 ps.exporters.append([exp.uid, exp.title])
468 ps.strasseSearchOptions = self.strasseSearchOptions
469 if ps.withBuchung:
470 ps.buchungsblattSearchOptions = self.buchungsblattSearchOptions
471 if ps.withEigentuemer:
472 ps.nameSearchOptions = self.nameSearchOptions
474 return ps
476 @gws.ext.command.api('alkisGetToponyms')
477 def get_toponyms(self, req: gws.WebRequester, p: GetToponymsRequest) -> GetToponymsResponse:
478 """Return all Toponyms (Gemeinde/Gemarkung/Strasse) in the area"""
480 req.user.require_project(p.projectUid)
482 gemeinde_dct = {}
483 gemarkung_dct = {}
484 strasse_lst = []
486 for s in self.ix.strasse_list():
487 gemeinde_dct[s.gemeinde.code] = [s.gemeinde.text, s.gemeinde.code]
488 gemarkung_dct[s.gemarkung.code] = [s.gemarkung.text, s.gemarkung.code, s.gemeinde.code]
489 strasse_lst.append([s.name, s.gemeinde.code, s.gemarkung.code])
491 return GetToponymsResponse(
492 gemeinde=sorted(gemeinde_dct.values()),
493 gemarkung=sorted(gemarkung_dct.values()),
494 strasse=sorted(strasse_lst),
495 )
497 @gws.ext.command.api('alkisFindAdresse')
498 def find_adresse(self, req: gws.WebRequester, p: FindAdresseRequest) -> FindAdresseResponse:
499 """Perform an Adresse search."""
501 project = req.user.require_project(p.projectUid)
502 crs = p.get('crs') or project.map.bounds.crs
504 ad_list, query = self.find_adresse_objects(req, p)
505 if not ad_list:
506 return FindAdresseResponse(
507 features=[],
508 total=0,
509 )
511 templates = [
512 self.root.app.templateMgr.find_template('adresse.title', where=[self], user=req.user),
513 self.root.app.templateMgr.find_template('adresse.teaser', where=[self], user=req.user),
514 self.root.app.templateMgr.find_template('adresse.label', where=[self], user=req.user),
515 ]
517 fprops = []
518 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user)
520 for ad in ad_list:
521 f = gws.base.feature.new(model=self.model)
522 f.attributes = vars(ad)
523 f.attributes['geometry'] = f.attributes.pop('shape')
524 f.transform_to(crs)
525 f.render_views(templates, user=req.user)
526 fprops.append(self.model.feature_to_view_props(f, mc))
528 return FindAdresseResponse(
529 features=fprops,
530 total=len(ad_list),
531 )
533 @gws.ext.command.api('alkisFindFlurstueck')
534 def find_flurstueck(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> FindFlurstueckResponse:
535 """Perform a Flurstueck search"""
537 project = req.user.require_project(p.projectUid)
538 crs = p.get('crs') or project.map.bounds.crs
540 fs_list, query = self.find_flurstueck_objects(req, p)
541 if not fs_list:
542 return FindFlurstueckResponse(
543 features=[],
544 total=0,
545 )
547 templates = [
548 self.root.app.templateMgr.find_template('flurstueck.title', where=[self], user=req.user),
549 self.root.app.templateMgr.find_template('flurstueck.teaser', where=[self], user=req.user),
550 ]
552 if query.options.displayThemes:
553 templates.append(
554 self.root.app.templateMgr.find_template('flurstueck.description', where=[self], user=req.user),
555 )
557 args = dict(
558 withHistory=query.options.withHistoryDisplay,
559 withDebug=bool(self.root.app.developer_option('alkis.debug_templates')),
560 )
562 ps = []
563 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user)
565 for fs in fs_list:
566 f = gws.base.feature.new(model=self.model)
567 f.attributes = dict(uid=fs.uid, fs=fs, geometry=fs.shape)
568 f.transform_to(crs)
569 f.render_views(templates, user=req.user, **args)
570 f.attributes.pop('fs')
571 ps.append(self.model.feature_to_view_props(f, mc))
573 return FindFlurstueckResponse(
574 features=sorted(ps, key=lambda p: p.views['title']),
575 total=len(fs_list),
576 )
578 def get_exporter(self, uid: Optional[str], user: gws.User) -> Optional[exporter.Object]:
579 for exp in self.exporters:
580 if user.can_use(exp) and (not uid or exp.uid == uid):
581 return exp
583 @gws.ext.command.api('alkisExportFlurstueck')
584 def export_flurstueck(self, req: gws.WebRequester, p: ExportFlurstueckRequest) -> ExportFlurstueckResponse:
585 exp = self.get_exporter(p.exporterUid, req.user)
586 if not exp:
587 raise gws.NotFoundError()
589 if exp.withEigentuemer:
590 self._check_eigentuemer_access(req, p.eigentuemerControlInput or '')
591 if exp.withBuchung:
592 self._check_buchung_access(req, p.eigentuemerControlInput or '')
594 find_request = p.findRequest
595 find_request.projectUid = p.projectUid
597 fs_list, _ = self.find_flurstueck_objects(req, find_request)
598 if not fs_list:
599 raise gws.NotFoundError()
601 args = exporter.Args(
602 fsList=fs_list,
603 user=req.user,
604 path = gws.u.ephemeral_path('alkis_export'),
605 )
606 exp.run(args)
608 return ExportFlurstueckResponse(
609 content=gws.u.read_file_b(args.path),
610 mime=exp.mimeType,
611 )
613 @gws.ext.command.api('alkisPrintFlurstueck')
614 def print_flurstueck(self, req: gws.WebRequester, p: PrintFlurstueckRequest) -> gws.JobStatusResponse:
615 """Print Flurstueck features"""
617 project = req.user.require_project(p.projectUid)
619 find_request = p.findRequest
620 find_request.projectUid = p.projectUid
622 fs_list, query = self.find_flurstueck_objects(req, find_request)
623 if not fs_list:
624 raise gws.NotFoundError()
626 print_request = p.printRequest
627 print_request.projectUid = p.projectUid
628 crs = print_request.get('crs') or project.map.bounds.crs
630 templates = [
631 self.root.app.templateMgr.find_template('flurstueck.label', where=[self], user=req.user),
632 ]
634 base_map = print_request.maps[0]
635 fs_maps = []
637 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user)
639 for fs in fs_list:
640 f = gws.base.feature.new(model=self.model)
641 f.attributes = dict(uid=fs.uid, fs=fs, geometry=fs.shape)
642 f.transform_to(crs)
643 f.render_views(templates, user=req.user)
644 f.cssSelector = p.featureStyle.cssSelector
646 c = f.shape().centroid()
647 fs_map = gws.PrintMap(base_map)
648 # @TODO scale to fit the fs?
649 fs_map.center = (c.x, c.y)
650 fs_plane = gws.PrintPlane(
651 type=gws.PrintPlaneType.features,
652 features=[self.model.feature_to_view_props(f, mc)],
653 )
654 fs_map.planes = [fs_plane] + base_map.planes
655 fs_maps.append(fs_map)
657 print_request.maps = fs_maps
658 print_request.args = dict(
659 flurstueckList=fs_list,
660 withHistory=query.options.withHistoryDisplay,
661 withDebug=bool(self.root.app.developer_option('alkis.debug_templates')),
662 )
664 return self.root.app.printerMgr.start_print_job(print_request, req.user)
666 @gws.ext.command.api('alkisSelectionStorage')
667 def handle_storage(self, req: gws.WebRequester, p: gws.base.storage.Request) -> gws.base.storage.Response:
668 if not self.storage:
669 raise gws.NotFoundError('no storage configured')
670 return self.storage.handle_request(req, p)
672 ##
674 def find_flurstueck_objects(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> tuple[list[dt.Flurstueck], dt.FlurstueckQuery]:
675 query = self._prepare_flurstueck_query(req, p)
676 fs_list = self.ix.find_flurstueck(query)
678 if query.options.withEigentuemer:
679 self._log_eigentuemer_access(
680 req,
681 p.eigentuemerControlInput or '',
682 is_ok=True,
683 total=len(fs_list),
684 fs_uids=[fs.uid for fs in fs_list],
685 )
687 return fs_list, query
689 def find_adresse_objects(self, req: gws.WebRequester, p: FindAdresseRequest) -> tuple[list[dt.Adresse], dt.AdresseQuery]:
690 query = self._prepare_adresse_query(req, p)
691 ad_list = self.ix.find_adresse(query)
692 return ad_list, query
694 FLURSTUECK_QUERY_FIELDS = [
695 'flurnummer',
696 'flurstuecksfolge',
697 'zaehler',
698 'nenner',
699 'flurstueckskennzeichen',
700 'flaecheBis',
701 'flaecheVon',
702 'gemarkung',
703 'gemarkungCode',
704 'gemeinde',
705 'gemeindeCode',
706 'kreis',
707 'kreisCode',
708 'land',
709 'landCode',
710 'regierungsbezirk',
711 'regierungsbezirkCode',
712 'strasse',
713 'hausnummer',
714 'personName',
715 'personVorname',
716 'uids',
717 ]
718 ADRESSE_QUERY_FIELDS = [
719 'gemarkung',
720 'gemarkungCode',
721 'gemeinde',
722 'gemeindeCode',
723 'kreis',
724 'kreisCode',
725 'land',
726 'landCode',
727 'regierungsbezirk',
728 'regierungsbezirkCode',
729 'strasse',
730 'hausnummer',
731 'bisHausnummer',
732 'hausnummerNotNull',
733 ]
734 COMBINED_FLURSTUECK_FIELDS = ['landCode', 'gemarkungCode', 'flurnummer', 'zaehler', 'nenner', 'flurstuecksfolge']
735 COMBINED_ADRESSE_FIELDS = ['strasse', 'hausnummer', 'plz', 'gemeinde', 'bisHausnummer']
737 def _prepare_flurstueck_query(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> dt.FlurstueckQuery:
738 query = dt.FlurstueckQuery()
740 for f in self.FLURSTUECK_QUERY_FIELDS:
741 setattr(query, f, getattr(p, f, None))
743 if p.combinedFlurstueckCode:
744 self._query_combined_code(query, p.combinedFlurstueckCode, self.COMBINED_FLURSTUECK_FIELDS)
746 if p.fsnummer:
747 self._query_fsnummer(query, p.fsnummer)
749 if p.shapes:
750 shapes = [gws.base.shape.from_props(s) for s in p.shapes]
751 query.shape = shapes[0] if len(shapes) == 1 else shapes[0].union(shapes[1:])
753 if p.bblatt:
754 query.buchungsblattkennzeichenList = p.bblatt.replace(';', ' ').replace(',', ' ').strip().split()
756 options = dt.FlurstueckQueryOptions(
757 strasseSearchOptions=self.strasseSearchOptions,
758 nameSearchOptions=self.nameSearchOptions,
759 buchungsblattSearchOptions=self.buchungsblattSearchOptions,
760 limit=self.limit,
761 withEigentuemer=False,
762 withBuchung=False,
763 withHistorySearch=bool(p.wantHistorySearch),
764 withHistoryDisplay=bool(p.wantHistoryDisplay),
765 displayThemes=p.displayThemes or [],
766 )
768 want_eigentuemer = (
769 p.wantEigentuemer
770 or dt.DisplayTheme.eigentuemer in options.displayThemes
771 or any(getattr(query, f) is not None for f in dt.EigentuemerAccessRequired)
772 )
773 if want_eigentuemer:
774 self._check_eigentuemer_access(req, p.eigentuemerControlInput or '')
775 options.withEigentuemer = True
777 want_buchung = dt.DisplayTheme.buchung in options.displayThemes or any(getattr(query, f) is not None for f in dt.BuchungAccessRequired)
778 if want_buchung:
779 self._check_buchung_access(req, p.eigentuemerControlInput or '')
780 options.withBuchung = True
782 # "eigentuemer" implies "buchung"
783 if want_eigentuemer and not want_buchung:
784 options.withBuchung = True
785 options.displayThemes.append(dt.DisplayTheme.buchung)
787 query.options = options
788 return query
790 def _prepare_adresse_query(self, req: gws.WebRequester, p: FindAdresseRequest) -> dt.AdresseQuery:
791 query = dt.AdresseQuery()
793 for f in self.ADRESSE_QUERY_FIELDS:
794 setattr(query, f, getattr(p, f, None))
796 if p.combinedAdresseCode:
797 self._query_combined_code(query, p.combinedAdresseCode, self.COMBINED_ADRESSE_FIELDS)
799 options = dt.AdresseQueryOptions(
800 strasseSearchOptions=self.strasseSearchOptions,
801 withHistorySearch=bool(p.wantHistorySearch),
802 )
804 query.options = options
805 return query
807 def _query_fsnummer(self, query: dt.FlurstueckQuery, vn: str):
808 if vn.startswith('DE'):
809 # search by gml_id
810 query.uids = query.uids or []
811 query.uids.append(vn)
812 return
814 if re.match('^[0-9_]+$', vn) and len(vn) >= 14:
815 # search by fs kennzeichen
816 # the length must be at least 2+4+3+5
817 # (see gdi6, AX_Flurstueck_Kerndaten.flurstueckskennzeichen)
818 query.flurstueckskennzeichen = vn
819 return
821 # search by a compound fs number
822 parts = index.parse_fsnummer(vn)
823 if parts is None:
824 raise gws.BadRequestError(f'invalid fsnummer {vn!r}')
825 query.update(parts)
827 def _query_combined_code(self, query: dt.FlurstueckQuery | dt.AdresseQuery, code_value: str, code_fields: list[str]):
828 for val, field in zip(code_value.split('_'), code_fields):
829 if val and val != '0':
830 setattr(query, field, val)
832 ##
834 def _check_eigentuemer_access(self, req: gws.WebRequester, control_input: str):
835 if not req.user.can_read(self.eigentuemer):
836 raise gws.ForbiddenError('cannot read eigentuemer')
837 if self.eigentuemer.controlMode and not self._check_eigentuemer_control_input(control_input):
838 self._log_eigentuemer_access(req, is_ok=False, control_input=control_input)
839 raise gws.ForbiddenError('eigentuemer control input failed')
841 def _check_buchung_access(self, req: gws.WebRequester, control_input: str):
842 if not req.user.can_read(self.buchung):
843 raise gws.ForbiddenError('cannot read buchung')
845 def _log_eigentuemer_access(self, req: gws.WebRequester, control_input: str, is_ok: bool, total=None, fs_uids=None):
846 if self.eigentuemer.logTable is None:
847 return
849 data = dict(
850 app_name='gws',
851 date_time=gws.lib.datetimex.now(),
852 ip=req.env('REMOTE_ADDR', ''),
853 login=req.user.uid,
854 user_name=req.user.displayName,
855 control_input=(control_input or '').strip(),
856 control_result=1 if is_ok else 0,
857 fs_count=total or 0,
858 fs_ids=','.join(fs_uids or []),
859 )
861 with self.ix.db.connect() as conn:
862 conn.execute(sa.insert(self.eigentuemer.logTable).values([data]))
863 conn.commit()
865 gws.log.debug(f'alkis: _log_eigentuemer_access {is_ok=}')
867 def _check_eigentuemer_control_input(self, control_input):
868 if not self.eigentuemer.controlRules:
869 return True
871 control_input = (control_input or '').strip()
873 for rule in self.eigentuemer.controlRules:
874 if re.search(rule, control_input):
875 return True
877 return False