Coverage for ckanext/udc/plugin.py: 71%

338 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2026-06-23 19:31 +0000

1from __future__ import annotations 

2 

3import sys 

4import json 

5import traceback 

6import chalk 

7from collections import OrderedDict 

8from typing import ( 

9 Any, 

10 Callable, 

11 Collection, 

12 KeysView, 

13 Optional, 

14 Union, 

15 cast, 

16 Iterable, 

17 List, 

18) 

19from functools import partial 

20 

21from ckanext.udc.search.logic.actions import filter_facets_get 

22from ckan.types import Schema, Context, CKANApp, Response, SignalMapping 

23import ckan 

24import ckan.plugins as plugins 

25import ckan.logic as logic 

26import ckan.model as model 

27import ckan.plugins.toolkit as tk 

28import ckan.lib.base as base 

29from ckan.plugins.toolkit import chained_action, side_effect_free 

30import ckan.lib.helpers as h 

31from ckan.lib.helpers import Page 

32from ckan.common import asbool, current_user, CKANConfig, request, config, _ 

33from ckan.lib.search import SearchQueryError, SearchError 

34from ckan.lib.plugins import DefaultTranslation 

35 

36from flask import Blueprint 

37 

38import logging 

39from ckanext.udc.cli import udc as cli_udc 

40from ckanext.udc.validator import udc_config_validator 

41from ckanext.udc.helpers import ( 

42 config_option_update, 

43 get_default_facet_titles, 

44 process_facets_fields, 

45 humanize_entity_type, 

46 get_maturity_percentages, 

47 package_update, 

48 package_delete, 

49 get_system_info, 

50 udc_json_attr, 

51 render_markdown, 

52 udc_search_details, 

53) 

54from ckanext.udc.solr.config import pick_locale, pick_locale_with_fallback, get_udc_langs, get_current_lang 

55from ckanext.udc.search.params import facet_alias_map, get_search_details 

56from ckanext.udc.graph.sparql_client import SparqlClient 

57from ckanext.udc.graph.preload import preload_ontologies 

58from ckanext.udc.graph.logic import get_catalogue_graph 

59from babel import Locale 

60 

61from ckanext.udc.licenses.logic.action import ( 

62 license_create, 

63 license_delete, 

64 licenses_get, 

65 license_update, 

66 init_licenses, 

67 test_long_task, 

68) 

69from ckanext.udc.licenses.utils import license_options_details 

70from ckanext.udc.file_format.logic import ( 

71 file_format_create, 

72 file_format_delete, 

73 file_formats_get, 

74) 

75from ckanext.udc.desc.actions import ( 

76 summary_generate, 

77 update_summary, 

78 default_ai_summary_config, 

79) 

80from ckanext.udc.desc.utils import init_plugin as init_udc_desc 

81from ckanext.udc.error_handler import override_error_handler 

82from ckanext.udc.system.actions import reload_supervisord, get_system_stats 

83from ckanext.udc.version.actions import udc_version_meta 

84from ckanext.udc.solr.solr import update_solr_maturity_model_fields 

85from ckanext.udc.solr.index import before_dataset_index as _before_dataset_index 

86 

87from ckanext.udc.search.logic.actions import filter_facets_get 

88from ckanext.udc.user.actions import ( 

89 deleted_users_list, 

90 purge_deleted_users, 

91 udc_user_list, 

92 udc_user_reset_password, 

93 udc_user_delete, 

94) 

95from ckanext.udc.organization.actions import ( 

96 udc_organization_list, 

97 udc_deleted_organization_list, 

98 udc_organization_delete, 

99 udc_purge_deleted_organizations, 

100 udc_organization_packages_list, 

101 udc_organization_packages_ids, 

102 udc_organization_packages_delete, 

103) 

104from ckanext.udc.organization import auth as organization_auth 

105from ckanext.udc.user import auth as user_auth 

106from .i18n import ( 

107 udc_lang_object, 

108 udc_json_dump, 

109 udc_json_load, 

110 udc_core_translated_to_extras, 

111 udc_set_core_from_translated, 

112 udc_lang_string_list, 

113 udc_set_core_tags_from_translated, 

114 udc_fill_tags_translated_from_core, 

115 udc_seed_translated_from_core, 

116 udc_fill_translated_from_core_on_show, 

117) 

118 

119 

