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

291 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2026-03-30 22:15 +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, g, 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) 

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

54from ckanext.udc.graph.sparql_client import SparqlClient 

55from ckanext.udc.graph.preload import preload_ontologies 

56from ckanext.udc.graph.logic import get_catalogue_graph 

57from babel import Locale 

58 

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

60 license_create, 

61 license_delete, 

62 licenses_get, 

63 license_update, 

64 init_licenses, 

65 test_long_task, 

66) 

67from ckanext.udc.licenses.utils import license_options_details 

68from ckanext.udc.file_format.logic import ( 

69 file_format_create, 

70 file_format_delete, 

71 file_formats_get, 

72) 

73from ckanext.udc.desc.actions import ( 

74 summary_generate, 

75 update_summary, 

76 default_ai_summary_config, 

77) 

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

79from ckanext.udc.error_handler import override_error_handler 

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

81from ckanext.udc.version.actions import udc_version_meta 

82from ckanext.udc.solr.solr import update_solr_maturity_model_fields 

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

84 

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

86from ckanext.udc.user.actions import ( 

87 deleted_users_list, 

88 purge_deleted_users, 

89 udc_user_list, 

90 udc_user_reset_password, 

91 udc_user_delete, 

92) 

93from ckanext.udc.organization.actions import ( 

94 udc_organization_list, 

95 udc_deleted_organization_list, 

96 udc_organization_delete, 

97 udc_purge_deleted_organizations, 

98 udc_organization_packages_list, 

99 udc_organization_packages_ids, 

100 udc_organization_packages_delete, 

101) 

102from ckanext.udc.organization import auth as organization_auth 

103from ckanext.udc.user import auth as user_auth 

104from .i18n import ( 

105 udc_lang_object, 

106 udc_json_dump, 

107 udc_json_load, 

108 udc_core_translated_to_extras, 

109 udc_set_core_from_translated, 

110 udc_lang_string_list, 

111 udc_set_core_tags_from_translated, 

112 udc_fill_tags_translated_from_core, 

113 udc_seed_translated_from_core, 

114 udc_fill_translated_from_core_on_show, 

115) 

116 

117 

118""" 

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

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

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

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

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

124""" 

125log = logging.getLogger(__name__) 

126 

127# Add UDC CLI 

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

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

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

131 

132 

133@tk.blanket.blueprints 

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

135 plugins.implements(plugins.IConfigurer) 

136 plugins.implements(plugins.IConfigurable) 

137 plugins.implements(plugins.IDatasetForm) 

138 plugins.implements(plugins.ITemplateHelpers) 

139 plugins.implements(plugins.IActions) 

140 plugins.implements(plugins.IAuthFunctions) 

141 plugins.implements(plugins.IFacets) 

142 plugins.implements(plugins.IPackageController) 

143 plugins.implements(plugins.IMiddleware) 

144 plugins.implements(plugins.IBlueprint) 

145 plugins.implements(plugins.IValidators) 

146 plugins.implements(plugins.ITranslation) 

147 

148 disable_graphdb = False 

149 maturity_model = [] 

150 mappings = {} 

151 preload_ontologies = {} 

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

153 facet_titles = {} 

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

155 text_fields: List[str] = [] 

156 date_fields: List[str] = [] 

157 multiple_select_fields: List[str] = [] 

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

159 

160 def update_config(self, config_): 

161 tk.add_template_directory(config_, "templates") 

162 tk.add_public_directory(config_, "public") 

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

164 

165 def _cli_configure(self): 

166 """Partial load for CLI""" 

167 self.disable_graphdb = True 

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

169 if existing_config: 

170 try: 

171 # Call our plugin to update the config 

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

173 except: 

174 log.error 

175 

176 # Load custom licenses 

177 init_licenses() 

178 

179 def configure(self, config: CKANConfig): 

180 log.info(sys.argv) 

181 if not ( 

182 "run" in sys.argv 

183 or "uwsgi" in sys.argv 

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

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

186 or ("translation" in sys.argv) 

187 or ("asset" in sys.argv) 

188 ): 

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

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

191 self._cli_configure() 

192 return 

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

194 # print(existing_config) 

195 

196 # Load sparql client 

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

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

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

200 

201 if endpoint is None: 

202 self.disable_graphdb = True 

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

204 

205 else: 

206 self.sparql_client = SparqlClient( 

207 endpoint, username=username, password=password 

208 ) 

209 if self.sparql_client.test_connecetion(): 

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

211 else: 

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

213 self.disable_graphdb = True 

214 

215 # Load config 

216 if existing_config: 

217 try: 

218 # Call our plugin to update the config 

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

220 except: 

221 log.error 

222 

223 # Load custom licenses 

224 init_licenses() 

225 

226 # Init chatgpt summary plugin 

227 init_udc_desc() 

228 

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

230 

231 def reload_config(self, config: list): 

232 try: 

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

234 # log.info(config) 

235 all_fields = [] 

236 self.facet_titles.clear() 

237 self.facet_titles_raw.clear() 

238 self.text_fields.clear() 

239 self.date_fields.clear() 

240 self.multiple_select_fields.clear() 

241 self.dropdown_options.clear() 

242 for level in config["maturity_model"]: 

243 for field in level["fields"]: 

244 field_name = field.get("name") 

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

246 field_name = "portal_type" 

247 

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

249 all_fields.append(field_name) 

250 type = field.get("type") 

251 if field_name and ( 

252 type == "" 

253 or type is None 

254 or type == "text" 

255 or type == "single_select" 

256 or type == "multiple_select" 

257 or type == "number" 

258 or type == "date" 

259 ): 

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

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

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

263 self.text_fields.append(field_name) 

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

265 self.date_fields.append(field_name) 

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

267 self.multiple_select_fields.append(field_name) 

268 

269 # Preload ontologies 

270 if not self.disable_graphdb: 

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

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

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

274 # This will preload ontologies and 

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

276 preload_ontologies( 

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

278 ) 

279 

280 # Store dropdown options 

281 for level in config["maturity_model"]: 

282 for field in level["fields"]: 

283 if ( 

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

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

286 ): 

287 field_name = field.get("name") 

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

289 field_name = "portal_type" 

290 if not field_name: 

291 continue 

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

293 for option in field["options"]: 

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

295 log.info( 

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

297 ) 

298 

299 # Update solr index 

300 update_solr_maturity_model_fields(config["maturity_model"]) 

301 

302 # Do not mutate the vars 

303 self.all_fields.clear() 

304 self.all_fields.extend(all_fields) 

305 self.maturity_model.clear() 

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

307 self.mappings.clear() 

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

309 self.preload_ontologies.clear() 

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

311 

312 except Exception as e: 

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

314 traceback.print_exc() 

315 

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

317 """ 

318 Wire CUDC custom fields into CKAN: 

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

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

321 """ 

322 # our custom fields 

323 for field in self.all_fields: 

324 if field in self.text_fields: 

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

326 schema.update( 

327 { 

328 field: [ 

329 tk.get_validator("ignore_missing"), 

330 udc_json_load, 

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

332 udc_json_dump, # store as JSON string in extras 

333 tk.get_converter("convert_to_extras"), 

334 ], 

335 } 

336 ) 

337 else: 

338 # Non-text 

339 schema.update( 

340 { 

341 field: [ 

342 tk.get_validator("ignore_missing"), 

343 tk.get_converter("convert_to_extras"), 

344 ], 

345 } 

346 ) 

347 

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

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

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

351 

352 schema.update( 

353 { 

354 "title_translated": [ 

355 tk.get_validator("ignore_missing"), 

356 udc_json_load, 

357 udc_seed_translated_from_core( 

358 "title" 

359 ), # seed from core title if missing 

360 udc_lang_object, 

361 udc_core_translated_to_extras("title"), 

362 udc_json_dump, 

363 tk.get_converter("convert_to_extras"), 

364 ], 

365 "notes_translated": [ 

366 tk.get_validator("ignore_missing"), 

367 udc_json_load, 

368 udc_seed_translated_from_core( 

369 "notes" 

370 ), # seed from core notes if missing 

371 udc_lang_object, 

372 udc_core_translated_to_extras("notes"), 

373 udc_json_dump, 

374 tk.get_converter("convert_to_extras"), 

375 ], 

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

377 "tags_translated": [ 

378 tk.get_validator("ignore_missing"), 

379 udc_json_load, 

380 udc_lang_string_list, 

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

382 udc_json_dump, 

383 tk.get_converter("convert_to_extras"), 

384 ], 

385 "udc_import_extras": [ 

386 tk.get_validator("ignore_missing"), 

387 udc_json_dump, 

388 tk.get_converter("convert_to_extras"), 

389 ], 

390 } 

391 ) 

