forked from jazzband/django-constance
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Iurchenko Sergei
committed
Nov 21, 2023
1 parent
d03bea8
commit 7807dd6
Showing
9 changed files
with
108 additions
and
149 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,129 +1,47 @@ | ||
from django.core.cache import caches | ||
from django.core.cache.backends.locmem import LocMemCache | ||
from django.core.exceptions import ImproperlyConfigured | ||
from django.db import ( | ||
IntegrityError, | ||
OperationalError, | ||
ProgrammingError, | ||
transaction, | ||
) | ||
from django.db.models.signals import post_save | ||
|
||
from constance.backends import Backend | ||
from constance import settings, signals, config | ||
from constance.models import Constance | ||
|
||
|
||
class DatabaseBackend(Backend): | ||
def __init__(self): | ||
from constance.models import Constance | ||
self._model = Constance | ||
self._prefix = settings.DATABASE_PREFIX | ||
self._autofill_timeout = settings.DATABASE_CACHE_AUTOFILL_TIMEOUT | ||
self._autofill_cachekey = 'autofilled' | ||
|
||
if self._model._meta.app_config is None: | ||
raise ImproperlyConfigured( | ||
"The constance.backends.database app isn't installed " | ||
"correctly. Make sure it's in your INSTALLED_APPS setting.") | ||
|
||
if settings.DATABASE_CACHE_BACKEND: | ||
self._cache = caches[settings.DATABASE_CACHE_BACKEND] | ||
if isinstance(self._cache, LocMemCache): | ||
raise ImproperlyConfigured( | ||
"The CONSTANCE_DATABASE_CACHE_BACKEND setting refers to a " | ||
"subclass of Django's local-memory backend (%r). Please " | ||
"set it to a backend that supports cross-process caching." | ||
% settings.DATABASE_CACHE_BACKEND) | ||
else: | ||
self._cache = None | ||
self.autofill() | ||
# Clear simple cache. | ||
post_save.connect(self.clear, sender=self._model) | ||
|
||
def add_prefix(self, key): | ||
return "%s%s" % (self._prefix, key) | ||
|
||
def autofill(self): | ||
if not self._autofill_timeout or not self._cache: | ||
return | ||
full_cachekey = self.add_prefix(self._autofill_cachekey) | ||
if self._cache.get(full_cachekey): | ||
return | ||
autofill_values = {} | ||
autofill_values[full_cachekey] = 1 | ||
for key, value in self.mget(settings.CONFIG): | ||
autofill_values[self.add_prefix(key)] = value | ||
self._cache.set_many(autofill_values, timeout=self._autofill_timeout) | ||
return "%s%s" % (settings.DATABASE_PREFIX, key) | ||
|
||
def mget(self, keys): | ||
if not keys: | ||
return | ||
keys = {self.add_prefix(key): key for key in keys} | ||
try: | ||
stored = self._model._default_manager.filter(key__in=keys) | ||
for const in stored: | ||
yield keys[const.key], const.value | ||
except (OperationalError, ProgrammingError): | ||
pass | ||
return {} | ||
|
||
objects = Constance.objects.filter(key__in=[self.add_prefix(key) for key in keys]) | ||
# all keys should be present in result even they are absent in database | ||
result = {key: self.get_default(key) for key in keys} | ||
for obj in objects: | ||
result[obj.key] = obj.value | ||
return result | ||
|
||
def get(self, key): | ||
key = self.add_prefix(key) | ||
if self._cache: | ||
value = self._cache.get(key) | ||
if value is None: | ||
self.autofill() | ||
value = self._cache.get(key) | ||
else: | ||
try: | ||
obj = Constance.objects.get(key=self.add_prefix(key)) | ||
value = obj.value | ||
except Constance.DoesNotExist: | ||
value = None | ||
if value is None: | ||
try: | ||
value = self._model._default_manager.get(key=key).value | ||
except (OperationalError, ProgrammingError, self._model.DoesNotExist): | ||
pass | ||
else: | ||
if self._cache: | ||
self._cache.add(key, value) | ||
return value | ||
|
||
def set(self, key, value): | ||
key = self.add_prefix(key) | ||
created = False | ||
queryset = self._model._default_manager.all() | ||
# Set _for_write attribute as get_or_create method does | ||
# https://github.com/django/django/blob/2.2.11/django/db/models/query.py#L536 | ||
queryset._for_write = True | ||
db_key = self.add_prefix(key) | ||
|
||
try: | ||
constance = queryset.get(key=key) | ||
except (OperationalError, ProgrammingError): | ||
# database is not created, noop | ||
return | ||
except self._model.DoesNotExist: | ||
try: | ||
with transaction.atomic(using=queryset.db): | ||
queryset.create(key=key, value=value) | ||
created = True | ||
except IntegrityError as error: | ||
# Allow concurrent writes | ||
constance = queryset.get(key=key) | ||
|
||
if not created: | ||
old_value = constance.value | ||
constance.value = value | ||
constance.save() | ||
else: | ||
old_value = None | ||
|
||
if self._cache: | ||
self._cache.set(key, value) | ||
obj = Constance.objects.get(key=db_key) | ||
old_value = obj.value | ||
if value == old_value: | ||
return | ||
else: | ||
obj.value = value | ||
obj.save() | ||
except Constance.DoesNotExist: | ||
old_value = self.get_default(key) | ||
Constance.objects.create(key=db_key, value=value) | ||
|
||
signals.config_updated.send( | ||
sender=config, key=key, old_value=old_value, new_value=value | ||
) | ||
|
||
def clear(self, sender, instance, created, **kwargs): | ||
if self._cache and not created: | ||
keys = [self.add_prefix(k) for k in settings.CONFIG] | ||
keys.append(self.add_prefix(self._autofill_cachekey)) | ||
self._cache.delete_many(keys) | ||
self.autofill() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,44 @@ | ||
from django.core.exceptions import AppRegistryNotReady | ||
|
||
from . import settings, utils | ||
|
||
|
||
class Config: | ||
""" | ||
The global config wrapper that handles the backend. | ||
""" | ||
def __init__(self): | ||
super().__setattr__('_backend', | ||
utils.import_module_attr(settings.BACKEND)()) | ||
def _get_config_class(): | ||
|
||
def __getattr__(self, key): | ||
try: | ||
if not len(settings.CONFIG[key]) in (2, 3): | ||
raise AttributeError(key) | ||
default = settings.CONFIG[key][0] | ||
except KeyError: | ||
raise AttributeError(key) | ||
result = self._backend.get(key) | ||
if result is None: | ||
result = default | ||
setattr(self, key, default) | ||
is_ready = False | ||
|
||
class _Config: | ||
""" | ||
The global config wrapper that handles the backend. | ||
""" | ||
|
||
def init(self): | ||
super().__setattr__('_backend', utils.import_module_attr(settings.BACKEND)()) | ||
nonlocal is_ready | ||
is_ready = True | ||
|
||
def __getattr__(self, key): | ||
if not is_ready: | ||
raise AppRegistryNotReady("Apps aren't loaded yet.") | ||
|
||
result = self._backend.get(key) | ||
if result is None: | ||
result = self._backend.get_default(key) | ||
return result | ||
return result | ||
return result | ||
|
||
def __setattr__(self, key, value): | ||
if key not in settings.CONFIG: | ||
raise AttributeError(key) | ||
self._backend.set(key, value) | ||
def __setattr__(self, key, value): | ||
if not is_ready: | ||
raise AppRegistryNotReady("Apps aren't loaded yet.") | ||
|
||
if key not in settings.CONFIG: | ||
raise AttributeError(key) | ||
self._backend.set(key, value) | ||
|
||
def __dir__(self): | ||
return settings.CONFIG.keys() | ||
|
||
return _Config | ||
|
||
|
||
def __dir__(self): | ||
return settings.CONFIG.keys() | ||
Config = _get_config_class() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters