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

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 return self.ix.status() 

434 

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

436 

437 def props(self, user): 

438 if not self.ixStatus.basic: 

439 return None 

440 

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 ) 

454 

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

464 

465 ps.strasseSearchOptions = self.strasseSearchOptions 

466 if ps.withBuchung: 

467 ps.buchungsblattSearchOptions = self.buchungsblattSearchOptions 

468 if ps.withEigentuemer: 

469 ps.nameSearchOptions = self.nameSearchOptions 

470 

471 return ps 

472 

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

476 

477 req.user.require_project(p.projectUid) 

478 

479 gemeinde_dct = {} 

480 gemarkung_dct = {} 

481 strasse_lst = [] 

482 

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

487 

488 return GetToponymsResponse( 

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

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

491 strasse=sorted(strasse_lst), 

492 ) 

493 

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

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

496 """Perform an Adresse search.""" 

497 

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

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

500 

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

502 if not ad_list: 

503 return FindAdresseResponse( 

504 features=[], 

505 total=0, 

506 ) 

507 

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 ] 

513 

514 fprops = [] 

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

516 

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

524 

525 return FindAdresseResponse( 

526 features=fprops, 

527 total=len(ad_list), 

528 ) 

529 

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

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

532 """Perform a Flurstueck search""" 

533 

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

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

536 

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

538 if not fs_list: 

539 return FindFlurstueckResponse( 

540 features=[], 

541 total=0, 

542 ) 

543 

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 ] 

548 

549 if query.options.displayThemes: 

550 templates.append( 

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

552 ) 

553 

554 args = dict( 

555 withHistory=query.options.withHistoryDisplay, 

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

557 ) 

558 

559 ps = [] 

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

561 

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

569 

570 return FindFlurstueckResponse( 

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

572 total=len(fs_list), 

573 ) 

574 

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 

579 

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

585 

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

590 

591 find_request = p.findRequest 

592 find_request.projectUid = p.projectUid 

593 

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

595 if not fs_list: 

596 raise gws.NotFoundError() 

597 

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) 

604 

605 return ExportFlurstueckResponse( 

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

607 mime=exp.mimeType, 

608 ) 

609 

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

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

612 """Print Flurstueck features""" 

613 

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

615 

616 find_request = p.findRequest 

617 find_request.projectUid = p.projectUid 

618 

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

620 if not fs_list: 

621 raise gws.NotFoundError() 

622 

623 print_request = p.printRequest 

624 print_request.projectUid = p.projectUid 

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

626 

627 templates = [ 

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

629 ] 

630 

631 base_map = print_request.maps[0] 

632 fs_maps = [] 

633 

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

635 

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 

642 

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) 

653 

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 ) 

660 

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

662 

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) 

668 

669 ## 

670 

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) 

674 

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 ) 

683 

684 return fs_list, query 

685 

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 

690 

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

733 

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

735 query = dt.FlurstueckQuery() 

736 

737 for f in self.FLURSTUECK_QUERY_FIELDS: 

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

739 

740 if p.combinedFlurstueckCode: 

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

742 

743 if p.fsnummer: 

744 self._query_fsnummer(query, p.fsnummer) 

745 

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

749 

750 if p.bblatt: 

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

752 

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 ) 

764 

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 

773 

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 

778 

779 # "eigentuemer" implies "buchung" 

780 if want_eigentuemer and not want_buchung: 

781 options.withBuchung = True 

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

783 

784 query.options = options 

785 return query 

786 

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

788 query = dt.AdresseQuery() 

789 

790 for f in self.ADRESSE_QUERY_FIELDS: 

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

792 

793 if p.combinedAdresseCode: 

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

795 

796 options = dt.AdresseQueryOptions( 

797 strasseSearchOptions=self.strasseSearchOptions, 

798 withHistorySearch=bool(p.wantHistorySearch), 

799 ) 

800 

801 query.options = options 

802 return query 

803 

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 

810 

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 

817 

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) 

823 

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) 

828 

829 ## 

830 

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

837 

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

841 

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 

845 

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 ) 

857 

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

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

860 conn.commit() 

861 

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

863 

864 def _check_eigentuemer_control_input(self, control_input): 

865 if not self.eigentuemer.controlRules: 

866 return True 

867 

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

869 

870 for rule in self.eigentuemer.controlRules: 

871 if re.search(rule, control_input): 

872 return True 

873 

874 return False