120""" 

121See https://docs.ckan.org/en/latest/theming/templates.html 

122See https://docs.ckan.org/en/latest/extensions/adding-custom-fields.html 

123See https://docs.ckan.org/en/2.10/extensions/remote-config-update.html 

124See https://docs.ckan.org/en/2.10/extensions/custom-config-settings.html?highlight=config%20declaration 

125See https://docs.ckan.org/en/2.10/theming/webassets.html 

126""" 

127log = logging.getLogger(__name__) 

128 

129 

130def _is_catalogue_search_fq(fq: Any) -> bool: 

131 if isinstance(fq, str): 

132 return "dataset_type:catalogue" in fq 

133 if isinstance(fq, Iterable): 

134 return any("dataset_type:catalogue" in item for item in fq if isinstance(item, str)) 

135 return False 

136 

137# Add UDC CLI 

138# We can then `ckan -c /etc/ckan/default/ckan.ini udc move-to-catalogues` to run the migration script 

139if hasattr(ckan, "cli") and hasattr(ckan.cli, "cli"): 

140 ckan.cli.cli.ckan.add_command(cli_udc.udc) 

141 

142 

143@tk.blanket.blueprints 

144class UdcPlugin(plugins.SingletonPlugin, tk.DefaultDatasetForm, DefaultTranslation): 

145 plugins.implements(plugins.IConfigurer) 

146 plugins.implements(plugins.IConfigurable) 

147 plugins.implements(plugins.IDatasetForm) 

148 plugins.implements(plugins.ITemplateHelpers) 

149 plugins.implements(plugins.IActions) 

150 plugins.implements(plugins.IAuthFunctions) 

151 plugins.implements(plugins.IFacets) 

152 plugins.implements(plugins.IPackageController) 

153 plugins.implements(plugins.IMiddleware) 

154 plugins.implements(plugins.IBlueprint) 

155 plugins.implements(plugins.IValidators) 

156 plugins.implements(plugins.ITranslation) 

157 

158 disable_graphdb = False 

159 maturity_model = [] 

160 mappings = {} 

161 preload_ontologies = {} 

162 all_fields: List[str] = [] # This does not contain core CKAN fields 

163 facet_titles = {} 

164 facet_titles_raw = {} # multilingual titles, not picked to current locale 

165 text_fields: List[str] = [] 

166 date_fields: List[str] = [] 

167 multiple_select_fields: List[str] = [] 

168 dropdown_options: dict[str, dict[str, str]] = {} 

169 

170 def update_config(self, config_): 

171 tk.add_template_directory(config_, "templates") 

172 tk.add_public_directory(config_, "public") 

173 tk.add_resource("assets", "udc") 

174 

175 def _cli_configure(self): 

176 """Partial load for CLI""" 

177 self.disable_graphdb = True 

178 existing_config = ckan.model.system_info.get_system_info("ckanext.udc.config") 

179 if existing_config: 

180 try: 

181 # Call our plugin to update the config 

182 self.reload_config(json.loads(existing_config)) 

183 except: 

184 log.error 

185 

186 # Load custom licenses 

187 init_licenses() 

188 

189 def configure(self, config: CKANConfig): 

190 log.info(sys.argv) 

191 if not ( 

192 "run" in sys.argv 

193 or "uwsgi" in sys.argv 

194 or ("jobs" in sys.argv and "worker" in sys.argv) 

195 or ("search-index" in sys.argv) 

196 or ("translation" in sys.argv) 

197 or ("asset" in sys.argv) 

198 ): 

199 log.info("Skipping UDC Plugin Configuration") 

200 # Do not load the plugin if we are running the CLI 

201 self._cli_configure() 

202 return 

203 existing_config = ckan.model.system_info.get_system_info("ckanext.udc.config") 

204 # print(existing_config) 

205 

206 # Load sparql client 

207 endpoint = tk.config.get("udc.sparql.endpoint") 

208 username = tk.config.get("udc.sparql.username") or None 

209 password = tk.config.get("udc.sparql.password") or None 

210 

211 if endpoint is None: 

212 self.disable_graphdb = True 

213 log.info("No GraphDB Endpoint is provided.") 

214 

215 else: 

216 self.sparql_client = SparqlClient( 

217 endpoint, username=username, password=password 

218 ) 

219 if self.sparql_client.test_connecetion(): 

220 log.info("GraphDB connected: " + endpoint) 

221 else: 

222 log.error("UDC cannot connect to the GraphDB") 

223 self.disable_graphdb = True 

224 

225 # Load config 

226 if existing_config: 

227 try: 

228 # Call our plugin to update the config 

