Coverage for gws-app/gws/plugin/alkis/action.py: 0%

481 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 12:46 +0200

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[exporter.Props] 

158 limit: int 

159 printer: Optional[gws.base.printer.Props] 

160 ui: Ui 

161 storage: Optional[gws.base.storage.Props] 

162 strasseSearchOptions: Optional[gws.TextSearchOptions] 

163 nameSearchOptions: Optional[gws.TextSearchOptions] 

164 buchungsblattSearchOptions: Optional[gws.TextSearchOptions] 

165 withBuchung: bool 

166 withEigentuemer: bool 

167 withEigentuemerControl: bool 

168 withFlurnummer: bool 

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 modelUids: Optional[list[str]] 

280 eigentuemerControlInput: Optional[str] 

281 

282 

283class ExportFlurstueckResponse(gws.Response): 

284 content: str 

285 mime: str 

286 

287 

288## 

289 

290 

291_dir = os.path.dirname(__file__) 

292 

293_DEFAULT_TEMPLATES = [ 

294 gws.Config( 

295 subject='flurstueck.title', 

296 type='html', 

297 path=f'{_dir}/templates/flurstueck_title.cx.html', 

298 ), 

299 gws.Config( 

300 subject='flurstueck.teaser', 

301 type='html', 

302 path=f'{_dir}/templates/flurstueck_title.cx.html', 

303 ), 

304 gws.Config( 

305 subject='flurstueck.label', 

306 type='html', 

307 path=f'{_dir}/templates/flurstueck_label.cx.html', 

308 ), 

309 gws.Config( 

310 subject='flurstueck.description', 

311 type='html', 

312 path=f'{_dir}/templates/description.cx.html', 

313 ), 

314 gws.Config( 

315 subject='adresse.title', 

316 type='html', 

317 path=f'{_dir}/templates/adresse_title.cx.html', 

318 ), 

319 gws.Config( 

320 subject='adresse.teaser', 

321 type='html', 

322 path=f'{_dir}/templates/adresse_title.cx.html', 

323 ), 

324 gws.Config( 

325 subject='adresse.label', 

326 type='html', 

327 path=f'{_dir}/templates/adresse_label.cx.html', 

328 ), 

329] 

330 

331_DEFAULT_PRINTER = gws.Config( 

332 uid='gws.plugin.alkis.default_printer', 

333 template=gws.Config( 

334 type='html', 

335 path=f'{_dir}/templates/print.cx.html', 

336 ), 

337 qualityLevels=[gws.TemplateQualityLevel(dpi=72)], 

338) 

339 

340 

341## 

342 

343 

344class Model(gws.base.model.default_model.Object): 

345 def configure(self): 

346 self.uidName = 'uid' 

347 self.geometryName = 'geometry' 

348 self.loadingStrategy = gws.FeatureLoadingStrategy.all 

349 

350 

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

352 db: gws.DatabaseProvider 

353 

354 ix: index.Object 

355 ixStatus: dt.IndexStatus 

356 

357 buchung: BuchungOptions 

358 eigentuemer: EigentuemerOptions 

359 

360 dataSchema: str 

361 indexSchema: str 

362 

363 model: gws.Model 

364 ui: Ui 

365 limit: int 

366 

367 templates: list[gws.Template] 

368 printers: list[gws.Printer] 

369 

370 exporters: list[exporter.Object] 

371 

372 strasseSearchOptions: gws.TextSearchOptions 

373 nameSearchOptions: gws.TextSearchOptions 

374 buchungsblattSearchOptions: gws.TextSearchOptions 

375 

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

377 

378 def configure(self): 

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

380 

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

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

383 

384 self.ix = self.root.create( 

385 index.Object, 

386 _defaultDb=self.db, 

387 crs=self.cfg('crs'), 

388 schema=self.indexSchema, 

389 excludeGemarkung=self.cfg('excludeGemarkung'), 

390 gemarkungFilter=self.cfg('gemarkungFilter'), 

391 uid=f'gws.plugin.alkis.data.index.{self.indexSchema}', 

392 ) 

393 

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

395 self.model = self.create_child(Model) 

396 self.ui = self.cfg( 

397 'ui', 

398 default=Ui( 

399 gemarkungListMode=GemarkungListMode.combined, 

400 strasseListMode=StrasseListMode.plain, 

401 ), 

402 ) 

