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

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

2 

3from typing import Optional 

4 

5import os 

6import re 

7 

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 

22 

23from .data import index, export 

24from .data import types as dt 

25 

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

27 

28 

29class EigentuemerConfig(gws.ConfigWithAccess): 

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

31 

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

38 

39 

40class EigentuemerOptions(gws.Node): 

41 controlMode: bool 

42 controlRules: list[str] 

43 logTableName: str 

44 logTable: Optional[sa.Table] 

45 

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 

51 

52 

53class BuchungConfig(gws.ConfigWithAccess): 

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

55 pass 

56 

57 

58class BuchungOptions(gws.Node): 

59 pass 

60 

61 

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

71 

72 

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

84 

85 

86class Ui(gws.Config): 

87 """Flurstückssuche UI configuration.""" 

88 

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

107 

108 

109class Config(gws.ConfigWithAccess): 

110 """Flurstückssuche action""" 

111 

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

122 

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

135 

136 strasseSearchOptions: Optional[gws.TextSearchOptions] 

137 nameSearchOptions: Optional[gws.TextSearchOptions] 

138 buchungsblattSearchOptions: Optional[gws.TextSearchOptions] 

139 

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

141 """storage configuration""" 

142 

143 export: Optional[export.Config] 

144 """csv export configuration""" 

145 

146 

147## 

148 

149class ExportGroupProps(gws.Props): 

150 index: int 

151 title: str 

152 

153 

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 

167 

168 

169## 

170 

171class GetToponymsRequest(gws.Request): 

172 pass 

173 

174 

175class GetToponymsResponse(gws.Response): 

176 gemeinde: list[list[str]] 

177 gemarkung: list[list[str]] 

178 strasse: list[list[str]] 

179 

180 

181class FindFlurstueckRequest(gws.Request): 

182 flurnummer: Optional[str] 

183 flurstuecksfolge: Optional[str] 

184 zaehler: Optional[str] 

185 nenner: Optional[str] 

186 fsnummer: Optional[str] 

187 

188 flaecheBis: Optional[float] 

189 flaecheVon: Optional[float] 

190 

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] 

201 

202 strasse: Optional[str] 

203 hausnummer: Optional[str] 

204 

205 bblatt: Optional[str] 

206 

207 personName: Optional[str] 

208 personVorname: Optional[str] 

209 

210 combinedFlurstueckCode: Optional[str] 

211 

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

213 

214 uids: Optional[list[str]] 

215 

216 crs: Optional[gws.CrsName] 

217 eigentuemerControlInput: Optional[str] 

218 limit: Optional[int] 

219 

220 wantEigentuemer: Optional[bool] 

221 wantHistorySearch: Optional[bool] 

222 wantHistoryDisplay: Optional[bool] 

223 

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

225 

226 

227class FindFlurstueckResponse(gws.Response): 

228 features: list[gws.FeatureProps] 

229 total: int 

230 

231 

232class FindFlurstueckResult(gws.Data): 

233 flurstueckList: list[dt.Flurstueck] 

234 total: int 

235 query: dt.FlurstueckQuery 

236 

237 

238class FindAdresseRequest(gws.Request): 

239 crs: Optional[gws.Crs] 

240 

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] 

251 

252 strasse: Optional[str] 

253 hausnummer: Optional[str] 

254 bisHausnummer: Optional[str] 

255 hausnummerNotNull: Optional[bool] 

256 

257 wantHistorySearch: Optional[bool] 

258 

259 combinedAdresseCode: Optional[str] 

260 

261 

262class FindAdresseResponse(gws.Response): 

263 features: list[gws.FeatureProps] 

264 total: int 

265 

266 

267class PrintFlurstueckRequest(gws.Request): 

268 findRequest: FindFlurstueckRequest 

269 printRequest: gws.PrintRequest 

270 featureStyle: gws.StyleProps 

271 

272 

273class ExportFlurstueckRequest(gws.Request): 

274 findRequest: FindFlurstueckRequest 

275 groupIndexes: list[int] 

276 

277 

278class ExportFlurstueckResponse(gws.Response): 

279 content: str 

280 mime: str 

281 

282 

283## 

284 

285 

286_dir = os.path.dirname(__file__) 

287 

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] 

325 

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) 

334 

335 

336## 

337 

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 

343 

344 

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

346 db: gws.DatabaseProvider 

347 

348 ix: index.Object 

349 ixStatus: index.Status 

350 

351 buchung: BuchungOptions 

352 eigentuemer: EigentuemerOptions 

353 

354 dataSchema: str 

355 

356 model: gws.Model 

357 ui: Ui 

358 limit: int 

359 

360 templates: list[gws.Template] 

361 printers: list[gws.Printer] 

362 

363 export: Optional[export.Object] 

364 

365 strasseSearchOptions: gws.TextSearchOptions 

366 nameSearchOptions: gws.TextSearchOptions 

367 buchungsblattSearchOptions: gws.TextSearchOptions 

368 

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

370 

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 ) 

381 

382 self.limit = self.cfg('limit') 

383 self.model = self.create_child(Model) 

384 self.ui = self.cfg('ui') 

385 

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

387 

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

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

390 

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

393 

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) 

398 

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

400 

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

402 

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) 