229 self.reload_config(json.loads(existing_config)) 

230 except: 

231 log.error 

232 

233 # Load custom licenses 

234 init_licenses() 

235 

236 # Init chatgpt summary plugin 

237 init_udc_desc() 

238 

239 log.info("UDC Plugin Loaded!") 

240 

241 def reload_config(self, config: list): 

242 try: 

243 # log.info("tring to load udc config:") 

244 # log.info(config) 

245 all_fields = [] 

246 self.facet_titles.clear() 

247 self.facet_titles_raw.clear() 

248 self.text_fields.clear() 

249 self.date_fields.clear() 

250 self.multiple_select_fields.clear() 

251 self.dropdown_options.clear() 

252 for level in config["maturity_model"]: 

253 for field in level["fields"]: 

254 field_name = field.get("name") 

255 if field.get("ckanField") == "portal_type": 

256 field_name = "portal_type" 

257 

258 if field_name and field.get("name"): 

259 all_fields.append(field_name) 

260 type = field.get("type") 

261 if field_name and ( 

262 type == "" 

263 or type is None 

264 or type == "text" 

265 or type == "single_select" 

266 or type == "multiple_select" 

267 or type == "number" 

268 or type == "date" 

269 ): 

270 self.facet_titles[field_name] = tk._(pick_locale(field["label"], 'en')) 

271 self.facet_titles_raw[field_name] = field["label"] 

272 if field_name and (type == "text" or type is None): 

273 self.text_fields.append(field_name) 

274 if field.get("type") == "date": 

275 self.date_fields.append(field_name) 

276 if field.get("type") == "multiple_select": 

277 self.multiple_select_fields.append(field_name) 

278 

279 # Preload ontologies 

280 if not self.disable_graphdb: 

281 endpoint = tk.config.get("udc.sparql.endpoint") 

282 username = tk.config.get("udc.sparql.username") or None 

283 password = tk.config.get("udc.sparql.password") or None 

284 # This will preload ontologies and 

285 # populate options to the fields that uses 'optionsFromQuery' 

286 preload_ontologies( 

287 config, endpoint, username, password, self.sparql_client 

288 ) 

289 

290 # Store dropdown options 

291 for level in config["maturity_model"]: 

292 for field in level["fields"]: 

293 if ( 

294 field.get("type") == "multiple_select" 

295 or field.get("type") == "single_select" 

296 ): 

297 field_name = field.get("name") 

298 if field.get("ckanField") == "portal_type": 

299 field_name = "portal_type" 

300 if not field_name: 

301 continue 

302 options = self.dropdown_options[field_name] = {} 

303 for option in field["options"]: 

304 options[option["value"]] = tk._(option["text"]) 

305 log.info( 

306 f"Dropdown options for field {field_name} loaded: {len(options.keys())}" 

307 ) 

308 

309 # Update solr index 

310 update_solr_maturity_model_fields(config["maturity_model"]) 

311 

312 # Do not mutate the vars 

313 self.all_fields.clear() 

314 self.all_fields.extend(all_fields) 

315 self.maturity_model.clear() 

316 self.maturity_model.extend(config["maturity_model"]) 

317 self.mappings.clear() 

318 self.mappings.update(config["mappings"]) 

319 self.preload_ontologies.clear() 

320 self.preload_ontologies.update(config["preload_ontologies"]) 

321 

322 except Exception as e: 

323 log.error("UDC Plugin Error:") 

324 traceback.print_exc() 

325 

326 def _modify_package_schema(self, schema: Schema) -> Schema: 

327 """ 

328 Wire CUDC custom fields into CKAN: 

329 - For type="text" custom fields: accept multilingual {lang: value} and store as JSON string in extras. 

330 - For other types (date/number/select/etc.): keep original convert_to_extras for now. 

331 """ 

332 # our custom fields 

333 for field in self.all_fields: 

334 if field in self.text_fields: 

335 # Multilingual text: validate object -> dump JSON -> extras 

336 schema.update( 

337 { 

338 field: [ 

339 tk.get_validator("ignore_missing"), 

340 udc_json_load, 

341 udc_lang_object, # expects {"en": "...", "fr": "..."} (fr optional) 

342 udc_json_dump, # store as JSON string in extras 

343 tk.get_converter("convert_to_extras"), 

344 ], 

345 } 

346 ) 

347 else: 

348 # Non-text 