403 

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

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

406 

407 self.printers = self.create_children(gws.ext.object.printer, self.cfg('printers')) 

408 self.printers.append(self.create_child(gws.ext.object.printer, _DEFAULT_PRINTER)) 

409 

410 d = gws.TextSearchOptions(type=gws.TextSearchType.begin, caseSensitive=False) 

411 self.strasseSearchOptions = self.cfg('strasseSearchOptions', default=d) 

412 

413 d = gws.TextSearchOptions(type=gws.TextSearchType.begin, caseSensitive=False) 

414 self.nameSearchOptions = self.cfg('nameSearchOptions', default=d) 

415 

416 d = gws.TextSearchOptions(type=gws.TextSearchType.exact, caseSensitive=False) 

417 self.buchungsblattSearchOptions = self.cfg('buchungsblattSearchOptions', default=d) 

418 

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

420 

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

422 

423 self.eigentuemer = self.create_child(EigentuemerOptions, self.cfg('eigentuemer', default=deny_all)) 

424 if self.eigentuemer.logTableName: 

425 self.eigentuemer.logTable = self.ix.db.table(self.eigentuemer.logTableName) 

426 

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

428 

429 self.exporters = [] 

430 p = self.cfg('exporters') 

431 if p: 

432 self.exporters = self.create_children(exporter.Object, p) 

433 elif self.ui.useExport: 

434 self.exporters.append(self.create_child(exporter.Object)) 

435 

436 def activate(self): 

437 def _load(): 

438 s = self.ix.status() 

439 if s.missing: 

440 gws.log.warning(f'ALKIS: index not found in schema {self.indexSchema}') 

441 return s 

442 

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

444 

445 def props(self, user): 

446 if not self.ixStatus.basic: 

447 return None 

448 

449 ps: Props = cast( 

450 Props, 

451 gws.u.merge( 

452 super().props(user), 

453 limit=self.limit, 

454 printer=gws.u.first(p for p in self.printers if user.can_use(p)), 

455 ui=self.ui, 

456 storage=self.storage, 

457 withBuchung=(self.ixStatus.buchung and user.can_read(self.buchung)), 

458 withEigentuemer=(self.ixStatus.eigentuemer and user.can_read(self.eigentuemer)), 

459 withEigentuemerControl=(self.ixStatus.eigentuemer and user.can_read(self.eigentuemer) and self.eigentuemer.controlMode), 

460 ), 

461 ) 

462 

463 ps.exporters = [] 

464 for exp in self.exporters: 

465 p = exp.props_with_flags(user, withEigentuemer=ps.withEigentuemer, withBuchung=ps.withBuchung) 

466 if p: 

467 ps.exporters.append(p) 

468 

469 ps.strasseSearchOptions = self.strasseSearchOptions 

470 if ps.withBuchung: 

471 ps.buchungsblattSearchOptions = self.buchungsblattSearchOptions 

472 if ps.withEigentuemer: 

473 ps.nameSearchOptions = self.nameSearchOptions 

474 

475 return ps 

476 

477 @gws.ext.command.api('alkisGetToponyms') 

478 def get_toponyms(self, req: gws.WebRequester, p: GetToponymsRequest) -> GetToponymsResponse: 

479 """Return all Toponyms (Gemeinde/Gemarkung/Strasse) in the area""" 

480 

481 req.user.require_project(p.projectUid) 

482 

483 gemeinde_dct = {} 

484 gemarkung_dct = {} 

485 strasse_lst = [] 

486 

487 for s in self.ix.strasse_list(): 

488 gemeinde_dct[s.gemeinde.code] = [s.gemeinde.text, s.gemeinde.code] 

489 gemarkung_dct[s.gemarkung.code] = [s.gemarkung.text, s.gemarkung.code, s.gemeinde.code] 

490 strasse_lst.append([s.name, s.gemeinde.code, s.gemarkung.code]) 

491 

492 return GetToponymsResponse( 

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

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

495 strasse=sorted(strasse_lst), 

496 ) 

497 

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

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

500 """Perform an Adresse search.""" 

501 

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

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

504 

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

506 if not ad_list: 

507 return FindAdresseResponse( 

508 features=[], 

509 total=0, 

510 ) 

511 

