Coverage for gws-app/gws/plugin/alkis/action.py: 0%
476 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
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 return self.ix.status()
435 self.ixStatus = gws.u.get_server_global(f'gws.plugin.alkis.action.ixStatus.{self.indexSchema}', _load)
437 def props(self, user):
438 if not self.ixStatus.basic:
439 return None
441 ps: Props = cast(
442 Props,
443 gws.u.merge(
444 super().props(user),
445 limit=self.limit,
446 printer=gws.u.first(p for p in self.printers if user.can_use(p)),
447 ui=self.ui,
448 storage=self.storage,
449 withBuchung=(self.ixStatus.buchung and user.can_read(self.buchung)),
450 withEigentuemer=(self.ixStatus.eigentuemer and user.can_read(self.eigentuemer)),
451 withEigentuemerControl=(self.ixStatus.eigentuemer and user.can_read(self.eigentuemer) and self.eigentuemer.controlMode),
452 ),
453 )
455 ps.exporters = []
456 for exp in self.exporters:
457 if not user.can_use(exp):
458 continue
459 if exp.withEigentuemer and not ps.withEigentuemer:
460 continue
461 if exp.withBuchung and not ps.withBuchung:
462 continue
463 ps.exporters.append([exp.uid, exp.title])
465 ps.strasseSearchOptions = self.strasseSearchOptions
466 if ps.withBuchung:
467 ps.buchungsblattSearchOptions = self.buchungsblattSearchOptions
468 if ps.withEigentuemer:
469 ps.nameSearchOptions = self.nameSearchOptions
471 return ps
473 @gws.ext.command.api('alkisGetToponyms')
474 def get_toponyms(self, req: gws.WebRequester, p: GetToponymsRequest) -> GetToponymsResponse:
475 """Return all Toponyms (Gemeinde/Gemarkung/Strasse) in the area"""
477 req.user.require_project(p.projectUid)
479 gemeinde_dct = {}
480 gemarkung_dct = {}
481 strasse_lst = []
483 for s in self.ix.strasse_list():
484 gemeinde_dct[s.gemeinde.code] = [s.gemeinde.text, s.gemeinde.code]
485 gemarkung_dct[s.gemarkung.code] = [s.gemarkung.text, s.gemarkung.code, s.gemeinde.code]
486 strasse_lst.append([s.name, s.gemeinde.code, s.gemarkung.code])
488 return GetToponymsResponse(
489 gemeinde=sorted(gemeinde_dct.values()),
490 gemarkung=sorted(gemarkung_dct.values()),
491 strasse=sorted(strasse_lst),
492 )
494 @gws.ext.command.api('alkisFindAdresse')
495 def find_adresse(self, req: gws.WebRequester, p: FindAdresseRequest) -> FindAdresseResponse:
496 """Perform an Adresse search."""
498 project = req.user.require_project(p.projectUid)
499 crs = p.get('crs') or project.map.bounds.crs
501 ad_list, query = self.find_adresse_objects(req, p)
502 if not ad_list:
503 return FindAdresseResponse(
504 features=[],
505 total=0,
506 )
508 templates = [
509 self.root.app.templateMgr.find_template('adresse.title', where=[self], user=req.user),
510 self.root.app.templateMgr.find_template('adresse.teaser', where=[self], user=req.user),
511 self.root.app.templateMgr.find_template('adresse.label', where=[self], user=req.user),
512 ]
514 fprops = []
515 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user)
517 for ad in ad_list:
518 f = gws.base.feature.new(model=self.model)
519 f.attributes = vars(ad)
520 f.attributes['geometry'] = f.attributes.pop('shape')
521 f.transform_to(crs)
522 f.render_views(templates, user=req.user)
523 fprops.append(self.model.feature_to_view_props(f, mc))
525 return FindAdresseResponse(
526 features=fprops,
527 total=len(ad_list),
528 )
530 @gws.ext.command.api('alkisFindFlurstueck')
531 def find_flurstueck(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> FindFlurstueckResponse:
532 """Perform a Flurstueck search"""
534 project = req.user.require_project(p.projectUid)
535 crs = p.get('crs') or project.map.bounds.crs
537 fs_list, query = self.find_flurstueck_objects(req, p)
538 if not fs_list:
539 return FindFlurstueckResponse(
540 features=[],
541 total=0,
542 )
544 templates = [
545 self.root.app.templateMgr.find_template('flurstueck.title', where=[self], user=req.user),
546 self.root.app.templateMgr.find_template('flurstueck.teaser', where=[self], user=req.user),
547 ]
549 if query.options.displayThemes:
550 templates.append(
551 self.root.app.templateMgr.find_template('flurstueck.description', where=[self], user=req.user),
552 )
554 args = dict(
555 withHistory=query.options.withHistoryDisplay,
556 withDebug=bool(self.root.app.developer_option('alkis.debug_templates')),
557 )
559 ps = []
560 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user)
562 for fs in fs_list:
563 f = gws.base.feature.new(model=self.model)
564 f.attributes = dict(uid=fs.uid, fs=fs, geometry=fs.shape)
565 f.transform_to(crs)
566 f.render_views(templates, user=req.user, **args)
567 f.attributes.pop('fs')
568 ps.append(self.model.feature_to_view_props(f, mc))
570 return FindFlurstueckResponse(
571 features=sorted(ps, key=lambda p: p.views['title']),
572 total=len(fs_list),
573 )
575 def get_exporter(self, uid: Optional[str], user: gws.User) -> Optional[exporter.Object]:
576 for exp in self.exporters:
577 if user.can_use(exp) and (not uid or exp.uid == uid):
578 return exp
580 @gws.ext.command.api('alkisExportFlurstueck')
581 def export_flurstueck(self, req: gws.WebRequester, p: ExportFlurstueckRequest) -> ExportFlurstueckResponse:
582 exp = self.get_exporter(p.exporterUid, req.user)
583 if not exp:
584 raise gws.NotFoundError()
586 if exp.withEigentuemer:
587 self._check_eigentuemer_access(req, p.eigentuemerControlInput or '')
588 if exp.withBuchung:
589 self._check_buchung_access(req, p.eigentuemerControlInput or '')
591 find_request = p.findRequest
592 find_request.projectUid = p.projectUid
594 fs_list, _ = self.find_flurstueck_objects(req, find_request)
595 if not fs_list:
596 raise gws.NotFoundError()
598 args = exporter.Args(
599 fsList=fs_list,
600 user=req.user,
601 path = gws.u.ephemeral_path('alkis_export'),
602 )
603 exp.run(args)
605 return ExportFlurstueckResponse(
606 content=gws.u.read_file_b(args.path),
607 mime=exp.mimeType,
608 )
610 @gws.ext.command.api('alkisPrintFlurstueck')
611 def print_flurstueck(self, req: gws.WebRequester, p: PrintFlurstueckRequest) -> gws.JobStatusResponse:
612 """Print Flurstueck features"""
614 project = req.user.require_project(p.projectUid)
616 find_request = p.findRequest
617 find_request.projectUid = p.projectUid
619 fs_list, query = self.find_flurstueck_objects(req, find_request)
620 if not fs_list:
621 raise gws.NotFoundError()
623 print_request = p.printRequest
624 print_request.projectUid = p.projectUid
625 crs = print_request.get('crs') or project.map.bounds.crs
627 templates = [
628 self.root.app.templateMgr.find_template('flurstueck.label', where=[self], user=req.user),
629 ]
631 base_map = print_request.maps[0]
632 fs_maps = []
634 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user)
636 for fs in fs_list:
637 f = gws.base.feature.new(model=self.model)
638 f.attributes = dict(uid=fs.uid, fs=fs, geometry=fs.shape)
639 f.transform_to(crs)
640 f.render_views(templates, user=req.user)
641 f.cssSelector = p.featureStyle.cssSelector
643 c = f.shape().centroid()
644 fs_map = gws.PrintMap(base_map)
645 # @TODO scale to fit the fs?
646 fs_map.center = (c.x, c.y)
647 fs_plane = gws.PrintPlane(
648 type=gws.PrintPlaneType.features,
649 features=[self.model.feature_to_view_props(f, mc)],
650 )
651 fs_map.planes = [fs_plane] + base_map.planes
652 fs_maps.append(fs_map)
654 print_request.maps = fs_maps
655 print_request.args = dict(
656 flurstueckList=fs_list,
657 withHistory=query.options.withHistoryDisplay,
658 withDebug=bool(self.root.app.developer_option('alkis.debug_templates')),
659 )
661 return self.root.app.printerMgr.start_print_job(print_request, req.user)
663 @gws.ext.command.api('alkisSelectionStorage')
664 def handle_storage(self, req: gws.WebRequester, p: gws.base.storage.Request) -> gws.base.storage.Response:
665 if not self.storage:
666 raise gws.NotFoundError('no storage configured')
667 return self.storage.handle_request(req, p)
669 ##
671 def find_flurstueck_objects(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> tuple[list[dt.Flurstueck], dt.FlurstueckQuery]:
672 query = self._prepare_flurstueck_query(req, p)
673 fs_list = self.ix.find_flurstueck(query)
675 if query.options.withEigentuemer:
676 self._log_eigentuemer_access(
677 req,
678 p.eigentuemerControlInput or '',
679 is_ok=True,
680 total=len(fs_list),
681 fs_uids=[fs.uid for fs in fs_list],
682 )
684 return fs_list, query
686 def find_adresse_objects(self, req: gws.WebRequester, p: FindAdresseRequest) -> tuple[list[dt.Adresse], dt.AdresseQuery]:
687 query = self._prepare_adresse_query(req, p)
688 ad_list = self.ix.find_adresse(query)
689 return ad_list, query
691 FLURSTUECK_QUERY_FIELDS = [
692 'flurnummer',
693 'flurstuecksfolge',
694 'zaehler',
695 'nenner',
696 'flurstueckskennzeichen',
697 'flaecheBis',
698 'flaecheVon',
699 'gemarkung',
700 'gemarkungCode',
701 'gemeinde',
702 'gemeindeCode',
703 'kreis',
704 'kreisCode',
705 'land',
706 'landCode',
707 'regierungsbezirk',
708 'regierungsbezirkCode',
709 'strasse',
710 'hausnummer',
711 'personName',
712 'personVorname',
713 'uids',
714 ]
715 ADRESSE_QUERY_FIELDS = [
716 'gemarkung',
717 'gemarkungCode',
718 'gemeinde',
719 'gemeindeCode',
720 'kreis',
721 'kreisCode',
722 'land',
723 'landCode',
724 'regierungsbezirk',
725 'regierungsbezirkCode',
726 'strasse',
727 'hausnummer',
728 'bisHausnummer',
729 'hausnummerNotNull',
730 ]
731 COMBINED_FLURSTUECK_FIELDS = ['landCode', 'gemarkungCode', 'flurnummer', 'zaehler', 'nenner', 'flurstuecksfolge']
732 COMBINED_ADRESSE_FIELDS = ['strasse', 'hausnummer', 'plz', 'gemeinde', 'bisHausnummer']
734 def _prepare_flurstueck_query(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> dt.FlurstueckQuery:
735 query = dt.FlurstueckQuery()
737 for f in self.FLURSTUECK_QUERY_FIELDS:
738 setattr(query, f, getattr(p, f, None))
740 if p.combinedFlurstueckCode:
741 self._query_combined_code(query, p.combinedFlurstueckCode, self.COMBINED_FLURSTUECK_FIELDS)
743 if p.fsnummer:
744 self._query_fsnummer(query, p.fsnummer)
746 if p.shapes:
747 shapes = [gws.base.shape.from_props(s) for s in p.shapes]
748 query.shape = shapes[0] if len(shapes) == 1 else shapes[0].union(shapes[1:])
750 if p.bblatt:
751 query.buchungsblattkennzeichenList = p.bblatt.replace(';', ' ').replace(',', ' ').strip().split()
753 options = dt.FlurstueckQueryOptions(
754 strasseSearchOptions=self.strasseSearchOptions,
755 nameSearchOptions=self.nameSearchOptions,
756 buchungsblattSearchOptions=self.buchungsblattSearchOptions,
757 limit=self.limit,
758 withEigentuemer=False,
759 withBuchung=False,
760 withHistorySearch=bool(p.wantHistorySearch),
761 withHistoryDisplay=bool(p.wantHistoryDisplay),
762 displayThemes=p.displayThemes or [],
763 )
765 want_eigentuemer = (
766 p.wantEigentuemer
767 or dt.DisplayTheme.eigentuemer in options.displayThemes
768 or any(getattr(query, f) is not None for f in dt.EigentuemerAccessRequired)
769 )
770 if want_eigentuemer:
771 self._check_eigentuemer_access(req, p.eigentuemerControlInput or '')
772 options.withEigentuemer = True
774 want_buchung = dt.DisplayTheme.buchung in options.displayThemes or any(getattr(query, f) is not None for f in dt.BuchungAccessRequired)
775 if want_buchung:
776 self._check_buchung_access(req, p.eigentuemerControlInput or '')
777 options.withBuchung = True
779 # "eigentuemer" implies "buchung"
780 if want_eigentuemer and not want_buchung:
781 options.withBuchung = True
782 options.displayThemes.append(dt.DisplayTheme.buchung)
784 query.options = options
785 return query
787 def _prepare_adresse_query(self, req: gws.WebRequester, p: FindAdresseRequest) -> dt.AdresseQuery:
788 query = dt.AdresseQuery()
790 for f in self.ADRESSE_QUERY_FIELDS:
791 setattr(query, f, getattr(p, f, None))
793 if p.combinedAdresseCode:
794 self._query_combined_code(query, p.combinedAdresseCode, self.COMBINED_ADRESSE_FIELDS)
796 options = dt.AdresseQueryOptions(
797 strasseSearchOptions=self.strasseSearchOptions,
798 withHistorySearch=bool(p.wantHistorySearch),
799 )
801 query.options = options
802 return query
804 def _query_fsnummer(self, query: dt.FlurstueckQuery, vn: str):
805 if vn.startswith('DE'):
806 # search by gml_id
807 query.uids = query.uids or []
808 query.uids.append(vn)
809 return
811 if re.match('^[0-9_]+$', vn) and len(vn) >= 14:
812 # search by fs kennzeichen
813 # the length must be at least 2+4+3+5
814 # (see gdi6, AX_Flurstueck_Kerndaten.flurstueckskennzeichen)
815 query.flurstueckskennzeichen = vn
816 return
818 # search by a compound fs number
819 parts = index.parse_fsnummer(vn)
820 if parts is None:
821 raise gws.BadRequestError(f'invalid fsnummer {vn!r}')
822 query.update(parts)
824 def _query_combined_code(self, query: dt.FlurstueckQuery | dt.AdresseQuery, code_value: str, code_fields: list[str]):
825 for val, field in zip(code_value.split('_'), code_fields):
826 if val and val != '0':
827 setattr(query, field, val)
829 ##
831 def _check_eigentuemer_access(self, req: gws.WebRequester, control_input: str):
832 if not req.user.can_read(self.eigentuemer):
833 raise gws.ForbiddenError('cannot read eigentuemer')
834 if self.eigentuemer.controlMode and not self._check_eigentuemer_control_input(control_input):
835 self._log_eigentuemer_access(req, is_ok=False, control_input=control_input)
836 raise gws.ForbiddenError('eigentuemer control input failed')
838 def _check_buchung_access(self, req: gws.WebRequester, control_input: str):
839 if not req.user.can_read(self.buchung):
840 raise gws.ForbiddenError('cannot read buchung')
842 def _log_eigentuemer_access(self, req: gws.WebRequester, control_input: str, is_ok: bool, total=None, fs_uids=None):
843 if self.eigentuemer.logTable is None:
844 return
846 data = dict(
847 app_name='gws',
848 date_time=gws.lib.datetimex.now(),
849 ip=req.env('REMOTE_ADDR', ''),
850 login=req.user.uid,
851 user_name=req.user.displayName,
852 control_input=(control_input or '').strip(),
853 control_result=1 if is_ok else 0,
854 fs_count=total or 0,
855 fs_ids=','.join(fs_uids or []),
856 )
858 with self.ix.db.connect() as conn:
859 conn.execute(sa.insert(self.eigentuemer.logTable).values([data]))
860 conn.commit()
862 gws.log.debug(f'alkis: _log_eigentuemer_access {is_ok=}')
864 def _check_eigentuemer_control_input(self, control_input):
865 if not self.eigentuemer.controlRules:
866 return True
868 control_input = (control_input or '').strip()
870 for rule in self.eigentuemer.controlRules:
871 if re.search(rule, control_input):
872 return True
874 return False