Coverage for ckanext/udc/helpers.py: 65%
175 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 json
4import traceback
5import re
6from collections import OrderedDict
7from typing import Any, Callable, Collection, KeysView, Optional, Union, cast
9from ckan.types import Schema, Context
10from ckan.common import _
11import ckan
12import ckan.plugins as plugins
13import ckan.logic as logic
14import ckan.model as model
15import ckan.plugins.toolkit as tk
16from ckan.plugins.toolkit import (chained_action, side_effect_free, chained_helper)
17import ckan.lib.helpers as h
18from ckan.common import current_user, _
20from .graph.logic import onUpdateCatalogue, onDeleteCatalogue, get_catalogue_graph
21from .search.params import get_search_details
22from ckanext.udc.file_format.logic import before_package_update as before_package_update_for_file_format
24import logging
25import json
26import chalk
28log = logging.getLogger(__name__)
31import time
33# Register a chained action after `config_option_update(...)` is triggered, i.e. config is saved from the settings page.
34# We need to reload the UDC plugin to make sure the maturity model is up to date.
35@side_effect_free
36@chained_action
37def config_option_update(original_action, context, data_dict):
38 try:
39 # Call our plugin to update the config
40 log.info("config_option_update: Update UDC Config")
41 plugins.get_plugin('udc').reload_config(
42 json.loads(data_dict["ckanext.udc.config"]))
43 except:
44 log.error
46 res = original_action(context, data_dict)
47 return res
49@side_effect_free
50@chained_action
51def package_update(original_action, context, data_dict):
52 # Pre-process custom file format
53 before_package_update_for_file_format(context, data_dict)
55 result = original_action(context, data_dict)
56 try:
57 if not plugins.get_plugin('udc').disable_graphdb:
58 onUpdateCatalogue(context, result)
59 except Exception as e:
60 log.error(e)
61 print(e)
62 raise logic.ValidationError([_("Error occurred in updating the knowledge graph, please contact administrator:\n") + str(e)])
63 return result
65@side_effect_free
66@chained_action
67def package_delete(original_action, context, data_dict):
68 print(f"Package Delete: ", data_dict)
69 result = original_action(context, data_dict)
70 try:
71 if not plugins.get_plugin('udc').disable_graphdb:
72 onDeleteCatalogue(context, data_dict)
73 except Exception as e:
74 log.error(e)
75 print(e)
76 raise logic.ValidationError([_("Error occurred in updating the knowledge graph, please contact administrator:\n") + str(e)])
77 return result
80# Register a chained helpers for humanize_entity_type() to change labels.
81@chained_helper
82def humanize_entity_type(next_helper: Callable[..., Any],
83 entity_type: str, object_type: str, purpose: str):
85 if (entity_type, object_type) == ("package", "catalogue"):
86 if purpose == "main nav":
87 return _("Catalogue")
88 elif purpose == "search placeholder":
89 return _("Search Catalogue Entries")
90 elif purpose == "search_placeholder":
91 # Don't know where is this used.
92 return _("Catalogue Entry")
93 elif purpose == "create title":
94 return _("Create Catalogue Entry")
95 elif purpose == "create label":
96 return _("Create Catalogue Entry")
97 elif purpose == "add link":
98 return _("Add Catalogue Entry")
99 elif purpose == "no description":
100 return _("There is no description for this catalogue entry")
101 elif purpose == "view label":
102 return _("View Catalogue Entry")
105 original_text = next_helper(entity_type, object_type, purpose)
106 # print(entity_type, object_type, purpose, original_text)
108 return original_text
111def render_markdown(data: str, **kwargs):
112 allow_html = tk.asbool(tk.config.get("ckanext.udc.render_markdown_allow_html", False))
113 kwargs.setdefault("allow_html", allow_html)
114 return h.render_markdown(data, **kwargs)
117def get_default_facet_titles():
118 facets: dict[str, str] = OrderedDict()
120 # Copied from ckan.views.dataset.search
121 org_label = h.humanize_entity_type(
122 u'organization',
123 h.default_group_type(u'organization'),
124 u'facet label') or _(u'Organizations')
126 group_label = h.humanize_entity_type(
127 u'group',
128 h.default_group_type(u'group'),
129 u'facet label') or _(u'Groups')
131 default_facet_titles = {
132 u'organization': org_label,
133 u'groups': group_label,
134 u'tags': _(u'Tags'),
135 u'res_format': _(u'Formats'),
136 u'license_id': _(u'Licenses'),
137 }
139 for facet in h.facets():
140 if facet in default_facet_titles:
141 facets[facet] = default_facet_titles[facet]
142 else:
143 facets[facet] = facet
145 # Facet titles
146 for plugin in plugins.PluginImplementations(plugins.IFacets):
147 facets = plugin.dataset_facets(facets, "catalogue")
148 return facets
150def process_facets_fields(facets_fields: dict):
151 """For search page displaying search filters"""
152 results = {}
153 for field in facets_fields:
154 if field.startswith("filter-logic"):
155 continue
157 field_value = facets_fields[field]
158 field_name = field_value.get("ui") if isinstance(field_value, dict) else None
159 if field_name:
160 pass
161 elif field.startswith("extras_"):
162 field_name = field[7:]
163 elif field.endswith("_ngram"):
164 field_name = field[:-6]
165 else:
166 field_name = field
168 if field_name not in results:
169 results[field_name] = {"logic": "or", "values": []}
171 if isinstance(field_value, list):
172 for item in field_value:
173 results[field_name]["values"].append({
174 "ori_field": field,
175 "ori_value": item,
176 "value": item,
177 })
178 continue
180 if isinstance(field_value, dict) and 'values' in field_value:
181 values = field_value['values']
182 is_fts = field_value.get('fts', False)
183 params = field_value.get("params", [])
184 for index, item in enumerate(values):
185 results[field_name]["values"].append({
186 "ori_field": params[index] if index < len(params) else field,
187 "ori_value": item,
188 "value": f'Search for "{item}"' if is_fts else item,
189 })
190 elif isinstance(field_value, dict):
191 # Date or number ranges
192 min = field_value.get('min')
193 max = field_value.get('max')
195 if min:
196 results[field_name]["values"].append({
197 "ori_field": field_value.get("min_param", "min_" + field_name),
198 "ori_value": min,
199 "value": f"From: {min}",
200 })
201 if max:
202 results[field_name]["values"].append({
203 "ori_field": field_value.get("max_param", "max_" + field_name),
204 "ori_value": max,
205 "value": f"To: {max}",
206 })
209 if "filter-logic-" + field in facets_fields and facets_fields["filter-logic-" + field][0] == "and":
210 results[field_name]["logic"] = "and"
212 return results
214def udc_search_details(default_fields: Optional[dict] = None):
215 details = get_search_details()
216 fields_grouped = {}
217 if isinstance(default_fields, dict):
218 fields_grouped.update(default_fields)
219 fields_grouped.update(details["fields_grouped"])
220 details["fields_grouped"] = fields_grouped
221 return details
223def get_maturity_percentages(config, pkg_dict):
224 percentages = []
225 for idx, level in enumerate(config):
226 num_not_empty = 0
227 total_size = 0
228 for field in level["fields"]:
229 if field.get("ckanField"):
230 # Skip custom_fields
231 if field.get("ckanField") in ['custom_fields']:
232 continue
233 # organization_and_visibility is always filled
234 if field.get("ckanField") == 'organization_and_visibility':
235 num_not_empty += 2
236 total_size += 1
237 # `description` is stored as `notes`
238 elif field.get("ckanField") == 'description' and pkg_dict.get("notes"):
239 num_not_empty += 1
240 # `source` is stored as `url`
241 elif field.get("ckanField") == 'source' and pkg_dict.get("url"):
242 num_not_empty += 1
243 elif pkg_dict.get(field["ckanField"]):
244 num_not_empty += 1
245 else:
246 if field.get("name") and field.get("label") and pkg_dict.get(field["name"]):
247 num_not_empty += 1
249 total_size += 1
250 percentages.append(str(round(num_not_empty / total_size * 100)) + "%")
252 return percentages
255def get_system_info(name: str):
256 return model.system_info.get_system_info(name)
259def udc_json_attr(value):
260 """Return a JSON string safe to embed in an HTML attribute.
262 - If value is already a string, assume it is JSON (or plain text) and
263 return it as-is; Jinja's autoescape will handle HTML encoding.
264 - If value is a dict/list/etc, json.dumps it to a compact string.
265 """
267 if value is None:
268 return ""
269 if isinstance(value, str):
270 return value
271 try:
272 return json.dumps(value, ensure_ascii=False)
273 except Exception:
274 return ""