1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
|
try:
from collections.abc import Mapping
except ImportError:
from collections import Mapping
from .settings import preferences_settings
from .exceptions import CachedValueNotFound, DoesNotExist
from .signals import preference_updated
class PreferencesManager(Mapping):
"""Handle retrieving / caching of preferences"""
def __init__(self, model, registry, **kwargs):
self.model = model
self.registry = registry
self.instance = kwargs.get("instance")
@property
def queryset(self):
qs = self.model.objects.all()
if self.instance:
qs = qs.filter(instance=self.instance)
return qs
@property
def cache(self):
from django.core.cache import caches
return caches[preferences_settings.CACHE_NAME]
def __getitem__(self, key):
return self.get(key)
def __setitem__(self, key, value):
section, name = self.parse_lookup(key)
preference = self.registry.get(section=section, name=name, fallback=False)
preference.validate(value)
self.update_db_pref(section=section, name=name, value=value)
def __repr__(self):
return repr(self.all())
def __iter__(self):
return self.all().__iter__()
def __len__(self):
return len(self.all())
def by_name(self):
"""Return a dictionary with preferences identifiers and values, but without the section name in the identifier"""
return {
key.split(preferences_settings.SECTION_KEY_SEPARATOR)[-1]: value
for key, value in self.all().items()
}
def get_by_name(self, name):
return self.get(self.registry.get_by_name(name).identifier())
def get_cache_key(self, section, name):
"""Return the cache key corresponding to a given preference"""
if not self.instance:
return "dynamic_preferences_{0}_{1}_{2}".format(
self.model.__name__, section, name
)
return "dynamic_preferences_{0}_{1}_{2}_{3}".format(
self.model.__name__, self.instance.pk, section, name, self.instance.pk
)
def from_cache(self, section, name):
"""Return a preference raw_value from cache"""
cached_value = self.cache.get(
self.get_cache_key(section, name), CachedValueNotFound
)
if cached_value is CachedValueNotFound:
raise CachedValueNotFound
if cached_value == preferences_settings.CACHE_NONE_VALUE:
cached_value = None
return self.registry.get(section=section, name=name).serializer.deserialize(
cached_value
)
def many_from_cache(self, preferences):
"""
Return cached value for given preferences
missing preferences will be skipped
"""
keys = {p: self.get_cache_key(p.section.name, p.name) for p in preferences}
cached = self.cache.get_many(list(keys.values()))
for k, v in cached.items():
# we replace dummy cached values by None here, if needed
if v == preferences_settings.CACHE_NONE_VALUE:
cached[k] = None
# we have to remap returned value since the underlying cached keys
# are not usable for an end user
return {
p.identifier(): p.serializer.deserialize(cached[k])
for p, k in keys.items()
if k in cached
}
def to_cache(self, *prefs):
"""
Update/create the cache value for the given preference model instances
"""
update_dict = {}
for pref in prefs:
key = self.get_cache_key(pref.section, pref.name)
value = pref.raw_value
if value is None or value == "":
# some cache backends refuse to cache None or empty values
# resulting in more DB queries, so we cache an arbitrary value
# to ensure the cache is hot (even with empty values)
value = preferences_settings.CACHE_NONE_VALUE
update_dict[key] = value
self.cache.set_many(update_dict)
def pref_obj(self, section, name):
return self.registry.get(section=section, name=name)
def parse_lookup(self, lookup):
try:
section, name = lookup.split(preferences_settings.SECTION_KEY_SEPARATOR)
except ValueError:
name = lookup
section = None
return section, name
def get(self, key, no_cache=False):
"""Return the value of a single preference using a dotted path key
:arg no_cache: if true, the cache is bypassed
"""
section, name = self.parse_lookup(key)
preference = self.registry.get(section=section, name=name, fallback=False)
if no_cache or not preferences_settings.ENABLE_CACHE:
return self.get_db_pref(section=section, name=name).value
try:
return self.from_cache(section, name)
except CachedValueNotFound:
pass
db_pref = self.get_db_pref(section=section, name=name)
self.to_cache(db_pref)
return db_pref.value
def get_db_pref(self, section, name):
try:
pref = self.queryset.get(section=section, name=name)
except self.model.DoesNotExist:
pref_obj = self.pref_obj(section=section, name=name)
pref = self.create_db_pref(
section=section, name=name, value=pref_obj.get("default")
)
return pref
def update_db_pref(self, section, name, value):
try:
db_pref = self.queryset.get(section=section, name=name)
old_value = db_pref.value
db_pref.value = value
db_pref.save()
preference_updated.send(
sender=self.__class__,
section=section,
name=name,
old_value=old_value,
new_value=value,
instance=db_pref
)
except self.model.DoesNotExist:
return self.create_db_pref(section, name, value)
return db_pref
def create_db_pref(self, section, name, value):
kwargs = {
"section": section,
"name": name,
}
if self.instance:
kwargs["instance"] = self.instance
# this is a just a shortcut to get the raw, serialized value
# so we can pass it to get_or_create
m = self.model(**kwargs)
m.value = value
raw_value = m.raw_value
db_pref, created = self.model.objects.get_or_create(**kwargs)
if created and db_pref.raw_value != raw_value:
db_pref.raw_value = raw_value
db_pref.save()
return db_pref
def all(self):
"""Return a dictionary containing all preferences by section
Loaded from cache or from db in case of cold cache
"""
if not preferences_settings.ENABLE_CACHE:
return self.load_from_db()
preferences = self.registry.preferences()
# first we hit the cache once for all existing preferences
a = self.many_from_cache(preferences)
if len(a) == len(preferences):
return a # avoid database hit if not necessary
# then we fill those that miss, but exist in the database
# (just hit the database for all of them, filtering is complicated, and
# in most cases you'd need to grab the majority of them anyway)
a.update(self.load_from_db(cache=True))
return a
def load_from_db(self, cache=False):
"""Return a dictionary of preferences by section directly from DB"""
a = {}
db_prefs = {p.preference.identifier(): p for p in self.queryset}
cache_prefs = []
for preference in self.registry.preferences():
try:
db_pref = db_prefs[preference.identifier()]
except KeyError:
db_pref = self.create_db_pref(
section=preference.section.name,
name=preference.name,
value=preference.get("default"),
)
else:
# cache if create_db_pref() hasn't already done so
if cache:
cache_prefs.append(db_pref)
a[preference.identifier()] = db_pref.value
if cache_prefs:
self.to_cache(*cache_prefs)
return a
|