406 

407 self.storage = self.create_child_if_configured( 

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

409 

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 

417 

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) 

422 

423 def props(self, user): 

424 if not self.ixStatus.basic: 

425 return None 

426 

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

430 

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 ) 

452 

453 ps.strasseSearchOptions = self.strasseSearchOptions 

454 if ps.withBuchung: 

455 ps.buchungsblattSearchOptions = self.buchungsblattSearchOptions 

456 if ps.withEigentuemer: 

457 ps.nameSearchOptions = self.nameSearchOptions 

458 

459 return ps 

460 

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

464 

465 req.user.require_project(p.projectUid) 

466 

467 gemeinde_dct = {} 

468 gemarkung_dct = {} 

469 strasse_lst = [] 

470 

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

475 

476 return GetToponymsResponse( 

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

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

479 strasse=sorted(strasse_lst) 

480 ) 

481 

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

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

484 """Perform an Adresse search.""" 

485 

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

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

488 

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

490 if not ad_list: 

491 return FindAdresseResponse( 

492 features=[], 

493 total=0, 

494 ) 

495 

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 ] 

501 

502 fprops = [] 

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

504 

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

512 

513 return FindAdresseResponse( 

514 features=fprops, 

515 total=len(ad_list), 

516 ) 

517 

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

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

520 """Perform a Flurstueck search""" 

521 

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

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

524 

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

526 if not fs_list: 

527 return FindFlurstueckResponse( 

528 features=[], 

529 total=0, 

530 ) 

531 

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 ] 

536 

537 if query.options.displayThemes: 

538 templates.append( 

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

540 ) 

541 

542 args = dict( 

543 withHistory=query.options.withHistoryDisplay, 

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

545 ) 

546 

547 ps = [] 

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

549 

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

557 

558 return FindFlurstueckResponse( 

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

560 total=len(fs_list), 

561 ) 

562 

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

567 

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

571 

572 find_request = p.findRequest 

573 find_request.projectUid = p.projectUid 

574 

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

576 if not fs_list: 

577 raise gws.NotFoundError() 

578 

579 csv_bytes = self.export.export_as_csv(fs_list, groups, req.user) 

580 

581 return ExportFlurstueckResponse(content=csv_bytes, mime='text/csv') 

582 

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

584 def print_flurstueck(self, req: gws.WebRequester, p: PrintFlurstueckRequest) -> gws.PrintJobResponse: 

585 """Print Flurstueck features""" 

586 

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

588 

589 find_request = p.findRequest 

590 find_request.projectUid = p.projectUid 

591 

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

593 if not fs_list: 

594 raise gws.NotFoundError() 

595 

596 print_request = p.printRequest 

597 print_request.projectUid = p.projectUid 

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

599 

600 templates = [ 

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

602 ] 

603 

604 base_map = print_request.maps[0] 

605 fs_maps = [] 

606 

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

608 

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 

615 

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) 

626 

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 ) 

633 

634 job = self.root.app.printerMgr.start_job(print_request, req.user) 

635 return self.root.app.printerMgr.status(job) 

636 

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) 

642 

643 ## 

644 

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) 

648 

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 ) 

657 

658 return fs_list, query 

659 

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 

664 

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 ] 

711 

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

713 query = dt.FlurstueckQuery() 

714 

715 for f in self.FLURSTUECK_QUERY_FIELDS: 

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

717 

718 if p.combinedFlurstueckCode: 

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

720 

721 if p.fsnummer: 

722 self._query_fsnummer(query, p.fsnummer) 

723 

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

727 

728 if p.bblatt: 

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

730 

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 ) 

742 

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 

751 

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 

759 

760 # "eigentuemer" implies "buchung" 

761 if want_eigentuemer and not want_buchung: 

762 options.withBuchung = True 

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

764 

765 query.options = options 

766 return query 

767 

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

769 query = dt.AdresseQuery() 

770 

771 for f in self.ADRESSE_QUERY_FIELDS: 

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

773 

774 if p.combinedAdresseCode: 

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

776 

777 options = dt.AdresseQueryOptions( 

778 strasseSearchOptions=self.strasseSearchOptions, 

779 withHistorySearch=bool(p.wantHistorySearch), 

780 ) 

781 

782 query.options = options 

783 return query 

784 

785 def _query_fsnummer(self, query: dt.FlurstueckQuery, vn: str): 

786 

787 if vn.startswith('DE'): 

788 # search by gml_id 

789 query.uids = query.uids or [] 

790 query.uids.append(vn) 

791 return 

792 

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 

799 

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) 

805 

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) 

810 

811 ## 

812 

813 def _export_groups(self, user): 

814 if not self.export: 

815 return [] 

816 

817 groups = [] 

818 

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) 

825 

826 return groups 

827 

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

834 

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

838 

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 

842 

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 ) 

854 

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

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

857 conn.commit() 

858 

859 gws.log.debug(f'_log_eigentuemer_access {is_ok=}') 

860 

861 def _check_eigentuemer_control_input(self, control_input): 

862 if not self.eigentuemer.controlRules: 

863 return True 

864 

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

866 

867 for rule in self.eigentuemer.controlRules: 

868 if re.search(rule, control_input): 

869 return True 

870 

871 return False