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
« prev ^ index » next coverage.py v7.7.1, created at 2026-06-23 19:31 +0000
1from __future__ import annotations
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
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
36from flask import Blueprint
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
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
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)
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__)
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
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)
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)
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]] = {}
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")
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
186 # Load custom licenses
187 init_licenses()
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)
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
211 if endpoint is None:
212 self.disable_graphdb = True
213 log.info("No GraphDB Endpoint is provided.")
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
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
233 # Load custom licenses
234 init_licenses()
236 # Init chatgpt summary plugin
237 init_udc_desc()
239 log.info("UDC Plugin Loaded!")
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"
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)
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 )
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 )
309 # Update solr index
310 update_solr_maturity_model_fields(config["maturity_model"])
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"])
322 except Exception as e:
323 log.error("UDC Plugin Error:")
324 traceback.print_exc()
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 )
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.
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 )
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
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)
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)
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 )
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"))
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 )
552 return schema
554 def get_helpers(self):
555 langs = get_udc_langs()
556 language_labels = {}
558 for code in langs:
559 label = self._language_label(code, langs[0] if langs else "en")
560 language_labels[code] = label
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 }
583 def _language_label(self, code: str, fallback: str) -> str:
584 """Return a human readable language name for templates."""
586 default_lang = (fallback or tk.config.get("ckan.locale_default", "en") or "en").replace("-", "_")
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
597 return code.upper()
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
604 def package_types(self): # -> list[str]
605 # This plugin just registers itself as the 'catalogue'.
606 return ["dataset", "catalogue"]
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()
614 def update_config_schema(self, schema: Schema):
616 ignore_missing = tk.get_validator("ignore_missing")
617 unicode_safe = tk.get_validator("unicode_safe")
618 json_object = tk.get_validator("json_object")
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 )
641 return schema
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 }
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 }
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])
722 return facets_dict
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
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
740 def read(self, entity: "model.Package") -> None:
741 print(chalk.red("create"))
742 pass
744 def create(self, entity: "model.Package") -> None:
745 pass
747 def edit(self, entity: "model.Package") -> None:
748 pass
750 def delete(self, entity: "model.Package") -> None:
751 pass
753 def after_dataset_create(self, context: Context, pkg_dict: dict[str, Any]) -> None:
754 pass
756 def after_dataset_update(self, context: Context, pkg_dict: dict[str, Any]) -> None:
757 pass
759 def after_dataset_delete(self, context: Context, pkg_dict: dict[str, Any]) -> None:
760 pass
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 = []
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 )
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 )
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 )
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 )
829 pkg_dict["related_packages"] = related_packages
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]
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")
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")
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
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))
870 details = get_search_details(ext_items)
871 if details["fq"]:
872 params["fq"] = f"{fq} {details['fq']}".strip()
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
878 params.setdefault("extras", {})["__udc_facet_alias_to_solr"] = dict(alias_to_solr)
879 return params
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
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)
903 solr_to_alias = {solr_name: alias for alias, solr_name in alias_to_solr.items()}
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
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
918 def before_dataset_index(self, pkg_dict: dict[str, Any]) -> dict[str, Any]:
919 return _before_dataset_index(pkg_dict)
921 def before_dataset_view(self, pkg_dict: dict[str, Any]) -> dict[str, Any]:
922 return pkg_dict
924 # IMiddleware
925 def make_middleware(self, app: CKANApp, config: CKANConfig) -> CKANApp:
926 override_error_handler(app, config)
927 return app
929 def make_error_log_middleware(self, app, config: CKANConfig) -> CKANApp:
930 return app
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 }