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

1"""Backend for the Flurstückssuche (cadaster parlcels search) form.""" 

2 

3from typing import Optional, cast 

4 

5import os 

6import re 

7 

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 

18 

19from .data import index, exporter 

20from .data import types as dt 

21 

22gws.ext.new.action('alkis') 

23 

24 

25class EigentuemerConfig(gws.ConfigWithAccess): 

26 """Access to the Eigentümer (owner) information""" 

27 

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.""" 

34 

35 

36class EigentuemerOptions(gws.Node): 

37 controlMode: bool 

38 controlRules: list[str] 

39 logTableName: str 

40 logTable: Optional[sa.Table] 

41 

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 

47 

48 

49class BuchungConfig(gws.ConfigWithAccess): 

50 """Access to the Grundbuch (register) information""" 

51 

52 pass 

53 

54 

55class BuchungOptions(gws.Node): 

56 pass 

57 

58 

59class GemarkungListMode(gws.Enum): 

60 """Gemarkung (Administrative Unit) list mode.""" 

61 

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.""" 

70 

71 

72class StrasseListMode(gws.Enum): 

73 """Strasse (street) list entry format.""" 

74 

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.""" 

85 

86 

87class Ui(gws.Config): 

88 """Flurstückssuche UI configuration.""" 

89 

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.""" 

108 

109 

110class Config(gws.ConfigWithAccess): 

111 """Flurstückssuche action configuration.""" 

112 

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.""" 

125 

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.""" 

138 

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.""" 

145 

146 storage: Optional[gws.base.storage.Config] 

147 """Storage configuration.""" 

148 

149 exporters: Optional[list[exporter.Config]] 

150 """Export configurations.""" 

151 

152 

153## 

154 

155 

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 

169 

170 

171## 

172 

173 

174class GetToponymsRequest(gws.Request): 

175 pass 

176 

177 

178class GetToponymsResponse(gws.Response): 

179 gemeinde: list[list[str]] 

180 gemarkung: list[list[str]] 

181 strasse: list[list[str]] 

182 

183 

184class FindFlurstueckRequest(gws.Request): 

185 flurnummer: Optional[str] 

186 flurstuecksfolge: Optional[str] 

187 zaehler: Optional[str] 

188 nenner: Optional[str] 

189 fsnummer: Optional[str] 

190 

191 flaecheBis: Optional[float] 

192 flaecheVon: Optional[float] 

193 

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] 

204 

205 strasse: Optional[str] 

206 hausnummer: Optional[str] 

207 

208 bblatt: Optional[str] 

209 

210 personName: Optional[str] 

211 personVorname: Optional[str] 

212 

213 combinedFlurstueckCode: Optional[str] 

214 

215 shapes: Optional[list[gws.base.shape.Props]] 

216 

217 uids: Optional[list[str]] 

218 

219 crs: Optional[gws.CrsName] 

220 eigentuemerControlInput: Optional[str] 

221 limit: Optional[int] 

222 

223 wantEigentuemer: Optional[bool] 

224 wantHistorySearch: Optional[bool] 

225 wantHistoryDisplay: Optional[bool] 

226 

227 displayThemes: Optional[list[dt.DisplayTheme]] 

228 

229 

230class FindFlurstueckResponse(gws.Response): 

231 features: list[gws.FeatureProps] 

232 total: int 

233 

234 

235class FindFlurstueckResult(gws.Data): 

236 flurstueckList: list[dt.Flurstueck] 

237 total: int 

238 query: dt.FlurstueckQuery 

239 

240 

241class FindAdresseRequest(gws.Request): 

242 crs: Optional[gws.CrsName] 

243 

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] 

254 

255 strasse: Optional[str] 

256 hausnummer: Optional[str] 

257 bisHausnummer: Optional[str] 

258 hausnummerNotNull: Optional[bool] 

259 

260 wantHistorySearch: Optional[bool] 

261 

262 combinedAdresseCode: Optional[str] 

263 

264 

265class FindAdresseResponse(gws.Response): 

266 features: list[gws.FeatureProps] 

267 total: int 

268 

269 

270class PrintFlurstueckRequest(gws.Request): 

271 findRequest: FindFlurstueckRequest 

272 printRequest: gws.PrintRequest 

273 featureStyle: gws.StyleProps 

274 

275 

276class ExportFlurstueckRequest(gws.Request): 

277 findRequest: FindFlurstueckRequest 

278 exporterUid: str 

279 eigentuemerControlInput: Optional[str] 

280 

281 

282class ExportFlurstueckResponse(gws.Response): 

283 content: str 

284 mime: str 

285 

286 

287## 

288 

289 

290_dir = os.path.dirname(__file__) 

291 

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] 

329 

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) 

338 

339 

340## 

341 

342 

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 

348 

349 

350class Object(gws.base.action.Object): 

351 db: gws.DatabaseProvider 

352 

353 ix: index.Object 

354 ixStatus: dt.IndexStatus 

355 

356 buchung: BuchungOptions 

357 eigentuemer: EigentuemerOptions 

358 

359 dataSchema: str 

360 indexSchema: str 

361 

362 model: gws.Model 

363 ui: Ui 

364 limit: int 

365 

366 templates: list[gws.Template] 

367 printers: list[gws.Printer] 

368 

369 exporters: list[exporter.Object] 

370 

371 strasseSearchOptions: gws.TextSearchOptions 

372 nameSearchOptions: gws.TextSearchOptions 

373 buchungsblattSearchOptions: gws.TextSearchOptions 

374 

375 storage: Optional[gws.base.storage.Object] 

376 

377 def configure(self): 

378 gws.config.util.configure_database_provider_for(self, ext_type='postgres') 

379 

380 self.dataSchema = self.cfg('dataSchema') 

381 self.indexSchema = self.cfg('indexSchema') 

382 

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 ) 

392 

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 ) 

402 

403 p = self.cfg('templates', default=[]) + _DEFAULT_TEMPLATES 

404 self.templates = [self.create_child(gws.ext.object.template, c) for c in p] 

405 

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)) 

408 

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) 

413 

414 deny_all = gws.Config(access='deny all') 

415 

416 self.buchung = self.create_child(BuchungOptions, self.cfg('buchung', default=deny_all)) 

417 

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) 

421 

422 self.storage = self.create_child_if_configured(gws.base.storage.Object, self.cfg('storage'), categoryName='Alkis') 

423 

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)) 

430 

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 

437 

438 self.ixStatus = gws.u.get_server_global(f'gws.plugin.alkis.action.ixStatus.{self.indexSchema}', _load) 

439 

440 def props(self, user): 

441 if not self.ixStatus.basic: 

442 return None 

443 

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 ) 

457 

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]) 

467 

468 ps.strasseSearchOptions = self.strasseSearchOptions 

469 if ps.withBuchung: 

470 ps.buchungsblattSearchOptions = self.buchungsblattSearchOptions 

471 if ps.withEigentuemer: 

472 ps.nameSearchOptions = self.nameSearchOptions 

473 

474 return ps 

475 

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""" 