392 

393 schema.update( 

394 { 

395 # For import from other portals plugin 

396 "cudc_import_config_id": [ 

397 tk.get_validator("ignore_missing"), 

398 tk.get_converter("convert_to_extras"), 

399 ], 

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

401 "cudc_import_remote_id": [ 

402 tk.get_validator("ignore_missing"), 

403 tk.get_converter("convert_to_extras"), 

404 ], 

405 "is_unified": [ 

406 tk.get_validator("ignore_missing"), 

407 tk.get_validator("boolean_validator"), 

408 tk.get_converter("convert_to_extras"), 

409 ], 

410 "source_last_updated": [ 

411 tk.get_validator("ignore_missing"), 

412 tk.get_converter("convert_to_extras"), 

413 ], 

414 # ---- chatgpt summary ------ 

415 "summary": [ 

416 tk.get_validator("ignore_missing"), 

417 tk.get_converter("convert_to_extras"), 

418 ], 

419 "portal_type": [ 

420 tk.get_validator("ignore_missing"), 

421 tk.get_converter("convert_to_extras"), 

422 ], 

423 } 

424 ) 

425 return schema 

426 

427 def create_package_schema(self) -> Schema: 

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

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

430 return self._modify_package_schema(schema) 

431 

432 def update_package_schema(self) -> Schema: 

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

434 # our custom field 

435 return self._modify_package_schema(schema) 

436 

437 def show_package_schema(self) -> Schema: 

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

439 for field in self.all_fields: 

440 if field in self.text_fields: 

441 # Multilingual text: load JSON from extras 

442 schema.update( 

443 { 

444 field: [ 

445 tk.get_converter("convert_from_extras"), 

446 udc_json_load, # load JSON string from extras 

447 tk.get_validator("ignore_missing"), 

448 ], 

449 } 

450 ) 

451 else: 

452 # Non-text: load directly from extras 

453 schema.update( 

454 { 

455 field: [ 

456 tk.get_converter("convert_from_extras"), 

457 tk.get_validator("ignore_missing"), 

458 ], 

459 } 

460 ) 

461 

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

463 schema.update( 

464 { 

465 "title_translated": [ 

466 tk.get_converter("convert_from_extras"), 

467 udc_json_load, 

468 udc_fill_translated_from_core_on_show("title"), 

469 tk.get_validator("ignore_missing"), 

470 ], 

471 "notes_translated": [ 

472 tk.get_converter("convert_from_extras"), 

473 udc_json_load, 

474 udc_fill_translated_from_core_on_show("notes"), 

475 tk.get_validator("ignore_missing"), 

476 ], 

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

478 "tags_translated": [ 

479 tk.get_converter("convert_from_extras"), 

480 udc_json_load, 

481 udc_fill_tags_translated_from_core, 

482 tk.get_validator("ignore_missing"), 

483 ], 

484 } 

485 ) 

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

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

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

489 

490 schema.update( 

491 { 

492 # For import from other portals plugin 

493 "cudc_import_config_id": [ 

494 tk.get_converter("convert_from_extras"), 

495 tk.get_validator("ignore_missing"), 

496 ], 

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

498 "cudc_import_remote_id": [ 

499 tk.get_converter("convert_from_extras"), 

500 tk.get_validator("ignore_missing"), 

501 ], 

502 # For Unified Package: 

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

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

505 "is_unified": [ 

506 tk.get_converter("convert_from_extras"), 

507 tk.get_validator("ignore_missing"), 

508 ], 

509 "source_last_updated": [ 

510 tk.get_converter("convert_from_extras"), 

511 tk.get_validator("ignore_missing"), 

512 ], 

513 # ---- chatgpt summary ------ 

514 "summary": [ 

515 tk.get_converter("convert_from_extras"), 

516 tk.get_validator("ignore_missing"), 

517 ], 

518 "portal_type": [ 

519 tk.get_converter("convert_from_extras"), 

520 tk.get_validator("ignore_missing"), 

521 ], 

522 # Version relations: decode JSON stored in extras into 

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

524 "version_dataset": [ 

525 tk.get_converter("convert_from_extras"), 

526 udc_json_load, 

527 tk.get_validator("ignore_missing"), 

528 ], 

529 "dataset_versions": [ 

530 tk.get_converter("convert_from_extras"), 

531 udc_json_load, 

532 tk.get_validator("ignore_missing"), 

533 ], 

534 "udc_import_extras": [ 

535 tk.get_converter("convert_from_extras"), 

536 udc_json_load, 

537 tk.get_validator("ignore_missing"), 

538 ], 

539 } 

540 ) 