349 schema.update( 

350 { 

351 field: [ 

352 tk.get_validator("ignore_missing"), 

353 tk.get_converter("convert_to_extras"), 

354 ], 

355 } 

356 ) 

357 

358 # ---- Multilingual core fields (replicate fluent_core_translated) ---- 

359 # title_translated / notes_translated store {lang: value} as JSON in extras, 

360 # and keep `title` / `notes` as the default-locale strings. 

361 

362 schema.update( 

363 { 

364 "title_translated": [ 

365 tk.get_validator("ignore_missing"), 

366 udc_json_load, 

367 udc_seed_translated_from_core( 

368 "title" 

369 ), # seed from core title if missing 

370 udc_lang_object, 

371 udc_core_translated_to_extras("title"), 

372 udc_json_dump, 

373 tk.get_converter("convert_to_extras"), 

374 ], 

375 "notes_translated": [ 

376 tk.get_validator("ignore_missing"), 

377 udc_json_load, 

378 udc_seed_translated_from_core( 

379 "notes" 

380 ), # seed from core notes if missing 

381 udc_lang_object, 

382 udc_core_translated_to_extras("notes"), 

383 udc_json_dump, 

384 tk.get_converter("convert_to_extras"), 

385 ], 

386 # Accept: {"en": ["roads","transport"], "fr": ["routes","transport"]} 

387 "tags_translated": [ 

388 tk.get_validator("ignore_missing"), 

389 udc_json_load, 

390 udc_lang_string_list, 

391 udc_set_core_tags_from_translated, # set core 'tags' from tags_translated[default_lang] 

392 udc_json_dump, 

393 tk.get_converter("convert_to_extras"), 

394 ], 

395 "udc_import_extras": [ 

396 tk.get_validator("ignore_missing"), 

397 udc_json_dump, 

398 tk.get_converter("convert_to_extras"), 

399 ], 

400 } 

401 ) 

402 

403 schema.update( 

404 { 

405 # For import from other portals plugin 

406 "cudc_import_config_id": [ 

407 tk.get_validator("ignore_missing"), 

408 tk.get_converter("convert_to_extras"), 

409 ], 

410 # Keep track of the remote ID from the source portal 

411 "cudc_import_remote_id": [ 

412 tk.get_validator("ignore_missing"), 

413 tk.get_converter("convert_to_extras"), 

414 ], 

415 "is_unified": [ 

416 tk.get_validator("ignore_missing"), 

417 tk.get_validator("boolean_validator"), 

418 tk.get_converter("convert_to_extras"), 

419 ], 

420 "source_last_updated": [ 

421 tk.get_validator("ignore_missing"), 

422 tk.get_converter("convert_to_extras"), 

423 ], 

424 # ---- chatgpt summary ------ 

425 "summary": [ 

426 tk.get_validator("ignore_missing"), 

427 tk.get_converter("convert_to_extras"), 

428 ], 

429 "portal_type": [ 

430 tk.get_validator("ignore_missing"), 

431 tk.get_converter("convert_to_extras"), 

432 ], 

433 } 

434 ) 

435 return schema 

436 

437 def create_package_schema(self) -> Schema: 

438 # let's grab the default schema in our plugin 

439 schema: Schema = super(UdcPlugin, self).create_package_schema() 

440 return self._modify_package_schema(schema) 

441 

442 def update_package_schema(self) -> Schema: 

443 schema: Schema = super(UdcPlugin, self).update_package_schema() 

444 # our custom field 

445 return self._modify_package_schema(schema) 

446 

447 def show_package_schema(self) -> Schema: 

448 schema: Schema = super(UdcPlugin, self).show_package_schema() 

449 for field in self.all_fields: 

450 if field in self.text_fields: 

451 # Multilingual text: load JSON from extras 

452 schema.update( 

453 { 

454 field: [ 

455 tk.get_converter("convert_from_extras"), 

456 udc_json_load, # load JSON string from extras 

457 tk.get_validator("ignore_missing"), 

458 ], 

459 } 

460 ) 

461 else: 

462 # Non-text: load directly from extras 

463 schema.update( 

464 { 

465 field: [ 

466 tk.get_converter("convert_from_extras"), 

467 tk.get_validator("ignore_missing"), 

468 ], 

469 } 

470 ) 

471 

472 # bring translated JSON back and ensure core fields are present 

