Coverage for gws-app/gws/plugin/alkis/action.py: 0%
481 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +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[exporter.Props]
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 modelUids: Optional[list[str]]
280 eigentuemerControlInput: Optional[str]
283class ExportFlurstueckResponse(gws.Response):
284 content: str
285 mime: str
288##
291_dir = os.path.dirname(__file__)
293_DEFAULT_TEMPLATES = [
294 gws.Config(
295 subject='flurstueck.title',
296 type='html',
297 path=f'{_dir}/templates/flurstueck_title.cx.html',
298 ),
299 gws.Config(
300 subject='flurstueck.teaser',
301 type='html',
302 path=f'{_dir}/templates/flurstueck_title.cx.html',
303 ),
304 gws.Config(
305 subject='flurstueck.label',
306 type='html',
307 path=f'{_dir}/templates/flurstueck_label.cx.html',
308 ),
309 gws.Config(
310 subject='flurstueck.description',
311 type='html',
312 path=f'{_dir}/templates/description.cx.html',
313 ),
314 gws.Config(
315 subject='adresse.title',
316 type='html',
317 path=f'{_dir}/templates/adresse_title.cx.html',
318 ),
319 gws.Config(
320 subject='adresse.teaser',
321 type='html',
322 path=f'{_dir}/templates/adresse_title.cx.html',
323 ),
324 gws.Config(
325 subject='adresse.label',
326 type='html',
327 path=f'{_dir}/templates/adresse_label.cx.html',
328 ),
329]
331_DEFAULT_PRINTER = gws.Config(
332 uid='gws.plugin.alkis.default_printer',
333 template=gws.Config(
334 type='html',
335 path=f'{_dir}/templates/print.cx.html',
336 ),
337 qualityLevels=[gws.TemplateQualityLevel(dpi=72)],
338)
341##
344class Model(gws.base.model.default_model.Object):
345 def configure(self):
346 self.uidName = 'uid'
347 self.geometryName = 'geometry'
348 self.loadingStrategy = gws.FeatureLoadingStrategy.all
351class Object(gws.base.action.Object):
352 db: gws.DatabaseProvider
354 ix: index.Object
355 ixStatus: dt.IndexStatus
357 buchung: BuchungOptions
358 eigentuemer: EigentuemerOptions
360 dataSchema: str
361 indexSchema: str
363 model: gws.Model
364 ui: Ui
365 limit: int
367 templates: list[gws.Template]
368 printers: list[gws.Printer]
370 exporters: list[exporter.Object]
372 strasseSearchOptions: gws.TextSearchOptions
373 nameSearchOptions: gws.TextSearchOptions
374 buchungsblattSearchOptions: gws.TextSearchOptions
376 storage: Optional[gws.base.storage.Object]
378 def configure(self):
379 gws.config.util.configure_database_provider_for(self, ext_type='postgres')
381 self.dataSchema = self.cfg('dataSchema')
382 self.indexSchema = self.cfg('indexSchema')
384 self.ix = self.root.create(
385 index.Object,
386 _defaultDb=self.db,
387 crs=self.cfg('crs'),
388 schema=self.indexSchema,
389 excludeGemarkung=self.cfg('excludeGemarkung'),
390 gemarkungFilter=self.cfg('gemarkungFilter'),
391 uid=f'gws.plugin.alkis.data.index.{self.indexSchema}',
392 )
394 self.limit = self.cfg('limit')
395 self.model = self.create_child(Model)
396 self.ui = self.cfg(
397 'ui',
398 default=Ui(
399 gemarkungListMode=GemarkungListMode.combined,
400 strasseListMode=StrasseListMode.plain,
401 ),
402 )
404 p = self.cfg('templates', default=[]) + _DEFAULT_TEMPLATES
405 self.templates = [self.create_child(gws.ext.object.template, c) for c in p]
407 self.printers = self.create_children(gws.ext.object.printer, self.cfg('printers'))
408 self.printers.append(self.create_child(gws.ext.object.printer, _DEFAULT_PRINTER))
410 d = gws.TextSearchOptions(type=gws.TextSearchType.begin, caseSensitive=False)
411 self.strasseSearchOptions = self.cfg('strasseSearchOptions', default=d)
413 d = gws.TextSearchOptions(type=gws.TextSearchType.begin, caseSensitive=False)
414 self.nameSearchOptions = self.cfg('nameSearchOptions', default=d)
416 d = gws.TextSearchOptions(type=gws.TextSearchType.exact, caseSensitive=False)
417 self.buchungsblattSearchOptions = self.cfg('buchungsblattSearchOptions', default=d)
419 deny_all = gws.Config(access='deny all')
421 self.buchung = self.create_child(BuchungOptions, self.cfg('buchung', default=deny_all))
423 self.eigentuemer = self.create_child(EigentuemerOptions, self.cfg('eigentuemer', default=deny_all))
424 if self.eigentuemer.logTableName:
425 self.eigentuemer.logTable = self.ix.db.table(self.eigentuemer.logTableName)
427 self.storage = self.create_child_if_configured(gws.base.storage.Object, self.cfg('storage'), categoryName='Alkis')
429 self.exporters = []
430 p = self.cfg('exporters')
431 if p:
432 self.exporters = self.create_children(exporter.Object, p)
433 elif self.ui.useExport:
434 self.exporters.append(self.create_child(exporter.Object))
436 def activate(self):
437 def _load():
438 s = self.ix.status()
439 if s.missing:
440 gws.log.warning(f'ALKIS: index not found in schema {self.indexSchema}')
441 return s
443 self.ixStatus = gws.u.get_server_global(f'gws.plugin.alkis.action.ixStatus.{self.indexSchema}', _load)
445 def props(self, user):
446 if not self.ixStatus.basic:
447 return None
449 ps: Props = cast(
450 Props,
451 gws.u.merge(
452 super().props(user),
453 limit=self.limit,
454 printer=gws.u.first(p for p in self.printers if user.can_use(p)),
455 ui=self.ui,
456 storage=self.storage,
457 withBuchung=(self.ixStatus.buchung and user.can_read(self.buchung)),
458 withEigentuemer=(self.ixStatus.eigentuemer and user.can_read(self.eigentuemer)),
459 withEigentuemerControl=(self.ixStatus.eigentuemer and user.can_read(self.eigentuemer) and self.eigentuemer.controlMode),
460 ),
461 )
463 ps.exporters = []
464 for exp in self.exporters:
465 p = exp.props_with_flags(user, withEigentuemer=ps.withEigentuemer, withBuchung=ps.withBuchung)
466 if p:
467 ps.exporters.append(p)
469 ps.strasseSearchOptions = self.strasseSearchOptions
470 if ps.withBuchung:
471 ps.buchungsblattSearchOptions = self.buchungsblattSearchOptions
472 if ps.withEigentuemer:
473 ps.nameSearchOptions = self.nameSearchOptions
475 return ps
477 @gws.ext.command.api('alkisGetToponyms')
478 def get_toponyms(self, req: gws.WebRequester, p: GetToponymsRequest) -> GetToponymsResponse:
479 """Return all Toponyms (Gemeinde/Gemarkung/Strasse) in the area"""
481 req.user.require_project(p.projectUid)
483 gemeinde_dct = {}
484 gemarkung_dct = {}
485 strasse_lst = []
487 for s in self.ix.strasse_list():
488 gemeinde_dct[s.gemeinde.code] = [s.gemeinde.text, s.gemeinde.code]
489 gemarkung_dct[s.gemarkung.code] = [s.gemarkung.text, s.gemarkung.code, s.gemeinde.code]
490 strasse_lst.append([s.name, s.gemeinde.code, s.gemarkung.code])
492 return GetToponymsResponse(
493 gemeinde=sorted(gemeinde_dct.values()),
494 gemarkung=sorted(gemarkung_dct.values()),
495 strasse=sorted(strasse_lst),
496 )
498 @gws.ext.command.api('alkisFindAdresse')
499 def find_adresse(self, req: gws.WebRequester, p: FindAdresseRequest) -> FindAdresseResponse:
500 """Perform an Adresse search."""
502 project = req.user.require_project(p.projectUid)
503 crs = p.get('crs') or project.map.bounds.crs
505 ad_list, query = self.find_adresse_objects(req, p)
506 if not ad_list:
507 return FindAdresseResponse(
508 features=[],
509 total=0,
510 )
512 templates = [
513 self.root.app.templateMgr.find_template('adresse.title', where=[self], user=req.user),
514 self.root.app.templateMgr.find_template('adresse.teaser', where=[self], user=req.user),
515 self.root.app.templateMgr.find_template('adresse.label', where=[self], user=req.user),
516 ]
518 fprops = []
519 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user)
521 for ad in ad_list:
522 f = gws.base.feature.new(model=self.model)
523 f.attributes = vars(ad)
524 f.attributes['geometry'] = f.attributes.pop('shape')
525 f.transform_to(crs)
526 f.render_views(templates, user=req.user)
527 fprops.append(self.model.feature_to_view_props(f, mc))
529 return FindAdresseResponse(
530 features=fprops,
531 total=len(ad_list),
532 )
534 @gws.ext.command.api('alkisFindFlurstueck')
535 def find_flurstueck(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> FindFlurstueckResponse:
536 """Perform a Flurstueck search"""
538 project = req.user.require_project(p.projectUid)
539 crs = p.get('crs') or project.map.bounds.crs
541 fs_list, query = self.find_flurstueck_objects(req, p)
542 if not fs_list:
543 return FindFlurstueckResponse(
544 features=[],
545 total=0,
546 )
548 templates = [
549 self.root.app.templateMgr.find_template('flurstueck.title', where=[self], user=req.user),
550 self.root.app.templateMgr.find_template('flurstueck.teaser', where=[self], user=req.user),
551 ]
553 if query.options.displayThemes:
554 templates.append(
555 self.root.app.templateMgr.find_template('flurstueck.description', where=[self], user=req.user),
556 )
558 args = dict(
559 withHistory=query.options.withHistoryDisplay,
560 withDebug=bool(self.root.app.developer_option('alkis.debug_templates')),
561 )
563 ps = []
564 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user)
566 for fs in fs_list:
567 f = gws.base.feature.new(model=self.model)
568 f.attributes = dict(uid=fs.uid, fs=fs, geometry=fs.shape)
569 f.transform_to(crs)
570 f.render_views(templates, user=req.user, **args)
571 f.attributes.pop('fs')
572 ps.append(self.model.feature_to_view_props(f, mc))
574 return FindFlurstueckResponse(
575 features=sorted(ps, key=lambda p: p.views['title']),
576 total=len(fs_list),
577 )
579 def get_exporter(self, uid: Optional[str], user: gws.User) -> Optional[exporter.Object]:
580 for exp in self.exporters:
581 if user.can_use(exp) and (not uid or exp.uid == uid):
582 return exp
584 @gws.ext.command.api('alkisExportFlurstueck')
585 def export_flurstueck(self, req: gws.WebRequester, p: ExportFlurstueckRequest) -> ExportFlurstueckResponse:
586 exp = self.get_exporter(p.exporterUid, req.user)
587 if not exp:
588 raise gws.NotFoundError()
590 models = exp.get_models(req.user, p.modelUids)
591 if not models:
592 raise gws.NotFoundError()
594 if any(m.withEigentuemer for m in models):
595 self._check_eigentuemer_access(req, p.findRequest.eigentuemerControlInput or '')
596 if any(m.withBuchung for m in models):
597 self._check_buchung_access(req, p.findRequest.eigentuemerControlInput or '')
599 find_request = p.findRequest
600 find_request.projectUid = p.projectUid
602 fs_list, _ = self.find_flurstueck_objects(req, find_request)
603 if not fs_list:
604 raise gws.NotFoundError()
606 args = exporter.Args(
607 fsList=fs_list,
608 user=req.user,
609 models=models,
610 path = gws.u.ephemeral_path('alkis_export'),
611 )
613 exp.run(args)
615 return ExportFlurstueckResponse(
616 content=gws.u.read_file_b(args.path),
617 mime=exp.mimeType,
618 )
620 @gws.ext.command.api('alkisPrintFlurstueck')
621 def print_flurstueck(self, req: gws.WebRequester, p: PrintFlurstueckRequest) -> gws.JobStatusResponse:
622 """Print Flurstueck features"""
624 project = req.user.require_project(p.projectUid)
626 find_request = p.findRequest
627 find_request.projectUid = p.projectUid
629 fs_list, query = self.find_flurstueck_objects(req, find_request)
630 if not fs_list:
631 raise gws.NotFoundError()
633 print_request = p.printRequest
634 print_request.projectUid = p.projectUid
635 crs = print_request.get('crs') or project.map.bounds.crs
637 templates = [
638 self.root.app.templateMgr.find_template('flurstueck.label', where=[self], user=req.user),
639 ]
641 base_map = print_request.maps[0]
642 fs_maps = []
644 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user)
646 for fs in fs_list:
647 f = gws.base.feature.new(model=self.model)
648 f.attributes = dict(uid=fs.uid, fs=fs, geometry=fs.shape)
649 f.transform_to(crs)
650 f.render_views(templates, user=req.user)
651 f.cssSelector = p.featureStyle.cssSelector
653 c = f.shape().centroid()
654 fs_map = gws.PrintMap(base_map)
655 # @TODO scale to fit the fs?
656 fs_map.center = (c.x, c.y)
657 fs_plane = gws.PrintPlane(
658 type=gws.PrintPlaneType.features,
659 features=[self.model.feature_to_view_props(f, mc)],
660 )
661 fs_map.planes = [fs_plane] + base_map.planes
662 fs_maps.append(fs_map)
664 print_request.maps = fs_maps
665 print_request.args = dict(
666 flurstueckList=fs_list,
667 withHistory=query.options.withHistoryDisplay,
668 withDebug=bool(self.root.app.developer_option('alkis.debug_templates')),
669 )
671 return self.root.app.printerMgr.start_print_job(print_request, req.user)
673 @gws.ext.command.api('alkisSelectionStorage')
674 def handle_storage(self, req: gws.WebRequester, p: gws.base.storage.Request) -> gws.base.storage.Response:
675 if not self.storage:
676 raise gws.NotFoundError('no storage configured')
677 return self.storage.handle_request(req, p)
679 ##
681 def find_flurstueck_objects(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> tuple[list[dt.Flurstueck], dt.FlurstueckQuery]:
682 query = self._prepare_flurstueck_query(req, p)
683 fs_list = self.ix.find_flurstueck(query)
685 if query.options.withEigentuemer:
686 self._log_eigentuemer_access(
687 req,
688 p.eigentuemerControlInput or '',
689 is_ok=True,
690 total=len(fs_list),
691 fs_uids=[fs.uid for fs in fs_list],
692 )
694 return fs_list, query
696 def find_adresse_objects(self, req: gws.WebRequester, p: FindAdresseRequest) -> tuple[list[dt.Adresse], dt.AdresseQuery]:
697 query = self._prepare_adresse_query(req, p)
698 ad_list = self.ix.find_adresse(query)
699 return ad_list, query
701 FLURSTUECK_QUERY_FIELDS = [
702 'flurnummer',
703 'flurstuecksfolge',
704 'zaehler',
705 'nenner',
706 'flurstueckskennzeichen',
707 'flaecheBis',
708 'flaecheVon',
709 'gemarkung',
710 'gemarkungCode',
711 'gemeinde',
712 'gemeindeCode',
713 'kreis',
714 'kreisCode',
715 'land',
716 'landCode',
717 'regierungsbezirk',
718 'regierungsbezirkCode',
719 'strasse',
720 'hausnummer',
721 'personName',
722 'personVorname',
723 'uids',
724 ]
725 ADRESSE_QUERY_FIELDS = [
726 'gemarkung',
727 'gemarkungCode',
728 'gemeinde',
729 'gemeindeCode',
730 'kreis',
731 'kreisCode',
732 'land',
733 'landCode',
734 'regierungsbezirk',
735 'regierungsbezirkCode',
736 'strasse',
737 'hausnummer',
738 'bisHausnummer',
739 'hausnummerNotNull',
740 ]
741 COMBINED_FLURSTUECK_FIELDS = ['landCode', 'gemarkungCode', 'flurnummer', 'zaehler', 'nenner', 'flurstuecksfolge']
742 COMBINED_ADRESSE_FIELDS = ['strasse', 'hausnummer', 'plz', 'gemeinde', 'bisHausnummer']
744 def _prepare_flurstueck_query(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> dt.FlurstueckQuery:
745 query = dt.FlurstueckQuery()
747 for f in self.FLURSTUECK_QUERY_FIELDS:
748 setattr(query, f, getattr(p, f, None))
750 if p.combinedFlurstueckCode:
751 self._query_combined_code(query, p.combinedFlurstueckCode, self.COMBINED_FLURSTUECK_FIELDS)
753 if p.fsnummer:
754 self._query_fsnummer(query, p.fsnummer)
756 if p.shapes:
757 shapes = [gws.base.shape.from_props(s) for s in p.shapes]
758 query.shape = shapes[0] if len(shapes) == 1 else shapes[0].union(shapes[1:])
760 if p.bblatt:
761 query.buchungsblattkennzeichenList = p.bblatt.replace(';', ' ').replace(',', ' ').strip().split()
763 options = dt.FlurstueckQueryOptions(
764 strasseSearchOptions=self.strasseSearchOptions,
765 nameSearchOptions=self.nameSearchOptions,
766 buchungsblattSearchOptions=self.buchungsblattSearchOptions,
767 limit=self.limit,
768 withEigentuemer=False,
769 withBuchung=False,
770 withHistorySearch=bool(p.wantHistorySearch),
771 withHistoryDisplay=bool(p.wantHistoryDisplay),
772 displayThemes=p.displayThemes or [],
773 )
775 want_eigentuemer = (
776 p.wantEigentuemer
777 or dt.DisplayTheme.eigentuemer in options.displayThemes
778 or any(getattr(query, f) is not None for f in dt.EigentuemerAccessRequired)
779 )
780 if want_eigentuemer:
781 self._check_eigentuemer_access(req, p.eigentuemerControlInput or '')
782 options.withEigentuemer = True
784 want_buchung = dt.DisplayTheme.buchung in options.displayThemes or any(getattr(query, f) is not None for f in dt.BuchungAccessRequired)
785 if want_buchung:
786 self._check_buchung_access(req, p.eigentuemerControlInput or '')
787 options.withBuchung = True
789 # "eigentuemer" implies "buchung"
790 if want_eigentuemer and not want_buchung:
791 options.withBuchung = True
792 options.displayThemes.append(dt.DisplayTheme.buchung)
794 query.options = options
795 return query
797 def _prepare_adresse_query(self, req: gws.WebRequester, p: FindAdresseRequest) -> dt.AdresseQuery:
798 query = dt.AdresseQuery()
800 for f in self.ADRESSE_QUERY_FIELDS:
801 setattr(query, f, getattr(p, f, None))
803 if p.combinedAdresseCode:
804 self._query_combined_code(query, p.combinedAdresseCode, self.COMBINED_ADRESSE_FIELDS)
806 options = dt.AdresseQueryOptions(
807 strasseSearchOptions=self.strasseSearchOptions,
808 withHistorySearch=bool(p.wantHistorySearch),
809 )
811 query.options = options
812 return query
814 def _query_fsnummer(self, query: dt.FlurstueckQuery, vn: str):
815 if vn.startswith('DE'):
816 # search by gml_id
817 query.uids = query.uids or []
818 query.uids.append(vn)
819 return
821 if re.match('^[0-9_]+$', vn) and len(vn) >= 14:
822 # search by fs kennzeichen
823 # the length must be at least 2+4+3+5
824 # (see gdi6, AX_Flurstueck_Kerndaten.flurstueckskennzeichen)
825 query.flurstueckskennzeichen = vn
826 return
828 # search by a compound fs number
829 parts = index.parse_fsnummer(vn)
830 if parts is None:
831 raise gws.BadRequestError(f'invalid fsnummer {vn!r}')
832 query.update(parts)
834 def _query_combined_code(self, query: dt.FlurstueckQuery | dt.AdresseQuery, code_value: str, code_fields: list[str]):
835 for val, field in zip(code_value.split('_'), code_fields):
836 if val and val != '0':
837 setattr(query, field, val)
839 ##
841 def _check_eigentuemer_access(self, req: gws.WebRequester, control_input: str):
842 if not req.user.can_read(self.eigentuemer):
843 raise gws.ForbiddenError('cannot read eigentuemer')
844 if self.eigentuemer.controlMode and not self._check_eigentuemer_control_input(control_input):
845 self._log_eigentuemer_access(req, is_ok=False, control_input=control_input)
846 raise gws.ForbiddenError('eigentuemer control input failed')
848 def _check_buchung_access(self, req: gws.WebRequester, control_input: str):
849 if not req.user.can_read(self.buchung):
850 raise gws.ForbiddenError('cannot read buchung')
852 def _log_eigentuemer_access(self, req: gws.WebRequester, control_input: str, is_ok: bool, total=None, fs_uids=None):
853 if self.eigentuemer.logTable is None:
854 return
856 data = dict(
857 app_name='gws',
858 date_time=gws.lib.datetimex.now(),
859 ip=req.ip,
860 login=req.user.uid,
861 user_name=req.user.displayName,
862 control_input=(control_input or '').strip(),
863 control_result=1 if is_ok else 0,
864 fs_count=total or 0,
865 fs_ids=','.join(fs_uids or []),
866 )
868 with self.ix.db.connect() as conn:
869 conn.execute(sa.insert(self.eigentuemer.logTable).values([data]))
870 conn.commit()
872 gws.log.debug(f'alkis: _log_eigentuemer_access {is_ok=}')
874 def _check_eigentuemer_control_input(self, control_input):
875 if not self.eigentuemer.controlRules:
876 return True
878 control_input = (control_input or '').strip()
880 for rule in self.eigentuemer.controlRules:
881 if re.search(rule, control_input):
882 return True
884 return False