Coverage for gws-app/gws/plugin/alkis/action.py: 0%
474 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1"""Backend for the Flurstückssuche (cadaster parlcels search) form."""
3from typing import Optional
5import os
6import re
8import gws
9import gws.base.action
10import gws.base.database
11import gws.base.feature
12import gws.base.model
13import gws.base.printer
14import gws.base.shape
15import gws.base.storage
16import gws.base.template
17import gws.base.web
18import gws.config.util
19import gws.lib.datetimex
20import gws.lib.sa as sa
21import gws.lib.style
23from .data import index, export
24from .data import types as dt
26gws.ext.new.action('alkis')
29class EigentuemerConfig(gws.ConfigWithAccess):
30 """Access to the Eigentümer (owner) information"""
32 controlMode: bool = False
33 """restricted mode enabled"""
34 controlRules: Optional[list[str]]
35 """regular expression for the restricted input control"""
36 logTable: str = ''
37 """data access protocol table name"""
40class EigentuemerOptions(gws.Node):
41 controlMode: bool
42 controlRules: list[str]
43 logTableName: str
44 logTable: Optional[sa.Table]
46 def configure(self):
47 self.controlMode = self.cfg('controlMode')
48 self.controlRules = self.cfg('controlRules', default=[])
49 self.logTableName = self.cfg('logTable')
50 self.logTable = None
53class BuchungConfig(gws.ConfigWithAccess):
54 """Access to the Grundbuch (register) information"""
55 pass
58class BuchungOptions(gws.Node):
59 pass
62class GemarkungListMode(gws.Enum):
63 none = 'none'
64 """do not show the list"""
65 plain = 'plain'
66 """only "gemarkung"""
67 combined = 'combined'
68 """"gemarkung (gemeinde)"""
69 tree = 'tree'
70 """a tree with level 1 = gemeinde and level 2 = gemarkung """
73class StrasseListMode(gws.Enum):
74 plain = 'plain'
75 """just strasse"""
76 withGemeinde = 'withGemeinde'
77 """strasse (gemeinde)"""
78 withGemarkung = 'withGemarkung'
79 """strasse (gemarkung)"""
80 withGemeindeIfRepeated = 'withGemeindeIfRepeated'
81 """strasse (gemeinde), when needed for disambiguation """
82 withGemarkungIfRepeated = 'withGemarkungIfRepeated'
83 """strasse (gemarkung), when needed for disambiguation """
86class Ui(gws.Config):
87 """Flurstückssuche UI configuration."""
89 useExport: bool = False
90 """export function enabled"""
91 useSelect: bool = False
92 """select mode enabled"""
93 usePick: bool = False
94 """pick mode enabled"""
95 useHistory: bool = False
96 """history controls enabled"""
97 searchSelection: bool = False
98 """search in selection enabled"""
99 searchSpatial: bool = False
100 """spatial search enabled"""
101 gemarkungListMode: GemarkungListMode = 'combined'
102 """gemarkung list mode"""
103 strasseListMode: StrasseListMode = 'plain'
104 """strasse list entry format"""
105 autoSpatialSearch: bool = False
106 """activate spatial search after submit"""
109class Config(gws.ConfigWithAccess):
110 """Flurstückssuche action"""
112 dbUid: str = ''
113 """database provider ID"""
114 crs: gws.CrsName
115 """CRS for the ALKIS data"""
116 dataSchema: str = 'public'
117 """schema where ALKIS tables are stored"""
118 indexSchema: str = 'gws8'
119 """schema to store GWS internal indexes"""
120 excludeGemarkung: Optional[list[str]]
121 """Gemarkung (Administrative Unit) IDs to exclude from search"""
123 eigentuemer: Optional[EigentuemerConfig]
124 """access to the Eigentümer (owner) information"""
125 buchung: Optional[BuchungConfig]
126 """access to the Grundbuch (register) information"""
127 limit: int = 100
128 """search results limit"""
129 templates: Optional[list[gws.ext.config.template]]
130 """templates for Flurstueck details"""
131 printers: Optional[list[gws.base.printer.Config]]
132 """print configurations"""
133 ui: Optional[Ui] = {}
134 """ui options"""
136 strasseSearchOptions: Optional[gws.TextSearchOptions]
137 nameSearchOptions: Optional[gws.TextSearchOptions]
138 buchungsblattSearchOptions: Optional[gws.TextSearchOptions]
140 storage: Optional[gws.base.storage.Config]
141 """storage configuration"""
143 export: Optional[export.Config]
144 """csv export configuration"""
147##
149class ExportGroupProps(gws.Props):
150 index: int
151 title: str
154class Props(gws.base.action.Props):
155 exportGroups: list[ExportGroupProps]
156 limit: int
157 printer: Optional[gws.base.printer.Props]
158 ui: Ui
159 storage: Optional[gws.base.storage.Props]
160 strasseSearchOptions: Optional[gws.TextSearchOptions]
161 nameSearchOptions: Optional[gws.TextSearchOptions]
162 buchungsblattSearchOptions: Optional[gws.TextSearchOptions]
163 withBuchung: bool
164 withEigentuemer: bool
165 withEigentuemerControl: bool
166 withFlurnummer: bool
169##
171class GetToponymsRequest(gws.Request):
172 pass
175class GetToponymsResponse(gws.Response):
176 gemeinde: list[list[str]]
177 gemarkung: list[list[str]]
178 strasse: list[list[str]]
181class FindFlurstueckRequest(gws.Request):
182 flurnummer: Optional[str]
183 flurstuecksfolge: Optional[str]
184 zaehler: Optional[str]
185 nenner: Optional[str]
186 fsnummer: Optional[str]
188 flaecheBis: Optional[float]
189 flaecheVon: Optional[float]
191 gemarkung: Optional[str]
192 gemarkungCode: Optional[str]
193 gemeinde: Optional[str]
194 gemeindeCode: Optional[str]
195 kreis: Optional[str]
196 kreisCode: Optional[str]
197 land: Optional[str]
198 landCode: Optional[str]
199 regierungsbezirk: Optional[str]
200 regierungsbezirkCode: Optional[str]
202 strasse: Optional[str]
203 hausnummer: Optional[str]
205 bblatt: Optional[str]
207 personName: Optional[str]
208 personVorname: Optional[str]
210 combinedFlurstueckCode: Optional[str]
212 shapes: Optional[list[gws.base.shape.Props]]
214 uids: Optional[list[str]]
216 crs: Optional[gws.CrsName]
217 eigentuemerControlInput: Optional[str]
218 limit: Optional[int]
220 wantEigentuemer: Optional[bool]
221 wantHistorySearch: Optional[bool]
222 wantHistoryDisplay: Optional[bool]
224 displayThemes: Optional[list[dt.DisplayTheme]]
227class FindFlurstueckResponse(gws.Response):
228 features: list[gws.FeatureProps]
229 total: int
232class FindFlurstueckResult(gws.Data):
233 flurstueckList: list[dt.Flurstueck]
234 total: int
235 query: dt.FlurstueckQuery
238class FindAdresseRequest(gws.Request):
239 crs: Optional[gws.Crs]
241 gemarkung: Optional[str]
242 gemarkungCode: Optional[str]
243 gemeinde: Optional[str]
244 gemeindeCode: Optional[str]
245 kreis: Optional[str]
246 kreisCode: Optional[str]
247 land: Optional[str]
248 landCode: Optional[str]
249 regierungsbezirk: Optional[str]
250 regierungsbezirkCode: Optional[str]
252 strasse: Optional[str]
253 hausnummer: Optional[str]
254 bisHausnummer: Optional[str]
255 hausnummerNotNull: Optional[bool]
257 wantHistorySearch: Optional[bool]
259 combinedAdresseCode: Optional[str]
262class FindAdresseResponse(gws.Response):
263 features: list[gws.FeatureProps]
264 total: int
267class PrintFlurstueckRequest(gws.Request):
268 findRequest: FindFlurstueckRequest
269 printRequest: gws.PrintRequest
270 featureStyle: gws.StyleProps
273class ExportFlurstueckRequest(gws.Request):
274 findRequest: FindFlurstueckRequest
275 groupIndexes: list[int]
278class ExportFlurstueckResponse(gws.Response):
279 content: str
280 mime: str
283##
286_dir = os.path.dirname(__file__)
288_DEFAULT_TEMPLATES = [
289 gws.Config(
290 subject='flurstueck.title',
291 type='html',
292 path=f'{_dir}/templates/title.cx.html',
293 ),
294 gws.Config(
295 subject='flurstueck.teaser',
296 type='html',
297 path=f'{_dir}/templates/title.cx.html',
298 ),
299 gws.Config(
300 subject='flurstueck.label',
301 type='html',
302 path=f'{_dir}/templates/title.cx.html',
303 ),
304 gws.Config(
305 subject='flurstueck.description',
306 type='html',
307 path=f'{_dir}/templates/description.cx.html',
308 ),
309 gws.Config(
310 subject='adresse.title',
311 type='html',
312 path=f'{_dir}/templates/adresse_title.cx.html',
313 ),
314 gws.Config(
315 subject='adresse.teaser',
316 type='html',
317 path=f'{_dir}/templates/adresse_title.cx.html',
318 ),
319 gws.Config(
320 subject='adresse.label',
321 type='html',
322 path=f'{_dir}/templates/adresse_title.cx.html',
323 ),
324]
326_DEFAULT_PRINTER = gws.Config(
327 uid='gws.plugin.alkis.default_printer',
328 template=gws.Config(
329 type='html',
330 path=f'{_dir}/templates/print.cx.html',
331 ),
332 qualityLevels=[{'dpi': 72}],
333)
336##
338class Model(gws.base.model.default_model.Object):
339 def configure(self):
340 self.uidName = 'uid'
341 self.geometryName = 'geometry'
342 self.loadingStrategy = gws.FeatureLoadingStrategy.all
345class Object(gws.base.action.Object):
346 db: gws.DatabaseProvider
348 ix: index.Object
349 ixStatus: index.Status
351 buchung: BuchungOptions
352 eigentuemer: EigentuemerOptions
354 dataSchema: str
356 model: gws.Model
357 ui: Ui
358 limit: int
360 templates: list[gws.Template]
361 printers: list[gws.Printer]
363 export: Optional[export.Object]
365 strasseSearchOptions: gws.TextSearchOptions
366 nameSearchOptions: gws.TextSearchOptions
367 buchungsblattSearchOptions: gws.TextSearchOptions
369 storage: Optional[gws.base.storage.Object]
371 def configure(self):
372 gws.config.util.configure_database_provider_for(self, ext_type='postgres')
373 self.ix = self.root.create_shared(
374 index.Object,
375 _defaultDb=self.db,
376 crs=self.cfg('crs'),
377 schema=self.cfg('indexSchema'),
378 excludeGemarkung=self.cfg('excludeGemarkung'),
379 uid='gws.plugin.alkis.data.index.' + self.cfg('indexSchema')
380 )
382 self.limit = self.cfg('limit')
383 self.model = self.create_child(Model)
384 self.ui = self.cfg('ui')
386 self.dataSchema = self.cfg('dataSchema')
388 p = self.cfg('templates', default=[]) + _DEFAULT_TEMPLATES
389 self.templates = [self.create_child(gws.ext.object.template, c) for c in p]
391 self.printers = self.create_children(gws.ext.object.printer, self.cfg('printers'))
392 self.printers.append(self.root.create_shared(gws.ext.object.printer, _DEFAULT_PRINTER))
394 d = gws.TextSearchOptions(type='begin', caseSensitive=False)
395 self.strasseSearchOptions = self.cfg('strasseSearchOptions', default=d)
396 self.nameSearchOptions = self.cfg('nameSearchOptions', default=d)
397 self.buchungsblattSearchOptions = self.cfg('buchungsblattSearchOptions', default=d)
399 deny_all = gws.Config(access='deny all')
401 self.buchung = self.create_child(BuchungOptions, self.cfg('buchung', default=deny_all))
403 self.eigentuemer = self.create_child(EigentuemerOptions, self.cfg('eigentuemer', default=deny_all))
404 if self.eigentuemer.logTableName:
405 self.eigentuemer.logTable = self.ix.db.table(self.eigentuemer.logTableName)
407 self.storage = self.create_child_if_configured(
408 gws.base.storage.Object, self.cfg('storage'), categoryName='Alkis')
410 p = self.cfg('export')
411 if p:
412 self.export = self.create_child(export.Object, p)
413 elif self.ui.useExport:
414 self.export = self.create_child(export.Object)
415 else:
416 self.export = None
418 def activate(self):
419 def _load():
420 return self.ix.status()
421 self.ixStatus = gws.u.get_server_global('gws.plugin.alkis.action.ixStatus', _load)
423 def props(self, user):
424 if not self.ixStatus.basic:
425 return None
427 export_groups = []
428 if self.ui.useExport:
429 export_groups = [ExportGroupProps(title=g.title, index=g.index) for g in self._export_groups(user)]
431 ps = gws.u.merge(
432 super().props(user),
433 exportGroups=export_groups,
434 limit=self.limit,
435 printer=gws.u.first(p for p in self.printers if user.can_use(p)),
436 ui=self.ui,
437 storage=self.storage,
438 withBuchung=(
439 self.ixStatus.buchung
440 and user.can_read(self.buchung)
441 ),
442 withEigentuemer=(
443 self.ixStatus.eigentuemer
444 and user.can_read(self.eigentuemer)
445 ),
446 withEigentuemerControl=(
447 self.ixStatus.eigentuemer
448 and user.can_read(self.eigentuemer)
449 and self.eigentuemer.controlMode
450 ),
451 )
453 ps.strasseSearchOptions = self.strasseSearchOptions
454 if ps.withBuchung:
455 ps.buchungsblattSearchOptions = self.buchungsblattSearchOptions
456 if ps.withEigentuemer:
457 ps.nameSearchOptions = self.nameSearchOptions
459 return ps
461 @gws.ext.command.api('alkisGetToponyms')
462 def get_toponyms(self, req: gws.WebRequester, p: GetToponymsRequest) -> GetToponymsResponse:
463 """Return all Toponyms (Gemeinde/Gemarkung/Strasse) in the area"""
465 req.user.require_project(p.projectUid)
467 gemeinde_dct = {}
468 gemarkung_dct = {}
469 strasse_lst = []
471 for s in self.ix.strasse_list():
472 gemeinde_dct[s.gemeinde.code] = [s.gemeinde.text, s.gemeinde.code]
473 gemarkung_dct[s.gemarkung.code] = [s.gemarkung.text, s.gemarkung.code, s.gemeinde.code]
474 strasse_lst.append([s.name, s.gemeinde.code, s.gemarkung.code])
476 return GetToponymsResponse(
477 gemeinde=sorted(gemeinde_dct.values()),
478 gemarkung=sorted(gemarkung_dct.values()),
479 strasse=sorted(strasse_lst)
480 )
482 @gws.ext.command.api('alkisFindAdresse')
483 def find_adresse(self, req: gws.WebRequester, p: FindAdresseRequest) -> FindAdresseResponse:
484 """Perform an Adresse search."""
486 project = req.user.require_project(p.projectUid)
487 crs = p.get('crs') or project.map.bounds.crs
489 ad_list, query = self.find_adresse_objects(req, p)
490 if not ad_list:
491 return FindAdresseResponse(
492 features=[],
493 total=0,
494 )
496 templates = [
497 self.root.app.templateMgr.find_template('adresse.title', where=[self], user=req.user),
498 self.root.app.templateMgr.find_template('adresse.teaser', where=[self], user=req.user),
499 self.root.app.templateMgr.find_template('adresse.label', where=[self], user=req.user),
500 ]
502 fprops = []
503 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user)
505 for ad in ad_list:
506 f = gws.base.feature.new(model=self.model)
507 f.attributes = vars(ad)
508 f.attributes['geometry'] = f.attributes.pop('shape')
509 f.transform_to(crs)
510 f.render_views(templates, user=req.user)
511 fprops.append(self.model.feature_to_view_props(f, mc))
513 return FindAdresseResponse(
514 features=fprops,
515 total=len(ad_list),
516 )
518 @gws.ext.command.api('alkisFindFlurstueck')
519 def find_flurstueck(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> FindFlurstueckResponse:
520 """Perform a Flurstueck search"""
522 project = req.user.require_project(p.projectUid)
523 crs = p.get('crs') or project.map.bounds.crs
525 fs_list, query = self.find_flurstueck_objects(req, p)
526 if not fs_list:
527 return FindFlurstueckResponse(
528 features=[],
529 total=0,
530 )
532 templates = [
533 self.root.app.templateMgr.find_template('flurstueck.title', where=[self], user=req.user),
534 self.root.app.templateMgr.find_template('flurstueck.teaser', where=[self], user=req.user),
535 ]
537 if query.options.displayThemes:
538 templates.append(
539 self.root.app.templateMgr.find_template('flurstueck.description', where=[self], user=req.user),
540 )
542 args = dict(
543 withHistory=query.options.withHistoryDisplay,
544 withDebug=bool(self.root.app.developer_option('alkis.debug_templates')),
545 )
547 ps = []
548 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user)
550 for fs in fs_list:
551 f = gws.base.feature.new(model=self.model)
552 f.attributes = dict(uid=fs.uid, fs=fs, geometry=fs.shape)
553 f.transform_to(crs)
554 f.render_views(templates, user=req.user, **args)
555 f.attributes.pop('fs')
556 ps.append(self.model.feature_to_view_props(f, mc))
558 return FindFlurstueckResponse(
559 features=sorted(ps, key=lambda p: p.views['title']),
560 total=len(fs_list),
561 )
563 @gws.ext.command.api('alkisExportFlurstueck')
564 def export_flurstueck(self, req: gws.WebRequester, p: ExportFlurstueckRequest) -> ExportFlurstueckResponse:
565 if not self.export:
566 raise gws.NotFoundError()
568 groups = [g for g in self._export_groups(req.user) if g.index in p.groupIndexes]
569 if not groups:
570 raise gws.NotFoundError()
572 find_request = p.findRequest
573 find_request.projectUid = p.projectUid
575 fs_list, _ = self.find_flurstueck_objects(req, find_request)
576 if not fs_list:
577 raise gws.NotFoundError()
579 csv_bytes = self.export.export_as_csv(fs_list, groups, req.user)
581 return ExportFlurstueckResponse(content=csv_bytes, mime='text/csv')
583 @gws.ext.command.api('alkisPrintFlurstueck')
584 def print_flurstueck(self, req: gws.WebRequester, p: PrintFlurstueckRequest) -> gws.PrintJobResponse:
585 """Print Flurstueck features"""
587 project = req.user.require_project(p.projectUid)
589 find_request = p.findRequest
590 find_request.projectUid = p.projectUid
592 fs_list, query = self.find_flurstueck_objects(req, find_request)
593 if not fs_list:
594 raise gws.NotFoundError()
596 print_request = p.printRequest
597 print_request.projectUid = p.projectUid
598 crs = print_request.get('crs') or project.map.bounds.crs
600 templates = [
601 self.root.app.templateMgr.find_template('flurstueck.label', where=[self], user=req.user),
602 ]
604 base_map = print_request.maps[0]
605 fs_maps = []
607 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user)
609 for fs in fs_list:
610 f = gws.base.feature.new(model=self.model)
611 f.attributes = dict(uid=fs.uid, fs=fs, geometry=fs.shape)
612 f.transform_to(crs)
613 f.render_views(templates, user=req.user)
614 f.cssSelector = p.featureStyle.cssSelector
616 c = f.shape().centroid()
617 fs_map = gws.PrintMap(base_map)
618 # @TODO scale to fit the fs?
619 fs_map.center = (c.x, c.y)
620 fs_plane = gws.PrintPlane(
621 type=gws.PrintPlaneType.features,
622 features=[self.model.feature_to_view_props(f, mc)],
623 )
624 fs_map.planes = [fs_plane] + base_map.planes
625 fs_maps.append(fs_map)
627 print_request.maps = fs_maps
628 print_request.args = dict(
629 flurstueckList=fs_list,
630 withHistory=query.options.withHistoryDisplay,
631 withDebug=bool(self.root.app.developer_option('alkis.debug_templates')),
632 )
634 job = self.root.app.printerMgr.start_job(print_request, req.user)
635 return self.root.app.printerMgr.status(job)
637 @gws.ext.command.api('alkisSelectionStorage')
638 def handle_storage(self, req: gws.WebRequester, p: gws.base.storage.Request) -> gws.base.storage.Response:
639 if not self.storage:
640 raise gws.NotFoundError('no storage configured')
641 return self.storage.handle_request(req, p)
643 ##
645 def find_flurstueck_objects(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> tuple[list[dt.Flurstueck], dt.FlurstueckQuery]:
646 query = self._prepare_flurstueck_query(req, p)
647 fs_list = self.ix.find_flurstueck(query)
649 if query.options.withEigentuemer:
650 self._log_eigentuemer_access(
651 req,
652 p.eigentuemerControlInput,
653 is_ok=True,
654 total=len(fs_list),
655 fs_uids=[fs.uid for fs in fs_list]
656 )
658 return fs_list, query
660 def find_adresse_objects(self, req: gws.WebRequester, p: FindAdresseRequest) -> tuple[list[dt.Adresse], dt.AdresseQuery]:
661 query = self._prepare_adresse_query(req, p)
662 ad_list = self.ix.find_adresse(query)
663 return ad_list, query
665 FLURSTUECK_QUERY_FIELDS = [
666 'flurnummer',
667 'flurstuecksfolge',
668 'zaehler',
669 'nenner',
670 'flurstueckskennzeichen',
671 'flaecheBis',
672 'flaecheVon',
673 'gemarkung',
674 'gemarkungCode',
675 'gemeinde',
676 'gemeindeCode',
677 'kreis',
678 'kreisCode',
679 'land',
680 'landCode',
681 'regierungsbezirk',
682 'regierungsbezirkCode',
683 'strasse',
684 'hausnummer',
685 'personName',
686 'personVorname',
687 'uids',
688 ]
689 ADRESSE_QUERY_FIELDS = [
690 'gemarkung',
691 'gemarkungCode',
692 'gemeinde',
693 'gemeindeCode',
694 'kreis',
695 'kreisCode',
696 'land',
697 'landCode',
698 'regierungsbezirk',
699 'regierungsbezirkCode',
700 'strasse',
701 'hausnummer',
702 'bisHausnummer',
703 'hausnummerNotNull',
704 ]
705 COMBINED_FLURSTUECK_FIELDS = [
706 'landCode', 'gemarkungCode', 'flurnummer', 'zaehler', 'nenner', 'flurstuecksfolge'
707 ]
708 COMBINED_ADRESSE_FIELDS = [
709 'strasse', 'hausnummer', 'plz', 'gemeinde', 'bisHausnummer'
710 ]
712 def _prepare_flurstueck_query(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> dt.FlurstueckQuery:
713 query = dt.FlurstueckQuery()
715 for f in self.FLURSTUECK_QUERY_FIELDS:
716 setattr(query, f, getattr(p, f, None))
718 if p.combinedFlurstueckCode:
719 self._query_combined_code(query, p.combinedFlurstueckCode, self.COMBINED_FLURSTUECK_FIELDS)
721 if p.fsnummer:
722 self._query_fsnummer(query, p.fsnummer)
724 if p.shapes:
725 shapes = [gws.base.shape.from_props(s) for s in p.shapes]
726 query.shape = shapes[0] if len(shapes) == 1 else shapes[0].union(shapes[1:])
728 if p.bblatt:
729 query.buchungsblattkennzeichenList = p.bblatt.replace(';', ' ').replace(',', ' ').strip().split()
731 options = dt.FlurstueckQueryOptions(
732 strasseSearchOptions=self.strasseSearchOptions,
733 nameSearchOptions=self.nameSearchOptions,
734 buchungsblattSearchOptions=self.buchungsblattSearchOptions,
735 hardLimit=self.limit,
736 withEigentuemer=False,
737 withBuchung=False,
738 withHistorySearch=bool(p.wantHistorySearch),
739 withHistoryDisplay=bool(p.wantHistoryDisplay),
740 displayThemes=p.displayThemes or [],
741 )
743 want_eigentuemer = (
744 p.wantEigentuemer
745 or dt.DisplayTheme.eigentuemer in options.displayThemes
746 or any(getattr(query, f) is not None for f in dt.EigentuemerAccessRequired)
747 )
748 if want_eigentuemer:
749 self._check_eigentuemer_access(req, p.eigentuemerControlInput)
750 options.withEigentuemer = True
752 want_buchung = (
753 dt.DisplayTheme.buchung in options.displayThemes
754 or any(getattr(query, f) is not None for f in dt.BuchungAccessRequired)
755 )
756 if want_buchung:
757 self._check_buchung_access(req, p.eigentuemerControlInput)
758 options.withBuchung = True
760 # "eigentuemer" implies "buchung"
761 if want_eigentuemer and not want_buchung:
762 options.withBuchung = True
763 options.displayThemes.append(dt.DisplayTheme.buchung)
765 query.options = options
766 return query
768 def _prepare_adresse_query(self, req: gws.WebRequester, p: FindAdresseRequest) -> dt.AdresseQuery:
769 query = dt.AdresseQuery()
771 for f in self.ADRESSE_QUERY_FIELDS:
772 setattr(query, f, getattr(p, f, None))
774 if p.combinedAdresseCode:
775 self._query_combined_code(query, p.combinedAdresseCode, self.COMBINED_ADRESSE_FIELDS)
777 options = dt.AdresseQueryOptions(
778 strasseSearchOptions=self.strasseSearchOptions,
779 withHistorySearch=bool(p.wantHistorySearch),
780 )
782 query.options = options
783 return query
785 def _query_fsnummer(self, query: dt.FlurstueckQuery, vn: str):
787 if vn.startswith('DE'):
788 # search by gml_id
789 query.uids = query.uids or []
790 query.uids.append(vn)
791 return
793 if re.match('^[0-9_]+$', vn) and len(vn) >= 14:
794 # search by fs kennzeichen
795 # the length must be at least 2+4+3+5
796 # (see gdi6, AX_Flurstueck_Kerndaten.flurstueckskennzeichen)
797 query.flurstueckskennzeichen = vn
798 return
800 # search by a compound fs number
801 parts = index.parse_fsnummer(vn)
802 if parts is None:
803 raise gws.BadRequestError(f'invalid fsnummer {vn!r}')
804 query.update(parts)
806 def _query_combined_code(self, query: dt.FlurstueckQuery | dt.AdresseQuery, code_value: str, code_fields: list[str]):
807 for val, field in zip(code_value.split('_'), code_fields):
808 if val and val != '0':
809 setattr(query, field, val)
811 ##
813 def _export_groups(self, user):
814 if not self.export:
815 return []
817 groups = []
819 for g in self.export.groups:
820 if g.withBuchung and not user.can_read(self.buchung):
821 continue
822 if g.withEigentuemer and not user.can_read(self.eigentuemer):
823 continue
824 groups.append(g)
826 return groups
828 def _check_eigentuemer_access(self, req: gws.WebRequester, control_input: str):
829 if not req.user.can_read(self.eigentuemer):
830 raise gws.ForbiddenError('cannot read eigentuemer')
831 if self.eigentuemer.controlMode and not self._check_eigentuemer_control_input(control_input):
832 self._log_eigentuemer_access(req, is_ok=False, control_input=control_input)
833 raise gws.ForbiddenError('eigentuemer control input failed')
835 def _check_buchung_access(self, req: gws.WebRequester, control_input: str):
836 if not req.user.can_read(self.buchung):
837 raise gws.ForbiddenError('cannot read buchung')
839 def _log_eigentuemer_access(self, req: gws.WebRequester, control_input: str, is_ok: bool, total=None, fs_uids=None):
840 if self.eigentuemer.logTable is None:
841 return
843 data = dict(
844 app_name='gws',
845 date_time=gws.lib.datetimex.now(),
846 ip=req.env('REMOTE_ADDR', ''),
847 login=req.user.uid,
848 user_name=req.user.displayName,
849 control_input=(control_input or '').strip(),
850 control_result=1 if is_ok else 0,
851 fs_count=total or 0,
852 fs_ids=','.join(fs_uids or []),
853 )
855 with self.ix.db.connect() as conn:
856 conn.execute(sa.insert(self.eigentuemer.logTable).values([data]))
857 conn.commit()
859 gws.log.debug(f'_log_eigentuemer_access {is_ok=}')
861 def _check_eigentuemer_control_input(self, control_input):
862 if not self.eigentuemer.controlRules:
863 return True
865 control_input = (control_input or '').strip()
867 for rule in self.eigentuemer.controlRules:
868 if re.search(rule, control_input):
869 return True
871 return False