473 schema.update( 

474 { 

475 "title_translated": [ 

476 tk.get_converter("convert_from_extras"), 

477 udc_json_load, 

478 udc_fill_translated_from_core_on_show("title"), 

479 tk.get_validator("ignore_missing"), 

480 ], 

481 "notes_translated": [ 

482 tk.get_converter("convert_from_extras"), 

483 udc_json_load, 

484 udc_fill_translated_from_core_on_show("notes"), 

485 tk.get_validator("ignore_missing"), 

486 ], 

487 # Return tags_translated as a dict; if missing, seed from core tags 

488 "tags_translated": [ 

489 tk.get_converter("convert_from_extras"), 

490 udc_json_load, 

491 udc_fill_tags_translated_from_core, 

492 tk.get_validator("ignore_missing"), 

493 ], 

494 } 

495 ) 

496 # ensure title/notes fallback to default locale from *_translated on show 

497 schema.setdefault("title", []).append(udc_set_core_from_translated("title")) 

498 schema.setdefault("notes", []).append(udc_set_core_from_translated("notes")) 

499 

500 schema.update( 

501 { 

502 # For import from other portals plugin 

503 "cudc_import_config_id": [ 

504 tk.get_converter("convert_from_extras"), 

505 tk.get_validator("ignore_missing"), 

506 ], 

507 # Keep track of the remote ID from the source portal 

508 "cudc_import_remote_id": [ 

509 tk.get_converter("convert_from_extras"), 

510 tk.get_validator("ignore_missing"), 

511 ], 

512 # For Unified Package: 

513 # If the package is unified, it uses 'unified_has_versions' to link other packages. 

514 # For every other packages that is not unified, use 'potential_duplicates' to link other duplicated packages. 

515 "is_unified": [ 

516 tk.get_converter("convert_from_extras"), 

517 tk.get_validator("ignore_missing"), 

518 ], 

519 "source_last_updated": [ 

520 tk.get_converter("convert_from_extras"), 

521 tk.get_validator("ignore_missing"), 

522 ], 

523 # ---- chatgpt summary ------ 

524 "summary": [ 

525 tk.get_converter("convert_from_extras"), 

526 tk.get_validator("ignore_missing"), 

527 ], 

528 "portal_type": [ 

529 tk.get_converter("convert_from_extras"), 

530 tk.get_validator("ignore_missing"), 

531 ], 

532 # Version relations: decode JSON stored in extras into 

533 # native dict/list structures for templates and Solr indexer. 

534 "version_dataset": [ 

535 tk.get_converter("convert_from_extras"), 

536 udc_json_load, 

537 tk.get_validator("ignore_missing"), 

538 ], 

539 "dataset_versions": [ 

540 tk.get_converter("convert_from_extras"), 

541 udc_json_load, 

542 tk.get_validator("ignore_missing"), 

543 ], 

544 "udc_import_extras": [ 

545 tk.get_converter("convert_from_extras"), 

546 udc_json_load, 

547 tk.get_validator("ignore_missing"), 

548 ], 

549 } 

550 ) 

551 

552 return schema 

553 

554 def get_helpers(self): 

555 langs = get_udc_langs() 

556 language_labels = {} 

557 

558 for code in langs: 

559 label = self._language_label(code, langs[0] if langs else "en") 

560 language_labels[code] = label 

561 

562 return { 

563 "render_markdown": render_markdown, 

564 "config": self.maturity_model, 

565 "maturity_model": self.maturity_model, 

566 "facet_titles": self.facet_titles, 

567 "facet_titles_raw": self.facet_titles_raw, 

568 "maturity_model_text_fields": self.text_fields, 

569 "udc_languages": langs, 

570 "udc_default_lang": langs[0] if langs else "en", 

571 "udc_language_labels": language_labels, 

572 "get_default_facet_titles": get_default_facet_titles, 

573 "process_facets_fields": process_facets_fields, 

574 "humanize_entity_type": humanize_entity_type, 

575 "get_maturity_percentages": get_maturity_percentages, 

576 "get_system_info": get_system_info, 

577 "udc_json_attr": udc_json_attr, 

578 "udc_search_details": udc_search_details, 

579 "license_options_details": license_options_details, 

580 "pick_locale_with_fallback": pick_locale_with_fallback, 

581 } 

582 

583 def _language_label(self, code: str, fallback: str) -> str: 

584 """Return a human readable language name for templates.""" 

585 

586 default_lang = (fallback or tk.config.get("ckan.locale_default", "en") or "en").replace("-", "_") 

587 

588 try: 

589 display_locale = Locale.parse(default_lang) 

590 target = Locale.parse(code.replace("-", "_")) 