512 templates = [ 

513 self.root.app.templateMgr.find_template('adresse.title', where=[self], user=req.user), 

514 self.root.app.templateMgr.find_template('adresse.teaser', where=[self], user=req.user), 

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

516 ] 

517 

518 fprops = [] 

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

520 

521 for ad in ad_list: 

522 f = gws.base.feature.new(model=self.model) 

523 f.attributes = vars(ad) 

524 f.attributes['geometry'] = f.attributes.pop('shape') 

525 f.transform_to(crs) 

526 f.render_views(templates, user=req.user) 

527 fprops.append(self.model.feature_to_view_props(f, mc)) 

528 

529 return FindAdresseResponse( 

530 features=fprops, 

531 total=len(ad_list), 

532 ) 

533 

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

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

536 """Perform a Flurstueck search""" 

537 

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

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

540 

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

542 if not fs_list: 

543 return FindFlurstueckResponse( 

544 features=[], 

545 total=0, 

546 ) 

547 

548 templates = [ 

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

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

551 ] 

552 

553 if query.options.displayThemes: 

554 templates.append( 

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

556 ) 

557 

558 args = dict( 

559 withHistory=query.options.withHistoryDisplay, 

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

561 ) 

562 

563 ps = [] 

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

565 

566 for fs in fs_list: 

567 f = gws.base.feature.new(model=self.model) 

568 f.attributes = dict(uid=fs.uid, fs=fs, geometry=fs.shape) 

569 f.transform_to(crs) 

570 f.render_views(templates, user=req.user, **args) 

571 f.attributes.pop('fs') 

572 ps.append(self.model.feature_to_view_props(f, mc)) 

573 

574 return FindFlurstueckResponse( 

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

576 total=len(fs_list), 

577 ) 

578 

579 def get_exporter(self, uid: Optional[str], user: gws.User) -> Optional[exporter.Object]: 

580 for exp in self.exporters: 

581 if user.can_use(exp) and (not uid or exp.uid == uid): 

582 return exp 

583 

584 @gws.ext.command.api('alkisExportFlurstueck') 

585 def export_flurstueck(self, req: gws.WebRequester, p: ExportFlurstueckRequest) -> ExportFlurstueckResponse: 

586 exp = self.get_exporter(p.exporterUid, req.user) 

587 if not exp: 

588 raise gws.NotFoundError() 

589 

590 models = exp.get_models(req.user, p.modelUids) 

591 if not models: 

592 raise gws.NotFoundError() 

593 

594 if any(m.withEigentuemer for m in models): 

595 self._check_eigentuemer_access(req, p.findRequest.eigentuemerControlInput or '') 

596 if any(m.withBuchung for m in models): 

597 self._check_buchung_access(req, p.findRequest.eigentuemerControlInput or '') 

598 

599 find_request = p.findRequest 

600 find_request.projectUid = p.projectUid 

601 

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

603 if not fs_list: 

604 raise gws.NotFoundError() 

605 

606 args = exporter.Args( 

607 fsList=fs_list, 

608 user=req.user, 

609 models=models, 

610 path = gws.u.ephemeral_path('alkis_export'), 

611 ) 

612 

613 exp.run(args) 

614 

615 return ExportFlurstueckResponse( 

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

617 mime=exp.mimeType, 

618 ) 

619 

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

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

622 """Print Flurstueck features""" 

623 

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

625 

626 find_request = p.findRequest 

627 find_request.projectUid = p.projectUid 

628 

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

630 if not fs_list: 

631 raise gws.NotFoundError() 

632 

633 print_request = p.printRequest 

634 print_request.projectUid = p.projectUid 

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

636 

637 templates = [ 

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

639 ] 

640 

641 base_map = print_request.maps[0] 

642 fs_maps = [] 

643 

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

645 

646 for fs in fs_list: 

647 f = gws.base.feature.new(model=self.model) 

648 f.attributes = dict(uid=fs.uid, fs=fs, geometry=fs.shape) 

649 f.transform_to(crs) 

650 f.render_views(templates, user=req.user) 

651 f.cssSelector = p.featureStyle.cssSelector 

652 

653 c = f.shape().centroid() 

654 fs_map = gws.PrintMap(base_map) 

655 # @TODO scale to fit the fs? 

656 fs_map.center = (c.x, c.y) 

