You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

239 lines
8.0 KiB

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, pref):
"""
Update/create the cache value for the given preference model instance
"""
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
self.cache.set(key, value)
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,
)
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}
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:
self.to_cache(db_pref)
a[preference.identifier()] = db_pref.value
return a