541 

542 return schema 

543 

544 def get_helpers(self): 

545 langs = get_udc_langs() 

546 language_labels = {} 

547 

548 for code in langs: 

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

550 language_labels[code] = label 

551 

552 return { 

553 "render_markdown": render_markdown, 

554 "config": self.maturity_model, 

555 "maturity_model": self.maturity_model, 

556 "facet_titles": self.facet_titles, 

557 "facet_titles_raw": self.facet_titles_raw, 

558 "maturity_model_text_fields": self.text_fields, 

559 "udc_languages": langs, 

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

561 "udc_language_labels": language_labels, 

562 "get_default_facet_titles": get_default_facet_titles, 

563 "process_facets_fields": process_facets_fields, 

564 "humanize_entity_type": humanize_entity_type, 

565 "get_maturity_percentages": get_maturity_percentages, 

566 "get_system_info": get_system_info, 

567 "udc_json_attr": udc_json_attr, 

568 "license_options_details": license_options_details, 

569 "pick_locale_with_fallback": pick_locale_with_fallback, 

570 } 

571 

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

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

574 

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

576 

577 try: 

578 display_locale = Locale.parse(default_lang) 

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

580 label = target.get_display_name(display_locale) 

581 if isinstance(label, str) and label: 

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

583 except Exception: 

584 pass 

585 

586 return code.upper() 

587 

588 def is_fallback(self): 

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

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

591 return True 

592 

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

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

595 return ["dataset", "catalogue"] 

596 

597 def update_config_schema(self, schema: Schema): 

598 

599 ignore_missing = tk.get_validator("ignore_missing") 

600 unicode_safe = tk.get_validator("unicode_safe") 

601 json_object = tk.get_validator("json_object") 

602 

603 schema.update( 

604 { 

605 # This is a custom configuration option 

606 "ckanext.udc.config": [ 

607 ignore_missing, 

608 unicode_safe, 

609 udc_config_validator, 

610 ], 

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

612 "ckanext.udc.organization_side_panel_text": [ 

613 ignore_missing, 

614 unicode_safe, 

615 ], 

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

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

618 # ---- chatgpt summary config ------ 

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

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

621 } 

622 ) 

623 

624 return schema 

625 

626 def get_actions(self): 

627 """ 

628 Override CKAN's default actions. 

629 """ 

630 return { 

631 "config_option_update": config_option_update, 

632 "package_update": package_update, 

633 "package_delete": package_delete, 

634 # Custom Licenses 

635 "license_create": license_create, 

636 "license_delete": license_delete, 

637 "licenses_get": licenses_get, 

638 "license_update": license_update, 

639 "test_long_task": test_long_task, 

640 # Custom file format 

641 "file_format_create": file_format_create, 

642 "file_format_delete": file_format_delete, 

643 "file_formats_get": file_formats_get, 

644 # Chatgpt summary actions 

645 "summary_generate": summary_generate, 

646 "update_summary": update_summary, 

647 "default_ai_summary_config": default_ai_summary_config, 

648 # System actions 

649 "reload_supervisord": reload_supervisord, 

650 "get_system_stats": get_system_stats, 

651 # Version metadata helper 

652 "udc_version_meta": udc_version_meta, 

653 # "maturity_model_get": get_maturity_model, 

654 # Filters 

655 "filter_facets_get": filter_facets_get, 

656 # User management 

657 "deleted_users_list": deleted_users_list, 

658 "purge_deleted_users": purge_deleted_users, 

659 "udc_user_list": udc_user_list, 

660 "udc_user_reset_password": udc_user_reset_password, 

661 "udc_user_delete": udc_user_delete, 

662 # Organization management 

663 "udc_organization_list": udc_organization_list, 

664 "udc_deleted_organization_list": udc_deleted_organization_list, 

665 "udc_organization_delete": udc_organization_delete, 

666 "udc_purge_deleted_organizations": udc_purge_deleted_organizations, 

667 "udc_organization_packages_list": udc_organization_packages_list, 

668 "udc_organization_packages_ids": udc_organization_packages_ids, 

669 "udc_organization_packages_delete": udc_organization_packages_delete, 

670 } 