591 label = target.get_display_name(display_locale) 

592 if isinstance(label, str) and label: 

593 return label[0].upper() + label[1:] 

594 except Exception: 

595 pass 

596 

597 return code.upper() 

598 

599 def is_fallback(self): 

600 # Return True to register this plugin as the default handler for 

601 # package types not handled by any other IDatasetForm plugin. 

602 return True 

603 

604 def package_types(self): # -> list[str] 

605 # This plugin just registers itself as the 'catalogue'. 

606 return ["dataset", "catalogue"] 

607 

608 def search_template(self, package_type: Optional[str] = None) -> str: 

609 """Return custom search template for catalogue package type.""" 

610 if package_type == "catalogue": 

611 return "package/custom_search.html" 

612 return super().search_template() 

613 

614 def update_config_schema(self, schema: Schema): 

615 

616 ignore_missing = tk.get_validator("ignore_missing") 

617 unicode_safe = tk.get_validator("unicode_safe") 

618 json_object = tk.get_validator("json_object") 

619 

620 schema.update( 

621 { 

622 # This is a custom configuration option 

623 "ckanext.udc.config": [ 

624 ignore_missing, 

625 unicode_safe, 

626 udc_config_validator, 

627 ], 

628 "ckanext.udc.group_side_panel_text": [ignore_missing, unicode_safe], 

629 "ckanext.udc.organization_side_panel_text": [ 

630 ignore_missing, 

631 unicode_safe, 

632 ], 

633 "ckanext.udc.dataset_side_panel_text": [ignore_missing, unicode_safe], 

634 "ckanext.udc.catalogue_side_panel_text": [ignore_missing, unicode_safe], 

635 # ---- chatgpt summary config ------ 

636 "ckanext.udc.desc.config": [ignore_missing, unicode_safe], 

637 "ckanext.udc.maintenance_mode": [ignore_missing, unicode_safe], 

638 } 

639 ) 

640 

641 return schema 

642 

643 def get_actions(self): 

644 """ 

645 Override CKAN's default actions. 

646 """ 

647 return { 

648 "config_option_update": config_option_update, 

649 "package_update": package_update, 

650 "package_delete": package_delete, 

651 # Custom Licenses 

652 "license_create": license_create, 

653 "license_delete": license_delete, 

654 "licenses_get": licenses_get, 

655 "license_update": license_update, 

656 "test_long_task": test_long_task, 

657 # Custom file format 

658 "file_format_create": file_format_create, 

659 "file_format_delete": file_format_delete, 

660 "file_formats_get": file_formats_get, 

661 # Chatgpt summary actions 

662 "summary_generate": summary_generate, 

663 "update_summary": update_summary, 

664 "default_ai_summary_config": default_ai_summary_config, 

665 # System actions 

666 "reload_supervisord": reload_supervisord, 

667 "get_system_stats": get_system_stats, 

668 # Version metadata helper 

669 "udc_version_meta": udc_version_meta, 

670 # "maturity_model_get": get_maturity_model, 

671 # Filters 

672 "filter_facets_get": filter_facets_get, 

673 # User management 

674 "deleted_users_list": deleted_users_list, 

675 "purge_deleted_users": purge_deleted_users, 

676 "udc_user_list": udc_user_list, 

677 "udc_user_reset_password": udc_user_reset_password, 

678 "udc_user_delete": udc_user_delete, 

679 # Organization management 

680 "udc_organization_list": udc_organization_list, 

681 "udc_deleted_organization_list": udc_deleted_organization_list, 

682 "udc_organization_delete": udc_organization_delete, 

683 "udc_purge_deleted_organizations": udc_purge_deleted_organizations, 

684 "udc_organization_packages_list": udc_organization_packages_list, 

685 "udc_organization_packages_ids": udc_organization_packages_ids, 

686 "udc_organization_packages_delete": udc_organization_packages_delete, 

687 } 

688 

689 def get_auth_functions(self): 

690 """ 

691 Register custom authorization functions. 

692 """ 