479 

480 req.user.require_project(p.projectUid) 

481 

482 gemeinde_dct = {} 

483 gemarkung_dct = {} 

484 strasse_lst = [] 

485 

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]) 

490 

491 return GetToponymsResponse( 

492 gemeinde=sorted(gemeinde_dct.values()), 

493 gemarkung=sorted(gemarkung_dct.values()), 

494 strasse=sorted(strasse_lst), 

495 ) 

496 

497 @gws.ext.command.api('alkisFindAdresse') 

498 def find_adresse(self, req: gws.WebRequester, p: FindAdresseRequest) -> FindAdresseResponse: 

499 """Perform an Adresse search.""" 

500 

501 project = req.user.require_project(p.projectUid) 

502 crs = p.get('crs') or project.map.bounds.crs 

503 

504 ad_list, query = self.find_adresse_objects(req, p) 

505 if not ad_list: 

506 return FindAdresseResponse( 

507 features=[], 

508 total=0, 

509 ) 

510 

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 ] 

516 

517 fprops = [] 

518 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user) 

519 

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)) 

527 

528 return FindAdresseResponse( 

529 features=fprops, 

530 total=len(ad_list), 

531 ) 

532 

533 @gws.ext.command.api('alkisFindFlurstueck') 

534 def find_flurstueck(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> FindFlurstueckResponse: 

535 """Perform a Flurstueck search""" 

536 

537 project = req.user.require_project(p.projectUid) 

538 crs = p.get('crs') or project.map.bounds.crs 

539 

540 fs_list, query = self.find_flurstueck_objects(req, p) 

541 if not fs_list: 

542 return FindFlurstueckResponse( 

543 features=[], 

544 total=0, 

545 ) 

546 

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 ] 

551 

552 if query.options.displayThemes: 

553 templates.append( 

554 self.root.app.templateMgr.find_template('flurstueck.description', where=[self], user=req.user), 

555 ) 

556 

557 args = dict( 

558 withHistory=query.options.withHistoryDisplay, 

559 withDebug=bool(self.root.app.developer_option('alkis.debug_templates')), 

560 ) 

