Compare commits

..

No commits in common. "master" and "1.2" have entirely different histories.
master ... 1.2

128 changed files with 1917 additions and 5897 deletions

View file

@ -1,9 +1,6 @@
[run]
source = constance
branch = 1
omit =
*/pytest.py
*/tests/*
[report]
omit = *tests*,*migrations*,.tox/*,setup.py,*settings.py
omit = *tests*,*migrations*

View file

@ -1,13 +0,0 @@
### Describe the problem
Tell us about the problem you're having.
### Steps to reproduce
Tell us how to reproduce it.
### System configuration
* Django version:
* Python version:
* Django-Constance version:

View file

@ -1,18 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
commit-message:
prefix: "chore(ci): "
groups:
github-actions:
patterns:
- "*"
open-pull-requests-limit: 1

View file

@ -1,24 +0,0 @@
name: Docs
on: [push, pull_request]
jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: 'pip'
cache-dependency-path: 'docs/requirements.txt'
- name: Install dependencies
run: pip install -r docs/requirements.txt
- name: Build docs
run: |
cd docs
make html

View file

@ -1,37 +0,0 @@
name: Release
on:
push:
tags:
- '*'
jobs:
build:
if: github.repository == 'jazzband/django-constance'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U build setuptools twine wheel
- name: Build package
run: |
python -m build
twine check dist/*
- name: Upload packages to Jazzband
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@v1.13.0
with:
user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
repository_url: https://jazzband.co/projects/django-constance/upload

View file

@ -1,54 +0,0 @@
name: Test
on: [push, pull_request]
jobs:
ruff-format:
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- uses: actions/checkout@v6
- uses: chartboost/ruff-action@v1
with:
version: 0.5.0
args: 'format --check'
ruff-lint:
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- uses: actions/checkout@v6
- uses: chartboost/ruff-action@v1
with:
version: 0.5.0
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 5
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade tox tox-gh-actions
- name: Tox tests
run: |
tox -v
- name: Upload coverage
uses: codecov/codecov-action@v5
with:
name: Python ${{ matrix.python-version }}

3
.gitignore vendored
View file

@ -7,7 +7,4 @@ dist/
test.db
.tox
.coverage
coverage.xml
docs/_build
.idea
constance/_version.py

View file

@ -1,22 +0,0 @@
repos:
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- id: python-check-blanket-noqa
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-merge-conflict
- id: check-yaml
- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
hooks:
- id: pyupgrade
args: [ --py38-plus ]
exclude: /migrations/
ci:
autoupdate_schedule: quarterly

View file

@ -1,18 +0,0 @@
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
build:
os: ubuntu-lts-latest
tools:
python: "3.12"
sphinx:
configuration: docs/conf.py
python:
install:
- requirements: docs/requirements.txt
- method: pip
path: .

37
.travis.yml Normal file
View file

@ -0,0 +1,37 @@
sudo: false
language: python
cache:
directories:
- "$HOME/.cache/pip"
install:
- pip install tox
env:
- TOXENV=py27-django-17
- TOXENV=py27-django-18
- TOXENV=py33-django-17
- TOXENV=py33-django-18
- TOXENV=py34-django-17
- TOXENV=py34-django-18
- TOXENV=pypy-django-17
- TOXENV=pypy-django-18
- TOXENV=pypy-django-19
- TOXENV=py27-django-master
- TOXENV=pypy-django-master
script:
- tox
matrix:
include:
- python: 3.5
env: TOXENV=py35-django-19
- python: 3.5
env: TOXENV=py35-django-master
deploy:
provider: pypi
user: jazzband
distributions: "sdist bdist_wheel"
password:
secure: VD+63Tnv0VYNfFQv9f1KZ0k79HSX8veNk4dTy42Hriteci50z5uSQdZMnqqD83xQJa4VF6N7DHkxHnBVOWLCqGQZeYqR/5BuDFNUewcr6O14dk31HvxMsWDaN1KW0Qwtus8ZrztwGhZtZ/92ODA6luHI4mCTzqX0gcG0/aKd75s=
on:
tags: true
repo: jazzband/django-constance
condition: "$TOXENV = py27-django-18"

8
.tx/config Normal file
View file

@ -0,0 +1,8 @@
[main]
host = https://www.transifex.com
lang_map = sr@latin:sr_Latn
[django-constance.main]
file_filter = constance/locale/<lang>/LC_MESSAGES/django.po
source_file = constance/locale/en/LC_MESSAGES/django.po
source_lang = en

11
AUTHORS
View file

@ -1,23 +1,17 @@
Ales Zoulek <ales.zoulek@gmail.com>
Alexander Frenzel <alex@relatedworks.com>
Alexandr Artemyev <mogost@gmail.com>
Alexander frenzel <alex@relatedworks.com>
Bouke Haarsma <bouke@webatoom.nl>
Camilo Nova <camilo.nova@gmail.com>
Charlie Hornsby <charlie.hornsby@hotmail.co.uk>
Curtis Maloney <curtis@tinbrain.net>
Dan Poirier <dpoirier@caktusgroup.com>
David Burke <dmbst32@gmail.com>
Dmitriy Tatarkin <mail@dtatarkin.ru>
Elisey Zanko <elisey.zanko@gmail.com>
Florian Apolloner <florian@apolloner.eu>
Igor Támara <igor@axiacore.com>
Ilya Chichak <ilyachch@gmail.com>
Ivan Klass <klass.ivanklass@gmail.com>
Jake Merdich <jmerdich@users.noreply.github.com>
Jannis Leidel <jannis@leidel.info>
Janusz Harkot <janusz.harkot@gmail.com>
Jiri Barton <jbar@hosting4u.cz>
John Carter <john@therefromhere.org>
Jonas <jvp@jonasundderwolf.de>
Kuba Zarzycki <jakubzarzycki@gmail.com>
Leandra Finger <leandra.finger@gmail.com>
@ -26,18 +20,15 @@ Lin Xianyi <iynaix@gmail.com>
Marcin Baran <marcin.baran@agencjawmc.pl>
Mario Orlandi <morlandi@brainstorm.it>
Mario Rosa <mario@dwaiter.com>
Mariusz Felisiak <felisiak.mariusz@gmail.com>
Mattia Larentis <mattia@larentis.eu>
Merijn Bertels <merijn.bertels@gmail.com>
Omer Katz <omer.drow@gmail.com>
Petr Knap <dev@petrknap.cz>
Philip Neustrom <philipn@gmail.com>
Philipp Thumfart <philipp@thumfart.eu>
Pierre-Olivier Marec <pomarec@free.fr>
Roman Krejcik <farin@farin.cz>
Silvan Spross <silvan.spross@gmail.com>
Sławek Ehlert <slafs@op.pl>
Vladas Tamoshaitis <amd.vladas@gmail.com>
Vojtech Jasny <voy@voy.cz>
Yin Jifeng <jifeng.yin@gmail.com>
illumin-us-r3v0lution <luminaries@riseup.net>

View file

@ -1,3 +0,0 @@
# Django Constance Code of Conduct
The django-constance project utilizes the [Django Commons Code of Conduct](https://github.com/django-commons/membership/blob/main/CODE_OF_CONDUCT.md).

View file

@ -1,3 +1,3 @@
[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/)
This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/docs/conduct) and follow the [guidelines](https://jazzband.co/docs/guidelines).
This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Condut](https://jazzband.co/docs/conduct) and follow the [guidelines](https://jazzband.co/docs/guidelines).

View file

@ -1,4 +1,4 @@
Copyright (c) 2009-2017, Jazzband
Copyright (c) 2009-2015, Comoga and individual contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

View file

@ -1,3 +1,2 @@
recursive-include constance/templates *.html
recursive-include constance/locale *.po *.mo
recursive-include constance/static *

View file

@ -1,28 +1,20 @@
Constance - Dynamic Django settings
===================================
.. image:: https://secure.travis-ci.org/jazzband/django-constance.png
:alt: Build Status
:target: http://travis-ci.org/jazzband/django-constance
.. image:: https://jazzband.co/static/img/badge.svg
:alt: Jazzband
:target: https://jazzband.co/
.. image:: https://img.shields.io/readthedocs/django-constance.svg
:target: https://django-constance.readthedocs.io/
:alt: Documentation
.. image:: https://github.com/jazzband/django-constance/workflows/Test/badge.svg
:target: https://github.com/jazzband/django-constance/actions
:alt: GitHub Actions
.. image:: https://codecov.io/gh/jazzband/django-constance/branch/master/graph/badge.svg
:target: https://codecov.io/gh/jazzband/django-constance
:alt: Coverage
A Django app for storing dynamic settings in pluggable backends (Redis and
Django model backend built in) with an integration with the Django admin app.
For more information see the documentation at:
https://django-constance.readthedocs.io/
http://django-constance.readthedocs.org/
If you have questions or have trouble using the app please file a bug report
at:

View file

@ -1,11 +1,13 @@
from django.utils.functional import LazyObject
__version__ = '1.2'
default_app_config = 'constance.apps.ConstanceConfig'
class LazyConfig(LazyObject):
def _setup(self):
from .base import Config
self._wrapped = Config()
config = LazyConfig()

View file

@ -1,159 +1,191 @@
import json
from collections import OrderedDict
from datetime import date
from datetime import datetime
from datetime import datetime, date, time
from decimal import Decimal
import hashlib
from operator import itemgetter
from django import forms
from django import get_version
from django.apps import apps
from django.contrib import admin
from django.contrib import messages
from django import forms, VERSION
from django.conf.urls import url
from django.contrib import admin, messages
from django.contrib.admin import widgets
from django.contrib.admin.options import csrf_protect_m
from django.core.exceptions import PermissionDenied
from django.core.exceptions import PermissionDenied, ImproperlyConfigured
from django.forms import fields
from django.http import HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import path
from django.utils import six
from django.utils.encoding import smart_bytes
from django.utils.formats import localize
from django.utils.translation import gettext_lazy as _
from django.utils.module_loading import import_string
from django.utils.translation import ugettext_lazy as _
import django
from . import LazyConfig
from . import settings
from .forms import ConstanceForm
from .utils import get_values
from . import LazyConfig, settings
config = LazyConfig()
NUMERIC_WIDGET = forms.TextInput(attrs={'size': 10})
INTEGER_LIKE = (fields.IntegerField, {'widget': NUMERIC_WIDGET})
STRING_LIKE = (fields.CharField, {
'widget': forms.Textarea(attrs={'rows': 3}),
'required': False,
})
FIELDS = {
bool: (fields.BooleanField, {'required': False}),
int: INTEGER_LIKE,
Decimal: (fields.DecimalField, {'widget': NUMERIC_WIDGET}),
str: STRING_LIKE,
datetime: (fields.SplitDateTimeField, {'widget': widgets.AdminSplitDateTime}),
date: (fields.DateField, {'widget': widgets.AdminDateWidget}),
time: (fields.TimeField, {'widget': widgets.AdminTimeWidget}),
float: (fields.FloatField, {'widget': NUMERIC_WIDGET}),
}
def parse_additional_fields(fields):
for key in fields:
field = list(fields[key])
if len(field) == 1:
field.append({})
field[0] = import_string(field[0])
if 'widget' in field[1]:
klass = import_string(field[1]['widget'])
field[1]['widget'] = klass(**(field[1].get('widget_kwargs', {}) or {}))
if 'widget_kwargs' in field[1]:
del field[1]['widget_kwargs']
fields[key] = field
return fields
FIELDS.update(parse_additional_fields(settings.ADDITIONAL_FIELDS))
if not six.PY3:
FIELDS.update({
long: INTEGER_LIKE,
unicode: STRING_LIKE,
})
class ConstanceForm(forms.Form):
version = forms.CharField(widget=forms.HiddenInput)
def __init__(self, initial, *args, **kwargs):
super(ConstanceForm, self).__init__(*args, initial=initial, **kwargs)
version_hash = hashlib.md5()
for name, options in settings.CONFIG.items():
default, help_text = options[0], options[1]
if len(options) == 3:
config_type = options[2]
else:
config_type = type(default)
if config_type not in FIELDS:
raise ImproperlyConfigured(_("Constance doesn't support "
"config values of the type "
"%(config_type)s. Please fix "
"the value of '%(name)s'.")
% {'config_type': config_type,
'name': name})
field_class, kwargs = FIELDS[config_type]
self.fields[name] = field_class(label=name, **kwargs)
version_hash.update(smart_bytes(initial.get(name, '')))
self.initial['version'] = version_hash.hexdigest()
def save(self):
for name in settings.CONFIG:
setattr(config, name, self.cleaned_data[name])
def clean_version(self):
value = self.cleaned_data['version']
if settings.IGNORE_ADMIN_VERSION_CHECK:
return value
if value != self.initial['version']:
raise forms.ValidationError(_('The settings have been modified '
'by someone else. Please reload the '
'form and resubmit your changes.'))
return value
class ConstanceAdmin(admin.ModelAdmin):
change_list_template = "admin/constance/change_list.html"
change_list_template = 'admin/constance/change_list.html'
change_list_form = ConstanceForm
def __init__(self, model, admin_site):
model._meta.concrete_model = Config
super().__init__(model, admin_site)
def get_urls(self):
info = f"{self.model._meta.app_label}_{self.model._meta.module_name}"
info = self.model._meta.app_label, self.model._meta.module_name
return [
path("", self.admin_site.admin_view(self.changelist_view), name=f"{info}_changelist"),
path("", self.admin_site.admin_view(self.changelist_view), name=f"{info}_add"),
url(r'^$',
self.admin_site.admin_view(self.changelist_view),
name='%s_%s_changelist' % info),
url(r'^$',
self.admin_site.admin_view(self.changelist_view),
name='%s_%s_add' % info),
]
def get_config_value(self, name, options, form, initial):
default, help_text = options[0], options[1]
field_type = None
if len(options) == 3:
field_type = options[2]
# First try to load the value from the actual backend
value = initial.get(name)
# Then if the returned value is None, get the default
if value is None:
value = getattr(config, name)
form_field = form[name]
config_value = {
"name": name,
"default": localize(default),
"raw_default": default,
"help_text": _(help_text),
"value": localize(value),
"modified": localize(value) != localize(default),
"form_field": form_field,
"is_date": isinstance(default, date),
"is_datetime": isinstance(default, datetime),
"is_checkbox": isinstance(form_field.field.widget, forms.CheckboxInput),
"is_multi_select": isinstance(
form_field.field.widget, (forms.SelectMultiple, forms.CheckboxSelectMultiple)
),
"is_file": isinstance(form_field.field.widget, forms.FileInput),
}
if config_value["is_multi_select"]:
config_value["json_default"] = json.dumps(default if isinstance(default, list) else [default])
if field_type and field_type in settings.ADDITIONAL_FIELDS:
serialized_default = form[name].field.prepare_value(default)
config_value["default"] = serialized_default
config_value["raw_default"] = serialized_default
config_value["value"] = form[name].field.prepare_value(value)
return config_value
def get_changelist_form(self, request):
"""Returns a Form class for use in the changelist_view."""
# Defaults to self.change_list_form in order to preserve backward
# compatibility
return self.change_list_form
@csrf_protect_m
def changelist_view(self, request, extra_context=None):
if not self.has_view_or_change_permission(request):
# First load a mapping between config name and default value
if not self.has_change_permission(request, None):
raise PermissionDenied
initial = get_values()
form_cls = self.get_changelist_form(request)
form = form_cls(initial=initial, request=request)
if request.method == "POST" and request.user.has_perm("constance.change_config"):
form = form_cls(data=request.POST, files=request.FILES, initial=initial, request=request)
default_initial = ((name, options[0])
for name, options in settings.CONFIG.items())
# Then update the mapping with actually values from the backend
initial = dict(default_initial,
**dict(config._backend.mget(settings.CONFIG.keys())))
form = self.change_list_form(initial=initial)
if request.method == 'POST':
form = self.change_list_form(data=request.POST, initial=initial)
if form.is_valid():
form.save()
messages.add_message(request, messages.SUCCESS, _("Live settings updated successfully."))
return HttpResponseRedirect(".")
messages.add_message(request, messages.ERROR, _("Failed to update live settings."))
# In django 1.5 this can be replaced with self.message_user
messages.add_message(
request,
messages.SUCCESS,
_('Live settings updated successfully.'),
)
return HttpResponseRedirect('.')
context = {
**self.admin_site.each_context(request),
**(extra_context or {}),
"config_values": [],
"title": self.model._meta.app_config.verbose_name,
"app_label": "constance",
"opts": self.model._meta,
"form": form,
"media": self.media + form.media,
"icon_type": "svg",
"django_version": get_version(),
'config_values': [],
'title': _('Constance config'),
'app_label': 'constance',
'opts': Config._meta,
'form': form,
'media': self.media + form.media,
'icon_type': 'gif' if VERSION < (1, 9) else 'svg',
}
for name, options in settings.CONFIG.items():
context["config_values"].append(self.get_config_value(name, options, form, initial))
if settings.CONFIG_FIELDSETS:
if isinstance(settings.CONFIG_FIELDSETS, dict):
fieldset_items = settings.CONFIG_FIELDSETS.items()
else:
fieldset_items = settings.CONFIG_FIELDSETS
context["fieldsets"] = []
for fieldset_title, fieldset_data in fieldset_items:
if isinstance(fieldset_data, dict):
fields_list = fieldset_data["fields"]
collapse = fieldset_data.get("collapse", False)
else:
fields_list = fieldset_data
collapse = False
absent_fields = [field for field in fields_list if field not in settings.CONFIG]
if any(absent_fields):
raise ValueError(
"CONSTANCE_CONFIG_FIELDSETS contains field(s) that does not exist(s): {}".format(
", ".join(absent_fields)
)
)
config_values = []
for name in fields_list:
options = settings.CONFIG.get(name)
if options:
config_values.append(self.get_config_value(name, options, form, initial))
fieldset_context = {"title": fieldset_title, "config_values": config_values}
if collapse:
fieldset_context["collapse"] = True
context["fieldsets"].append(fieldset_context)
if not isinstance(settings.CONFIG_FIELDSETS, (OrderedDict, tuple)):
context["fieldsets"].sort(key=itemgetter("title"))
if not isinstance(settings.CONFIG, OrderedDict):
context["config_values"].sort(key=itemgetter("name"))
default, help_text = options[0], options[1]
# First try to load the value from the actual backend
value = initial.get(name)
# Then if the returned value is None, get the default
if value is None:
value = getattr(config, name)
context['config_values'].append({
'name': name,
'default': localize(default),
'help_text': _(help_text),
'value': localize(value),
'modified': value != default,
'form_field': form[name],
})
context['config_values'].sort(key=itemgetter('name'))
request.current_app = self.admin_site.name
return TemplateResponse(request, self.change_list_template, context)
# compatibility to be removed when 1.7 is deprecated
extra = {'current_app': self.admin_site.name} if VERSION < (1, 8) else {}
return TemplateResponse(request, self.change_list_template, context,
**extra)
def has_add_permission(self, *args, **kwargs):
return False
@ -164,37 +196,23 @@ class ConstanceAdmin(admin.ModelAdmin):
def has_change_permission(self, request, obj=None):
if settings.SUPERUSER_ONLY:
return request.user.is_superuser
return super().has_change_permission(request, obj)
return super(ConstanceAdmin, self).has_change_permission(request, obj)
class Config:
class Meta:
app_label = "constance"
object_name = "Config"
concrete_model = None
model_name = module_name = "config"
verbose_name_plural = _("config")
class Config(object):
class Meta(object):
app_label = 'constance'
object_name = 'Config'
model_name = module_name = 'config'
verbose_name_plural = _('config')
abstract = False
swapped = False
is_composite_pk = False
def get_ordered_objects(self):
return False
def get_change_permission(self):
return f"change_{self.model_name}"
@property
def app_config(self):
return apps.get_app_config(self.app_label)
@property
def label(self):
return f"{self.app_label}.{self.object_name}"
@property
def label_lower(self):
return f"{self.app_label}.{self.model_name}"
return 'change_%s' % self.model_name
_meta = Meta()

View file

@ -1,14 +1,34 @@
from django.db.models import signals
from django import VERSION
from django.apps import AppConfig
from django.core import checks
from django.utils.translation import gettext_lazy as _
from constance.checks import check_fieldsets
from django.utils.translation import ugettext_lazy as _
class ConstanceConfig(AppConfig):
name = "constance"
verbose_name = _("Constance")
default_auto_field = "django.db.models.AutoField"
name = 'constance'
verbose_name = _('Constance')
def ready(self):
checks.register(check_fieldsets, "constance")
super(ConstanceConfig, self).ready()
signals.post_migrate.connect(self.create_perm,
dispatch_uid='constance.create_perm')
def create_perm(self, *args, **kwargs):
"""
Creates a fake content type and permission
to be able to check for permissions
"""
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
if ContentType._meta.installed and Permission._meta.installed:
extra = {} if VERSION >= (1, 8) else {'name': 'config'}
content_type, created = ContentType.objects.get_or_create(
app_label='constance',
model='config',
**extra)
permission, created = Permission.objects.get_or_create(
name='Can change config',
content_type=content_type,
codename='change_config')

View file

@ -1,50 +1,26 @@
"""Defines the base constance backend."""
from abc import ABC
from abc import abstractmethod
"""
Defines the base constance backend
"""
class Backend(ABC):
@abstractmethod
class Backend(object):
def get(self, key):
"""
Get the key from the backend store and return the value.
Return None if not found.
"""
...
raise NotImplementedError
@abstractmethod
async def aget(self, key):
"""
Get the key from the backend store and return the value.
Return None if not found.
"""
...
@abstractmethod
def mget(self, keys):
"""
Get the keys from the backend store and return a dict mapping
each found key to its value. Return an empty dict if no keys
are provided or none are found.
Get the keys from the backend store and return a list of the values.
Return an empty list if not found.
"""
...
raise NotImplementedError
@abstractmethod
async def amget(self, keys):
"""
Get the keys from the backend store and return a dict mapping
each found key to its value. Return an empty dict if no keys
are provided or none are found.
"""
...
@abstractmethod
def set(self, key, value):
"""Add the value to the backend store given the key."""
...
@abstractmethod
async def aset(self, key, value):
"""Add the value to the backend store given the key."""
...
"""
Add the value to the backend store given the key.
"""
raise NotImplementedError

View file

@ -1,176 +0,0 @@
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
from django.db import OperationalError
from django.db import ProgrammingError
from django.db import transaction
from django.db.models.signals import post_save
from constance import config
from constance import settings
from constance import signals
from constance.backends import Backend
from constance.codecs import dumps
from constance.codecs import loads
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 "
f"subclass of Django's local-memory backend ({settings.DATABASE_CACHE_BACKEND!r}). Please "
"set it to a backend that supports cross-process caching."
)
else:
self._cache = None
self.autofill()
# Clear simple cache.
post_save.connect(self.clear, sender=self._model)
def add_prefix(self, key):
return f"{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 = {full_cachekey: 1}
for key, value in self.mget(settings.CONFIG).items():
autofill_values[self.add_prefix(key)] = value
self._cache.set_many(autofill_values, timeout=self._autofill_timeout)
def mget(self, keys):
result = {}
if not keys:
return result
keys = {self.add_prefix(key): key for key in keys}
try:
stored = self._model._default_manager.filter(key__in=keys)
for const in stored:
result[keys[const.key]] = loads(const.value)
except (OperationalError, ProgrammingError):
pass
return result
def get(self, key):
key = self.add_prefix(key)
value = None
if self._cache:
value = self._cache.get(key)
if value is None:
self.autofill()
value = self._cache.get(key)
if value is None:
match = self._model._default_manager.filter(key=key).only("value").first()
if match:
value = loads(match.value)
if self._cache:
self._cache.add(key, value)
return value
async def aget(self, key):
from asgiref.sync import sync_to_async
prefixed_key = self.add_prefix(key)
value = None
if self._cache:
value = await self._cache.aget(prefixed_key)
if value is None:
await sync_to_async(self.autofill, thread_sensitive=True)()
value = await self._cache.aget(prefixed_key)
if value is None:
match = await self._model._default_manager.filter(key=prefixed_key).only("value").afirst()
if match:
value = loads(match.value)
if self._cache:
await self._cache.aadd(prefixed_key, value)
return value
async def amget(self, keys):
if not keys:
return {}
prefixed_keys_map = {self.add_prefix(key): key for key in keys}
results = {}
if self._cache:
cache_results = await self._cache.aget_many(prefixed_keys_map.keys())
for prefixed_key, value in cache_results.items():
results[prefixed_keys_map[prefixed_key]] = value
missing_prefixed_keys = [k for k in prefixed_keys_map if prefixed_keys_map[k] not in results]
if missing_prefixed_keys:
try:
async for const in self._model._default_manager.filter(key__in=missing_prefixed_keys):
results[prefixed_keys_map[const.key]] = loads(const.value)
except (OperationalError, ProgrammingError):
pass
return results
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
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=dumps(value))
created = True
except IntegrityError:
# Allow concurrent writes
constance = queryset.get(key=key)
if not created:
old_value = loads(constance.value)
constance.value = dumps(value)
constance.save(update_fields=["value"])
else:
old_value = None
if self._cache:
self._cache.set(key, value)
signals.config_updated.send(sender=config, key=key, old_value=old_value, new_value=value)
async def aset(self, key, value):
from asgiref.sync import sync_to_async
# We use sync_to_async because Django's transaction.atomic() and database connections are thread-local.
# This ensures the operation runs in the correct database thread until native async transactions are supported.
return await sync_to_async(self.set, thread_sensitive=True)(key, 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()

View file

@ -0,0 +1,90 @@
from django.core.cache import caches
from django.core.cache.backends.locmem import LocMemCache
from django.core.exceptions import ImproperlyConfigured
from django.db.models.signals import post_save
from .. import Backend
from ... import settings
class DatabaseBackend(Backend):
def __init__(self):
from .models import Constance
self._model = Constance
self._prefix = settings.DATABASE_PREFIX
self._autofill_timeout = settings.DATABASE_CACHE_AUTOFILL_TIMEOUT
self._autofill_cachekey = 'autofilled'
if not self._model._meta.installed:
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.keys()):
autofill_values[self.add_prefix(key)] = value
self._cache.set_many(autofill_values, timeout=self._autofill_timeout)
def mget(self, keys):
if not keys:
return
keys = dict((self.add_prefix(key), key) for key in keys)
stored = self._model._default_manager.filter(key__in=keys.keys())
for const in stored:
yield keys[const.key], const.value
def get(self, key):
key = self.add_prefix(key)
if self._cache:
value = self._cache.get(key)
else:
value = None
if value is None:
try:
value = self._model._default_manager.get(key=key).value
except self._model.DoesNotExist:
pass
else:
if self._cache:
self._cache.add(key, value)
return value
def set(self, key, value):
constance, created = self._model._default_manager.get_or_create(
key=self.add_prefix(key), defaults={'value': value}
)
if not created:
constance.value = value
constance.save()
def clear(self, sender, instance, created, **kwargs):
if self._cache and not created:
keys = [self.add_prefix(k)
for k in settings.CONFIG.keys()]
keys.append(self.add_prefix(self._autofill_cachekey))
self._cache.delete_many(keys)
self.autofill()

View file

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import picklefield.fields
class Migration(migrations.Migration):
dependencies = []
operations = [
migrations.CreateModel(
name='Constance',
fields=[
('id', models.AutoField(verbose_name='ID', primary_key=True,
auto_created=True, serialize=False)),
('key', models.CharField(unique=True, max_length=255)),
('value', picklefield.fields.PickledObjectField(editable=False)),
],
options={
'verbose_name': 'constance',
'verbose_name_plural': 'constances',
'db_table': 'constance_config',
},
bases=(models.Model,),
),
]

View file

@ -0,0 +1,24 @@
from django.db import models
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import ugettext_lazy as _
try:
from picklefield import PickledObjectField
except ImportError:
raise ImproperlyConfigured("Couldn't find the the 3rd party app "
"django-picklefield which is required for "
"the constance database backend.")
class Constance(models.Model):
key = models.CharField(max_length=255, unique=True)
value = PickledObjectField()
class Meta:
verbose_name = _('constance')
verbose_name_plural = _('constances')
db_table = 'constance_config'
def __unicode__(self):
return self.key

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from south.db import db
from south.v2 import SchemaMigration
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Constance'
db.create_table('constance_config', (
('id', self.gf('django.db.models.fields.AutoField')(
primary_key=True)),
('key', self.gf('django.db.models.fields.TextField')()),
('value', self.gf('picklefield.fields.PickledObjectField')()),
))
db.send_create_signal('database', ['Constance'])
def backwards(self, orm):
# Deleting model 'Constance'
db.delete_table('constance_config')
models = {
'database.constance': {
'Meta': {'object_name': 'Constance',
'db_table': "'constance_config'"},
'id': ('django.db.models.fields.AutoField', [],
{'primary_key': 'True'}),
'key': ('django.db.models.fields.TextField', [], {}),
'value': ('picklefield.fields.PickledObjectField', [], {})
}
}
complete_apps = ['database']

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from south.db import db
from south.v2 import SchemaMigration
class Migration(SchemaMigration):
def forwards(self, orm):
# Changing field 'Constance.key'
db.alter_column('constance_config', 'key',
self.gf('django.db.models.fields.CharField')(
max_length=255))
# Adding unique constraint on 'Constance', fields ['key']
db.create_unique('constance_config', ['key'])
def backwards(self, orm):
# Removing unique constraint on 'Constance', fields ['key']
db.delete_unique('constance_config', ['key'])
# Changing field 'Constance.key'
db.alter_column('constance_config', 'key',
self.gf('django.db.models.fields.TextField')())
models = {
'database.constance': {
'Meta': {'object_name': 'Constance',
'db_table': "'constance_config'"},
'id': ('django.db.models.fields.AutoField', [],
{'primary_key': 'True'}),
'key': ('django.db.models.fields.CharField', [],
{'unique': 'True', 'max_length': '255'}),
'value': ('picklefield.fields.PickledObjectField', [], {})
}
}
complete_apps = ['database']

View file

@ -1,46 +0,0 @@
from threading import Lock
from constance import config
from constance import signals
from . import Backend
class MemoryBackend(Backend):
"""Simple in-memory backend that should be mostly used for testing purposes."""
_storage = {}
_lock = Lock()
def __init__(self):
super().__init__()
def get(self, key):
with self._lock:
return self._storage.get(key)
async def aget(self, key):
# Memory operations are fast enough that we don't need true async here
return self.get(key)
def mget(self, keys):
if not keys:
return {}
with self._lock:
return {key: self._storage[key] for key in keys if key in self._storage}
async def amget(self, keys):
if not keys:
return {}
with self._lock:
return {key: self._storage[key] for key in keys if key in self._storage}
def set(self, key, value):
with self._lock:
old_value = self._storage.get(key)
self._storage[key] = value
signals.config_updated.send(sender=config, key=key, old_value=old_value, new_value=value)
async def aset(self, key, value):
# Memory operations are fast enough that we don't need true async here
self.set(key, value)

View file

@ -1,62 +1,37 @@
import asyncio
from threading import RLock
from time import monotonic
from django.core.exceptions import ImproperlyConfigured
from django.utils import six
from django.utils.six.moves import zip
from constance import config
from constance import settings
from constance import signals
from constance import utils
from constance.backends import Backend
from constance.codecs import dumps
from constance.codecs import loads
from . import Backend
from .. import settings, utils
try:
from cPickle import loads, dumps
except ImportError:
from pickle import loads, dumps
class RedisBackend(Backend):
def __init__(self):
super().__init__()
super(RedisBackend, self).__init__()
self._prefix = settings.REDIS_PREFIX
connection_cls = settings.REDIS_CONNECTION_CLASS
async_connection_cls = settings.REDIS_ASYNC_CONNECTION_CLASS
if connection_cls:
if connection_cls is not None:
self._rd = utils.import_module_attr(connection_cls)()
else:
try:
import redis
except ImportError:
raise ImproperlyConfigured("The Redis backend requires redis-py to be installed.") from None
raise ImproperlyConfigured(
"The Redis backend requires redis-py to be installed.")
if isinstance(settings.REDIS_CONNECTION, six.string_types):
self._rd = redis.from_url(settings.REDIS_CONNECTION)
else:
if isinstance(settings.REDIS_CONNECTION, str):
self._rd = redis.from_url(settings.REDIS_CONNECTION)
else:
self._rd = redis.Redis(**settings.REDIS_CONNECTION)
if async_connection_cls:
self._ard = utils.import_module_attr(async_connection_cls)()
else:
try:
import redis.asyncio as aredis
except ImportError:
# We set this to none instead of raising an error to indicate that async support is not available
# without breaking existing sync usage.
self._ard = None
else:
if isinstance(settings.REDIS_CONNECTION, str):
self._ard = aredis.from_url(settings.REDIS_CONNECTION)
else:
self._ard = aredis.Redis(**settings.REDIS_CONNECTION)
self._rd = redis.Redis(**settings.REDIS_CONNECTION)
def add_prefix(self, key):
return f"{self._prefix}{key}"
def _check_async_support(self):
if self._ard is None:
raise ImproperlyConfigured(
"Async support for the Redis backend requires redis>=4.2.0 "
"or a custom CONSTANCE_REDIS_ASYNC_CONNECTION_CLASS to be configured."
)
return "%s%s" % (self._prefix, key)
def get(self, key):
value = self._rd.get(self.add_prefix(key))
@ -64,156 +39,13 @@ class RedisBackend(Backend):
return loads(value)
return None
async def aget(self, key):
self._check_async_support()
value = await self._ard.get(self.add_prefix(key))
if value:
return loads(value)
return None
def mget(self, keys):
if not keys:
return {}
return
prefixed_keys = [self.add_prefix(key) for key in keys]
return {key: loads(value) for key, value in zip(keys, self._rd.mget(prefixed_keys)) if value}
async def amget(self, keys):
if not keys:
return {}
self._check_async_support()
prefixed_keys = [self.add_prefix(key) for key in keys]
values = await self._ard.mget(prefixed_keys)
return {key: loads(value) for key, value in zip(keys, values) if value}
for key, value in zip(keys, self._rd.mget(prefixed_keys)):
if value:
yield key, loads(value)
def set(self, key, value):
old_value = self.get(key)
self._rd.set(self.add_prefix(key), dumps(value))
signals.config_updated.send(sender=config, key=key, old_value=old_value, new_value=value)
async def _aset_internal(self, key, value, old_value):
"""
Internal set operation. Separated to allow subclasses to provide old_value
without going through self.aget() which may have locking behavior.
"""
self._check_async_support()
await self._ard.set(self.add_prefix(key), dumps(value))
signals.config_updated.send(sender=config, key=key, old_value=old_value, new_value=value)
async def aset(self, key, value):
old_value = await self.aget(key)
await self._aset_internal(key, value, old_value)
class CachingRedisBackend(RedisBackend):
_sentinel = object()
_lock = RLock()
_async_lock = None # Lazy-initialized asyncio.Lock
def __init__(self):
super().__init__()
self._timeout = settings.REDIS_CACHE_TIMEOUT
self._cache = {}
self._sentinel = object()
def _get_async_lock(self):
# Lazily create the asyncio lock to avoid issues with event loops
if self._async_lock is None:
self._async_lock = asyncio.Lock()
return self._async_lock
def _has_expired(self, value):
return value[0] <= monotonic()
def _cache_value(self, key, new_value):
self._cache[key] = (monotonic() + self._timeout, new_value)
def get(self, key):
value = self._cache.get(key, self._sentinel)
if value is self._sentinel or self._has_expired(value):
with self._lock:
new_value = super().get(key)
self._cache_value(key, new_value)
return new_value
return value[1]
async def _aget_unlocked(self, key):
"""
Get value with cache support but without acquiring lock.
Caller must already hold the lock.
"""
value = self._cache.get(key, self._sentinel)
if value is self._sentinel or self._has_expired(value):
new_value = await super().aget(key)
self._cache_value(key, new_value)
return new_value
return value[1]
async def aget(self, key):
value = self._cache.get(key, self._sentinel)
if value is self._sentinel or self._has_expired(value):
async with self._get_async_lock():
# Double-check after acquiring lock, then delegate to unlocked version
return await self._aget_unlocked(key)
return value[1]
def set(self, key, value):
with self._lock:
super().set(key, value)
self._cache_value(key, value)
async def aset(self, key, value):
async with self._get_async_lock():
# Use unlocked version since we already hold the lock
old_value = await self._aget_unlocked(key)
# Use internal method to avoid lock recursion (super().aset calls self.aget)
await self._aset_internal(key, value, old_value)
self._cache_value(key, value)
def mget(self, keys):
if not keys:
return {}
result = {}
for key in keys:
value = self.get(key)
if value is not None:
result[key] = value
return result
async def amget(self, keys):
if not keys:
return {}
results = {}
missing_keys = []
# First, check the local cache for all keys
for key in keys:
value = self._cache.get(key, self._sentinel)
if value is not self._sentinel and not self._has_expired(value):
results[key] = value[1]
else:
missing_keys.append(key)
# Fetch missing keys from Redis
if missing_keys:
async with self._get_async_lock():
# Re-check cache for keys that might have been fetched while waiting for lock
still_missing = []
for key in missing_keys:
value = self._cache.get(key, self._sentinel)
if value is not self._sentinel and not self._has_expired(value):
results[key] = value[1]
else:
still_missing.append(key)
if still_missing:
fetched = await super().amget(still_missing)
for key, value in fetched.items():
self._cache_value(key, value)
results[key] = value
return results

View file

@ -1,149 +1,32 @@
import asyncio
import warnings
from . import settings
from . import utils
from . import settings, utils
class AsyncValueProxy:
def __init__(self, key, config, default):
self._key = key
self._config = config
self._default = default
self._value = None
self._fetched = False
def __await__(self):
return self._get_value().__await__()
async def _get_value(self):
if not self._fetched:
result = await self._config._backend.aget(self._key)
if result is None:
result = self._default
await self._config.aset(self._key, result)
self._value = result
self._fetched = True
return self._value
def _get_sync_value(self):
warnings.warn(
f"Synchronous access to Constance setting '{self._key}' inside an async loop. "
f"Use 'await config.{self._key}' instead.",
RuntimeWarning,
stacklevel=3,
)
return self._config._get_sync_value(self._key, self._default)
def __str__(self):
return str(self._get_sync_value())
def __repr__(self):
return repr(self._get_sync_value())
def __int__(self):
return int(self._get_sync_value())
def __float__(self):
return float(self._get_sync_value())
def __bool__(self):
return bool(self._get_sync_value())
def __eq__(self, other):
return self._get_sync_value() == other
def __ne__(self, other):
return self._get_sync_value() != other
def __lt__(self, other):
return self._get_sync_value() < other
def __le__(self, other):
return self._get_sync_value() <= other
def __gt__(self, other):
return self._get_sync_value() > other
def __ge__(self, other):
return self._get_sync_value() >= other
def __getitem__(self, key):
return self._get_sync_value()[key]
def __iter__(self):
return iter(self._get_sync_value())
def __len__(self):
return len(self._get_sync_value())
def __contains__(self, item):
return item in self._get_sync_value()
def __hash__(self):
return hash(self._get_sync_value())
def __add__(self, other):
return self._get_sync_value() + other
def __sub__(self, other):
return self._get_sync_value() - other
def __mul__(self, other):
return self._get_sync_value() * other
def __truediv__(self, other):
return self._get_sync_value() / other
class Config:
"""The global config wrapper that handles the backend."""
class Config(object):
"""
The global config wrapper that handles the backend.
"""
def __init__(self):
super().__setattr__("_backend", utils.import_module_attr(settings.BACKEND)())
super(Config, self).__setattr__('_backend',
utils.import_module_attr(settings.BACKEND)())
def _get_sync_value(self, key, default):
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)
return result
return result
def __getattr__(self, key):
if key == "_backend":
return super().__getattribute__(key)
try:
if len(settings.CONFIG[key]) not in (2, 3):
raise AttributeError(key)
default = settings.CONFIG[key][0]
except KeyError as e:
raise AttributeError(key) from e
try:
asyncio.get_running_loop()
except RuntimeError:
return self._get_sync_value(key, default)
return AsyncValueProxy(key, self, default)
def __setattr__(self, key, value):
if key == "_backend":
super().__setattr__(key, value)
return
if key not in settings.CONFIG:
raise AttributeError(key)
self._backend.set(key, value)
return
async def aset(self, key, value):
if key not in settings.CONFIG:
raise AttributeError(key)
await self._backend.aset(key, value)
async def amget(self, keys):
backend_values = await self._backend.amget(keys)
# Merge with defaults like utils.get_values_for_keys
default_initial = {name: settings.CONFIG[name][0] for name in keys if name in settings.CONFIG}
return dict(default_initial, **backend_values)
def __dir__(self):
return settings.CONFIG.keys()

View file

@ -1,64 +0,0 @@
from __future__ import annotations
from django.core import checks
from django.core.checks import CheckMessage
from django.utils.translation import gettext_lazy as _
def check_fieldsets(*args, **kwargs) -> list[CheckMessage]:
"""
A Django system check to make sure that, if defined,
CONFIG_FIELDSETS is consistent with settings.CONFIG.
"""
from . import settings
errors = []
if hasattr(settings, "CONFIG_FIELDSETS") and settings.CONFIG_FIELDSETS:
missing_keys, extra_keys = get_inconsistent_fieldnames()
if missing_keys:
check = checks.Warning(
_("CONSTANCE_CONFIG_FIELDSETS is missing field(s) that exists in CONSTANCE_CONFIG."),
hint=", ".join(sorted(missing_keys)),
obj="settings.CONSTANCE_CONFIG",
id="constance.E001",
)
errors.append(check)
if extra_keys:
check = checks.Warning(
_("CONSTANCE_CONFIG_FIELDSETS contains extra field(s) that does not exist in CONFIG."),
hint=", ".join(sorted(extra_keys)),
obj="settings.CONSTANCE_CONFIG",
id="constance.E002",
)
errors.append(check)
return errors
def get_inconsistent_fieldnames() -> tuple[set, set]:
"""
Returns a pair of values:
1) set of keys from settings.CONFIG that are not accounted for in settings.CONFIG_FIELDSETS
2) set of keys from settings.CONFIG_FIELDSETS that are not present in settings.CONFIG
If there are no fieldnames in settings.CONFIG_FIELDSETS, returns an empty set.
"""
from . import settings
if isinstance(settings.CONFIG_FIELDSETS, dict):
fieldset_items = settings.CONFIG_FIELDSETS.items()
else:
fieldset_items = settings.CONFIG_FIELDSETS
unique_field_names = set()
for _fieldset_title, fields_list in fieldset_items:
# fields_list can be a dictionary, when a fieldset is defined as collapsible
# https://django-constance.readthedocs.io/en/latest/#fieldsets-collapsing
if isinstance(fields_list, dict) and "fields" in fields_list:
fields_list = fields_list["fields"]
unique_field_names.update(fields_list)
if not unique_field_names:
return unique_field_names, unique_field_names
config_keys = set(settings.CONFIG.keys())
missing_keys = config_keys - unique_field_names
extra_keys = unique_field_names - config_keys
return missing_keys, extra_keys

View file

@ -1,101 +0,0 @@
from __future__ import annotations
import json
import logging
import uuid
from datetime import date
from datetime import datetime
from datetime import time
from datetime import timedelta
from decimal import Decimal
from typing import Any
from typing import Protocol
from typing import TypeVar
logger = logging.getLogger(__name__)
DEFAULT_DISCRIMINATOR = "default"
class JSONEncoder(json.JSONEncoder):
"""Django-constance custom json encoder."""
def default(self, o):
for discriminator, (t, _, encoder) in _codecs.items():
if isinstance(o, t):
return _as(discriminator, encoder(o))
raise TypeError(f"Object of type {o.__class__.__name__} is not JSON serializable")
def _as(discriminator: str, v: Any) -> dict[str, Any]:
return {"__type__": discriminator, "__value__": v}
def dumps(obj, _dumps=json.dumps, cls=JSONEncoder, default_kwargs=None, **kwargs):
"""Serialize object to json string."""
default_kwargs = default_kwargs or {}
is_default_type = isinstance(obj, (list, dict, str, int, bool, float, type(None)))
return _dumps(
_as(DEFAULT_DISCRIMINATOR, obj) if is_default_type else obj, cls=cls, **dict(default_kwargs, **kwargs)
)
def loads(s, _loads=json.loads, *, first_level=True, **kwargs):
"""Deserialize json string to object."""
if first_level:
return _loads(s, object_hook=object_hook, **kwargs)
if isinstance(s, dict) and "__type__" not in s and "__value__" not in s:
return {k: loads(v, first_level=False) for k, v in s.items()}
if isinstance(s, list):
return list(loads(v, first_level=False) for v in s)
return _loads(s, object_hook=object_hook, **kwargs)
def object_hook(o: dict) -> Any:
"""Hook function to perform custom deserialization."""
if o.keys() == {"__type__", "__value__"}:
if o["__type__"] == DEFAULT_DISCRIMINATOR:
return o["__value__"]
codec = _codecs.get(o["__type__"])
if not codec:
raise ValueError(f"Unsupported type: {o['__type__']}")
return codec[1](o["__value__"])
if "__type__" not in o and "__value__" not in o:
return o
logger.error("Cannot deserialize object: %s", o)
raise ValueError(f"Invalid object: {o}")
T = TypeVar("T")
class Encoder(Protocol[T]):
def __call__(self, value: T, /) -> str: ... # pragma: no cover
class Decoder(Protocol[T]):
def __call__(self, value: str, /) -> T: ... # pragma: no cover
def register_type(t: type[T], discriminator: str, encoder: Encoder[T], decoder: Decoder[T]):
if not discriminator:
raise ValueError("Discriminator must be specified")
if _codecs.get(discriminator) or discriminator == DEFAULT_DISCRIMINATOR:
raise ValueError(f"Type with discriminator {discriminator} is already registered")
_codecs[discriminator] = (t, decoder, encoder)
_codecs: dict[str, tuple[type, Decoder, Encoder]] = {}
def _register_default_types():
# NOTE: datetime should be registered before date, because datetime is also instance of date.
register_type(datetime, "datetime", datetime.isoformat, datetime.fromisoformat)
register_type(date, "date", lambda o: o.isoformat(), lambda o: datetime.fromisoformat(o).date())
register_type(time, "time", lambda o: o.isoformat(), time.fromisoformat)
register_type(Decimal, "decimal", str, Decimal)
register_type(uuid.UUID, "uuid", lambda o: o.hex, uuid.UUID)
register_type(timedelta, "timedelta", lambda o: o.total_seconds(), lambda o: timedelta(seconds=o))
_register_default_types()

View file

@ -1,168 +0,0 @@
import hashlib
from datetime import date
from datetime import datetime
from datetime import time
from datetime import timedelta
from decimal import Decimal
from os.path import join
from django import conf
from django import forms
from django.contrib import messages
from django.contrib.admin import widgets
from django.core.exceptions import ImproperlyConfigured
from django.core.files.storage import default_storage
from django.forms import fields
from django.utils import timezone
from django.utils.encoding import smart_bytes
from django.utils.module_loading import import_string
from django.utils.text import normalize_newlines
from django.utils.translation import gettext_lazy as _
from . import LazyConfig
from . import settings
from .checks import get_inconsistent_fieldnames
config = LazyConfig()
NUMERIC_WIDGET = forms.TextInput(attrs={"size": 10})
INTEGER_LIKE = (fields.IntegerField, {"widget": NUMERIC_WIDGET})
STRING_LIKE = (
fields.CharField,
{
"widget": forms.Textarea(attrs={"rows": 3}),
"required": False,
},
)
FIELDS = {
bool: (fields.BooleanField, {"required": False}),
int: INTEGER_LIKE,
Decimal: (fields.DecimalField, {"widget": NUMERIC_WIDGET}),
str: STRING_LIKE,
datetime: (fields.SplitDateTimeField, {"widget": widgets.AdminSplitDateTime}),
timedelta: (fields.DurationField, {"widget": widgets.AdminTextInputWidget}),
date: (fields.DateField, {"widget": widgets.AdminDateWidget}),
time: (fields.TimeField, {"widget": widgets.AdminTimeWidget}),
float: (fields.FloatField, {"widget": NUMERIC_WIDGET}),
}
def parse_additional_fields(fields):
for key in fields:
field = list(fields[key])
if len(field) == 1:
field.append({})
field[0] = import_string(field[0])
if "widget" in field[1]:
klass = import_string(field[1]["widget"])
field[1]["widget"] = klass(**(field[1].get("widget_kwargs", {}) or {}))
if "widget_kwargs" in field[1]:
del field[1]["widget_kwargs"]
fields[key] = field
return fields
FIELDS.update(parse_additional_fields(settings.ADDITIONAL_FIELDS))
class ConstanceForm(forms.Form):
version = forms.CharField(widget=forms.HiddenInput)
def __init__(self, initial, request=None, *args, **kwargs):
super().__init__(*args, initial=initial, **kwargs)
version_hash = hashlib.sha256()
only_view = request and not request.user.has_perm("constance.change_config")
if only_view:
messages.warning(
request,
_("You don't have permission to change these values"),
)
for name, options in settings.CONFIG.items():
default = options[0]
if len(options) == 3:
config_type = options[2]
if config_type not in settings.ADDITIONAL_FIELDS and not isinstance(default, config_type):
raise ImproperlyConfigured(
_(
"Default value type must be "
"equal to declared config "
"parameter type. Please fix "
"the default value of "
"'%(name)s'."
)
% {"name": name}
)
else:
config_type = type(default)
if config_type not in FIELDS:
raise ImproperlyConfigured(
_(
"Constance doesn't support "
"config values of the type "
"%(config_type)s. Please fix "
"the value of '%(name)s'."
)
% {"config_type": config_type, "name": name}
)
field_class, kwargs = FIELDS[config_type]
if only_view:
kwargs["disabled"] = True
self.fields[name] = field_class(label=name, **kwargs)
version_hash.update(smart_bytes(initial.get(name, "")))
self.initial["version"] = version_hash.hexdigest()
def save(self):
for file_field in self.files:
file = self.cleaned_data[file_field]
self.cleaned_data[file_field] = default_storage.save(join(settings.FILE_ROOT, file.name), file)
for name in settings.CONFIG:
current = getattr(config, name)
new = self.cleaned_data[name]
if isinstance(new, str):
new = normalize_newlines(new)
if conf.settings.USE_TZ and isinstance(current, datetime) and not timezone.is_aware(current):
current = timezone.make_aware(current)
if current != new:
setattr(config, name, new)
def clean_version(self):
value = self.cleaned_data["version"]
if settings.IGNORE_ADMIN_VERSION_CHECK:
return value
if value != self.initial["version"]:
raise forms.ValidationError(
_("The settings have been modified by someone else. Please reload the form and resubmit your changes.")
)
return value
def clean(self):
cleaned_data = super().clean()
if not settings.CONFIG_FIELDSETS:
return cleaned_data
missing_keys, extra_keys = get_inconsistent_fieldnames()
if missing_keys or extra_keys:
raise forms.ValidationError(
_("CONSTANCE_CONFIG_FIELDSETS is missing field(s) that exists in CONSTANCE_CONFIG.")
)
return cleaned_data

View file

@ -1,100 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <med.b.makhlouf@gmail.com>, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-13 19:40+0530\n"
"PO-Revision-Date: 2020-11-30 23:15+0100\n"
"Language: ar\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Last-Translator: \n"
"Language-Team: \n"
"X-Generator: \n"
#: admin.py:113
#, python-format
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr ""
"يجب أن يكون نوع القيمة الافتراضي مساوياً لنوع معلمة التكوين المعلن. الرجاء "
"إصلاح القيمة الافتراضية لـ '%(name)s'."
#: admin.py:123
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr ""
"لا يعتمد كونستانس قيم التكوين من النوع %(config_type)s. الرجاء إصلاح قيمة "
"'%(name)s'."
#: admin.py:147
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr ""
"تم تعديل الإعدادات بواسطة شخص آخر. الرجاء إعادة تحميل النموذج وإعادة إرسال "
"التغييرات."
#: admin.py:160
msgid ""
"CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in "
"CONSTANCE_CONFIG."
msgstr ""
"لا يحتوي CONSTANCE_CONFIG_FIELDSETS على حقول موجودة في CONSTANCE_CONFIG."
#: admin.py:224
msgid "Live settings updated successfully."
msgstr "تم تحديث الإعدادات المباشرة بنجاح."
#: admin.py:285
msgid "config"
msgstr "التكوين"
#: apps.py:8
msgid "Constance"
msgstr "كونستانس"
#: backends/database/models.py:19
msgid "constance"
msgstr "كونستانس"
#: backends/database/models.py:20
msgid "constances"
msgstr "كونستانس"
#: management/commands/constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr ""
"الحصول على / تعيين إعدادات التكوين في قاعدة البيانات التي تعالجها كونستانس"
#: templates/admin/constance/change_list.html:75
msgid "Save"
msgstr "حفظ"
#: templates/admin/constance/change_list.html:84
msgid "Home"
msgstr "الصفحة الرئيسية"
#: templates/admin/constance/includes/results_list.html:5
msgid "Name"
msgstr "الإسم"
#: templates/admin/constance/includes/results_list.html:6
msgid "Default"
msgstr "افتراضي"
#: templates/admin/constance/includes/results_list.html:7
msgid "Value"
msgstr "القيمة"
#: templates/admin/constance/includes/results_list.html:8
msgid "Is modified"
msgstr "تم تعديله"

View file

@ -1,58 +1,48 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: django-constance\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-13 19:40+0530\n"
"POT-Creation-Date: 2014-11-27 19:05+0100\n"
"PO-Revision-Date: 2014-11-27 18:13+0000\n"
"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
"Language-Team: Czech (Czech Republic) (http://www.transifex.com/projects/p/"
"django-constance/language/cs_CZ/)\n"
"Language: cs_CZ\n"
"Language-Team: Czech (Czech Republic) (http://www.transifex.com/projects/p/django-constance/language/cs_CZ/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: cs_CZ\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
#: admin.py:113
#, python-format
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr ""
#: admin.py:123
#: admin.py:72
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr ""
#: admin.py:147
#: admin.py:91
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr ""
#: admin.py:160
msgid ""
"CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in "
"CONSTANCE_CONFIG."
msgstr ""
#: admin.py:224
#: admin.py:129
msgid "Live settings updated successfully."
msgstr "Nastavení bylo úspěšně uloženo."
#: admin.py:285
#: admin.py:134
msgid "Constance config"
msgstr "Nastavení konstant"
#: admin.py:177
msgid "config"
msgstr "nastavení"
#: apps.py:8
#: apps.py:9
msgid "Constance"
msgstr ""
@ -64,33 +54,26 @@ msgstr "konstanta"
msgid "constances"
msgstr "konstanty"
#: management/commands/constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr ""
#: templates/admin/constance/change_list.html:75
msgid "Save"
msgstr "Uložit"
#: templates/admin/constance/change_list.html:84
msgid "Home"
msgstr "Domů"
#: templates/admin/constance/includes/results_list.html:5
#: templates/admin/constance/change_list.html:50
msgid "Name"
msgstr "Název"
#: templates/admin/constance/includes/results_list.html:6
#: templates/admin/constance/change_list.html:51
msgid "Default"
msgstr "Výchozí hodnota"
#: templates/admin/constance/includes/results_list.html:7
#: templates/admin/constance/change_list.html:52
msgid "Value"
msgstr "Hodnota"
#: templates/admin/constance/includes/results_list.html:8
#: templates/admin/constance/change_list.html:53
msgid "Is modified"
msgstr "Je změněna?"
#~ msgid "Constance config"
#~ msgstr "Nastavení konstant"
#: templates/admin/constance/change_list.html:79
msgid "Save"
msgstr "Uložit"
#: templates/admin/constance/change_list.html:89
msgid "Home"
msgstr "Domů"

View file

@ -1,68 +1,49 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
#
# Translators:
# Jannis Leidel <jannis@leidel.info>, 2014
msgid ""
msgstr ""
"Project-Id-Version: django-constance\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-13 19:40+0530\n"
"POT-Creation-Date: 2014-11-27 19:05+0100\n"
"PO-Revision-Date: 2014-11-27 18:17+0000\n"
"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
"Language-Team: German (http://www.transifex.com/projects/p/django-constance/"
"language/de/)\n"
"Language: de\n"
"Language-Team: German (http://www.transifex.com/projects/p/django-constance/language/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: admin.py:113
#, fuzzy, python-format
#| msgid ""
#| "Constance doesn't support config values of the type %(config_type)s. "
#| "Please fix the value of '%(name)s'."
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr ""
"Konstanze unterstützt die Konfigurationswerte vom Typ %(config_type)s nicht. "
"Bitte den Ausgangswert von '%(name)s' ändern."
#: admin.py:123
#: admin.py:72
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr ""
"Konstanze unterstützt die Konfigurationswerte vom Typ %(config_type)s nicht. "
"Bitte den Ausgangswert von '%(name)s' ändern."
msgstr "Konstanze unterstützt die Konfigurationswerte vom Typ %(config_type)s nicht. Bitte den Ausgangswert von '%(name)s' ändern."
#: admin.py:147
#: admin.py:91
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr ""
"Die Konfiguration wurde seit Öffnen dieser Seite verändert. Bitte die Seite "
"neuladen und die Änderungen erneut vornehmen."
msgstr "Die Konfiguration wurde seit Öffnen dieser Seite verändert. Bitte die Seite neuladen und die Änderungen erneut vornehmen."
#: admin.py:160
msgid ""
"CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in "
"CONSTANCE_CONFIG."
msgstr ""
#: admin.py:224
#: admin.py:129
msgid "Live settings updated successfully."
msgstr "Die Livekonfiguration wurde erfolgreich aktualisiert."
#: admin.py:285
#: admin.py:134
msgid "Constance config"
msgstr "Constance Konfiguration"
#: admin.py:177
msgid "config"
msgstr "Konfiguration"
#: apps.py:8
#: apps.py:9
msgid "Constance"
msgstr "Konstanze"
@ -74,33 +55,26 @@ msgstr "Konstanze"
msgid "constances"
msgstr "Konstanzes"
#: management/commands/constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr ""
#: templates/admin/constance/change_list.html:75
msgid "Save"
msgstr "Sichern"
#: templates/admin/constance/change_list.html:84
msgid "Home"
msgstr "Start"
#: templates/admin/constance/includes/results_list.html:5
#: templates/admin/constance/change_list.html:50
msgid "Name"
msgstr "Name"
#: templates/admin/constance/includes/results_list.html:6
#: templates/admin/constance/change_list.html:51
msgid "Default"
msgstr "Voreinstellung"
#: templates/admin/constance/includes/results_list.html:7
#: templates/admin/constance/change_list.html:52
msgid "Value"
msgstr "Wert"
#: templates/admin/constance/includes/results_list.html:8
#: templates/admin/constance/change_list.html:53
msgid "Is modified"
msgstr "Ist modifiziert"
#~ msgid "Constance config"
#~ msgstr "Constance Konfiguration"
#: templates/admin/constance/change_list.html:79
msgid "Save"
msgstr "Sichern"
#: templates/admin/constance/change_list.html:89
msgid "Home"
msgstr "Start"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-19 21:00+0500\n"
"POT-Creation-Date: 2014-11-27 19:05+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,45 +17,32 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: admin.py:113
#, python-format
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr ""
#: admin.py:123
#: admin.py:72
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr ""
#: admin.py:147
#: admin.py:91
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr ""
#: admin.py:160
msgid ""
"CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in "
"CONSTANCE_CONFIG."
msgstr ""
#: admin.py:224
#: admin.py:129
msgid "Live settings updated successfully."
msgstr ""
#: admin.py:267
msgid "Failed to update live settings."
#: admin.py:134
msgid "Constance config"
msgstr ""
#: admin.py:285
#: admin.py:177
msgid "config"
msgstr ""
#: apps.py:8
#: apps.py:9
msgid "Constance"
msgstr ""
@ -67,30 +54,26 @@ msgstr ""
msgid "constances"
msgstr ""
#: management/commands/constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr ""
#: templates/admin/constance/change_list.html:75
msgid "Save"
msgstr ""
#: templates/admin/constance/change_list.html:84
msgid "Home"
msgstr ""
#: templates/admin/constance/includes/results_list.html:5
#: templates/admin/constance/change_list.html:50
msgid "Name"
msgstr ""
#: templates/admin/constance/includes/results_list.html:6
#: templates/admin/constance/change_list.html:51
msgid "Default"
msgstr ""
#: templates/admin/constance/includes/results_list.html:7
#: templates/admin/constance/change_list.html:52
msgid "Value"
msgstr ""
#: templates/admin/constance/includes/results_list.html:8
#: templates/admin/constance/change_list.html:53
msgid "Is modified"
msgstr ""
#: templates/admin/constance/change_list.html:79
msgid "Save"
msgstr ""
#: templates/admin/constance/change_list.html:89
msgid "Home"
msgstr ""

View file

@ -1,37 +1,24 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
#
# Translators:
# Igor Támara <igor@tamarapatino.org>, 2015
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-13 19:40+0530\n"
"POT-Creation-Date: 2015-01-15 16:23-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Igor Támara <igor@tamarapatino.org>\n"
"Language-Team: Spanish (http://www.transifex.com/projects/p/django-constance/"
"language/de/\n"
"Language-Team: Spanish (http://www.transifex.com/projects/p/django-constance/language/de/\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: admin.py:113
#, fuzzy, python-format
#| msgid ""
#| "Constance doesn't support config values of the type %(config_type)s. "
#| "Please fix the value of '%(name)s'."
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr ""
"Constance no soporta valores de configuración de los tipos %(config_type)s. "
"Por favor arregle el valor de '%(name)s'."
#: admin.py:123
#: admin.py:71
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
@ -40,7 +27,7 @@ msgstr ""
"Constance no soporta valores de configuración de los tipos %(config_type)s. "
"Por favor arregle el valor de '%(name)s'."
#: admin.py:147
#: admin.py:90
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
@ -48,21 +35,19 @@ msgstr ""
"La configuración ha sido modificada por alguien más. Por favor recargue el "
"formulario y reenvíe sus cambios."
#: admin.py:160
msgid ""
"CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in "
"CONSTANCE_CONFIG."
msgstr ""
#: admin.py:224
#: admin.py:128
msgid "Live settings updated successfully."
msgstr "Las configuraciones en vivo se actualizaron exitosamente."
#: admin.py:285
#: admin.py:133
msgid "Constance config"
msgstr "Configuración de Constance"
#: admin.py:176
msgid "config"
msgstr "configuración"
#: apps.py:8
#: apps.py:7
msgid "Constance"
msgstr "Constance"
@ -74,33 +59,26 @@ msgstr "constance"
msgid "constances"
msgstr "constances"
#: management/commands/constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr ""
#: templates/admin/constance/change_list.html:75
msgid "Save"
msgstr "Guardar"
#: templates/admin/constance/change_list.html:84
msgid "Home"
msgstr "Inicio"
#: templates/admin/constance/includes/results_list.html:5
#: templates/admin/constance/change_list.html:58
msgid "Name"
msgstr "Nombre"
#: templates/admin/constance/includes/results_list.html:6
#: templates/admin/constance/change_list.html:59
msgid "Default"
msgstr "Predeterminado"
#: templates/admin/constance/includes/results_list.html:7
#: templates/admin/constance/change_list.html:60
msgid "Value"
msgstr "Valor"
#: templates/admin/constance/includes/results_list.html:8
#: templates/admin/constance/change_list.html:61
msgid "Is modified"
msgstr "Está modificado"
#~ msgid "Constance config"
#~ msgstr "Configuración de Constance"
#: templates/admin/constance/change_list.html:87
msgid "Save"
msgstr "Guardar"
#: templates/admin/constance/change_list.html:96
msgid "Home"
msgstr "Inicio"

View file

@ -1,99 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-13 19:40+0530\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: admin.py:113
#, python-format
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr ""
"Vaikimisi väärtus peab olema vastav seade parameetri tüübile. Palun "
"sisestagekorrekne vaikimisi väärtus väljale '%(name)s'"
#: admin.py:123
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr ""
"Constance ei toeta seadete seda tüüpi %(config_type) seadete väärtusi. "
"Palunsisestage korrektne väärtus väljale '%(name)s'."
#: admin.py:147
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr ""
"Seadeid on vahepeal muudetud, palun esitage oma muudatused peale seda kui "
"olete lehe uuesti laadinud"
#: admin.py:160
msgid ""
"CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in "
"CONSTANCE_CONFIG."
msgstr ""
#: admin.py:224
msgid "Live settings updated successfully."
msgstr "Seaded edukalt muudetud."
#: admin.py:285
msgid "config"
msgstr "konfiguratsioon"
#: apps.py:8
msgid "Constance"
msgstr "Seaded"
#: backends/database/models.py:19
msgid "constance"
msgstr "seaded"
#: backends/database/models.py:20
msgid "constances"
msgstr "seaded"
#: management/commands/constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr "Loe/salvesta andmebaasi põhiseid seadeid"
#: templates/admin/constance/change_list.html:75
msgid "Save"
msgstr "Salvesta"
#: templates/admin/constance/change_list.html:84
msgid "Home"
msgstr ""
#: templates/admin/constance/includes/results_list.html:5
msgid "Name"
msgstr "Nimi"
#: templates/admin/constance/includes/results_list.html:6
msgid "Default"
msgstr "Vaikimisi"
#: templates/admin/constance/includes/results_list.html:7
msgid "Value"
msgstr "Väärtus"
#: templates/admin/constance/includes/results_list.html:8
msgid "Is modified"
msgstr "Muudetud"

View file

@ -1,104 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-13 19:40+0530\n"
"PO-Revision-Date: 2020-09-24 17:33+0330\n"
"Language: fa\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Last-Translator: Mehdi Namaki <mavenium@gmail.com>\n"
"Language-Team: \n"
"X-Generator: Poedit 2.3\n"
#: admin.py:113
#, python-format
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr ""
"نوع مقدار پیش فرض باید برابر با نوع پارامتر پیکربندی اعلام شده باشد. لطفاً "
"مقدار پیش فرض '%(name)s' را اصلاح کنید."
#: admin.py:123
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr ""
"تنظیمات مقادیر پیکربندی از نوع %(config_type)s را پشتیبانی نمی کند. لطفاً "
"مقدار '%(name)s' را اصلاح کنید."
#: admin.py:147
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr ""
"تنظیمات توسط شخص دیگری تغییر یافته است. لطفاً فرم را بارگیری کنید و تغییرات "
"خود را دوباره ارسال کنید."
#: admin.py:160
msgid ""
"CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in "
"CONSTANCE_CONFIG."
msgstr "CONSTANCE_CONFIG_FIELDSETS شامل فیلدهای CONSTANCE_CONFIG نیست."
#: admin.py:224
msgid "Live settings updated successfully."
msgstr "تنظیمات زنده با موفقیت به روز شد."
#: admin.py:285
msgid "config"
msgstr "پیکربندی"
#: apps.py:8
msgid "Constance"
msgstr "تنظیمات"
#: backends/database/models.py:19
msgid "constance"
msgstr "تنظیمات"
#: backends/database/models.py:20
msgid "constances"
msgstr "تنظیمات"
#: management/commands/constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr ""
"دریافت/تنظیم تنظیمات پیکربندی درون پایگاه داده که توسط تنظیمات بکار برده می "
"شود"
#: templates/admin/constance/change_list.html:75
msgid "Save"
msgstr "ذخیره"
#: templates/admin/constance/change_list.html:84
msgid "Home"
msgstr "خانه"
#: templates/admin/constance/includes/results_list.html:5
msgid "Name"
msgstr "نام"
#: templates/admin/constance/includes/results_list.html:6
msgid "Default"
msgstr "پیش‌فرض"
#: templates/admin/constance/includes/results_list.html:7
msgid "Value"
msgstr "مقدار"
#: templates/admin/constance/includes/results_list.html:8
msgid "Is modified"
msgstr "تغییر یافته"
#: templates/admin/constance/includes/results_list.html:44
msgid "Reset to default"
msgstr "بازنشانی به پیش‌فرض"

View file

@ -1,95 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-07-02 19:01+0100\n"
"PO-Revision-Date: 2017-07-03 12:35+0100\n"
"Last-Translator: Bruno Alla <alla.brunoo@gmail.com>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"Language-Team: \n"
"X-Generator: Poedit 2.0.2\n"
#: admin.py:111
#, python-format
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr ""
"Le type de la valeur par défaut doit être le même que le type déclaré dans "
"la configuration. Veuillez corriger la valeur par défaut de '%(name)s'."
#: admin.py:121
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr ""
"Constance ne prend pas en charge les valeurs de configuration du type "
"%(config_type)s. Veuillez corriger la valeur de %(name)s."
#: admin.py:145
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr ""
"Les paramètres ont été modifiés par quelqu'un d'autre. Veuillez rafraichir "
"le formulaire et soumettre de nouveau vos modifications."
#: admin.py:209
msgid "Live settings updated successfully."
msgstr "Paramètres mis à jour avec succès."
#: admin.py:271
msgid "config"
msgstr "config"
#: apps.py:8
msgid "Constance"
msgstr "Constance"
#: backends/database/models.py:19
msgid "constance"
msgstr "constance"
#: backends/database/models.py:20
msgid "constances"
msgstr "constances"
#: management/commands/constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr ""
"Obtenir/définir les paramètres de configuration de base de données gérées "
"par Constance"
#: templates/admin/constance/change_list.html:68
msgid "Save"
msgstr "Enregistrer"
#: templates/admin/constance/change_list.html:77
msgid "Home"
msgstr "Index"
#: templates/admin/constance/includes/results_list.html:5
msgid "Name"
msgstr "Nom"
#: templates/admin/constance/includes/results_list.html:6
msgid "Default"
msgstr "Défaut"
#: templates/admin/constance/includes/results_list.html:7
msgid "Value"
msgstr "Valeur"
#: templates/admin/constance/includes/results_list.html:8
msgid "Is modified"
msgstr "Est modifié"

View file

@ -1,69 +1,50 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: django-constance\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-13 19:40+0530\n"
"PO-Revision-Date: 2018-03-13 15:26+0100\n"
"Last-Translator: Paolo Melchiorre <paolo@melchiorre.org>\n"
"Language-Team: Italian (http://www.transifex.com/projects/p/django-constance/"
"language/it/)\n"
"Language: it\n"
"POT-Creation-Date: 2014-11-27 19:05+0100\n"
"PO-Revision-Date: 2014-11-27 18:13+0000\n"
"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
"Language-Team: Italian (http://www.transifex.com/projects/p/django-constance/language/it/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: it\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.0.4\n"
#: admin.py:113
#, python-format
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr ""
"Il tipo dei valori di default deve essere uguale al tipo del parametro. "
"Modifica il valore di default di '%(name)s'."
#: admin.py:123
#: admin.py:72
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr ""
"Constance non supporta valori di impostazioni di tipo %(config_type)s. "
"Modifica il valore di '%(name)s'."
#: admin.py:147
#: admin.py:91
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr ""
"Le impostazioni sono state modificate da qualcuno. Ricarica la pagina e "
"invia nuovamente le tue modifiche."
#: admin.py:160
msgid ""
"CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in "
"CONSTANCE_CONFIG."
msgstr ""
"CONSTANCE_CONFIG_FIELDSETS non contiene campi che esistono in "
"CONSTANCE_CONFIG."
#: admin.py:224
#: admin.py:129
msgid "Live settings updated successfully."
msgstr "Le impostazioni attive sono state aggiornate correttamente."
#: admin.py:285
msgid "config"
msgstr "configurazioni"
#: admin.py:134
msgid "Constance config"
msgstr "Configurazione Impostazioni"
#: apps.py:8
#: admin.py:177
msgid "config"
msgstr ""
#: apps.py:9
msgid "Constance"
msgstr "Impostazioni"
msgstr ""
#: backends/database/models.py:19
msgid "constance"
@ -73,33 +54,26 @@ msgstr "impostazione"
msgid "constances"
msgstr "impostazioni"
#: management/commands/constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr ""
#: templates/admin/constance/change_list.html:75
msgid "Save"
msgstr "Salva"
#: templates/admin/constance/change_list.html:84
msgid "Home"
msgstr "Inizio"
#: templates/admin/constance/includes/results_list.html:5
#: templates/admin/constance/change_list.html:50
msgid "Name"
msgstr "Nome"
#: templates/admin/constance/includes/results_list.html:6
#: templates/admin/constance/change_list.html:51
msgid "Default"
msgstr "Default"
#: templates/admin/constance/includes/results_list.html:7
#: templates/admin/constance/change_list.html:52
msgid "Value"
msgstr "Valore"
#: templates/admin/constance/includes/results_list.html:8
#: templates/admin/constance/change_list.html:53
msgid "Is modified"
msgstr "Modificato"
#~ msgid "Constance config"
#~ msgstr "Configurazione Impostazioni"
#: templates/admin/constance/change_list.html:79
msgid "Save"
msgstr "Salva"
#: templates/admin/constance/change_list.html:89
msgid "Home"
msgstr "Inizio"

View file

@ -1,59 +1,48 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: django-constance\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-13 19:40+0530\n"
"POT-Creation-Date: 2014-11-27 19:05+0100\n"
"PO-Revision-Date: 2014-11-27 18:13+0000\n"
"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
"Language-Team: Polish (http://www.transifex.com/projects/p/django-constance/"
"language/pl/)\n"
"Language: pl\n"
"Language-Team: Polish (http://www.transifex.com/projects/p/django-constance/language/pl/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
"Language: pl\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: admin.py:113
#, python-format
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr ""
#: admin.py:123
#: admin.py:72
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr ""
#: admin.py:147
#: admin.py:91
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr ""
#: admin.py:160
msgid ""
"CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in "
"CONSTANCE_CONFIG."
msgstr ""
#: admin.py:224
#: admin.py:129
msgid "Live settings updated successfully."
msgstr "Parametry zostały zaktualizowane"
#: admin.py:285
#: admin.py:134
msgid "Constance config"
msgstr "Konfiguracja Constance"
#: admin.py:177
msgid "config"
msgstr ""
#: apps.py:8
#: apps.py:9
msgid "Constance"
msgstr ""
@ -65,33 +54,26 @@ msgstr "parametr"
msgid "constances"
msgstr "parametry"
#: management/commands/constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr ""
#: templates/admin/constance/change_list.html:75
msgid "Save"
msgstr "Zapisz"
#: templates/admin/constance/change_list.html:84
msgid "Home"
msgstr "Początek"
#: templates/admin/constance/includes/results_list.html:5
#: templates/admin/constance/change_list.html:50
msgid "Name"
msgstr "Nazwa"
#: templates/admin/constance/includes/results_list.html:6
#: templates/admin/constance/change_list.html:51
msgid "Default"
msgstr "Domyślnie"
#: templates/admin/constance/includes/results_list.html:7
#: templates/admin/constance/change_list.html:52
msgid "Value"
msgstr "Wartość"
#: templates/admin/constance/includes/results_list.html:8
#: templates/admin/constance/change_list.html:53
msgid "Is modified"
msgstr "Zmodyfikowana"
#~ msgid "Constance config"
#~ msgstr "Konfiguracja Constance"
#: templates/admin/constance/change_list.html:79
msgid "Save"
msgstr "Zapisz"
#: templates/admin/constance/change_list.html:89
msgid "Home"
msgstr "Początek"

View file

@ -1,63 +1,48 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: django-constance\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-19 20:59+0500\n"
"POT-Creation-Date: 2014-11-27 19:05+0100\n"
"PO-Revision-Date: 2014-11-27 18:13+0000\n"
"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
"Language-Team: Russian (http://www.transifex.com/projects/p/django-constance/"
"language/ru/)\n"
"Language: ru\n"
"Language-Team: Russian (http://www.transifex.com/projects/p/django-constance/language/ru/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"Language: ru\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: admin.py:113
#, python-format
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr ""
#: admin.py:123
#: admin.py:72
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr ""
#: admin.py:147
#: admin.py:91
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr ""
#: admin.py:160
msgid ""
"CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in "
"CONSTANCE_CONFIG."
msgstr ""
#: admin.py:224
#: admin.py:129
msgid "Live settings updated successfully."
msgstr "Настройки успешно сохранены."
msgstr "Настройки успешно сохранены"
#: admin.py:267
msgid "Failed to update live settings."
msgstr "Не удалось сохранить настройки."
#: admin.py:134
msgid "Constance config"
msgstr "Настройки"
#: admin.py:285
#: admin.py:177
msgid "config"
msgstr "настройки"
#: apps.py:8
#: apps.py:9
msgid "Constance"
msgstr ""
@ -69,33 +54,26 @@ msgstr "настройки"
msgid "constances"
msgstr "настройки"
#: management/commands/constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr ""
#: templates/admin/constance/change_list.html:75
msgid "Save"
msgstr "Сохранить"
#: templates/admin/constance/change_list.html:84
msgid "Home"
msgstr "Главная"
#: templates/admin/constance/includes/results_list.html:5
#: templates/admin/constance/change_list.html:50
msgid "Name"
msgstr "Название"
#: templates/admin/constance/includes/results_list.html:6
#: templates/admin/constance/change_list.html:51
msgid "Default"
msgstr "По умолчанию"
#: templates/admin/constance/includes/results_list.html:7
#: templates/admin/constance/change_list.html:52
msgid "Value"
msgstr "Текущее значение"
#: templates/admin/constance/includes/results_list.html:8
#: templates/admin/constance/change_list.html:53
msgid "Is modified"
msgstr "Было изменено"
#~ msgid "Constance config"
#~ msgstr "Настройки"
#: templates/admin/constance/change_list.html:79
msgid "Save"
msgstr "Сохранить"
#: templates/admin/constance/change_list.html:89
msgid "Home"
msgstr "Главная"

View file

@ -1,104 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-09 19:14+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Ozcan Yarimdunya <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: constance/admin.py:116
#, python-format
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr ""
"Varsayılan değer tipi, tanımlanan ayarlar parametresi tipi ile aynı olmalıdır. Lütfen "
"'%(name)s' in varsayılan değerini düzeltin."
#: constance/admin.py:126
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr ""
"Constance %(config_type)s tipinin yapılandırma değerlerini desteklemiyor. Lütfen "
"'%(name)s' in değerini düzeltin."
#: constance/admin.py:160
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr ""
"Ayarlar başkası tarafından değiştirildi. Lütfen formu tekrar yükleyin ve "
"değişikliklerinizi tekrar kaydedin."
#: constance/admin.py:172 constance/checks.py:19
msgid ""
"CONSTANCE_CONFIG_FIELDSETS is missing field(s) that exists in "
"CONSTANCE_CONFIG."
msgstr ""
"CONSTANCE_CONFIG içinde mevcut olan alan(lar) için "
"CONSTANCE_CONFIG_FIELDSETS eksik."
#: constance/admin.py:240
msgid "Live settings updated successfully."
msgstr "Canlı ayarlar başarıyla güncellendi."
#: constance/admin.py:305
msgid "config"
msgstr "ayar"
#: constance/backends/database/models.py:19
msgid "constance"
msgstr "constance"
#: constance/backends/database/models.py:20
msgid "constances"
msgstr "constances"
#: constance/management/commands/constance.py:32
msgid "Get/Set In-database config settings handled by Constance"
msgstr "Constance tarafından veritabanında barındırılan ayarları görüntüle/değiştir"
#: constance/templates/admin/constance/change_list.html:60
msgid "Save"
msgstr "Kaydet"
#: constance/templates/admin/constance/change_list.html:69
msgid "Home"
msgstr "Anasayfa"
#: constance/templates/admin/constance/includes/results_list.html:6
msgid "Name"
msgstr "İsim"
#: constance/templates/admin/constance/includes/results_list.html:7
msgid "Default"
msgstr "Varsayılan"
#: constance/templates/admin/constance/includes/results_list.html:8
msgid "Value"
msgstr "Değer"
#: constance/templates/admin/constance/includes/results_list.html:9
msgid "Is modified"
msgstr "Değiştirildi mi"
#: constance/templates/admin/constance/includes/results_list.html:22
msgid "Current file"
msgstr "Geçerli dosya"
#: constance/templates/admin/constance/includes/results_list.html:39
msgid "Reset to default"
msgstr "Varsayılana dön"

View file

@ -1,121 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: django-constance\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-19 16:00+0000\n"
"PO-Revision-Date: 2014-11-27 18:13+0000\n"
"Last-Translator: Vasyl Dizhak <vasyl@dizhak.com>\n"
"Language-Team: (http://www.transifex.com/projects/p/django-constance/"
"language/uk/)\n"
"Language: uk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: admin.py:109
msgid "You don't have permission to change these values"
msgstr "У вас немає прав для зміни цих значень"
#: admin.py:117
#, python-format
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr ""
"Тип значення за замовчуванням повинен співпадати із вказаним типом параметра "
"конфігурації. Будь ласка, виправте значення за замовчуванням для '%(name)s'."
#: admin.py:127
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr ""
"Constance не підтрумує значення наступного типу %(config_type)s. Будь ласка, "
"змініть тип для значення '%(name)s'"
#: admin.py:166
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr ""
"Налаштування було змінено кимось іншим. Буд ласка, перезавантажте форму та "
"повторно збережіть зміни."
#: admin.py:178 checks.py:19
msgid ""
"CONSTANCE_CONFIG_FIELDSETS is missing field(s) that exists in "
"CONSTANCE_CONFIG."
msgstr ""
"Одне чи кілька полів з CONSTANCE_CONFIG відсутні в "
"CONSTANCE_CONFIG_FIELDSETS."
#: admin.py:250
msgid "Live settings updated successfully."
msgstr "Налаштування успішно збережені."
#: admin.py:267
msgid "Failed to update live settings."
msgstr "Не вдалося зберегти налаштування."
#: admin.py:326
msgid "config"
msgstr "налаштування"
#: apps.py:8
msgid "Constance"
msgstr ""
#: backends/database/models.py:19
msgid "constance"
msgstr "налаштування"
#: backends/database/models.py:20
msgid "constances"
msgstr "налаштування"
#: management/commands/constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr "Отримати/встановити налашування в базі даних, якими керує Constance"
#: templates/admin/constance/change_list.html:61
msgid "Save"
msgstr "Зберегти"
#: templates/admin/constance/change_list.html:70
msgid "Home"
msgstr "Головна"
#: templates/admin/constance/includes/results_list.html:6
msgid "Name"
msgstr "Назва"
#: templates/admin/constance/includes/results_list.html:7
msgid "Default"
msgstr "За замовчуванням"
#: templates/admin/constance/includes/results_list.html:8
msgid "Value"
msgstr "Поточне значення"
#: templates/admin/constance/includes/results_list.html:9
msgid "Is modified"
msgstr "Було змінено"
#: templates/admin/constance/includes/results_list.html:26
msgid "Current file"
msgstr "Поточний файл"
#: templates/admin/constance/includes/results_list.html:44
msgid "Reset to default"
msgstr "Скинути до значення за замовчуванням"
#~ msgid "Constance config"
#~ msgstr "Настройки"

View file

@ -1,62 +1,49 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
#
# Translators:
# Yifu Yu <root@jackyyf.com>, 2015
msgid ""
msgstr ""
"Project-Id-Version: django-constance\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-13 19:40+0530\n"
"POT-Creation-Date: 2014-11-27 19:05+0100\n"
"PO-Revision-Date: 2015-03-15 18:40+0000\n"
"Last-Translator: Yifu Yu <root@jackyyf.com>\n"
"Language-Team: Chinese (China) (http://www.transifex.com/jezdez/django-"
"constance/language/zh_CN/)\n"
"Language: zh_CN\n"
"Language-Team: Chinese (China) (http://www.transifex.com/jezdez/django-constance/language/zh_CN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_CN\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: admin.py:113
#, fuzzy, python-format
#| msgid ""
#| "Constance doesn't support config values of the type %(config_type)s. "
#| "Please fix the value of '%(name)s'."
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr "Constance不支持保存类型为%(config_type)s的值请修正%(name)s的值。"
#: admin.py:123
#: admin.py:72
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr "Constance不支持保存类型为%(config_type)s的值请修正%(name)s的值。"
#: admin.py:147
#: admin.py:91
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr "设置已经被他人修改过,请刷新页面并重新提交您的更改。"
#: admin.py:160
msgid ""
"CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in "
"CONSTANCE_CONFIG."
msgstr ""
#: admin.py:224
#: admin.py:129
msgid "Live settings updated successfully."
msgstr "成功更新实时配置"
#: admin.py:285
#: admin.py:134
msgid "Constance config"
msgstr "Constance 配置页面"
#: admin.py:177
msgid "config"
msgstr "配置"
#: apps.py:8
#: apps.py:9
msgid "Constance"
msgstr "Constance模块"
@ -68,33 +55,26 @@ msgstr "Constance模块"
msgid "constances"
msgstr "Constance模块"
#: management/commands/constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr ""
#: templates/admin/constance/change_list.html:75
msgid "Save"
msgstr "保存"
#: templates/admin/constance/change_list.html:84
msgid "Home"
msgstr "首页"
#: templates/admin/constance/includes/results_list.html:5
#: templates/admin/constance/change_list.html:50
msgid "Name"
msgstr "名称"
#: templates/admin/constance/includes/results_list.html:6
#: templates/admin/constance/change_list.html:51
msgid "Default"
msgstr "默认值"
#: templates/admin/constance/includes/results_list.html:7
#: templates/admin/constance/change_list.html:52
msgid "Value"
msgstr "值"
#: templates/admin/constance/includes/results_list.html:8
#: templates/admin/constance/change_list.html:53
msgid "Is modified"
msgstr "是否修改过"
#~ msgid "Constance config"
#~ msgstr "Constance 配置页面"
#: templates/admin/constance/change_list.html:79
msgid "Save"
msgstr "保存"
#: templates/admin/constance/change_list.html:89
msgid "Home"
msgstr "首页"

View file

@ -1,97 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Yifu Yu <root@jackyyf.com>, 2015.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-19 11:31+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: YinKH <614457662@qq.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: .\admin.py:115
#, python-format
msgid ""
"Default value type must be equal to declared config parameter type. Please "
"fix the default value of '%(name)s'."
msgstr "默认值的类型必须与参数声明类型相同,请修正%(name)s的值。"
#: .\admin.py:125
#, python-format
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr "Constance不支持保存类型为%(config_type)s的值请修正%(name)s的值。"
#: .\admin.py:157
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr "设置已经被他人修改过,请刷新页面并重新提交您的更改。"
#: .\admin.py:173
msgid ""
"CONSTANCE_CONFIG_FIELDSETS is missing field(s) that exists in "
"CONSTANCE_CONFIG."
msgstr "CONSTANCE_CONFIG_FIELDSETS中缺少在CONSTANCE_CONFIG中声明的字段。"
#: .\admin.py:240
msgid "Live settings updated successfully."
msgstr "实时配置更新成功"
#: .\admin.py:301
msgid "config"
msgstr "配置"
#: .\backends\database\models.py:19
msgid "constance"
msgstr "Constance模块"
#: .\backends\database\models.py:20
msgid "constances"
msgstr "Constance模块"
#: .\management\commands\constance.py:30
msgid "Get/Set In-database config settings handled by Constance"
msgstr "获取或设置由Constance模块处理的数据库配置"
#: .\templates\admin\constance\change_list.html:60
msgid "Save"
msgstr "保存"
#: .\templates\admin\constance\change_list.html:69
msgid "Home"
msgstr "首页"
#: .\templates\admin\constance\includes\results_list.html:5
msgid "Name"
msgstr "名称"
#: .\templates\admin\constance\includes\results_list.html:6
msgid "Default"
msgstr "默认值"
#: .\templates\admin\constance\includes\results_list.html:7
msgid "Value"
msgstr "当前值"
#: .\templates\admin\constance\includes\results_list.html:8
msgid "Is modified"
msgstr "是否修改过"
#: .\templates\admin\constance\includes\results_list.html:21
msgid "Current file"
msgstr "当前文件"
#: .\templates\admin\constance\includes\results_list.html:36
msgid "Reset to default"
msgstr "重置至默认值"

View file

@ -1,84 +0,0 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.management import BaseCommand
from django.core.management import CommandError
from django.utils.translation import gettext as _
from constance import config
from constance.forms import ConstanceForm
from constance.models import Constance
from constance.utils import get_values
def _set_constance_value(key, value):
"""
Parses and sets a Constance value from a string
:param key:
:param value:
:return:
"""
form = ConstanceForm(initial=get_values())
field = form.fields[key]
clean_value = field.clean(field.to_python(value))
setattr(config, key, clean_value)
class Command(BaseCommand):
help = _("Get/Set In-database config settings handled by Constance")
GET = "get"
SET = "set"
LIST = "list"
REMOVE_STALE_KEYS = "remove_stale_keys"
def add_arguments(self, parser):
subparsers = parser.add_subparsers(dest="command")
subparsers.add_parser(self.LIST, help="list all Constance keys and their values")
parser_get = subparsers.add_parser(self.GET, help="get the value of a Constance key")
parser_get.add_argument("key", help="name of the key to get", metavar="KEY")
parser_set = subparsers.add_parser(self.SET, help="set the value of a Constance key")
parser_set.add_argument("key", help="name of the key to set", metavar="KEY")
# use nargs='+' so that we pass a list to MultiValueField (eg SplitDateTimeField)
parser_set.add_argument("value", help="value to set", metavar="VALUE", nargs="+")
subparsers.add_parser(
self.REMOVE_STALE_KEYS,
help="delete all Constance keys and their values if they are not in settings.CONSTANCE_CONFIG (stale keys)",
)
def handle(self, command, key=None, value=None, *args, **options):
if command == self.GET:
try:
self.stdout.write(str(getattr(config, key)), ending="\n")
except AttributeError as e:
raise CommandError(f"{key} is not defined in settings.CONSTANCE_CONFIG") from e
elif command == self.SET:
try:
if len(value) == 1:
# assume that if a single argument was passed, the field doesn't expect a list
value = value[0]
_set_constance_value(key, value)
except KeyError as e:
raise CommandError(f"{key} is not defined in settings.CONSTANCE_CONFIG") from e
except ValidationError as e:
raise CommandError(", ".join(e)) from e
elif command == self.LIST:
for k, v in get_values().items():
self.stdout.write(f"{k}\t{v}", ending="\n")
elif command == self.REMOVE_STALE_KEYS:
prefix = getattr(settings, "CONSTANCE_DATABASE_PREFIX", "")
actual_keys = [f"{prefix}{key}" for key in settings.CONSTANCE_CONFIG]
stale_records = Constance.objects.exclude(key__in=actual_keys)
if stale_records:
self.stdout.write("The following record will be deleted:", ending="\n")
else:
self.stdout.write("There are no stale records in the database.", ending="\n")
for stale_record in stale_records:
self.stdout.write(f"{stale_record.key}\t{stale_record.value}", ending="\n")
stale_records.delete()
else:
raise CommandError("Invalid command")

View file

@ -1,24 +0,0 @@
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Constance",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("key", models.CharField(max_length=255, unique=True)),
("value", models.TextField(blank=True, editable=False, null=True)),
],
options={
"verbose_name": "constance",
"verbose_name_plural": "constances",
"permissions": [("change_config", "Can change config"), ("view_config", "Can view config")],
},
),
]

View file

@ -1,41 +0,0 @@
from logging import getLogger
from django.core.management.color import no_style
from django.db import migrations
logger = getLogger(__name__)
def _migrate_from_old_table(apps, schema_editor) -> None:
"""
Copies values from old table.
On new installations just ignore error that table does not exist.
"""
connection = schema_editor.connection
quoted_string = ", ".join([connection.ops.quote_name(item) for item in ["id", "key", "value"]])
old_table_name = "constance_config"
with connection.cursor() as cursor:
if old_table_name not in connection.introspection.table_names():
logger.info("Old table does not exist, skipping")
return
cursor.execute(
f"INSERT INTO constance_constance ( {quoted_string} ) SELECT {quoted_string} FROM {old_table_name}", # noqa: S608
[],
)
cursor.execute(f"DROP TABLE {old_table_name}", [])
Constance = apps.get_model("constance", "Constance")
sequence_sql = connection.ops.sequence_reset_sql(no_style(), [Constance])
with connection.cursor() as cursor:
for sql in sequence_sql:
cursor.execute(sql)
class Migration(migrations.Migration):
dependencies = [("constance", "0001_initial")]
atomic = False
operations = [
migrations.RunPython(_migrate_from_old_table, reverse_code=lambda x, y: None),
]

View file

@ -1,68 +0,0 @@
import json
import logging
import pickle
from base64 import b64decode
from importlib import import_module
from django.db import migrations
from constance import settings
from constance.codecs import dumps
logger = logging.getLogger(__name__)
def is_already_migrated(value):
try:
data = json.loads(value)
if isinstance(data, dict) and set(data.keys()) == {"__type__", "__value__"}:
return True
except (json.JSONDecodeError, TypeError, UnicodeDecodeError):
return False
return False
def import_module_attr(path):
package, module = path.rsplit(".", 1)
return getattr(import_module(package), module)
def migrate_pickled_data(apps, schema_editor) -> None: # pragma: no cover
Constance = apps.get_model("constance", "Constance")
for constance in Constance.objects.exclude(value=None):
if not is_already_migrated(constance.value):
constance.value = dumps(pickle.loads(b64decode(constance.value.encode()))) # noqa: S301
constance.save(update_fields=["value"])
if settings.BACKEND in (
"constance.backends.redisd.RedisBackend",
"constance.backends.redisd.CachingRedisBackend",
):
import redis
_prefix = settings.REDIS_PREFIX
connection_cls = settings.REDIS_CONNECTION_CLASS
if connection_cls is not None:
_rd = import_module_attr(connection_cls)()
else:
if isinstance(settings.REDIS_CONNECTION, str):
_rd = redis.from_url(settings.REDIS_CONNECTION)
else:
_rd = redis.Redis(**settings.REDIS_CONNECTION)
redis_migrated_data = {}
for key in settings.CONFIG:
prefixed_key = f"{_prefix}{key}"
value = _rd.get(prefixed_key)
if value is not None and not is_already_migrated(value):
redis_migrated_data[prefixed_key] = dumps(pickle.loads(value)) # noqa: S301
for prefixed_key, value in redis_migrated_data.items():
_rd.set(prefixed_key, value)
class Migration(migrations.Migration):
dependencies = [("constance", "0002_migrate_from_old_table")]
operations = [
migrations.RunPython(migrate_pickled_data),
]

View file

@ -1,18 +0,0 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class Constance(models.Model):
key = models.CharField(max_length=255, unique=True)
value = models.TextField(null=True, blank=True, editable=False)
class Meta:
verbose_name = _("constance")
verbose_name_plural = _("constances")
permissions = [
("change_config", "Can change config"),
("view_config", "Can view config"),
]
def __str__(self):
return self.key

View file

@ -1,31 +1,30 @@
from django.conf import settings
BACKEND = getattr(settings, "CONSTANCE_BACKEND", "constance.backends.redisd.RedisBackend")
BACKEND = getattr(settings, 'CONSTANCE_BACKEND',
'constance.backends.redisd.RedisBackend')
CONFIG = getattr(settings, "CONSTANCE_CONFIG", {})
CONFIG = getattr(settings, 'CONSTANCE_CONFIG', {})
CONFIG_FIELDSETS = getattr(settings, "CONSTANCE_CONFIG_FIELDSETS", {})
ADDITIONAL_FIELDS = getattr(settings, 'CONSTANCE_ADDITIONAL_FIELDS', {})
ADDITIONAL_FIELDS = getattr(settings, "CONSTANCE_ADDITIONAL_FIELDS", {})
DATABASE_CACHE_BACKEND = getattr(settings, 'CONSTANCE_DATABASE_CACHE_BACKEND',
None)
FILE_ROOT = getattr(settings, "CONSTANCE_FILE_ROOT", "")
DATABASE_CACHE_AUTOFILL_TIMEOUT = getattr(settings,
'CONSTANCE_DATABASE_CACHE_AUTOFILL_TIMEOUT',
60 * 60 * 24)
DATABASE_CACHE_BACKEND = getattr(settings, "CONSTANCE_DATABASE_CACHE_BACKEND", None)
DATABASE_PREFIX = getattr(settings, 'CONSTANCE_DATABASE_PREFIX', '')
DATABASE_CACHE_AUTOFILL_TIMEOUT = getattr(settings, "CONSTANCE_DATABASE_CACHE_AUTOFILL_TIMEOUT", 60 * 60 * 24)
REDIS_PREFIX = getattr(settings, 'CONSTANCE_REDIS_PREFIX', 'constance:')
DATABASE_PREFIX = getattr(settings, "CONSTANCE_DATABASE_PREFIX", "")
REDIS_CONNECTION_CLASS = getattr(settings, 'CONSTANCE_REDIS_CONNECTION_CLASS',
None)
REDIS_PREFIX = getattr(settings, "CONSTANCE_REDIS_PREFIX", "constance:")
REDIS_CONNECTION = getattr(settings, 'CONSTANCE_REDIS_CONNECTION', {})
REDIS_CACHE_TIMEOUT = getattr(settings, "CONSTANCE_REDIS_CACHE_TIMEOUT", 60)
SUPERUSER_ONLY = getattr(settings, 'CONSTANCE_SUPERUSER_ONLY', True)
REDIS_CONNECTION_CLASS = getattr(settings, "CONSTANCE_REDIS_CONNECTION_CLASS", None)
REDIS_ASYNC_CONNECTION_CLASS = getattr(settings, "CONSTANCE_REDIS_ASYNC_CONNECTION_CLASS", None)
REDIS_CONNECTION = getattr(settings, "CONSTANCE_REDIS_CONNECTION", {})
SUPERUSER_ONLY = getattr(settings, "CONSTANCE_SUPERUSER_ONLY", True)
IGNORE_ADMIN_VERSION_CHECK = getattr(settings, "CONSTANCE_IGNORE_ADMIN_VERSION_CHECK", False)
IGNORE_ADMIN_VERSION_CHECK = getattr(settings,
'CONSTANCE_IGNORE_ADMIN_VERSION_CHECK',
False)

View file

@ -1,3 +0,0 @@
import django.dispatch
config_updated = django.dispatch.Signal()

View file

@ -1,34 +0,0 @@
#result_list .changed {
background-color: #ffc;
}
#changelist table thead th .text {
padding: 2px 5px;
}
#changelist table tbody td:first-child {
text-align: left;
}
#changelist-form ul.errorlist {
margin: 0 !important;
}
.help {
font-weight: normal !important;
}
#results {
overflow-x: auto;
}
.item-anchor {
visibility: hidden;
margin-left: .1em;
}
.item-name {
white-space: nowrap;
}
.item-name:hover .item-anchor {
visibility: visible;
}
.sticky-footer {
position: sticky;
width: 100%;
left: 0;
bottom: 0;
}

View file

@ -1,38 +0,0 @@
(function($) {
'use strict';
$(function() {
$('#content-main').on('click', '.reset-link', function(e) {
e.preventDefault();
const field_selector = this.dataset.fieldId.replace(/ /g, "\\ ")
const field = $('#' + field_selector);
const fieldType = this.dataset.fieldType;
if (fieldType === 'checkbox') {
field.prop('checked', this.dataset.default === 'true');
} else if (fieldType === 'multi-select') {
const defaults = JSON.parse(this.dataset.default);
const stringDefaults = defaults.map(function(v) { return String(v); });
// CheckboxSelectMultiple: individual checkboxes inside a wrapper
field.find('input[type="checkbox"]').each(function() {
$(this).prop('checked', stringDefaults.indexOf($(this).val()) !== -1);
});
// SelectMultiple: <select multiple> element
field.find('option').each(function() {
$(this).prop('selected', stringDefaults.indexOf($(this).val()) !== -1);
});
} else if (fieldType === 'date') {
const defaultDate = new Date(this.dataset.default * 1000);
$('#' + this.dataset.fieldId).val(defaultDate.strftime(get_format('DATE_INPUT_FORMATS')[0]));
} else if (fieldType === 'datetime') {
const defaultDate = new Date(this.dataset.default * 1000);
$('#' + this.dataset.fieldId + '_0').val(defaultDate.strftime(get_format('DATE_INPUT_FORMATS')[0]));
$('#' + this.dataset.fieldId + '_1').val(defaultDate.strftime(get_format('TIME_INPUT_FORMATS')[0]));
} else {
field.val(this.dataset.default);
}
});
});
})(django.jQuery);

View file

@ -1,12 +1,29 @@
{% extends "admin/base_site.html" %}
{% load admin_list static i18n %}
{% load admin_static admin_list i18n %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/changelists.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/forms.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/changelists.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/forms.css' %}" />
{{ media.css }}
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/constance.css' %}">
<style>
#result_list .changed {
background-color: #ffc;
}
#changelist table thead th .text {
padding: 2px 5px;
}
#changelist table tbody td:first-child {
text-align: left;
}
#changelist-form ul.errorlist {
margin: 0 !important;
}
.help {
font-weight: normal !important;
}
</style>
{% endblock %}
{% block extrahead %}
@ -14,25 +31,14 @@
<script type="text/javascript" src="{{ jsi18nurl|default:'../../jsi18n/' }}"></script>
{{ block.super }}
{{ media.js }}
<script type="text/javascript" src="{% static 'admin/js/constance.js' %}"></script>
{% if django_version < "5.1" %}
<script type="text/javascript" src="{% static 'admin/js/collapse.js' %}"></script>
{% endif %}
{% endblock %}
{% block bodyclass %}{{ block.super }} change-list{% endblock %}
{% block bodyclass %}change-list{% endblock %}
{% block content %}
<div id="content-main" class="constance">
<div class="module" id="changelist">
<form id="changelist-form" action="" method="post" enctype="multipart/form-data">{% csrf_token %}
{% if form.non_field_errors %}
<ul class="errorlist">
{% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<form id="changelist-form" action="" method="post">{% csrf_token %}
{% if form.errors %}
<ul class="errorlist">
{% endif %}
@ -45,25 +51,39 @@
{% if form.errors %}
</ul>
{% endif %}
{% if fieldsets %}
{% for fieldset in fieldsets %}
<fieldset class="module{% if fieldset.collapse %} collapse{% endif %}">
{% if django_version >= "5.1" and fieldset.collapse %}<details><summary>{% endif %}
<h2 class="fieldset-heading">{{ fieldset.title }}</h2>
{% if django_version >= "5.1" and fieldset.collapse %}</summary>{% endif %}
{% with config_values=fieldset.config_values %}
{% include "admin/constance/includes/results_list.html" %}
{% endwith %}
{% if django_version >= "5.1" and fieldset.collapse %}</details>{% endif %}
</fieldset>
{% endfor %}
{% else %}
{% include "admin/constance/includes/results_list.html" %}
{% endif %}
<p class="paginator sticky-footer">
<input type="submit" name="_save" class="default" value="{% trans 'Save' %}">
<table cellspacing="0" id="result_list">
<thead>
<tr>
<th><div class="text">{% trans "Name" %}</div></th>
<th><div class="text">{% trans "Default" %}</div></th>
<th><div class="text">{% trans "Value" %}</div></th>
<th><div class="text">{% trans "Is modified" %}</div></th>
</tr>
</thead>
{% for item in config_values %}
<tr class="{% cycle 'row1' 'row2' %}">
<th>{{ item.name }}
<div class="help">{{ item.help_text|linebreaksbr }}</div>
</th>
<td>
{{ item.default }}
</td>
<td>
{{ item.form_field.errors }}
{{ item.form_field }}
</td>
<td>
{% if item.modified %}
<img src="{% static 'admin/img/icon-yes.'|add:icon_type %}" alt="{{ item.modified }}" />
{% else %}
<img src="{% static 'admin/img/icon-no.'|add:icon_type %}" alt="{{ item.modified }}" />
{% endif %}
</td>
</tr>
{% endfor %}
</table>
<p class="paginator">
<input type="submit" name="_save" class="default" value="{% trans 'Save' %}"/>
</p>
</form>
</div>
@ -72,8 +92,8 @@
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; {{ opts.verbose_name_plural|capfirst }}
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ app_label|capfirst|escape }}</a>
&rsaquo; {{ opts.verbose_name_plural|capfirst }}
</div>
{% endblock %}

View file

@ -1,59 +0,0 @@
{% load admin_list static i18n %}
<div id="results">
<table>
<thead>
<tr>
<th><div class="text">{% trans "Name" %}</div></th>
<th><div class="text">{% trans "Default" %}</div></th>
<th><div class="text">{% trans "Value" %}</div></th>
<th><div class="text">{% trans "Is modified" %}</div></th>
</tr>
</thead>
{% for item in config_values %}
<tr class="{% cycle 'row1' 'row2' %}">
<th>
<span class="item-name" id="{{ item.name|slugify }}">
{{ item.name }}
<a class="item-anchor" href="#{{ item.name|slugify }}" title="Link to this setting"></a>
</span>
<div class="help">{{ item.help_text|linebreaksbr }}</div>
</th>
<td>
{{ item.default|linebreaks }}
</td>
<td>
{{ item.form_field.errors }}
{% if item.is_file %}{% trans "Current file" %}: <a href="{% get_media_prefix as MEDIA_URL %}{{ MEDIA_URL }}{{ item.value }}" target="_blank">{{ item.value }}</a>{% endif %}
{{ item.form_field }}
{% if not item.is_file %}
<br>
<a href="#" class="reset-link"
data-field-id="{{ item.form_field.auto_id }}"
data-field-type="{% spaceless %}
{% if item.is_checkbox %}checkbox
{% elif item.is_multi_select %}multi-select
{% elif item.is_datetime %}datetime
{% elif item.is_date %}date
{% endif %}
{% endspaceless %}"
data-default="{% spaceless %}
{% if item.is_checkbox %}{% if item.raw_default %} true {% else %} false {% endif %}
{% elif item.is_multi_select %}{{ item.json_default }}
{% elif item.is_date %}{{ item.raw_default|date:"U" }}
{% elif item.is_datetime %}{{ item.raw_default|date:"U" }}
{% else %}{{ item.default }}
{% endif %}
{% endspaceless %}">{% trans "Reset to default" %}</a>
{% endif %}
</td>
<td>
{% if item.modified %}
<img src="{% static 'admin/img/icon-yes.'|add:icon_type %}" alt="{{ item.modified }}">
{% else %}
<img src="{% static 'admin/img/icon-no.'|add:icon_type %}" alt="{{ item.modified }}">
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>

View file

@ -1,3 +1 @@
from .unittest import override_config # pragma: no cover
__all__ = ["override_config"]
from .utils import override_config

View file

@ -1,65 +0,0 @@
"""
Pytest constance override config plugin.
Inspired by https://github.com/pytest-dev/pytest-django/.
"""
from contextlib import ContextDecorator
import pytest
from constance import config as constance_config
@pytest.hookimpl(trylast=True)
def pytest_configure(config): # pragma: no cover
"""Register override_config marker."""
config.addinivalue_line("markers", ("override_config(**kwargs): mark test to override django-constance config"))
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item): # pragma: no cover
"""Validate constance override marker params. Run test with overridden config."""
marker = item.get_closest_marker("override_config")
if marker is not None:
if marker.args:
pytest.fail("Constance override can not not accept positional args")
with override_config(**marker.kwargs):
yield
else:
yield
class override_config(ContextDecorator):
"""
Override config while running test function.
Act as context manager and decorator.
"""
def enable(self):
"""Store original config values and set overridden values."""
for key, value in self._to_override.items():
self._original_values[key] = getattr(constance_config, key)
setattr(constance_config, key, value)
def disable(self):
"""Set original values to the config."""
for key, value in self._original_values.items():
setattr(constance_config, key, value)
def __init__(self, **kwargs):
self._to_override = kwargs.copy()
self._original_values = {}
def __enter__(self):
self.enable()
def __exit__(self, exc_type, exc_val, exc_tb):
self.disable()
@pytest.fixture(name="override_config")
def _override_config():
"""Make override_config available as a function fixture."""
return override_config

View file

@ -1,12 +1,11 @@
from functools import wraps
from django import VERSION as DJANGO_VERSION
from django.test import SimpleTestCase
from django.test.utils import override_settings
from constance import config
from .. import config
__all__ = ("override_config",)
__all__ = ('override_config',)
class override_config(override_settings):
@ -15,23 +14,25 @@ class override_config(override_settings):
Based on django.test.utils.override_settings.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
super(override_config, self).__init__(**kwargs)
self.original_values = {}
def __call__(self, test_func):
"""Modify the decorated function to override config values."""
"""
Modify the decorated function to override config values.
"""
if isinstance(test_func, type):
if not issubclass(test_func, SimpleTestCase):
raise Exception("Only subclasses of Django SimpleTestCase can be decorated with override_config")
raise Exception(
"Only subclasses of Django SimpleTestCase can be "
"decorated with override_config")
return self.modify_test_case(test_func)
@wraps(test_func)
def inner(*args, **kwargs):
with self:
return test_func(*args, **kwargs)
else:
@wraps(test_func)
def inner(*args, **kwargs):
with self:
return test_func(*args, **kwargs)
return inner
def modify_test_case(self, test_case):
@ -45,20 +46,9 @@ class override_config(override_settings):
original_pre_setup = test_case._pre_setup
original_post_teardown = test_case._post_teardown
if DJANGO_VERSION < (5, 2):
def _pre_setup(inner_self):
self.enable()
original_pre_setup(inner_self)
else:
@classmethod
def _pre_setup(cls):
# NOTE: Django 5.2 turned this as a classmethod
# https://github.com/django/django/pull/18514/files
self.enable()
original_pre_setup()
def _pre_setup(inner_self):
self.enable()
original_pre_setup(inner_self)
def _post_teardown(inner_self):
original_post_teardown(inner_self)
@ -70,20 +60,26 @@ class override_config(override_settings):
return test_case
def enable(self):
"""Store original config values and set overridden values."""
"""
Store original config values and set overridden values.
"""
# Store the original values to an instance variable
for config_key in self.options:
for config_key in self.options.keys():
self.original_values[config_key] = getattr(config, config_key)
# Update config with the overridden values
# Update config with the overriden values
self.unpack_values(self.options)
def disable(self):
"""Set original values to the config."""
"""
Set original values to the config.
"""
self.unpack_values(self.original_values)
@staticmethod
def unpack_values(options):
"""Unpack values from the given dict to config."""
"""
Unpack values from the given dict to config.
"""
for name, value in options.items():
setattr(config, name, value)

View file

@ -1,76 +1,6 @@
from importlib import import_module
from . import LazyConfig
from . import settings
config = LazyConfig()
def import_module_attr(path):
package, module = path.rsplit(".", 1)
package, module = path.rsplit('.', 1)
return getattr(import_module(package), module)
def get_values():
"""
Get dictionary of values from the backend
:return:
"""
# First load a mapping between config name and default value
default_initial = ((name, options[0]) for name, options in settings.CONFIG.items())
# Then update the mapping with actually values from the backend
return dict(default_initial, **config._backend.mget(settings.CONFIG))
async def aget_values():
"""
Get dictionary of values from the backend asynchronously
:return:
"""
default_initial = {name: options[0] for name, options in settings.CONFIG.items()}
backend_values = await config.amget(settings.CONFIG.keys())
return dict(default_initial, **backend_values)
def get_values_for_keys(keys):
"""
Retrieve values for specified keys from the backend.
:param keys: List of keys to retrieve.
:return: Dictionary with values for the specified keys.
:raises AttributeError: If any key is not found in the configuration.
"""
if not isinstance(keys, (list, tuple, set)):
raise TypeError("keys must be a list, tuple, or set of strings")
# Prepare default initial mapping
default_initial = {name: options[0] for name, options in settings.CONFIG.items() if name in keys}
# Check if all keys are present in the default_initial mapping
missing_keys = [key for key in keys if key not in default_initial]
if missing_keys:
raise AttributeError(f'"{", ".join(missing_keys)}" keys not found in configuration.')
# Merge default values and backend values, prioritizing backend values
return dict(default_initial, **config._backend.mget(keys))
async def aget_values_for_keys(keys):
"""
Retrieve values for specified keys from the backend asynchronously.
:param keys: List of keys to retrieve.
:return: Dictionary with values for the specified keys.
:raises AttributeError: If any key is not found in the configuration.
"""
if not isinstance(keys, (list, tuple, set)):
raise TypeError("keys must be a list, tuple, or set of strings")
default_initial = {name: options[0] for name, options in settings.CONFIG.items() if name in keys}
missing_keys = [key for key in keys if key not in default_initial]
if missing_keys:
raise AttributeError(f'"{", ".join(missing_keys)}" keys not found in configuration.')
backend_values = await config.amget(keys)
return dict(default_initial, **backend_values)

View file

@ -1,20 +1,177 @@
# Minimal makefile for Sphinx documentation
# Makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
.PHONY: help Makefile
clean:
rm -rf $(BUILDDIR)/*
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-constance.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-constance.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/django-constance"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-constance"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View file

@ -10,9 +10,6 @@ configuration values. By default it uses the Redis backend. To override
the default please set the :setting:`CONSTANCE_BACKEND` setting to the appropriate
dotted path.
Configuration values are stored in JSON format and automatically serialized/deserialized
on access.
Redis
-----
@ -26,15 +23,7 @@ to add it to your project settings::
CONSTANCE_BACKEND = 'constance.backends.redisd.RedisBackend'
Default redis backend retrieves values every time. There is another redis backend with local cache.
`CachingRedisBackend` stores the value from a redis to memory at first access and checks a value ttl at next.
Configuration installation is simple::
CONSTANCE_BACKEND = 'constance.backends.redisd.CachingRedisBackend'
# optionally set a value ttl
CONSTANCE_REDIS_CACHE_TIMEOUT = 60
.. _`redis-py`: https://pypi.org/project/redis/
.. _`redis-py`: https://pypi.python.org/pypi/redis
Settings
^^^^^^^^
@ -63,11 +52,11 @@ An (optional) dotted import path to a connection to use, e.g.::
CONSTANCE_REDIS_CONNECTION_CLASS = 'myproject.myapp.mockup.Connection'
If you are using `django-redis <https://github.com/jazzband/django-redis>`_,
If you are using `django-redis <http://niwibe.github.io/django-redis/>`_,
feel free to use the ``CONSTANCE_REDIS_CONNECTION_CLASS`` setting to define
a callable that returns a redis connection, e.g.::
CONSTANCE_REDIS_CONNECTION_CLASS = 'django_redis.get_redis_connection'
CONSTANCE_REDIS_CONNECTION_CLASS = 'redis_cache.get_redis_connection'
``CONSTANCE_REDIS_PREFIX``
~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -77,25 +66,30 @@ database. Defaults to ``'constance:'``. E.g.::
CONSTANCE_REDIS_PREFIX = 'constance:myproject:'
``CONSTANCE_REDIS_CACHE_TIMEOUT``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The (optional) ttl of values in seconds used by `CachingRedisBackend` for storing in a local cache.
Defaults to `60` seconds.
Database
--------
Database backend stores configuration values in a standard Django model.
The database backend is optional and stores the configuration values in a
standard Django model. It requires the package `django-picklefield`_ for
storing those values. Please install it like so::
pip install django-constance[database]
You must set the ``CONSTANCE_BACKEND`` Django setting to::
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
Then add the database backend app to your :setting:`INSTALLED_APPS` setting to
make sure the data model is correctly created::
INSTALLED_APPS = (
# other apps
'constance.backends.database',
)
Please make sure to apply the database migrations::
python manage.py migrate
python manage.py migrate database
.. note:: If you're upgrading Constance to 1.0 and use Django 1.7 or higher
please make sure to let the migration system know that you've
@ -105,7 +99,6 @@ Please make sure to apply the database migrations::
python manage.py migrate database --fake
Just like the Redis backend you can set an optional prefix that is used during
database interactions (it defaults to an empty string, ``''``). To use
something else do this::
@ -134,32 +127,9 @@ configured cache backend to enable this feature, e.g. "default"::
cache backend included in Django because correct cache
invalidation can't be guaranteed.
If you try this, Constance will throw an error and refuse
to let your application start. You can work around this by
subclassing ``constance.backends.database.DatabaseBackend``
and overriding `__init__` to remove the check. You'll
want to consult the source code for that function to see
exactly how.
We're deliberately being vague about this, because it's
dangerous; the behavior is undefined, and could even cause
your app to crash. Nevertheless, there are some limited
circumstances in which this could be useful, but please
think carefully before going down this path.
.. note:: By default Constance will autofill the cache on startup and after
saving any of the config values. If you want to disable the cache
simply set the :setting:`CONSTANCE_DATABASE_CACHE_AUTOFILL_TIMEOUT`
setting to ``None``.
Memory
------
The configuration values are stored in a memory and do not persist between process
restarts. In order to use this backend you must set the ``CONSTANCE_BACKEND``
Django setting to::
CONSTANCE_BACKEND = 'constance.backends.memory.MemoryBackend'
The main purpose of this one is to be used mostly for testing/developing means,
so make sure you intentionally use it on production environments.
.. _django-picklefield: http://pypi.python.org/pypi/django-picklefield/

View file

@ -1,311 +1,5 @@
Changelog
---------
Starting with version 4.0.0, the changelog is maintained at the GitHub releases `GitHub releases`_
.. _GitHub releases: https://github.com/jazzband/django-constance/releases
v4.0.0 (2024/08/21)
~~~~~~~~~~~~~~~~~~~
* Replace `pickle` with JSON for the database backend
* Fix migration on MySQL
* Fix data loss using `DatabaseBackend` when the DB connection is unstable
* Fix typos in the documentation
* Fix small HTML errors
* Drop support for legacy Django versions
* Migrate JavaScript to ES2015
* Fix documentation build
* Add linters and formatters (using `ruff`)
* Add support for Django 5.1 and 5.2
* Migrate from `setup.py` to `pyproject.toml`
* Bump `tox`
* Declare support for Python 3.12
v3.1.0 (2023/08/21)
~~~~~~~~~~~~~~~~~~~
* Add support for using a subdirectory of `MEDIA_ROOT` for file fields
* Remove pypy from tox tests
v3.0.0 (2023/07/27)
~~~~~~~~~~~~~~~~~~~
* Refactor database backend
Backward incompatible changes:
remove ``'constance.backends.database'`` from ``INSTALLED_APPS``
* Dropped support for python < 3.7 and django < 3.2
* Example app now supports django 4.1
* Add support for django 4.2
* Forward the request when saving the admin changelist form
v2.9.1 (2022/08/11)
~~~~~~~~~~~~~~~~~~~
* Add support for gettext in fieldset headers
* Add support for Django 4.1
* Fix text format for MultiValueField usage
v2.9.0 (2022/03/11)
~~~~~~~~~~~~~~~~~~~
* Added arabic translation
* Add concrete_model class attribute to fake admin model
* Added tests for django 3.2
* Fix do not detect datetime fields as date type
* Added support for python 3.10
* Fixes for Ukrainian locale
* Added documentation for constance_dbs config
* Add caching redis backend
* Serialize according to widget
* Add default_auto_field to database backend
v2.8.0 (2020/11/19)
~~~~~~~~~~~~~~~~~~~
* Prevent reset to default for file field
* Fields_list can be a dictionary, when a fieldset is defined as collapsible
* Create and add fa language translations files
* Respect other classes added by admin templates
* Removed deprecated url()
* Use gettext_lazy instead of ugettext_lazy
* Updated python and django version support
v2.7.0 (2020/06/22)
~~~~~~~~~~~~~~~~~~~
* Deleted south migrations
* Improve grammar of documentation index file
* Simplify documentation installation section
* Fix IntegrityError after 2.5.0 release
(Allow concurrent calls to `DatabaseBackend.set()` method)
* Make groups of fieldsets collapsable
* Allow override_config for pytest
* Put back wheel generation in travis
* Fix wrong "is modified" in admin for multi line strings
* Switch md5 to sha256
* Fix Attempts to change config values fail silently and
appear to succeed when user does not have change permissions
* Make constance app verbose name translatable
* Update example project for Django>2
* Add anchors in admin for constance settings
* Added a sticky footer in django constance admin
* Add memory backend
* Added Ukrainian locale
* Added lazy checks for pytest
v2.6.0 (2020/01/29)
~~~~~~~~~~~~~~~~~~~
* Drop support py<3.5 django<2.2
* Set pickle protocol version for the Redis backend
* Add a command to delete stale records
v2.5.0 (2019/12/23)
~~~~~~~~~~~~~~~~~~~
* Made results table responsive for Django 2 admin
* Add a Django system check that CONFIG_FIELDSETS accounts for all of CONFIG
* Rewrite set() method of database backend to reduce number of queries
* Fixed "can't compare offset-naive and offset-aware datetimes" when USE_TZ = True
* Fixed compatibility issue with Django 3.0 due to django.utils.six
* Add Turkish language
v2.4.0 (2019/03/16)
~~~~~~~~~~~~~~~~~~~
* Show not existing fields in field_list
* Drop Django<1.11 and 2.0, fix tests vs Django 2.2b
* Fixed "Reset to default" button with constants whose name contains a space
* Use default_storage to save file
* Allow null & blank for PickleField
* Removed Python 3.4 since is not longer supported
v2.3.1 (2018/09/20)
~~~~~~~~~~~~~~~~~~~
* Fixes javascript typo.
v2.3.0 (2018/09/13)
~~~~~~~~~~~~~~~~~~~
* Added zh_Hans translation.
* Fixed TestAdmin.test_linebreaks() due to linebreaksbr() behavior change
on Django 2.1
* Improved chinese translation
* Fix bug of can't change permission chang_config's name
* Improve consistency of reset value handling for `date`
* Drop support for Python 3.3
* Added official Django 2.0 support.
* Added support for Django 2.1
v2.2.0 (2018/03/23)
~~~~~~~~~~~~~~~~~~~
* Fix ConstanceForm validation.
* `CONSTANCE_DBS` setting for directing constance permissions/content_type
settings to certain DBs only.
* Added config labels.
* Updated italian translations.
* Fix `CONSTANCE_CONFIG_FIELDSETS` mismatch issue.
v2.1.0 (2018/02/07)
~~~~~~~~~~~~~~~~~~~
* Move inline JavaScript to constance.js.
* Remove translation from the app name.
* Added file uploads.
* Update information on template context processors.
* Allow running set while database is not created.
* Moved inline css/javascripts out to their own files.
* Add French translations.
* Add testing for all supported Python and Django versions.
* Preserve sorting from fieldset config.
* Added datetime.timedelta support.
* Added Estonian translations.
* Account for server timezone for Date object.
v2.0.0 (2017/02/17)
~~~~~~~~~~~~~~~~~~~
* **BACKWARD INCOMPATIBLE** Added the old value to the config_updated signal.
* Added a `get_changelist_form` hook in the ModelAdmin.
* Fix create_perm in apps.py to use database alias given by the post_migrate
signal.
* Added tests for django 1.11.
* Fix Reset to default to work with boolean/checkboxes.
* Fix handling of MultiValueField's (eg SplitDateTimeField) on the command
line.
v1.3.4 (2016/12/23)
~~~~~~~~~~~~~~~~~~~
* Fix config ordering issue
* Added localize to check modified flag
* Allow to rename Constance in Admin
* Preserve line breaks in default value
* Added functionality from django-constance-cli
* Added "Reset to default" feature
v1.3.3 (2016/09/17)
~~~~~~~~~~~~~~~~~~~
* Revert broken release
v1.3.2 (2016/09/17)
~~~~~~~~~~~~~~~~~~~
* Fixes a bug where the signal was sent for fields without changes
v1.3.1 (2016/09/15)
~~~~~~~~~~~~~~~~~~~
* Improved the signal path to avoid import errors
* Improved the admin layout when using fieldsets
v1.3 (2016/09/14)
~~~~~~~~~~~~~~~~~
* **BACKWARD INCOMPATIBLE** Dropped support for Django < 1.8).
* Added ordering constance fields using OrderedDict
* Added a signal when updating constance fields
v1.2.1 (2016/09/1)
~~~~~~~~~~~~~~~~~~
* Added some fixes to small bugs
* Fix cache when key changes
* Upgrade django_redis connection string
* Autofill cache key if key is missing
* Added support for fieldsets
v1.2 (2016/05/14)
~~~~~~~~~~~~~~~~~
@ -366,7 +60,7 @@ v1.0 (2014/12/04)
* Added docs and set up Read The Docs project:
https://django-constance.readthedocs.io/
http://django-constance.readthedocs.org/
* Set up Transifex project for easier translations:

View file

@ -1,108 +1,268 @@
# Configuration file for the Sphinx documentation builder.
# -*- coding: utf-8 -*-
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
# django-constance documentation build configuration file, created by
# sphinx-quickstart on Tue Nov 25 19:38:51 2014.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
from datetime import datetime
import os
def get_version():
# Try to get version from installed package metadata
try:
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version
return version("django-constance")
except (ImportError, PackageNotFoundError):
pass
# Fall back to setuptools_scm generated version file
try:
from constance._version import __version__
return __version__
except ImportError:
pass
return "0.0.0"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings')
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath("extensions"))
sys.path.insert(0, os.path.abspath(".."))
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "django-constance"
project_copyright = datetime.now().year.__str__() + ", Jazzband"
# The full version, including alpha/beta/rc tags
release = get_version()
# The short X.Y version
version = ".".join(release.split(".")[:3])
sys.path.insert(0, os.path.abspath('extensions'))
sys.path.insert(0, os.path.abspath('..'))
# -- General configuration ------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx_search.extension",
"settings",
]
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
templates_path = ["_templates"]
source_suffix = ".rst"
root_doc = "index"
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
pygments_style = "sphinx"
html_last_updated_fmt = ""
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.intersphinx', 'settings']
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'django-constance'
copyright = u'2016, Comoga and individual contributors'
# The short X.Y version.
try:
from constance import __version__
# The short X.Y version.
version = '.'.join(__version__.split('.')[:2])
# The full version, including alpha/beta/rc tags.
release = __version__
except ImportError:
version = release = 'dev'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'django-constancedoc'
html_theme = "sphinx_rtd_theme"
html_static_path = ["_static"]
htmlhelp_basename = "django-constancedoc"
# -- Options for LaTeX output ---------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-latex-output
latex_elements = {}
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
("index", "django-constance.tex", "django-constance Documentation", "Jazzband", "manual"),
('index', 'django-constance.tex', u'django-constance Documentation',
u'Comoga and individual contributors', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-manual-page-output
man_pages = [("index", "django-constance", "django-constance Documentation", ["Jazzband"], 1)]
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'django-constance', u'django-constance Documentation',
[u'Comoga and individual contributors'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-texinfo-output
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
"index",
"django-constance",
"django-constance Documentation",
"Jazzband",
"django-constance",
"One line description of project.",
"Miscellaneous",
),
('index', 'django-constance', u'django-constance Documentation',
u'Comoga and individual contributors', 'django-constance', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"django": ("https://docs.djangoproject.com/en/dev/", "https://docs.djangoproject.com/en/dev/_objects/"),
'http://docs.python.org/': None,
'django': ('http://docs.djangoproject.com/en/dev/',
'http://docs.djangoproject.com/en/dev/_objects/'),
}

View file

@ -5,19 +5,27 @@ Features
--------
* Easily migrate your static settings to dynamic settings.
* Edit the dynamic settings in the Django admin interface.
* Admin interface to edit the dynamic settings.
.. image:: _static/screenshot2.png
.. image:: screenshot2.png
Quick Installation
------------------
Installation
------------
.. code-block::
Install from PyPI the backend specific variant of django-constance:
For the (default) Redis backend::
pip install "django-constance[redis]"
For complete installation instructions, including how to install the
database backend, see :ref:`Backends <backends>`.
For the database backend::
pip install "django-constance[database]"
Alternatively -- if you're sure that the dependencies are already
installed -- you can also run::
pip install django-constance
Configuration
-------------
@ -44,12 +52,6 @@ the :setting:`CONSTANCE_CONFIG` section, like this:
'The Universe, and Everything'),
}
.. note:: Add constance *before* your project apps.
.. note:: If you use admin extensions like
`Grapelli <https://grappelliproject.com/>`_, ``'constance'`` should be added
in :setting:`INSTALLED_APPS` *before* those extensions.
Here, ``42`` is the default value for the key ``THE_ANSWER`` if it is
not found in the backend. The other member of the tuple is a help text the
admin will show.
@ -58,56 +60,33 @@ See the :ref:`Backends <backends>` section how to setup the backend and
finish the configuration.
``django-constance``'s hashes generated in different instances of the same
application may differ, preventing data from being saved.
application may differ, preventing data from being saved.
Use :setting:`CONSTANCE_IGNORE_ADMIN_VERSION_CHECK` in order to skip hash
verification.
Use this option in order to skip hash verification.
.. code-block:: python
CONSTANCE_IGNORE_ADMIN_VERSION_CHECK = True
Signals
-------
Each time a value is changed it will trigger a ``config_updated`` signal.
.. code-block:: python
from constance.signals import config_updated
@receiver(config_updated)
def constance_updated(sender, key, old_value, new_value, **kwargs):
print(sender, key, old_value, new_value)
The sender is the ``config`` object, and the ``key`` and ``new_value``
are the changed settings.
Custom fields
-------------
You can set the field type with the third value in the ``CONSTANCE_CONFIG`` tuple.
You can set the field type with the third value in the `CONSTANCE_CONFIG` tuple.
The value can be one of the supported types or a string matching a key in your :setting:`CONSTANCE_ADDITIONAL_FIELDS`
The supported types are:
* ``bool``
* ``int``
* ``float``
* ``Decimal``
* ``str``
* ``datetime``
* ``timedelta``
* ``date``
* ``time``
* ``list``
* ``dict``
.. note::
To be able to use ``list`` and ``dict`` you need to set a widget and form field for these types as it is ambiguous what types shall be stored in the collection object.
You can do so with :setting:`CONSTANCE_ADDITIONAL_FIELDS` as explained below.
* `bool`
* `int`
* `float`
* `Decimal`
* `long` (on python 2)
* `str`
* `unicode` (on python 2)
* `datetime`
* `date`
* `time`
For example, to force a value to be handled as a string:
@ -121,17 +100,17 @@ Custom field types are supported using the dictionary :setting:`CONSTANCE_ADDITI
This is a mapping between a field label and a sequence (list or tuple). The first item in the sequence is the string
path of a field class, and the (optional) second item is a dictionary used to configure the field.
The ``widget`` and ``widget_kwargs`` keys in the field config dictionary can be used to configure the widget used in admin,
the other values will be passed as kwargs to the field's ``__init__()``
The `widget` and `widget_kwargs` keys in the field config dictionary can be used to configure the widget used in admin,
the other values will be passed as kwargs to the field's `__init__()`
.. note:: Use later evaluated strings instead of direct classes for the field and widget classes:
Note: Use later evaluated strings instead of direct classes for the field and widget classes:
.. code-block:: python
CONSTANCE_ADDITIONAL_FIELDS = {
'yes_no_null_select': ['django.forms.fields.ChoiceField', {
'widget': 'django.forms.Select',
'choices': ((None, "-----"), ("yes", "Yes"), ("no", "No"))
'choices': (("-----", None), ("yes", "Yes"), ("no", "No"))
}],
}
@ -139,141 +118,6 @@ the other values will be passed as kwargs to the field's ``__init__()``
'MY_SELECT_KEY': ('yes', 'select yes or no', 'yes_no_null_select'),
}
If you want to work with images or files you can use this configuration:
.. code-block:: python
CONSTANCE_ADDITIONAL_FIELDS = {
'image_field': ['django.forms.ImageField', {}]
}
CONSTANCE_CONFIG = {
'LOGO_IMAGE': ('default.png', 'Company logo', 'image_field'),
}
When used in a template you probably need to use:
.. code-block:: html
{% load static %}
{% get_media_prefix as MEDIA_URL %}
<img src="{{ MEDIA_URL }}{{ config.LOGO_IMAGE }}">
Images and files are uploaded to ``MEDIA_ROOT`` by default. You can specify a subdirectory of ``MEDIA_ROOT`` to use instead by adding the ``CONSTANCE_FILE_ROOT`` setting. E.g.:
.. code-block:: python
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
CONSTANCE_FILE_ROOT = 'constance'
This will result in files being placed in ``media/constance`` within your ``BASE_DIR``. You can use deeper nesting in this setting (e.g. ``constance/images``) but other relative path components (e.g. ``../``) will be rejected.
In case you want to store a list of ``int`` values in the constance config, a working setup is
.. code-block:: python
CONSTANCE_ADDITIONAL_FIELDS = {
list: ["django.forms.fields.JSONField", {"widget": "django.forms.Textarea"}],
}
CONSTANCE_CONFIG = {
'KEY': ([0, 10, 20], 'A list of integers', list),
}
Make sure to use the ``JSONField`` for this purpose as user input in the admin page may be understood and saved as ``str`` otherwise.
Ordered Fields in Django Admin
------------------------------
To sort the fields, you can use an OrderedDict:
.. code-block:: python
from collections import OrderedDict
CONSTANCE_CONFIG = OrderedDict([
('SITE_NAME', ('My Title', 'Website title')),
('SITE_DESCRIPTION', ('', 'Website description')),
('THEME', ('light-blue', 'Website theme')),
])
Fieldsets
---------
You can define fieldsets to group settings together:
.. code-block:: python
CONSTANCE_CONFIG = {
'SITE_NAME': ('My Title', 'Website title'),
'SITE_DESCRIPTION': ('', 'Website description'),
'THEME': ('light-blue', 'Website theme'),
}
CONSTANCE_CONFIG_FIELDSETS = {
'General Options': ('SITE_NAME', 'SITE_DESCRIPTION'),
'Theme Options': ('THEME',),
}
.. note:: CONSTANCE_CONFIG_FIELDSETS must contain all fields from CONSTANCE_CONFIG.
.. image:: _static/screenshot3.png
Fieldsets collapsing
--------------------
To make some fieldsets collapsing you can use new format in CONSTANCE_CONFIG_FIELDSETS. Here's an example:
.. code-block:: python
CONSTANCE_CONFIG = {
'SITE_NAME': ('My Title', 'Website title'),
'SITE_DESCRIPTION': ('', 'Website description'),
'THEME': ('light-blue', 'Website theme'),
}
CONSTANCE_CONFIG_FIELDSETS = {
'General Options': {
'fields': ('SITE_NAME', 'SITE_DESCRIPTION'),
'collapse': True
},
'Theme Options': ('THEME',),
}
Field internationalization
--------------------------
Field description and fieldset headers can be integrated into Django's
internationalization using the ``gettext_lazy`` function. Note that the
``CONSTANCE_CONFIG_FIELDSETS`` must be converted to a tuple instead of dict
as it is not possible to have lazy proxy objects as dictionary keys in the
settings file. Example:
.. code-block:: python
from django.utils.translation import gettext_lazy as _
CONSTANCE_CONFIG = {
'SITE_NAME': ('My Title', _('Website title')),
'SITE_DESCRIPTION': ('', _('Website description')),
'THEME': ('light-blue', _('Website theme')),
}
CONSTANCE_CONFIG_FIELDSETS = (
(
_('General Options'),
{
'fields': ('SITE_NAME', 'SITE_DESCRIPTION'),
'collapse': True,
},
),
(_('Theme Options'), ('THEME',)),
)
Usage
-----
@ -292,40 +136,10 @@ object and accessing the variables with attribute lookups::
if config.THE_ANSWER == 42:
answer_the_question()
Asynchronous usage
^^^^^^^^^^^^^^^^^^
If you are using Django's asynchronous features (like async views), you can ``await`` the settings directly on the standard ``config`` object::
from constance import config
async def my_async_view(request):
# Accessing settings is awaitable
if await config.THE_ANSWER == 42:
return await answer_the_question_async()
async def update_settings():
# Updating settings asynchronously
await config.aset('THE_ANSWER', 43)
# Bulk retrieval is supported as well
values = await config.amget(['THE_ANSWER', 'SITE_NAME'])
Performance and Safety
~~~~~~~~~~~~~~~~~~~~~~
While synchronous access (e.g., ``config.THE_ANSWER``) still works inside async views for some backends, it is highly discouraged:
* **Blocking:** Synchronous access blocks the event loop, reducing the performance of your entire application.
* **Safety Guards:** For the Database backend, Django's safety guards will raise a ``SynchronousOnlyOperation`` error if you attempt to access a setting synchronously from an async thread.
* **Automatic Detection:** Constance will emit a ``RuntimeWarning`` if it detects synchronous access inside an asynchronous event loop, helping you identify and fix these performance bottlenecks.
For peak performance, especially with the Redis backend, always use the ``await`` syntax which leverages native asynchronous drivers.
Django templates
^^^^^^^^^^^^^^^^
To access the config object from your template you can
To access the config object from your template you can either
pass the object to the template context:
.. code-block:: python
@ -336,13 +150,14 @@ pass the object to the template context:
def myview(request):
return render(request, 'my_template.html', {'config': config})
You can also use the included context processor.
Or you can use the included config context processor.:
Insert ``'constance.context_processors.config'`` at
the top of your ``TEMPLATES['OPTIONS']['context_processors']`` list. See the
`Django documentation`_ for details.
.. code-block:: python
.. _`Django documentation`: https://docs.djangoproject.com/en/1.11/ref/templates/upgrading/#the-templates-settings
TEMPLATE_CONTEXT_PROCESSORS = (
# ...
'constance.context_processors.config',
)
This will add the config instance to the context of any template
rendered with a ``RequestContext``.
@ -357,112 +172,22 @@ any other variable, e.g.:
Woohoo! Head over <a href="/sekrit/">here</a> to use the beta.
{% else %}
Sadly we haven't launched yet, click <a href="/newsletter/">here</a>
to signup for our newsletter.
to signup for our newletter.
{% endif %}
Command Line
^^^^^^^^^^^^
Constance settings can be get/set on the command line with the manage command :command:`constance`.
Available options are:
.. program:: constance
.. option:: list
list all Constance keys and their values
.. code-block:: console
$ ./manage.py constance list
THE_ANSWER 42
SITE_NAME My Title
.. option:: get <KEY>
get the value of a Constance key
.. code-block:: console
$ ./manage.py constance get THE_ANSWER
42
.. option:: set <KEY> <VALUE>
set the value of a Constance key
.. code-block:: console
$ ./manage.py constance set SITE_NAME "Another Title"
If the value contains spaces it should be wrapped in quotes.
.. note:: Set values are validated as per in admin, an error will be raised if validation fails:
E.g., given this config as per the example app:
.. code-block:: python
CONSTANCE_CONFIG = {
...
'DATE_ESTABLISHED': (date(1972, 11, 30), "the shop's first opening"),
}
Setting an invalid date will fail as follow:
.. code-block:: console
$ ./manage.py constance set DATE_ESTABLISHED '1999-12-00'
CommandError: Enter a valid date.
.. note:: If the admin field is a :class:`MultiValueField`, then the separate field values need to be provided as separate arguments.
E.g., a datetime using :class:`SplitDateTimeField`:
.. code-block:: python
CONSTANCE_CONFIG = {
'DATETIME_VALUE': (datetime(2010, 8, 23, 11, 29, 24), 'time of the first commit'),
}
Then this works (and the quotes are optional):
.. code-block:: console
./manage.py constance set DATETIME_VALUE '2011-09-24' '12:30:25'
This doesn't work:
.. code-block:: console
./manage.py constance set DATETIME_VALUE '2011-09-24 12:30:25'
CommandError: Enter a list of values.
.. option:: remove_stale_keys
delete all Constance keys and their values if they are not in settings.CONSTANCE_CONFIG (stale keys)
.. code-block:: console
$ ./manage.py constance remove_stale_keys
Record is considered stale if it exists in database but absent in config.
Editing
-------
Fire up your ``admin`` and you should see a new app called ``Constance``
with ``THE_ANSWER`` in the ``Config`` pseudo model.
By default, changing the settings via the admin is only allowed for superusers.
To change this, feel free to set the :setting:`CONSTANCE_SUPERUSER_ONLY`
setting to ``False`` and give users or user groups access to the
By default changing the settings via the admin is only allowed for super users.
But in case you want to use the admin's ability to implement custom
authorization checks, feel free to set the :setting:`CONSTANCE_SUPERUSER_ONLY`
setting to ``False`` and give the users or user groups access to the
``constance.change_config`` permission.
.. figure:: _static/screenshot1.png
.. figure:: screenshot1.png
The virtual application ``Constance`` among your regular applications.
@ -478,38 +203,19 @@ settings the way you like.
.. code-block:: python
from constance.admin import ConstanceAdmin, Config
from constance.forms import ConstanceForm
from constance.admin import ConstanceAdmin, ConstanceForm, Config
class CustomConfigForm(ConstanceForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
super(CustomConfigForm, self).__init__(*args, **kwargs)
#... do stuff to make your settings form nice ...
class ConfigAdmin(ConstanceAdmin):
change_list_form = CustomConfigForm
form = CustomConfigForm
change_list_template = 'admin/config/settings.html'
admin.site.unregister([Config])
admin.site.register([Config], ConfigAdmin)
You can also override the ``get_changelist_form`` method which is called in
``changelist_view`` to get the actual form used to change the settings. This
allows you to pick a different form according to the user that makes the
request. For example:
.. code-block:: python
class SuperuserForm(ConstanceForm):
# Do some stuff here
class MyConstanceAdmin(ConstanceAdmin):
def get_changelist_form(self, request):
if request.user.is_superuser:
return SuperuserForm:
else:
return super().get_changelist_form(request)
Note that the default method returns ``self.change_list_form``.
More documentation
------------------

View file

@ -1,16 +1,53 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
%SPHINXBUILD% >NUL 2>NUL
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
%SPHINXBUILD% 2> nul
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
@ -19,17 +56,187 @@ if errorlevel 9009 (
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
echo.http://sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-constance.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-constance.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
:end
popd

View file

@ -1,3 +0,0 @@
readthedocs-sphinx-search==0.3.2
sphinx==7.3.7
sphinx-rtd-theme==2.0.0

View file

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View file

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View file

@ -15,7 +15,7 @@ Usage
It can be used as a decorator at the :class:`~django.test.TestCase` level, the
method level and also as a
`context manager <https://peps.python.org/pep-0343/>`_.
`context manager <https://www.python.org/dev/peps/pep-0343/>`_.
.. code-block:: python
@ -38,87 +38,3 @@ method level and also as a
def test_what_is_your_favourite_color(self):
with override_config(YOUR_FAVOURITE_COLOR="Blue?"):
self.assertEqual(config.YOUR_FAVOURITE_COLOR, "Blue?")
Pytest usage
~~~~~~~~~~~~
Django-constance provides pytest plugin that adds marker
:class:`@pytest.mark.override_config()`. It handles config override for
module/class/function, and automatically revert any changes made to the
constance config values when test is completed.
.. py:function:: pytest.mark.override_config(**kwargs)
Specify different config values for the marked tests in kwargs.
Module scope override
.. code-block:: python
pytestmark = pytest.mark.override_config(API_URL="/awesome/url/")
def test_api_url_is_awesome():
...
Class/function scope
.. code-block:: python
from constance import config
@pytest.mark.override_config(API_URL="/awesome/url/")
class SomeClassTest:
def test_is_awesome_url(self):
assert config.API_URL == "/awesome/url/"
@pytest.mark.override_config(API_URL="/another/awesome/url/")
def test_another_awesome_url(self):
assert config.API_URL == "/another/awesome/url/"
If you want to use override as a context manager or decorator, consider using
.. code-block:: python
from constance.test.pytest import override_config
def test_override_context_manager():
with override_config(BOOL_VALUE=False):
...
# or
@override_config(BOOL_VALUE=False)
def test_override_context_manager():
...
Pytest fixture as function or method parameter.
.. note:: No import needed as fixture is available globally.
.. code-block:: python
def test_api_url_is_awesome(override_config):
with override_config(API_URL="/awesome/url/"):
...
Any scope, auto-used fixture alternative can also be implemented like this
.. code-block:: python
@pytest.fixture(scope='module', autouse=True) # e.g. module scope
def api_url(override_config):
with override_config(API_URL="/awesome/url/"):
yield
Memory backend
~~~~~~~~~~~~~~
If you don't want to rely on any external services such as Redis or database when
running your unittests you can select :class:`MemoryBackend` for a test Django settings file
.. code-block:: python
CONSTANCE_BACKEND = 'constance.backends.memory.MemoryBackend'
It will provide simple thread-safe backend which will reset to default values after each
test run.

View file

@ -1,5 +1,4 @@
from django.contrib import admin
from cheeseshop.apps.catalog.models import Brand
admin.site.register(Brand)

View file

@ -1,16 +0,0 @@
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = []
operations = [
migrations.CreateModel(
name="Brand",
fields=[
("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
("name", models.CharField(max_length=75)),
],
),
]

View file

@ -1,5 +1,5 @@
from django.db import models
class Brand(models.Model):
name = models.CharField(max_length=75)

View file

@ -1,7 +1,5 @@
from django.contrib import admin
from cheeseshop.apps.storage.models import Shelf
from cheeseshop.apps.storage.models import Supply
from cheeseshop.apps.storage.models import Shelf, Supply
admin.site.register(Shelf)
admin.site.register(Supply)

View file

@ -1,29 +0,0 @@
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = []
operations = [
migrations.CreateModel(
name="Shelf",
fields=[
("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
("name", models.CharField(max_length=75)),
],
options={
"verbose_name_plural": "shelves",
},
),
migrations.CreateModel(
name="Supply",
fields=[
("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
("name", models.CharField(max_length=75)),
],
options={
"verbose_name_plural": "supplies",
},
),
]

View file

@ -1,15 +1,14 @@
from django.db import models
class Shelf(models.Model):
name = models.CharField(max_length=75)
class Meta:
verbose_name_plural = "shelves"
verbose_name_plural = 'shelves'
class Supply(models.Model):
name = models.CharField(max_length=75)
class Meta:
verbose_name_plural = "supplies"
verbose_name_plural = 'supplies'

View file

@ -1,25 +0,0 @@
import json
from django.forms import fields
from django.forms import widgets
class JsonField(fields.CharField):
widget = widgets.Textarea
def __init__(self, rows: int = 5, **kwargs):
self.rows = rows
super().__init__(**kwargs)
def widget_attrs(self, widget: widgets.Widget):
attrs = super().widget_attrs(widget)
attrs["rows"] = self.rows
return attrs
def to_python(self, value):
if value:
return json.loads(value)
return {}
def prepare_value(self, value):
return json.dumps(value)

Some files were not shown because too many files have changed in this diff Show more