657 fs_plane = gws.PrintPlane( 

658 type=gws.PrintPlaneType.features, 

659 features=[self.model.feature_to_view_props(f, mc)], 

660 ) 

661 fs_map.planes = [fs_plane] + base_map.planes 

662 fs_maps.append(fs_map) 

663 

664 print_request.maps = fs_maps 

665 print_request.args = dict( 

666 flurstueckList=fs_list, 

667 withHistory=query.options.withHistoryDisplay, 

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

669 ) 

670 

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

672 

673 @gws.ext.command.api('alkisSelectionStorage') 

674 def handle_storage(self, req: gws.WebRequester, p: gws.base.storage.Request) -> gws.base.storage.Response: 

675 if not self.storage: 

676 raise gws.NotFoundError('no storage configured') 

677 return self.storage.handle_request(req, p) 

678 

679 ## 

680 

681 def find_flurstueck_objects(self, req: gws.WebRequester, p: FindFlurstueckRequest) -> tuple[list[dt.Flurstueck], dt.FlurstueckQuery]: 

682 query = self._prepare_flurstueck_query(req, p) 

683 fs_list = self.ix.find_flurstueck(query) 

684 

685 if query.options.withEigentuemer: 

686 self._log_eigentuemer_access( 

687 req, 

688 p.eigentuemerControlInput or '', 

689 is_ok=True, 

690 total=len(fs_list), 

691 fs_uids=[fs.uid for fs in fs_list], 

692 ) 

693 

694 return fs_list, query 

695 

696 def find_adresse_objects(self, req: gws.WebRequester, p: FindAdresseRequest) -> tuple[list[dt.Adresse], dt.AdresseQuery]: 

697 query = self._prepare_adresse_query(req, p) 

698 ad_list = self.ix.find_adresse(query) 

699 return ad_list, query 

700 

701 FLURSTUECK_QUERY_FIELDS = [ 

702 'flurnummer', 

703 'flurstuecksfolge', 

704 'zaehler', 

705 'nenner', 

706 'flurstueckskennzeichen', 

707 'flaecheBis', 

708 'flaecheVon', 

709 'gemarkung', 

710 'gemarkungCode', 

711 'gemeinde', 

712 'gemeindeCode', 

713 'kreis', 

714 'kreisCode', 

715 'land', 

716 'landCode', 

717 'regierungsbezirk', 

718 'regierungsbezirkCode', 

719 'strasse', 

720 'hausnummer', 

721 'personName', 

722 'personVorname', 

723 'uids', 

724 ] 

725 ADRESSE_QUERY_FIELDS = [ 

726 'gemarkung', 

727 'gemarkungCode', 

728 'gemeinde', 

729 'gemeindeCode', 

730 'kreis', 

731 'kreisCode', 

732 'land', 

733 'landCode', 

734 'regierungsbezirk', 

735 'regierungsbezirkCode', 

736 'strasse', 

737 'hausnummer', 

738 'bisHausnummer', 

739 'hausnummerNotNull', 

740 ] 

741 COMBINED_FLURSTUECK_FIELDS = ['landCode', 'gemarkungCode', 'flurnummer', 'zaehler', 'nenner', 'flurstuecksfolge'] 

742 COMBINED_ADRESSE_FIELDS = ['strasse', 'hausnummer', 'plz', 'gemeinde', 'bisHausnummer'] 

743 

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

745 query = dt.FlurstueckQuery() 

746 

747 for f in self.FLURSTUECK_QUERY_FIELDS: 

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

749 

750 if p.combinedFlurstueckCode: 

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

752 

753 if p.fsnummer: 

754 self._query_fsnummer(query, p.fsnummer) 

755 

756 if p.shapes: 

757 shapes = [gws.base.shape.from_props(s) for s in p.shapes] 

758 query.shape = shapes[0] if len(shapes) == 1 else shapes[0].union(shapes[1:]) 

759 

760 if p.bblatt: 

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

762 

763 options = dt.FlurstueckQueryOptions( 

764 strasseSearchOptions=self.strasseSearchOptions, 

765 nameSearchOptions=self.nameSearchOptions, 

766 buchungsblattSearchOptions=self.buchungsblattSearchOptions, 

767 limit=self.limit, 

768 withEigentuemer=False, 

769 withBuchung=False, 

770 withHistorySearch=bool(p.wantHistorySearch), 

771 withHistoryDisplay=bool(p.wantHistoryDisplay), 

772 displayThemes=p.displayThemes or [], 

773 ) 