693 return { 

694 "deleted_users_list": user_auth.deleted_users_list, 

695 "purge_deleted_users": user_auth.purge_deleted_users, 

696 "udc_user_list": user_auth.udc_user_list, 

697 "udc_user_reset_password": user_auth.udc_user_reset_password, 

698 "udc_user_delete": user_auth.udc_user_delete, 

699 "udc_organization_list": organization_auth.udc_organization_list, 

700 "udc_deleted_organization_list": organization_auth.udc_deleted_organization_list, 

701 "udc_organization_delete": organization_auth.udc_organization_delete, 

702 "udc_purge_deleted_organizations": organization_auth.udc_purge_deleted_organizations, 

703 "udc_organization_packages_list": organization_auth.udc_organization_packages_list, 

704 "udc_organization_packages_ids": organization_auth.udc_organization_packages_ids, 

705 "udc_organization_packages_delete": organization_auth.udc_organization_packages_delete, 

706 } 

707 

708 def dataset_facets(self, facets_dict: OrderedDict[str, Any], package_type: str): 

709 for name in self.facet_titles: 

710 if name == "portal_type": 

711 facets_dict[name] = pick_locale(self.facet_titles_raw[name]) 

712 continue 

713 if name in self.text_fields: 

714 # The text fields (solr type=text) can be used as facets 

715 # We need to use the builtin facet name 

716 # and not the extras_ prefix 

717 facets_dict[name] = pick_locale(self.facet_titles_raw[name]) 

718 else: 

719 # We need the extras_ prefix for our custom solr schema 

720 facets_dict["extras_" + name] = pick_locale(self.facet_titles_raw[name]) 

721 

722 return facets_dict 

723 

724 def group_facets( 

725 self, 

726 facets_dict: OrderedDict[str, Any], 

727 group_type: str, 

728 package_type: Optional[str], 

729 ): 

730 return facets_dict 

731 

732 def organization_facets( 

733 self, 

734 facets_dict: OrderedDict[str, Any], 

735 organization_type: str, 

736 package_type: Optional[str], 

737 ): 

738 return facets_dict 

739 

740 def read(self, entity: "model.Package") -> None: 

741 print(chalk.red("create")) 

742 pass 

743 

744 def create(self, entity: "model.Package") -> None: 

745 pass 

746 

747 def edit(self, entity: "model.Package") -> None: 

748 pass 

749 

750 def delete(self, entity: "model.Package") -> None: 

751 pass 

752 

753 def after_dataset_create(self, context: Context, pkg_dict: dict[str, Any]) -> None: 

754 pass 

755 

756 def after_dataset_update(self, context: Context, pkg_dict: dict[str, Any]) -> None: 

757 pass 

758 

759 def after_dataset_delete(self, context: Context, pkg_dict: dict[str, Any]) -> None: 

760 pass 

761 

762 def after_dataset_show(self, context: Context, pkg_dict: dict[str, Any]) -> None: 

763 if context.get("for_update"): 

764 # Avoid injecting related_packages and udc_import_extras during package_update/patch. 

765 pkg_dict.pop("udc_import_extras", None) 

766 return 

767 # Add related packages 

768 related_packages = [] 

769 

770 if pkg_dict.get("is_unified"): 

771 rel = ( 

772 model.meta.Session.query(model.PackageRelationship) 

773 .filter(model.PackageRelationship.subject_package_id == pkg_dict["id"]) 

774 .all() 

775 ) 

776 for r in rel: 

777 related_package = model.Package.get(r.object_package_id) 

778 related_packages.append( 

779 { 

780 "title": related_package.title, 

781 "id": related_package.id, 

782 "name": related_package.name, 

783 } 

784 ) 

785 else: 

786 # Non-unified package need to get the unified package and then get the related packages 

787 rel = ( 

788 model.meta.Session.query(model.PackageRelationship) 

789 .filter(model.PackageRelationship.object_package_id == pkg_dict["id"]) 

790 .all() 

791 ) 

792 

793 for r in rel: 

794 unified_package = model.Package.get(r.subject_package_id) 

795 related_packages.append( 

796 { 

797 "title": unified_package.title, 

798 "id": unified_package.id, 

799 "name": unified_package.name, 

800 } 

801 ) 

802 

803 rel2 = ( 

804 model.meta.Session.query(model.PackageRelationship) 

805 .filter( 

806 model.PackageRelationship.subject_package_id 

807 == unified_package.id 

808 ) 

809 .filter( 

810 model.PackageRelationship.object_package_id != pkg_dict["id"] 

811 ) 

812 .all() 

813 ) 

814 

815 for r in rel2: 

816 related_package = model.Package.get(r.object_package_id) 

817 # Check if exists 

818 if related_package.id not in set( 

819 [p["id"] for p in related_packages] 

820 ): 

821 related_packages.append( 

822 { 

823 "title": related_package.title, 

824 "id": related_package.id, 

825 "name": related_package.name, 

826 } 

827 ) 

