11import logging
2- from datetime import datetime
32
4- from cachetools import FIFOCache
53from django .db import connection
4+ from django .utils .translation import get_language , gettext as _
65
76from geonode .base .models import ThesaurusKeywordLabel , Thesaurus
87
9-
108logger = logging .getLogger (__name__ )
119
1210I18N_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()
5049def get_localized_label (lang , about ):
5150 # look for override
5251 ovr_qs = ThesaurusKeywordLabel .objects .filter (
@@ -65,32 +64,37 @@ 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
72+
6973
70- DATA_KEY_SCHEMA = "schema"
71- DATA_KEY_LABELS = "labels"
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 = 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
9599 return thesaurus_date , cached_entry .get (data_key , None )
96100 else :
@@ -99,7 +103,7 @@ def get_entry(self, lang, data_key):
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,43 @@ 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-
130122 def clear_schema_cache (self ):
131123 logger .info ("Clearing schema cache" )
132- while True :
133- try :
134- self .cache .popitem ()
135- except KeyError :
136- return
137-
124+ self .lang_cache .clear ()
138125
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 ()
145126
127+ class LabelResolver :
128+ CACHE_KEY_LABELS = "labels"
146129
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 ()
130+ def gettext (self , key , lang = None , fallback = True ):
131+ lang = lang or get_language ()
132+ return self .get_labels (lang ).get (key , None ) or (_ (key ) if fallback else None )
151133
134+ def get_labels (self , lang ):
135+ date , labels = i18nCache .get_entry (lang , self .CACHE_KEY_LABELS )
136+ if labels is None :
137+ labels = self ._create_labels_cache (lang )
138+ i18nCache .set (lang , self .CACHE_KEY_LABELS , labels , date )
139+ return labels
152140
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 ()
141+ def _create_labels_cache (self , lang ):
142+ labels = {}
143+ for i in get_localized_tkeywords (lang , I18N_THESAURUS_IDENTIFIER ):
144+ about = i ["about" ]
145+ if about .endswith (OVR_SUFFIX ) and not i ["label" ]:
146+ # we don't want default values for override entries
147+ continue
148+ labels [about ] = i ["label" ] or i ["default" ]
149+ return labels
159150
160151
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 ()
152+ i18nCache = I18nCache ()
153+ labelResolver = LabelResolver ()
154+ gettext = labelResolver .gettext
0 commit comments