774 

775 want_eigentuemer = ( 

776 p.wantEigentuemer 

777 or dt.DisplayTheme.eigentuemer in options.displayThemes 

778 or any(getattr(query, f) is not None for f in dt.EigentuemerAccessRequired) 

779 ) 

780 if want_eigentuemer: 

781 self._check_eigentuemer_access(req, p.eigentuemerControlInput or '') 

782 options.withEigentuemer = True 

783 

784 want_buchung = dt.DisplayTheme.buchung in options.displayThemes or any(getattr(query, f) is not None for f in dt.BuchungAccessRequired) 

785 if want_buchung: 

786 self._check_buchung_access(req, p.eigentuemerControlInput or '') 

787 options.withBuchung = True 

788 

789 # "eigentuemer" implies "buchung" 

790 if want_eigentuemer and not want_buchung: 

791 options.withBuchung = True 

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

793 

794 query.options = options 

795 return query 

796 

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

798 query = dt.AdresseQuery() 

799 

800 for f in self.ADRESSE_QUERY_FIELDS: 

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

802 

803 if p.combinedAdresseCode: 

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

805 

806 options = dt.AdresseQueryOptions( 

807 strasseSearchOptions=self.strasseSearchOptions, 

808 withHistorySearch=bool(p.wantHistorySearch), 

809 ) 

810 

811 query.options = options 

812 return query 

813 

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

815 if vn.startswith('DE'): 

816 # search by gml_id 

817 query.uids = query.uids or [] 

818 query.uids.append(vn) 

819 return 

820 

821 if re.match('^[0-9_]+$', vn) and len(vn) >= 14: 

822 # search by fs kennzeichen 

823 # the length must be at least 2+4+3+5 

824 # (see gdi6, AX_Flurstueck_Kerndaten.flurstueckskennzeichen) 

825 query.flurstueckskennzeichen = vn 

826 return 

827 

828 # search by a compound fs number 

829 parts = index.parse_fsnummer(vn) 

830 if parts is None: 

831 raise gws.BadRequestError(f'invalid fsnummer {vn!r}') 

832 query.update(parts) 

833 

834 def _query_combined_code(self, query: dt.FlurstueckQuery | dt.AdresseQuery, code_value: str, code_fields: list[str]): 

835 for val, field in zip(code_value.split('_'), code_fields): 

836 if val and val != '0': 

837 setattr(query, field, val) 

838 

839 ## 

840 

841 def _check_eigentuemer_access(self, req: gws.WebRequester, control_input: str): 

842 if not req.user.can_read(self.eigentuemer): 

843 raise gws.ForbiddenError('cannot read eigentuemer') 

844 if self.eigentuemer.controlMode and not self._check_eigentuemer_control_input(control_input): 

845 self._log_eigentuemer_access(req, is_ok=False, control_input=control_input) 

846 raise gws.ForbiddenError('eigentuemer control input failed') 

847 

848 def _check_buchung_access(self, req: gws.WebRequester, control_input: str): 

849 if not req.user.can_read(self.buchung): 

850 raise gws.ForbiddenError('cannot read buchung') 

851 

852 def _log_eigentuemer_access(self, req: gws.WebRequester, control_input: str, is_ok: bool, total=None, fs_uids=None): 

853 if self.eigentuemer.logTable is None: 

854 return 

855 

856 data = dict( 

857 app_name='gws', 

858 date_time=gws.lib.datetimex.now(), 

859 ip=req.ip, 

860 login=req.user.uid, 

861 user_name=req.user.displayName, 

862 control_input=(control_input or '').strip(), 

863 control_result=1 if is_ok else 0, 

864 fs_count=total or 0, 

865 fs_ids=','.join(fs_uids or []), 

866 ) 

867 

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

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

870 conn.commit() 

871 

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

873 

874 def _check_eigentuemer_control_input(self, control_input): 

875 if not self.eigentuemer.controlRules: 

876 return True 

877 

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

879 

880 for rule in self.eigentuemer.controlRules: 

881 if re.search(rule, control_input): 

882 return True 

883 

884 return False