828 

829 pkg_dict["related_packages"] = related_packages 

830 

831 def before_dataset_search(self, params: dict[str, Any]) -> dict[str, Any]: 

832 # print(chalk.red("before_dataset_search")) 

833 # print(search_params) 

834 # L = get_current_lang() 

835 # default = get_udc_langs()[0] 

836 

837 # qf = [ 

838 # f"title_{L}_txt^6", 

839 # f"notes_{L}_txt^3", 

840 # # include if you indexed tags_{L}_txt in index.py: 

841 # f"tags_{L}_txt^1", 

842 # ] 

843 # for name in self.text_fields: 

844 # qf.append(f"{name}_{L}_txt^2") 

845 

846 # if L != default: 

847 # qf += [ 

848 # f"title_{default}_txt^2", 

849 # f"notes_{default}_txt^1", 

850 # # f"tags_{default}_txt^0.5", 

851 # ] 

852 # for name in self.text_fields: 

853 # qf.append(f"{name}_{default}_txt^1") 

854 

855 # params['defType'] = 'edismax' 

856 # params['qf'] = ' '.join(qf) 

857 # params.setdefault('pf', f"title_{L}_txt^8") 

858 # params.setdefault('qs', '2') 

859 fq = params.get("fq") or "" 

860 if not _is_catalogue_search_fq(fq): 

861 return params 

862 

863 ext_items = [] 

864 for key, value in (params.get("extras") or {}).items(): 

865 if isinstance(value, list): 

866 ext_items.extend((key, item) for item in value) 

867 else: 

868 ext_items.append((key, value)) 

869 

870 details = get_search_details(ext_items) 

871 if details["fq"]: 

872 params["fq"] = f"{fq} {details['fq']}".strip() 

873 

874 facet_fields = params.get("facet.field") or [] 

875 solr_facet_fields, alias_to_solr = facet_alias_map(facet_fields) 

876 params["facet.field"] = solr_facet_fields 

877 

878 params.setdefault("extras", {})["__udc_facet_alias_to_solr"] = dict(alias_to_solr) 

879 return params 

880 

881 def after_dataset_search( 

882 self, search_results: dict[str, Any], search_params: dict[str, Any] 

883 ) -> dict[str, Any]: 

884 if not _is_catalogue_search_fq(search_params.get("fq") or ""): 

885 return search_results 

886 

887 alias_to_solr = (search_params.get("extras") or {}).get("__udc_facet_alias_to_solr") 

888 if not alias_to_solr: 

889 facet_keys = [ 

890 "organization", 

891 "groups", 

892 "tags", 

893 "res_format", 

894 "license_id", 

895 ] 

896 for name in self.facet_titles: 

897 if name == "portal_type" or name in self.text_fields: 

898 facet_keys.append(name) 

899 else: 

900 facet_keys.append("extras_" + name) 

901 _, alias_to_solr = facet_alias_map(facet_keys) 

902 

903 solr_to_alias = {solr_name: alias for alias, solr_name in alias_to_solr.items()} 

904 

905 raw_facets = search_results.get("search_facets", {}) 

906 stable_facets = {} 

907 for solr_name, payload in raw_facets.items(): 

908 stable_facets[solr_to_alias.get(solr_name, solr_name)] = payload 

909 search_results["search_facets"] = stable_facets 

910 

911 raw_counts = search_results.get("facets", {}) 

912 stable_counts = {} 

913 for solr_name, payload in raw_counts.items(): 

914 stable_counts[solr_to_alias.get(solr_name, solr_name)] = payload 

915 search_results["facets"] = stable_counts 

916 return search_results 

917 

918 def before_dataset_index(self, pkg_dict: dict[str, Any]) -> dict[str, Any]: 

919 return _before_dataset_index(pkg_dict) 

920 

921 def before_dataset_view(self, pkg_dict: dict[str, Any]) -> dict[str, Any]: 

922 return pkg_dict 

923 

924 # IMiddleware 

925 def make_middleware(self, app: CKANApp, config: CKANConfig) -> CKANApp: 

926 override_error_handler(app, config) 

927 return app 

928 

929 def make_error_log_middleware(self, app, config: CKANConfig) -> CKANApp: 

930 return app 

931 

932 def get_validators(self): 

933 return { 

934 "udc_lang_object": udc_lang_object, 

935 "udc_json_dump": udc_json_dump, 

936 "udc_json_load": udc_json_load, 

937 }