Skip to content

Commit afca096

Browse files
authored
[Fixes #13800] Refact metadata i18n mechanism
* [Fixes #13800] Refact metadata i18n mechanism * [Fixes #13800] Refact metadata i18n mechanism - gemini fixes * [Fixes #13800] Refact metadata i18n mechanism - use new gettext
1 parent f12421f commit afca096

File tree

16 files changed

+154
-126
lines changed

16 files changed

+154
-126
lines changed

geonode/base/apps.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,11 @@ class BaseAppConfig(NotificationsAppConfigBase, AppConfig):
4242
_("Resource is created"),
4343
),
4444
)
45+
46+
def ready(self):
47+
"""Finalize setup"""
48+
from geonode.base.signals import connect_signals
49+
50+
connect_signals()
51+
52+
super(BaseAppConfig, self).ready()
Lines changed: 52 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import logging
2-
from datetime import datetime
32

4-
from cachetools import FIFOCache
53
from django.db import connection
4+
from django.utils.translation import get_language, gettext as _
65

76
from geonode.base.models import ThesaurusKeywordLabel, Thesaurus
87

9-
108
logger = logging.getLogger(__name__)
119

1210
I18N_THESAURUS_IDENTIFIER = "labels-i18n"
@@ -47,6 +45,7 @@ def get_localized_tkeywords(lang, thesaurus_identifier: str):
4745
return sorted(ret.values(), key=lambda i: i["about"].lower())
4846

4947

48+
# TODO: deprecate and use LabelResolver.gettext()
5049
def get_localized_label(lang, about):
5150
# look for override
5251
ovr_qs = ThesaurusKeywordLabel.objects.filter(
@@ -65,41 +64,46 @@ def get_localized_label(lang, about):
6564
)
6665

6766

68-
class I18nCache:
67+
class I18nCacheEntry:
68+
def __init__(self):
69+
# the date field of the thesaurus when it was last loaded, it's used for the expiration check
70+
self.date: str | None = None
71+
self.caches: dict = {} # the caches for this language
6972

70-
DATA_KEY_SCHEMA = "schema"
71-
DATA_KEY_LABELS = "labels"
73+
74+
class I18nCache:
75+
"""
76+
Caches language related data.
77+
Synch is performed via date field in the "labels-i18n" thesaurus.
78+
"""
7279

7380
def __init__(self):
74-
# the cache has the lang as key, and various info in the dict value:
75-
# - date: the date field of the thesaurus when it was last loaded, it's used for the expiration check
76-
# - labels: the keyword labels from the i18n thesaurus
77-
# - schema: the localized json schema
78-
# FIFO bc we want to renew the data once in a while
79-
self.cache = FIFOCache(16)
81+
# the cache has the lang as key, and I18nCacheEntry as a value:
82+
self.lang_cache = {}
8083

8184
def get_entry(self, lang, data_key):
8285
"""
8386
returns date:str, data
8487
date is needed for checking the entry freshness when setting info
8588
data may be None if not cached or expired
8689
"""
87-
cached_entry = self.cache.get(lang, None)
90+
cached_entry: I18nCacheEntry = self.lang_cache.get(lang, None)
8891

92+
# TODO: thesaurus date check should be done only after a given time interval from last check
8993
thesaurus_date = ( # may be none if thesaurus does not exist
9094
Thesaurus.objects.filter(identifier=I18N_THESAURUS_IDENTIFIER).values_list("date", flat=True).first()
9195
)
9296
if cached_entry:
93-
if thesaurus_date == cached_entry["date"]:
97+
if thesaurus_date == cached_entry.date:
9498
# only return cached data if thesaurus has not been modified
95-
return thesaurus_date, cached_entry.get(data_key, None)
99+
return thesaurus_date, cached_entry.caches.get(data_key, None)
96100
else:
97101
logger.info(f"Schema for {lang}:{data_key} needs to be recreated")
98102

99103
return thesaurus_date, None
100104

101105
def set(self, lang: str, data_key: str, data: dict, request_date: str):
102-
cached_entry: dict = self.cache.setdefault(lang, {})
106+
cached_entry: I18nCacheEntry = self.lang_cache.setdefault(lang, I18nCacheEntry())
103107

