Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,10 @@ __pycache__

*.egg-info

venv
venv
.idea/.gitignore
.idea/ckanext-gdi-userportal.iml
.idea/misc.xml
.idea/modules.xml
.idea/vcs.xml
.idea/inspectionProfiles/profiles_settings.xml
119 changes: 119 additions & 0 deletions ckanext/gdi_userportal/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Template helpers for ckanext-gdi-userportal."""

from __future__ import annotations

from collections.abc import Iterable
from typing import Any

import ckan.plugins.toolkit as tk


def _ensure_data_dict(data: dict[str, Any] | None, package_id: str | None) -> dict[str, Any]:
"""Return a dataset dict for the current helper call.
When a dataset dict is not provided but an id is, fetch it via the
``package_show`` action. Failures are ignored so the helper still returns
a sensible default instead of aborting rendering.
"""
if data is not None:
return data

if not package_id:
return {}

try:
return tk.get_action("package_show")(
{"ignore_auth": True}, {"id": package_id}
)
except (tk.ObjectNotFound, tk.NotAuthorized):
return {}


def _value_from_extras(data_dict: dict[str, Any], field_name: str) -> Any:
extras = data_dict.get("extras")
if isinstance(extras, dict):
return extras.get(field_name)
if isinstance(extras, Iterable):
for extra in extras:
if not isinstance(extra, dict):
continue
if extra.get("key") == field_name:
return extra.get("value")
return None


def _extract_field_value(field: dict[str, Any], data_dict: dict[str, Any]) -> Any:
field_name = field.get("field_name")
if not field_name or not data_dict:
return None

if field_name in data_dict:
return data_dict[field_name]

return _value_from_extras(data_dict, field_name)


def _is_missing_value(value: Any) -> bool:
if value is None:
return True

if isinstance(value, str):
return value.strip() == ""

if isinstance(value, dict):
if not value:
return True
return all(_is_missing_value(v) for v in value.values())

if isinstance(value, (list, tuple, set)):
if not value:
return True
return all(_is_missing_value(v) for v in value)

return False


def scheming_missing_required_fields(
pages: list[dict[str, Any]],
data: dict[str, Any] | None = None,
package_id: str | None = None,
) -> list[list[str]]:
"""Return a list of missing required fields grouped per form page.
This helper acts as the base implementation expected by
``ckanext-fluent``. It mirrors the behaviour from the forked
ckanext-scheming version previously used in this project and makes sure
chained helpers can extend the result again.
"""
data_dict = _ensure_data_dict(data, package_id)

missing_per_page: list[list[str]] = []

for page in pages or []:
page_missing: list[str] = []
for field in page.get("fields", []):
# Ignore non-required fields early.
if not tk.h.scheming_field_required(field):
continue

value = _extract_field_value(field, data_dict)

# Repeating subfields can contain a list of child values; treat the
# field as present when at least one entry contains data.
if field.get("repeating_subfields") and isinstance(value, list):
if any(not _is_missing_value(item) for item in value):
continue
elif not _is_missing_value(value):
continue

field_name = field.get("field_name")
if field_name:
page_missing.append(field_name)

missing_per_page.append(page_missing)

return missing_per_page


def get_helpers() -> dict[str, Any]:
return {"scheming_missing_required_fields": scheming_missing_required_fields}
5 changes: 3 additions & 2 deletions ckanext/gdi_userportal/logic/action/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ def enhanced_package_search(context, data_dict) -> Dict:
lang = toolkit.request.headers.get("Accept-Language")
translations = get_translations(values_to_translate, lang=lang)
result["results"] = [
replace_package(package, translations) for package in result["results"]
replace_package(package, translations, lang=lang)
for package in result["results"]
]
if "search_facets" in result.keys():
result["search_facets"] = replace_search_facets(
Expand All @@ -43,4 +44,4 @@ def enhanced_package_show(context, data_dict) -> Dict:
values_to_translate = collect_values_to_translate(result)
lang = toolkit.request.headers.get("Accept-Language")
translations = get_translations(values_to_translate, lang=lang)
return replace_package(result, translations)
return replace_package(result, translations, lang=lang)
8 changes: 5 additions & 3 deletions ckanext/gdi_userportal/logic/action/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ def enhanced_package_search(context, data_dict=None) -> Dict:
try:
result = toolkit.get_action("package_search")(context, data_dict)
values_to_translate = collect_values_to_translate(result)
translations = get_translations(values_to_translate)
lang = toolkit.request.headers.get("Accept-Language")
translations = get_translations(values_to_translate, lang=lang)

result["results"] = [
replace_package(package, translations) for package in result["results"]
replace_package(package, translations, lang=lang)
for package in result["results"]
]

if "search_facets" in result:
result["search_facets"] = replace_search_facets(
result["search_facets"], translations
result["search_facets"], translations, lang=lang
)

return result
Expand Down
79 changes: 76 additions & 3 deletions ckanext/gdi_userportal/logic/action/translation_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from dataclasses import dataclass
import logging
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional

from ckan.common import config, request

Expand All @@ -23,6 +23,13 @@
"dcat_type",
]
RESOURCE_REPLACE_FIELDS = ["format", "language"]
TRANSLATED_SUFFIX = "_translated"
LANGUAGE_VALUE_FIELDS = {
"population_coverage",
"publisher_note",
"provenance",
"rights",
}
DEFAULT_FALLBACK_LANG = "en"
SUPPORTED_LANGUAGES = {DEFAULT_FALLBACK_LANG, "nl"}

Expand Down Expand Up @@ -121,13 +128,18 @@ def collect_values_to_translate(data: Any) -> List:
return list(set(values_to_translate))


def replace_package(data, translation_dict):
def replace_package(data, translation_dict, lang: Optional[str] = None):
preferred_lang = _get_language(lang)

_apply_translated_properties(data, preferred_lang)

data = _translate_fields(data, PACKAGE_REPLACE_FIELDS, translation_dict)
resources = data.get("resources", [])
data["resources"] = [
_translate_fields(item, RESOURCE_REPLACE_FIELDS, translation_dict)
for item in resources
]

return data


Expand Down Expand Up @@ -156,11 +168,72 @@ def _change_facet(facet, translation_dict):


def replace_search_facets(data, translation_dict, lang):
preferred_lang = _get_language(lang)
new_facets = {}
for key, facet in data.items():
title = facet["title"]
new_facets[key] = {"title": get_translations([title], lang=lang).get(title, title)}
new_facets[key] = {
"title": get_translations([title], lang=preferred_lang).get(title, title)
}
new_facets[key]["items"] = [
_change_facet(item, translation_dict) for item in facet["items"]
]
return new_facets


def _apply_translated_properties(data: Any, preferred_lang: str, fallback_lang: str = DEFAULT_FALLBACK_LANG):
if isinstance(data, dict):
for key, value in list(data.items()):
if isinstance(value, dict):
_apply_translated_properties(value, preferred_lang, fallback_lang)
elif isinstance(value, list):
data[key] = [
_apply_translated_properties(item, preferred_lang, fallback_lang)
if isinstance(item, (dict, list))
else item
for item in value
]

for key, value in list(data.items()):
if key.endswith(TRANSLATED_SUFFIX) and isinstance(value, dict):
base_key = key[:-len(TRANSLATED_SUFFIX)]
data[base_key] = _select_translated_value(value, preferred_lang, fallback_lang)
elif key in LANGUAGE_VALUE_FIELDS and isinstance(value, dict):
data[key] = _select_translated_value(value, preferred_lang, fallback_lang)
return data

if isinstance(data, list):
return [
_apply_translated_properties(item, preferred_lang, fallback_lang)
if isinstance(item, (dict, list))
else item
for item in data
]

return data


def _select_translated_value(values: Dict[str, Any], preferred_lang: str, fallback_lang: str) -> Any:
if not isinstance(values, dict):
return values

for lang in (preferred_lang, fallback_lang):
translated = values.get(lang)
if _has_content(translated):
return translated

for translated in values.values():
if _has_content(translated):
return translated

return next(iter(values.values()), "")


def _has_content(value: Any) -> bool:
if value is None:
return False
if isinstance(value, str):
return bool(value.strip())
if isinstance(value, (list, dict)):
return bool(value)
return True
5 changes: 5 additions & 0 deletions ckanext/gdi_userportal/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import ckan.plugins as plugins
import ckan.plugins.toolkit as toolkit
from ckanext.gdi_userportal.helpers import get_helpers as get_portal_helpers
from ckanext.gdi_userportal.logic.action.get import (
enhanced_package_search,
enhanced_package_show,
Expand Down Expand Up @@ -50,6 +51,7 @@ class GdiUserPortalPlugin(plugins.SingletonPlugin):
plugins.implements(plugins.IActions)
plugins.implements(plugins.IPackageController)
plugins.implements(plugins.IValidators)
plugins.implements(plugins.ITemplateHelpers, inherit=True)
plugins.implements(plugins.IMiddleware, inherit=True)
plugins.implements(plugins.IConfigurable, inherit=True)

Expand Down Expand Up @@ -105,6 +107,9 @@ def get_actions(self):
"enhanced_package_show": enhanced_package_show,
}

def get_helpers(self):
return get_portal_helpers()

def read(self, entity):
pass

Expand Down
Loading