Skip to content

Commit 0247133

Browse files
committed
[Fixes #13043] Metadata editor: allow customization of error messages
1 parent 8eb10ca commit 0247133

File tree

8 files changed

+209
-97
lines changed

8 files changed

+209
-97
lines changed

geonode/metadata/api/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def schema_instance(self, request, pk=None):
111111
# logger.debug(f"handling content {json.dumps(request.data, indent=3)}")
112112
# except Exception as e:
113113
# logger.warning(f"Can't parse JSON {request.data}: {e}")
114-
errors = metadata_manager.update_schema_instance(resource, request.data)
114+
errors = metadata_manager.update_schema_instance(resource, request.data, lang)
115115

116116
msg_t = (
117117
("m_metadata_update_error", "Some errors were found while updating the resource")

geonode/metadata/handlers/abstract.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
import logging
2121
from abc import ABCMeta, abstractmethod
22+
from collections import defaultdict
23+
2224
from typing_extensions import deprecated
2325

2426
from django.utils.translation import gettext as _
@@ -129,20 +131,30 @@ def _add_after(self, jsonschema, after_what, property_name, subschema):
129131

130132
@staticmethod
131133
def _set_error(errors: dict, path: list, msg: str):
132-
logger.info(f"Reported message: {'/'.join(path)}: {msg} ")
134+
logger.info(f"Setting error: {'/'.join(path)}: {msg}")
133135
elem = errors
134136
for step in path:
135137
elem = elem.setdefault(step, {})
136138
elem = elem.setdefault("__errors", [])
137139
elem.append(msg)
138140

141+
@staticmethod
142+
def localize_message(context: dict, msg_code: str, msg_info: dict):
143+
msg_loc: str = MetadataHandler._get_tkl_labels(context, None, msg_code)
144+
if msg_loc:
145+
tokens = defaultdict(lambda: "N/A", msg_info or {})
146+
return msg_loc.format_map(tokens)
147+
else:
148+
logger.warning(f"Missing i18n entry for key '{msg_code}' -- info is {msg_info}")
149+
return f"{msg_code}:{msg_info}"
150+
139151
@staticmethod
140152
def _localize_label(context, lang: str, text: str):
141153
label = MetadataHandler._get_tkl_labels(context, lang, text)
142154
return label if label else _(text)
143155

144156
@staticmethod
145-
def _get_tkl_labels(context, lang: str, text: str):
157+
def _get_tkl_labels(context, lang: str | None, text: str):
146158
return context["labels"].get(text, None)
147159

148160
@staticmethod

geonode/metadata/handlers/base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,8 @@ def update_resource(self, resource, field_name, json_instance, context, errors,
229229
setattr(resource, field_name, field_value)
230230
except Exception as e:
231231
logger.warning(f"Error setting field {field_name}={field_value}: {e}")
232-
self._set_error(errors, [field_name], "Error while storing field. Contact your administrator")
232+
self._set_error(
233+
errors,
234+
[field_name],
235+
self.localize_message(context, "metadata_error_store", {"fieldname": field_name, "exc": e}),
236+
)

geonode/metadata/handlers/contact.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,13 @@ def update_resource(self, resource, field_name, json_instance, context, errors,
142142
if rolename == Roles.OWNER.name:
143143
if not users:
144144
logger.warning(f"User not specified for role '{rolename}'")
145-
self._set_error(errors, ["contacts", rolename], f"User not specified for role '{rolename}'")
145+
self._set_error(
146+
errors,
147+
["contacts", rolename],
148+
self.localize_message(
149+
context, "metadata_contact_error_missing_role", {"fieldname": field_name, "role": rolename}
150+
),
151+
)
146152
else:
147153
resource.owner = get_user_model().objects.get(pk=users["id"])
148154
else:

geonode/metadata/handlers/sparse.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,22 +159,46 @@ def update_resource(self, resource, field_name, json_instance, context, errors,
159159
field_value = str(float(bare_value)) if bare_value is not None else None
160160
except ValueError as e:
161161
logger.warning(f"Error parsing sparse field '{field_name}'::'{field_type}'='{bare_value}': {e}")
162-
self._set_error(errors, [field_name], f"Error parsing number '{bare_value}'")
162+
self._set_error(
163+
errors,
164+
[field_name],
165+
self.localize_message(
166+
context,
167+
"metadata_sparse_error_parse",
168+
{"fieldname": field_name, "type": "number", "value": bare_value},
169+
),
170+
)
171+
163172
return
164173
elif self._check_type(field_type, "integer"):
165174
try:
166175
field_value = str(int(bare_value)) if bare_value is not None else None
167176
except ValueError as e:
168177
logger.warning(f"Error parsing sparse field '{field_name}'::'{field_type}'='{bare_value}': {e}")
169-
self._set_error(errors, [field_name], f"Error parsing integer '{bare_value}'")
178+
self._set_error(
179+
errors,
180+
[field_name],
181+
self.localize_message(
182+
context,
183+
"metadata_sparse_error_parse",
184+
{"fieldname": field_name, "type": "integer", "value": bare_value},
185+
),
186+
)
187+
170188
return
171189
elif field_type == "array":
172190
field_value = json.dumps(bare_value) if bare_value is not None else "[]"
173191
elif field_type == "object":
174192
field_value = json.dumps(bare_value) if bare_value is not None else "{}"
175193
else:
176194
logger.warning(f"Unhandled type '{field_type}' for sparse field '{field_name}'")
177-
self._set_error(errors, [field_name], f"Unhandled type {field_type}. Contact your administrator")
195+
self._set_error(
196+
errors,
197+
[field_name],
198+
self.localize_message(
199+
context, "metadata_sparse_error_type", {"fieldname": field_name, "type": field_type}
200+
),
201+
)
178202
return
179203

180204
try:
@@ -185,8 +209,16 @@ def update_resource(self, resource, field_name, json_instance, context, errors,
185209
elif is_nullable:
186210
SparseField.objects.filter(resource=resource, name=field_name).delete()
187211
else:
188-
self._set_error(errors, [field_name], f"Empty value not stored for field '{field_name}'")
212+
self._set_error(
213+
errors,
214+
[field_name],
215+
self.localize_message(context, "metadata_error_empty_field", {"fieldname": field_name}),
216+
)
189217
logger.debug(f"Not setting null value for {field_name}")
190218
except Exception as e:
191219
logger.warning(f"Error setting field {field_name}={field_value}: {e}")
192-
self._set_error(errors, [field_name], f"Error setting value: {e}")
220+
self._set_error(
221+
errors,
222+
[field_name],
223+
self.localize_message(context, "metadata_error_store", {"fieldname": field_name, "exc": e}),
224+
)

geonode/metadata/i18n.py

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import logging
2+
from datetime import datetime
23

4+
from cachetools import FIFOCache
35
from django.db import connection
46

5-
from geonode.base.models import ThesaurusKeywordLabel
7+
from geonode.base.models import ThesaurusKeywordLabel, Thesaurus
8+
69

710
logger = logging.getLogger(__name__)
811

@@ -54,10 +57,6 @@ def get_localized_tkeywords(lang, thesaurus_identifier: str):
5457
return sorted(ret.values(), key=lambda i: i["label"].lower())
5558

5659

57-
def get_localized_labels(lang, key="about"):
58-
return {i[key]: i["label"] for i in get_localized_tkeywords(lang, I18N_THESAURUS_IDENTIFIER)}
59-
60-
6160
def get_localized_label(lang, about):
6261
return (
6362
ThesaurusKeywordLabel.objects.filter(
@@ -66,3 +65,100 @@ def get_localized_label(lang, about):
6665
.values_list("label", flat=True)
6766
.first()
6867
)
68+
69+
70+
class I18nCache:
71+
72+
DATA_KEY_SCHEMA = "schema"
73+
DATA_KEY_LABELS = "labels"
74+
75+
def __init__(self):
76+
# the cache has the lang as key, and various info in the dict value:
77+
# - date: the date field of the thesaurus when it was last loaded, it's used for the expiration check
78+
# - labels: the keyword labels from the i18n thesaurus
79+
# - schema: the localized json schema
80+
# FIFO bc we want to renew the data once in a while
81+
self.cache = FIFOCache(16)
82+
83+
def get_entry(self, lang, data_key):
84+
"""
85+
returns date:str, data
86+
date is needed for checking the entry freshness when setting info
87+
data may be None if not cached or expired
88+
"""
89+
cached_entry = self.cache.get(lang, None)
90+
91+
thesaurus_date = ( # may be none if thesaurus does not exist
92+
Thesaurus.objects.filter(identifier=I18N_THESAURUS_IDENTIFIER).values_list("date", flat=True).first()
93+
)
94+
if cached_entry:
95+
if thesaurus_date == cached_entry["date"]:
96+
# only return cached data if thesaurus has not been modified
97+
return thesaurus_date, cached_entry.get(data_key, None)
98+
else:
99+
logger.info(f"Schema for {lang}:{data_key} needs to be recreated")
100+
101+
return thesaurus_date, None
102+
103+
def set(self, lang: str, data_key: str, data: dict, request_date: str):
104+
cached_entry: dict = self.cache.setdefault(lang, {})
105+
106+
latest_date = (
107+
Thesaurus.objects.filter(identifier=I18N_THESAURUS_IDENTIFIER).values_list("date", flat=True).first()
108+
)
109+
110+
if request_date == latest_date:
111+
# no changes after processing, set the info right away
112+
logger.debug(f"Caching lang:{lang} key:{data_key} date:{request_date}")
113+
cached_entry.update({"date": latest_date, data_key: data})
114+
else:
115+
logger.warning(
116+
f"Cache will not be updated for lang:{lang} key:{data_key} reqdate:{request_date} latest:{latest_date}"
117+
)
118+
119+
def get_labels(self, lang):
120+
date, labels = self.get_entry(lang, self.DATA_KEY_LABELS)
121+
if labels is None:
122+
labels = {i["about"]: i["label"] for i in get_localized_tkeywords(lang, I18N_THESAURUS_IDENTIFIER)}
123+
self.set(lang, self.DATA_KEY_LABELS, labels, date)
124+
125+
return labels
126+
127+
def clear_schema_cache(self):
128+
logger.info("Clearing schema cache")
129+
while True:
130+
try:
131+
self.cache.popitem()
132+
except KeyError:
133+
return
134+
135+
136+
def thesaurus_changed(sender, instance, **kwargs):
137+
if instance.identifier == I18N_THESAURUS_IDENTIFIER:
138+
if hasattr(instance, "_signal_handled"): # avoid signal recursion
139+
return
140+
logger.debug(f"Thesaurus changed: {instance.identifier}")
141+
_update_thesaurus_date()
142+
143+
144+
def thesaurusk_changed(sender, instance, **kwargs):
145+
if instance.thesaurus.identifier == I18N_THESAURUS_IDENTIFIER:
146+
logger.debug(f"ThesaurusKeyword changed: {instance.about} ALT:{instance.alt_label}")
147+
_update_thesaurus_date()
148+
149+
150+
def thesauruskl_changed(sender, instance, **kwargs):
151+
if instance.keyword.thesaurus.identifier == I18N_THESAURUS_IDENTIFIER:
152+
logger.debug(
153+
f"ThesaurusKeywordLabel changed: {instance.keyword.about} ALT:{instance.keyword.alt_label} L:{instance.lang}"
154+
)
155+
_update_thesaurus_date()
156+
157+
158+
def _update_thesaurus_date():
159+
logger.debug("Updating label thesaurus date")
160+
# update timestamp to invalidate other processes also
161+
i18n_thesaurus = Thesaurus.objects.get(identifier=I18N_THESAURUS_IDENTIFIER)
162+
i18n_thesaurus.date = datetime.now().replace(microsecond=0).isoformat()
163+
i18n_thesaurus._signal_handled = True
164+
i18n_thesaurus.save()

0 commit comments

Comments
 (0)