104108
latest_date = (
105109
Thesaurus.objects.filter(identifier=I18N_THESAURUS_IDENTIFIER).values_list("date", flat=True).first()
@@ -108,60 +112,48 @@ def set(self, lang: str, data_key: str, data: dict, request_date: str):
108112
if request_date == latest_date:
109113
# no changes after processing, set the info right away
110114
logger.debug(f"Caching lang:{lang} key:{data_key} date:{request_date}")
111-
cached_entry.update({"date": latest_date, data_key: data})
115+
cached_entry.date = latest_date
116+
cached_entry.caches[data_key] = data
112117
else:
113118
logger.warning(
114119
f"Cache will not be updated for lang:{lang} key:{data_key} reqdate:{request_date} latest:{latest_date}"
115120
)
116121

117-
def get_labels(self, lang):
118-
date, labels = self.get_entry(lang, self.DATA_KEY_LABELS)
119-
if labels is None:
120-
labels = {}
121-
for i in get_localized_tkeywords(lang, I18N_THESAURUS_IDENTIFIER):
122-
about = i["about"]
123-
if about.endswith(OVR_SUFFIX) and not i["label"]:
124-
# we don't want default values for override entries
125-
continue
126-
labels[about] = i["label"] or i["default"]
127-
self.set(lang, self.DATA_KEY_LABELS, labels, date)
128-
return labels
129-
130-
def clear_schema_cache(self):
122+
def clear(self):
131123
logger.info("Clearing schema cache")
132-
while True:
133-
try:
134-
self.cache.popitem()
135-
except KeyError:
136-
return
124+
self.lang_cache.clear()
137125

138126

139-
def thesaurus_changed(sender, instance, **kwargs):
140-
if instance.identifier == I18N_THESAURUS_IDENTIFIER:
141-
if hasattr(instance, "_signal_handled"): # avoid signal recursion
142-
return
143-
logger.debug(f"Thesaurus changed: {instance.identifier}")
144-
_update_thesaurus_date()
127+
class LabelResolver:
128+
CACHE_KEY_LABELS = "labels"
145129

130+
def gettext(self, key, lang=None, fallback=True):
131+
"""
132+
Return the translated text in the label Thesaurus, falling back to the PO/MO translation.
133+
If fallback=False only search in the label Thesaurus, and may return None if not found
134+
"""
135+
lang = lang or get_language()
136+
# TODO: implement the OVR search
137+
return self.get_labels(lang).get(key, None) or (_(key) if fallback else None)
146138

147-
def thesaurusk_changed(sender, instance, **kwargs):
148-
if instance.thesaurus.identifier == I18N_THESAURUS_IDENTIFIER:
149-
logger.debug(f"ThesaurusKeyword changed: {instance.about} ALT:{instance.alt_label}")
150-
_update_thesaurus_date()
151-
139+
def get_labels(self, lang):
140+
date, labels = i18nCache.get_entry(lang, self.CACHE_KEY_LABELS)
141+
if labels is None:
142+
labels = self._create_labels_cache(lang)
143+
i18nCache.set(lang, self.CACHE_KEY_LABELS, labels, date)
144+
return labels
152145

153-
def thesauruskl_changed(sender, instance, **kwargs):
154-
if instance.keyword.thesaurus.identifier == I18N_THESAURUS_IDENTIFIER:
155-
logger.debug(
156-
f"ThesaurusKeywordLabel changed: {instance.keyword.about} ALT:{instance.keyword.alt_label} L:{instance.lang}"
157-
)
158-
_update_thesaurus_date()
146+
def _create_labels_cache(self, lang):
147+
labels = {}
148+
for i in get_localized_tkeywords(lang, I18N_THESAURUS_IDENTIFIER):
149+
about = i["about"]
150+
if about.endswith(OVR_SUFFIX) and not i["label"]:
151+
# we don't want default values for override entries
152+
continue
153+
labels[about] = i["label"] or i["default"]
154+
return labels
159155

160156

161-
def _update_thesaurus_date():
162-
logger.debug("Updating label thesaurus date")
163-
# update timestamp to invalidate other processes also
164-
i18n_thesaurus = Thesaurus.objects.get(identifier=I18N_THESAURUS_IDENTIFIER)
165-
i18n_thesaurus.date = datetime.now().replace(microsecond=0).isoformat()
166-
i18n_thesaurus._signal_handled = True
167-
i18n_thesaurus.save()
157+
i18nCache = I18nCache()
158+
labelResolver = LabelResolver()
159+
gettext = labelResolver.gettext

geonode/base/signals.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import logging
2+
from datetime import datetime
3+
4+
from django.db.models.signals import post_save
5+
6+
from geonode.base.i18n import I18N_THESAURUS_IDENTIFIER
7+
from geonode.base.models import Thesaurus, ThesaurusKeyword, ThesaurusKeywordLabel
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
def connect_signals():
13+
logger.debug("Setting up signal connections...")
14+
post_save.connect(thesaurus_changed, sender=Thesaurus, weak=False, dispatch_uid="metadata_reset_t")
15+
post_save.connect(thesaurusk_changed, sender=ThesaurusKeyword, weak=False, dispatch_uid="metadata_reset_tk")
16+
post_save.connect(thesauruskl_changed, sender=ThesaurusKeywordLabel, weak=False, dispatch_uid="metadata_reset_tkl")
17+
logger.debug("Signal connections set")
18+
19+
20+
def thesaurus_changed(sender, instance, **kwargs):
21+
if instance.identifier == I18N_THESAURUS_IDENTIFIER:
22+
if hasattr(instance, "_signal_handled"): # avoid signal recursion
23+
return
24+
logger.debug(f"Thesaurus changed: {instance.identifier}")
25+
_update_thesaurus_date()
26+
27+
28+
def thesaurusk_changed(sender, instance, **kwargs):
29+
if instance.thesaurus.identifier == I18N_THESAURUS_IDENTIFIER:
30+
logger.debug(f"ThesaurusKeyword changed: {instance.about} ALT:{instance.alt_label}")
31+
_update_thesaurus_date()
32+
33+
34+
def thesauruskl_changed(sender, instance, **kwargs):
35+
if instance.keyword.thesaurus.identifier == I18N_THESAURUS_IDENTIFIER:
36+
logger.debug(
37+
f"ThesaurusKeywordLabel changed: {instance.keyword.about} ALT:{instance.keyword.alt_label} L:{instance.lang}"
38+
)
39+
_update_thesaurus_date()
40+
41+
42+
def _update_thesaurus_date():
43+
logger.debug("Updating label thesaurus date")
44+
# update timestamp to invalidate other processes also
45+
i18n_thesaurus = Thesaurus.objects.get(identifier=I18N_THESAURUS_IDENTIFIER)
46+
i18n_thesaurus.date = datetime.now().replace(microsecond=0).isoformat()
47+
i18n_thesaurus._signal_handled = True
48+
i18n_thesaurus.save()

geonode/indexing/tests/test_invoke.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from django.test import override_settings
2727

28+
from geonode.base.i18n import i18nCache
2829
from geonode.base.models import ResourceBase
2930
from geonode.metadata.handlers.multilang import MultiLangHandler
3031
from geonode.metadata.handlers.sparse import SparseHandler, SparseFieldRegistry
@@ -40,7 +41,7 @@
4041
class IndexingInvocationTests(GeoNodeBaseTestSupport):
4142

4243
def setUp(self):
43-
pass
44+
i18nCache.clear()
4445

4546
def tearDown(self):
4647
super().tearDown()

geonode/indexing/tests/test_tsindex.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from django.test import override_settings
2525

26+
from geonode.base.i18n import i18nCache
2627
from geonode.indexing.manager import TSVectorIndexManager
2728
from geonode.tests.base import GeoNodeBaseTestSupport
2829

@@ -33,7 +34,7 @@
3334
class IndexingTests(GeoNodeBaseTestSupport):
3435

3536
def setUp(self):
36-
pass
37+
i18nCache.clear()
3738

3839
def tearDown(self):
3940
super().tearDown()

geonode/metadata/api/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from geonode.base.views import LinkedResourcesAutocomplete, RegionAutocomplete, HierarchicalKeywordAutocomplete
3838
from geonode.groups.models import GroupProfile
3939
from geonode.metadata.handlers.abstract import MetadataHandler
40-
from geonode.metadata.i18n import get_localized_label
40+
from geonode.base.i18n import get_localized_label
4141
from geonode.metadata.manager import metadata_manager
4242
from geonode.metadata.multilang import utils as multi
4343
from geonode.people.utils import get_available_users

geonode/metadata/apps.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ def ready(self):
1919
def run_setup_hooks(*args, **kwargs):
2020
setup_metadata_handlers()
2121

22-
from geonode.metadata.signals import connect_signals
23-
24-
connect_signals()
25-
2622

2723
def setup_metadata_handlers():
2824
from geonode.metadata.manager import metadata_manager

geonode/metadata/handlers/abstract.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,8 @@
2323

2424
from typing_extensions import deprecated
2525

26-
from django.utils.translation import gettext as _
27-
2826
from geonode.base.models import ResourceBase
29-
from geonode.metadata.i18n import OVR_SUFFIX
27+
from geonode.base.i18n import OVR_SUFFIX, labelResolver
3028

3129
logger = logging.getLogger(__name__)
3230

@@ -51,7 +49,7 @@ def update_schema(self, jsonschema: dict, context: dict, lang=None):
5149
It adds the subschema handled by the handler, and returns the
5250
augmented instance of the JSON Schema.
5351
Context is populated by the manager with some common info:
54-
- key "labels": contains the localized label loaded from the db as a dict, where key is the ThesaurusKeyword about
52+
- key CONTEXT_KEY_LABELS: contains the localized label loaded from the db as a dict, where key is the ThesaurusKeyword about
5553
and value is the localized ThesaurusKeywordLabel, or the AltLabel if the localized label does not exist.
5654
"""
5755
pass
@@ -72,7 +70,7 @@ def update_resource(
7270
):
7371
"""
7472
Called when persisting data, updates the field `field_name` of the resource
75-
with the content content, where json_instance is the full JSON Schema instance,
73+
with the content, where json_instance is the full JSON Schema instance,
7674
in case the handler needs some cross related data contained in the resource.
7775
"""
7876
pass
@@ -162,35 +160,26 @@ def _set_error(errors: dict, path: list, msg: str):
162160

163161
@staticmethod
164162
def localize_message(context: dict, msg_code: str, msg_info: dict):
165-
msg_loc: str = MetadataHandler._get_tkl_labels(context, None, msg_code)
163+
msg_loc: str = labelResolver.gettext(msg_code)
166164
if msg_loc:
167165
tokens = defaultdict(lambda: "N/A", msg_info or {})
168166
return msg_loc.format_map(tokens)
169167
else:
170168
logger.warning(f"Missing i18n entry for key '{msg_code}' -- info is {msg_info}")
171169
return f"{msg_code}:{msg_info}"
172170

173-
@staticmethod
174-
def _localize_label(context, lang: str, text: str):
175-
label = MetadataHandler._get_tkl_labels(context, lang, text)
176-
return label or _(text)
177-
178-
@staticmethod
179-
def _get_tkl_labels(context, lang: str | None, text: str):
180-
return context.get("labels", {}).get(text, None)
181-
182171
@staticmethod
183172
def _localize_subschema_labels(context, subschema: dict, lang: str, property_name: str = None):
184173
for annotation_name, synt in (
185174
("title", ""),
186175
("description", "__descr"),
187176
):
188-
if ovr := MetadataHandler._get_tkl_labels(context, lang, f"{property_name}{synt}{OVR_SUFFIX}"):
177+
if ovr := labelResolver.gettext(f"{property_name}{synt}{OVR_SUFFIX}", lang, fallback=False):
189178
subschema[annotation_name] = ovr
190179
elif annotation_name in subschema:
191-
subschema[annotation_name] = MetadataHandler._localize_label(context, lang, subschema[annotation_name])
180+
subschema[annotation_name] = labelResolver.gettext(subschema[annotation_name], lang)
192181
elif property_name: # arrays may not have a name
193-
label = MetadataHandler._get_tkl_labels(context, lang, f"{property_name}{synt}")
182+
label = labelResolver.gettext(f"{property_name}{synt}", lang, fallback=False)
194183
if label:
195184
subschema[annotation_name] = label
196185

geonode/metadata/handlers/contact.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from django.contrib.auth import get_user_model
2525
from django.utils.translation import gettext as _
2626

27+
from geonode.base.i18n import labelResolver
2728
from geonode.metadata.handlers.abstract import MetadataHandler
2829
from geonode.people import Roles
2930
from geonode.resource.manager import resource_manager
@@ -66,7 +67,7 @@ def update_schema(self, jsonschema, context, lang=None):
6667
if role.is_multivalue:
6768
contact = {
6869
"type": "array",
69-
"title": self._localize_label(context, lang, role.label) + card,
70+
"title": labelResolver.gettext(role.label, lang) + card,
7071
"minItems": minitems,
7172
"items": {
7273
"type": "object",
@@ -86,7 +87,7 @@ def update_schema(self, jsonschema, context, lang=None):
8687
else:
8788
contact = {
8889
"type": "object",
89-
"title": self._localize_label(context, lang, role.label) + card,
90+
"title": labelResolver.gettext(role.label, lang) + card,
9091
"properties": {
9192
"id": {
9293
"type": "string",

0 commit comments

Comments
 (0)