561 

562 ps = [] 

563 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user) 

564 

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)) 

572 

573 return FindFlurstueckResponse( 

574 features=sorted(ps, key=lambda p: p.views['title']), 

575 total=len(fs_list), 

576 ) 

577 

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 

582 

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() 

588 

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 '') 

593 

594 find_request = p.findRequest 

595 find_request.projectUid = p.projectUid 

596 

597 fs_list, _ = self.find_flurstueck_objects(req, find_request) 

598 if not fs_list: 

599 raise gws.NotFoundError() 

600 

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) 

607 

608 return ExportFlurstueckResponse( 

609 content=gws.u.read_file_b(args.path), 

610 mime=exp.mimeType, 

611 ) 

612 

613 @gws.ext.command.api('alkisPrintFlurstueck') 

614 def print_flurstueck(self, req: gws.WebRequester, p: PrintFlurstueckRequest) -> gws.JobStatusResponse: 

615 """Print Flurstueck features""" 

616 

617 project = req.user.require_project(p.projectUid) 

618 

619 find_request = p.findRequest 

620 find_request.projectUid = p.projectUid 

621 

622 fs_list, query = self.find_flurstueck_objects(req, find_request) 

623 if not fs_list: 

624 raise gws.NotFoundError() 

625 

626 print_request = p.printRequest 

627 print_request.projectUid = p.projectUid 

628 crs = print_request.get('crs') or project.map.bounds.crs 

629 

630 templates = [ 

631 self.root.app.templateMgr.find_template('flurstueck.label', where=[self], user=req.user), 

632 ] 

633 

634 base_map = print_request.maps[0] 

635 fs_maps = [] 

636 

637 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=req.user) 

638 

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 

645 

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) 

656 

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 ) 

663 

664 return self.root.app.printerMgr.start_print_job(print_request, req.user) 

665 

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) 

671 

672 ## 

673 

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) 

677 

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 ) 

686 

687 return fs_list, query 

688 

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 

693 

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'] 

736 

737 def _prepare_flurstueck_query(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> dt.FlurstueckQuery: 

738 query = dt.FlurstueckQuery() 

739 

740 for f in self.FLURSTUECK_QUERY_FIELDS: 

741 setattr(query, f, getattr(p, f, None)) 

742 

743 if p.combinedFlurstueckCode: 

744 self._query_combined_code(query, p.combinedFlurstueckCode, self.COMBINED_FLURSTUECK_FIELDS) 

745 

746 if p.fsnummer: 

747 self._query_fsnummer(query, p.fsnummer) 

748 

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:]) 

752 

753 if p.bblatt: 

754 query.buchungsblattkennzeichenList = p.bblatt.replace(';', ' ').replace(',', ' ').strip().split() 

755 

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 ) 

767 

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 

776 

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 

781 

782 # "eigentuemer" implies "buchung" 

783 if want_eigentuemer and not want_buchung: 

784 options.withBuchung = True 

785 options.displayThemes.append(dt.DisplayTheme.buchung) 

786 

787 query.options = options 

788 return query 

789 

790 def _prepare_adresse_query(self, req: gws.WebRequester, p: FindAdresseRequest) -> dt.AdresseQuery: 

791 query = dt.AdresseQuery() 

792 

793 for f in self.ADRESSE_QUERY_FIELDS: 

794 setattr(query, f, getattr(p, f, None)) 

795 

796 if p.combinedAdresseCode: 

797 self._query_combined_code(query, p.combinedAdresseCode, self.COMBINED_ADRESSE_FIELDS) 

798 

799 options = dt.AdresseQueryOptions( 

800 strasseSearchOptions=self.strasseSearchOptions, 

801 withHistorySearch=bool(p.wantHistorySearch), 

802 ) 

803 

804 query.options = options 

805 return query 

806 

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 

813 

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 

820 

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) 

826 

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) 

831 

832 ## 

833 

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') 

840 

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') 

844 

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 

848 

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 ) 

860 

861 with self.ix.db.connect() as conn: 

862 conn.execute(sa.insert(self.eigentuemer.logTable).values([data])) 

863 conn.commit() 

864 

865 gws.log.debug(f'alkis: _log_eigentuemer_access {is_ok=}') 

866 

867 def _check_eigentuemer_control_input(self, control_input): 

868 if not self.eigentuemer.controlRules: 

869 return True 

870 

871 control_input = (control_input or '').strip() 

872 

873 for rule in self.eigentuemer.controlRules: 

874 if re.search(rule, control_input): 

875 return True 

876 

877 return False