671 

672 def get_auth_functions(self): 

673 """ 

674 Register custom authorization functions. 

675 """ 

676 return { 

677 "deleted_users_list": user_auth.deleted_users_list, 

678 "purge_deleted_users": user_auth.purge_deleted_users, 

679 "udc_user_list": user_auth.udc_user_list, 

680 "udc_user_reset_password": user_auth.udc_user_reset_password, 

681 "udc_user_delete": user_auth.udc_user_delete, 

682 "udc_organization_list": organization_auth.udc_organization_list, 

683 "udc_deleted_organization_list": organization_auth.udc_deleted_organization_list, 

684 "udc_organization_delete": organization_auth.udc_organization_delete, 

685 "udc_purge_deleted_organizations": organization_auth.udc_purge_deleted_organizations, 

686 "udc_organization_packages_list": organization_auth.udc_organization_packages_list, 

687 "udc_organization_packages_ids": organization_auth.udc_organization_packages_ids, 

688 "udc_organization_packages_delete": organization_auth.udc_organization_packages_delete, 

689 } 

690 

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

692 for name in self.facet_titles: 

693 if name == "portal_type": 

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

695 continue 

696 if name in self.text_fields: 

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

698 # We need to use the builtin facet name 

699 # and not the extras_ prefix 

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

701 else: 

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

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

704 

705 return facets_dict 

706 

707 def group_facets( 

708 self, 

709 facets_dict: OrderedDict[str, Any], 

710 group_type: str, 

711 package_type: Optional[str], 

712 ): 

713 return facets_dict 

714 

715 def organization_facets( 

716 self, 

717 facets_dict: OrderedDict[str, Any], 

718 organization_type: str, 

719 package_type: Optional[str], 

720 ): 

721 return facets_dict 

722 

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

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

725 pass 

726 

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

728 pass 

729 

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

731 pass 

732 

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

734 pass 

735 

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

737 pass 

738 

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

740 pass 

741 

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

743 pass 

744 

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

746 if context.get("for_update"): 

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

748 pkg_dict.pop("udc_import_extras", None) 

749 return 

750 # Add related packages 

751 related_packages = [] 

752 

753 if pkg_dict.get("is_unified"): 

754 rel = ( 

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

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

757 .all() 

758 ) 

759 for r in rel: 

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

761 related_packages.append( 

762 { 

763 "title": related_package.title, 

764 "id": related_package.id, 

765 "name": related_package.name, 

766 } 

767 ) 

768 else: 

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

770 rel = ( 

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

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

773 .all() 

774 ) 

775 

776 for r in rel: 

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

778 related_packages.append( 

779 { 

780 "title": unified_package.title, 

781 "id": unified_package.id, 

782 "name": unified_package.name, 

783 } 

784 ) 

785 

786 rel2 = ( 

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

788 .filter( 

789 model.PackageRelationship.subject_package_id 

790 == unified_package.id 

791 ) 

792 .filter( 

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

794 ) 

795 .all() 

796 ) 

797 

798 for r in rel2: 

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

800 # Check if exists 

801 if related_package.id not in set( 

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

803 ): 

804 related_packages.append( 

805 { 

806 "title": related_package.title, 

807 "id": related_package.id, 

808 "name": related_package.name, 

809 } 

810 ) 

811 

812 pkg_dict["related_packages"] = related_packages 

813 

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

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

816 # print(search_params) 

817 # L = get_current_lang() 

818 # default = get_udc_langs()[0] 

819 

820 # qf = [ 

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

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

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

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

825 # ] 

826 # for name in self.text_fields: 

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

828 

829 # if L != default: 

830 # qf += [ 

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

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

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

834 # ] 

835 # for name in self.text_fields: 

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

837 

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

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

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

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

842 return params 

843 

844 def after_dataset_search( 

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

846 ) -> dict[str, Any]: 

847 return search_results 

848 

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

850 return _before_dataset_index(pkg_dict) 

851 

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

853 return pkg_dict 

854 

855 # IMiddleware 

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

857 override_error_handler(app, config) 

858 return app 

859 

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

861 return app 

862 

863 def get_validators(self): 

864 return { 

865 "udc_lang_object": udc_lang_object, 

866 "udc_json_dump": udc_json_dump, 

867 "udc_json_load": udc_json_load, 

868 }