├── .coveragerc ├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── docs.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── constance ├── __init__.py ├── admin.py ├── apps.py ├── backends │ ├── __init__.py │ ├── database.py │ ├── memory.py │ └── redisd.py ├── base.py ├── checks.py ├── codecs.py ├── context_processors.py ├── forms.py ├── locale │ ├── ar │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── cs_CZ │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── et │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fa │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── it │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pl │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ru │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── tr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── uk │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── zh_CN │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── zh_Hans │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── constance.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_migrate_from_old_table.py │ ├── 0003_drop_pickle.py │ └── __init__.py ├── models.py ├── settings.py ├── signals.py ├── static │ └── admin │ │ ├── css │ │ └── constance.css │ │ └── js │ │ └── constance.js ├── templates │ └── admin │ │ └── constance │ │ ├── change_list.html │ │ └── includes │ │ └── results_list.html ├── test │ ├── __init__.py │ ├── pytest.py │ └── unittest.py └── utils.py ├── docs ├── Makefile ├── _static │ ├── screenshot1.png │ ├── screenshot2.png │ └── screenshot3.png ├── backends.rst ├── changes.rst ├── conf.py ├── extensions │ ├── __init__.py │ └── settings.py ├── index.rst ├── make.bat ├── requirements.txt └── testing.rst ├── example ├── cheeseshop │ ├── __init__.py │ ├── apps │ │ ├── __init__.py │ │ ├── catalog │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── migrations │ │ │ │ ├── 0001_initial.py │ │ │ │ └── __init__.py │ │ │ └── models.py │ │ └── storage │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ │ └── models.py │ ├── fields.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py └── requirements.txt ├── pyproject.toml ├── tests ├── __init__.py ├── backends │ ├── __init__.py │ ├── test_database.py │ ├── test_memory.py │ └── test_redis.py ├── redis_mockup.py ├── settings.py ├── storage.py ├── test_admin.py ├── test_checks.py ├── test_cli.py ├── test_codecs.py ├── test_form.py ├── test_pytest_overrides.py ├── test_test_overrides.py ├── test_utils.py └── urls.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = constance 3 | branch = 1 4 | omit = 5 | */pytest.py 6 | */tests/* 7 | 8 | [report] 9 | omit = *tests*,*migrations*,.tox/*,setup.py,*settings.py 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Describe the problem 2 | 3 | Tell us about the problem you're having. 4 | 5 | ### Steps to reproduce 6 | 7 | Tell us how to reproduce it. 8 | 9 | ### System configuration 10 | 11 | * Django version: 12 | * Python version: 13 | * Django-Constance version: 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | commit-message: 13 | prefix: "chore(ci): " 14 | groups: 15 | github-actions: 16 | patterns: 17 | - "*" 18 | open-pull-requests-limit: 1 19 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | docs: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | 11 | - name: Set up Python 12 | uses: actions/setup-python@v5 13 | with: 14 | python-version: '3.12' 15 | cache: 'pip' 16 | cache-dependency-path: 'docs/requirements.txt' 17 | 18 | - name: Install dependencies 19 | run: pip install -r docs/requirements.txt 20 | 21 | - name: Build docs 22 | run: | 23 | cd docs 24 | make html 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | if: github.repository == 'jazzband/django-constance' 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install -U pip 24 | python -m pip install -U build setuptools twine wheel 25 | 26 | - name: Build package 27 | run: | 28 | python -m build 29 | twine check dist/* 30 | 31 | - name: Upload packages to Jazzband 32 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 33 | uses: pypa/gh-action-pypi-publish@v1.12.4 34 | with: 35 | user: jazzband 36 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 37 | repository_url: https://jazzband.co/projects/django-constance/upload 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruff-format: 7 | runs-on: ubuntu-latest 8 | timeout-minutes: 1 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: chartboost/ruff-action@v1 12 | with: 13 | version: 0.5.0 14 | args: 'format --check' 15 | 16 | ruff-lint: 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 1 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: chartboost/ruff-action@v1 22 | with: 23 | version: 0.5.0 24 | 25 | build: 26 | runs-on: ubuntu-latest 27 | strategy: 28 | fail-fast: false 29 | max-parallel: 5 30 | matrix: 31 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - name: Set up Python ${{ matrix.python-version }} 37 | uses: actions/setup-python@v5 38 | with: 39 | python-version: ${{ matrix.python-version }} 40 | cache: 'pip' 41 | 42 | - name: Install dependencies 43 | run: | 44 | python -m pip install --upgrade pip 45 | python -m pip install --upgrade tox tox-gh-actions 46 | 47 | - name: Tox tests 48 | run: | 49 | tox -v 50 | 51 | - name: Upload coverage 52 | uses: codecov/codecov-action@v5 53 | with: 54 | name: Python ${{ matrix.python-version }} 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .pydevproject 3 | *.pyc 4 | *.egg-info 5 | build/ 6 | dist/ 7 | test.db 8 | .tox 9 | .coverage 10 | coverage.xml 11 | docs/_build 12 | .idea 13 | constance/_version.py 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pygrep-hooks 3 | rev: v1.10.0 4 | hooks: 5 | - id: python-check-blanket-noqa 6 | 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v5.0.0 9 | hooks: 10 | - id: check-merge-conflict 11 | - id: check-yaml 12 | 13 | - repo: https://github.com/asottile/pyupgrade 14 | rev: v3.19.1 15 | hooks: 16 | - id: pyupgrade 17 | args: [ --py38-plus ] 18 | 19 | exclude: /migrations/ 20 | 21 | ci: 22 | autoupdate_schedule: quarterly 23 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | version: 2 5 | 6 | build: 7 | os: ubuntu-lts-latest 8 | tools: 9 | python: "3.12" 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | python: 15 | install: 16 | - requirements: docs/requirements.txt 17 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Ales Zoulek 2 | Alexander Frenzel 3 | Alexandr Artemyev 4 | Bouke Haarsma 5 | Camilo Nova 6 | Charlie Hornsby 7 | Curtis Maloney 8 | Dan Poirier 9 | David Burke 10 | Dmitriy Tatarkin 11 | Elisey Zanko 12 | Florian Apolloner 13 | Igor Támara 14 | Ilya Chichak 15 | Ivan Klass 16 | Jake Merdich 17 | Jannis Leidel 18 | Janusz Harkot 19 | Jiri Barton 20 | John Carter 21 | Jonas 22 | Kuba Zarzycki 23 | Leandra Finger 24 | Les Orchard 25 | Lin Xianyi 26 | Marcin Baran 27 | Mario Orlandi 28 | Mario Rosa 29 | Mariusz Felisiak 30 | Mattia Larentis 31 | Merijn Bertels 32 | Omer Katz 33 | Petr Knap 34 | Philip Neustrom 35 | Pierre-Olivier Marec 36 | Roman Krejcik 37 | Silvan Spross 38 | Sławek Ehlert 39 | Vladas Tamoshaitis 40 | Vojtech Jasny 41 | Yin Jifeng 42 | illumin-us-r3v0lution 43 | mega 44 | saw2th 45 | trbs 46 | vl <1844144@gmail.com> 47 | vl 48 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) 2 | 3 | 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). 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2017, Jazzband 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include constance/templates *.html 2 | recursive-include constance/locale *.po *.mo 3 | recursive-include constance/static * 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Constance - Dynamic Django settings 2 | =================================== 3 | 4 | .. image:: https://jazzband.co/static/img/badge.svg 5 | :alt: Jazzband 6 | :target: https://jazzband.co/ 7 | 8 | .. image:: https://img.shields.io/readthedocs/django-constance.svg 9 | :target: https://django-constance.readthedocs.io/ 10 | :alt: Documentation 11 | 12 | .. image:: https://github.com/jazzband/django-constance/workflows/Test/badge.svg 13 | :target: https://github.com/jazzband/django-constance/actions 14 | :alt: GitHub Actions 15 | 16 | .. image:: https://codecov.io/gh/jazzband/django-constance/branch/master/graph/badge.svg 17 | :target: https://codecov.io/gh/jazzband/django-constance 18 | :alt: Coverage 19 | 20 | A Django app for storing dynamic settings in pluggable backends (Redis and 21 | Django model backend built in) with an integration with the Django admin app. 22 | 23 | For more information see the documentation at: 24 | 25 | https://django-constance.readthedocs.io/ 26 | 27 | If you have questions or have trouble using the app please file a bug report 28 | at: 29 | 30 | https://github.com/jazzband/django-constance/issues 31 | -------------------------------------------------------------------------------- /constance/__init__.py: -------------------------------------------------------------------------------- 1 | from django.utils.functional import LazyObject 2 | 3 | 4 | class LazyConfig(LazyObject): 5 | def _setup(self): 6 | from .base import Config 7 | 8 | self._wrapped = Config() 9 | 10 | 11 | config = LazyConfig() 12 | -------------------------------------------------------------------------------- /constance/admin.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from datetime import date 3 | from datetime import datetime 4 | from operator import itemgetter 5 | 6 | from django import forms 7 | from django import get_version 8 | from django.apps import apps 9 | from django.contrib import admin 10 | from django.contrib import messages 11 | from django.contrib.admin.options import csrf_protect_m 12 | from django.core.exceptions import PermissionDenied 13 | from django.http import HttpResponseRedirect 14 | from django.template.response import TemplateResponse 15 | from django.urls import path 16 | from django.utils.formats import localize 17 | from django.utils.translation import gettext_lazy as _ 18 | 19 | from . import LazyConfig 20 | from . import settings 21 | from .forms import ConstanceForm 22 | from .utils import get_values 23 | 24 | config = LazyConfig() 25 | 26 | 27 | class ConstanceAdmin(admin.ModelAdmin): 28 | change_list_template = 'admin/constance/change_list.html' 29 | change_list_form = ConstanceForm 30 | 31 | def __init__(self, model, admin_site): 32 | model._meta.concrete_model = Config 33 | super().__init__(model, admin_site) 34 | 35 | def get_urls(self): 36 | info = f'{self.model._meta.app_label}_{self.model._meta.module_name}' 37 | return [ 38 | path('', self.admin_site.admin_view(self.changelist_view), name=f'{info}_changelist'), 39 | path('', self.admin_site.admin_view(self.changelist_view), name=f'{info}_add'), 40 | ] 41 | 42 | def get_config_value(self, name, options, form, initial): 43 | default, help_text = options[0], options[1] 44 | field_type = None 45 | if len(options) == 3: 46 | field_type = options[2] 47 | # First try to load the value from the actual backend 48 | value = initial.get(name) 49 | # Then if the returned value is None, get the default 50 | if value is None: 51 | value = getattr(config, name) 52 | 53 | form_field = form[name] 54 | config_value = { 55 | 'name': name, 56 | 'default': localize(default), 57 | 'raw_default': default, 58 | 'help_text': _(help_text), 59 | 'value': localize(value), 60 | 'modified': localize(value) != localize(default), 61 | 'form_field': form_field, 62 | 'is_date': isinstance(default, date), 63 | 'is_datetime': isinstance(default, datetime), 64 | 'is_checkbox': isinstance(form_field.field.widget, forms.CheckboxInput), 65 | 'is_file': isinstance(form_field.field.widget, forms.FileInput), 66 | } 67 | if field_type and field_type in settings.ADDITIONAL_FIELDS: 68 | serialized_default = form[name].field.prepare_value(default) 69 | config_value['default'] = serialized_default 70 | config_value['raw_default'] = serialized_default 71 | config_value['value'] = form[name].field.prepare_value(value) 72 | 73 | return config_value 74 | 75 | def get_changelist_form(self, request): 76 | """Returns a Form class for use in the changelist_view.""" 77 | # Defaults to self.change_list_form in order to preserve backward 78 | # compatibility 79 | return self.change_list_form 80 | 81 | @csrf_protect_m 82 | def changelist_view(self, request, extra_context=None): 83 | if not self.has_view_or_change_permission(request): 84 | raise PermissionDenied 85 | initial = get_values() 86 | form_cls = self.get_changelist_form(request) 87 | form = form_cls(initial=initial, request=request) 88 | if request.method == 'POST' and request.user.has_perm('constance.change_config'): 89 | form = form_cls(data=request.POST, files=request.FILES, initial=initial, request=request) 90 | if form.is_valid(): 91 | form.save() 92 | messages.add_message(request, messages.SUCCESS, _('Live settings updated successfully.')) 93 | return HttpResponseRedirect('.') 94 | messages.add_message(request, messages.ERROR, _('Failed to update live settings.')) 95 | context = dict( 96 | self.admin_site.each_context(request), 97 | config_values=[], 98 | title=self.model._meta.app_config.verbose_name, 99 | app_label='constance', 100 | opts=self.model._meta, 101 | form=form, 102 | media=self.media + form.media, 103 | icon_type='svg', 104 | django_version=get_version(), 105 | ) 106 | for name, options in settings.CONFIG.items(): 107 | context['config_values'].append(self.get_config_value(name, options, form, initial)) 108 | 109 | if settings.CONFIG_FIELDSETS: 110 | if isinstance(settings.CONFIG_FIELDSETS, dict): 111 | fieldset_items = settings.CONFIG_FIELDSETS.items() 112 | else: 113 | fieldset_items = settings.CONFIG_FIELDSETS 114 | 115 | context['fieldsets'] = [] 116 | for fieldset_title, fieldset_data in fieldset_items: 117 | if isinstance(fieldset_data, dict): 118 | fields_list = fieldset_data['fields'] 119 | collapse = fieldset_data.get('collapse', False) 120 | else: 121 | fields_list = fieldset_data 122 | collapse = False 123 | 124 | absent_fields = [field for field in fields_list if field not in settings.CONFIG] 125 | if any(absent_fields): 126 | raise ValueError( 127 | 'CONSTANCE_CONFIG_FIELDSETS contains field(s) that does not exist(s): {}'.format( 128 | ', '.join(absent_fields) 129 | ) 130 | ) 131 | 132 | config_values = [] 133 | 134 | for name in fields_list: 135 | options = settings.CONFIG.get(name) 136 | if options: 137 | config_values.append(self.get_config_value(name, options, form, initial)) 138 | fieldset_context = {'title': fieldset_title, 'config_values': config_values} 139 | 140 | if collapse: 141 | fieldset_context['collapse'] = True 142 | context['fieldsets'].append(fieldset_context) 143 | if not isinstance(settings.CONFIG_FIELDSETS, (OrderedDict, tuple)): 144 | context['fieldsets'].sort(key=itemgetter('title')) 145 | 146 | if not isinstance(settings.CONFIG, OrderedDict): 147 | context['config_values'].sort(key=itemgetter('name')) 148 | request.current_app = self.admin_site.name 149 | return TemplateResponse(request, self.change_list_template, context) 150 | 151 | def has_add_permission(self, *args, **kwargs): 152 | return False 153 | 154 | def has_delete_permission(self, *args, **kwargs): 155 | return False 156 | 157 | def has_change_permission(self, request, obj=None): 158 | if settings.SUPERUSER_ONLY: 159 | return request.user.is_superuser 160 | return super().has_change_permission(request, obj) 161 | 162 | 163 | class Config: 164 | class Meta: 165 | app_label = 'constance' 166 | object_name = 'Config' 167 | concrete_model = None 168 | model_name = module_name = 'config' 169 | verbose_name_plural = _('config') 170 | abstract = False 171 | swapped = False 172 | is_composite_pk = False 173 | 174 | def get_ordered_objects(self): 175 | return False 176 | 177 | def get_change_permission(self): 178 | return f'change_{self.model_name}' 179 | 180 | @property 181 | def app_config(self): 182 | return apps.get_app_config(self.app_label) 183 | 184 | @property 185 | def label(self): 186 | return f'{self.app_label}.{self.object_name}' 187 | 188 | @property 189 | def label_lower(self): 190 | return f'{self.app_label}.{self.model_name}' 191 | 192 | _meta = Meta() 193 | 194 | 195 | admin.site.register([Config], ConstanceAdmin) 196 | -------------------------------------------------------------------------------- /constance/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.core import checks 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from constance.checks import check_fieldsets 6 | 7 | 8 | class ConstanceConfig(AppConfig): 9 | name = 'constance' 10 | verbose_name = _('Constance') 11 | default_auto_field = 'django.db.models.AutoField' 12 | 13 | def ready(self): 14 | checks.register(check_fieldsets, 'constance') 15 | -------------------------------------------------------------------------------- /constance/backends/__init__.py: -------------------------------------------------------------------------------- 1 | """Defines the base constance backend.""" 2 | 3 | 4 | class Backend: 5 | def get(self, key): 6 | """ 7 | Get the key from the backend store and return the value. 8 | Return None if not found. 9 | """ 10 | raise NotImplementedError 11 | 12 | def mget(self, keys): 13 | """ 14 | Get the keys from the backend store and return a list of the values. 15 | Return an empty list if not found. 16 | """ 17 | raise NotImplementedError 18 | 19 | def set(self, key, value): 20 | """Add the value to the backend store given the key.""" 21 | raise NotImplementedError 22 | -------------------------------------------------------------------------------- /constance/backends/database.py: -------------------------------------------------------------------------------- 1 | from django.core.cache import caches 2 | from django.core.cache.backends.locmem import LocMemCache 3 | from django.core.exceptions import ImproperlyConfigured 4 | from django.db import IntegrityError 5 | from django.db import OperationalError 6 | from django.db import ProgrammingError 7 | from django.db import transaction 8 | from django.db.models.signals import post_save 9 | 10 | from constance import config 11 | from constance import settings 12 | from constance import signals 13 | from constance.backends import Backend 14 | from constance.codecs import dumps 15 | from constance.codecs import loads 16 | 17 | 18 | class DatabaseBackend(Backend): 19 | def __init__(self): 20 | from constance.models import Constance 21 | 22 | self._model = Constance 23 | self._prefix = settings.DATABASE_PREFIX 24 | self._autofill_timeout = settings.DATABASE_CACHE_AUTOFILL_TIMEOUT 25 | self._autofill_cachekey = 'autofilled' 26 | 27 | if self._model._meta.app_config is None: 28 | raise ImproperlyConfigured( 29 | "The constance.backends.database app isn't installed " 30 | "correctly. Make sure it's in your INSTALLED_APPS setting." 31 | ) 32 | 33 | if settings.DATABASE_CACHE_BACKEND: 34 | self._cache = caches[settings.DATABASE_CACHE_BACKEND] 35 | if isinstance(self._cache, LocMemCache): 36 | raise ImproperlyConfigured( 37 | 'The CONSTANCE_DATABASE_CACHE_BACKEND setting refers to a ' 38 | f"subclass of Django's local-memory backend ({settings.DATABASE_CACHE_BACKEND!r}). Please " 39 | 'set it to a backend that supports cross-process caching.' 40 | ) 41 | else: 42 | self._cache = None 43 | self.autofill() 44 | # Clear simple cache. 45 | post_save.connect(self.clear, sender=self._model) 46 | 47 | def add_prefix(self, key): 48 | return f'{self._prefix}{key}' 49 | 50 | def autofill(self): 51 | if not self._autofill_timeout or not self._cache: 52 | return 53 | full_cachekey = self.add_prefix(self._autofill_cachekey) 54 | if self._cache.get(full_cachekey): 55 | return 56 | autofill_values = {} 57 | autofill_values[full_cachekey] = 1 58 | for key, value in self.mget(settings.CONFIG): 59 | autofill_values[self.add_prefix(key)] = value 60 | self._cache.set_many(autofill_values, timeout=self._autofill_timeout) 61 | 62 | def mget(self, keys): 63 | if not keys: 64 | return 65 | keys = {self.add_prefix(key): key for key in keys} 66 | try: 67 | stored = self._model._default_manager.filter(key__in=keys) 68 | for const in stored: 69 | yield keys[const.key], loads(const.value) 70 | except (OperationalError, ProgrammingError): 71 | pass 72 | 73 | def get(self, key): 74 | key = self.add_prefix(key) 75 | value = None 76 | if self._cache: 77 | value = self._cache.get(key) 78 | if value is None: 79 | self.autofill() 80 | value = self._cache.get(key) 81 | if value is None: 82 | match = self._model._default_manager.filter(key=key).first() 83 | if match: 84 | value = loads(match.value) 85 | if self._cache: 86 | self._cache.add(key, value) 87 | return value 88 | 89 | def set(self, key, value): 90 | key = self.add_prefix(key) 91 | created = False 92 | queryset = self._model._default_manager.all() 93 | # Set _for_write attribute as get_or_create method does 94 | # https://github.com/django/django/blob/2.2.11/django/db/models/query.py#L536 95 | queryset._for_write = True 96 | 97 | try: 98 | constance = queryset.get(key=key) 99 | except (OperationalError, ProgrammingError): 100 | # database is not created, noop 101 | return 102 | except self._model.DoesNotExist: 103 | try: 104 | with transaction.atomic(using=queryset.db): 105 | queryset.create(key=key, value=dumps(value)) 106 | created = True 107 | except IntegrityError: 108 | # Allow concurrent writes 109 | constance = queryset.get(key=key) 110 | 111 | if not created: 112 | old_value = loads(constance.value) 113 | constance.value = dumps(value) 114 | constance.save(update_fields=['value']) 115 | else: 116 | old_value = None 117 | 118 | if self._cache: 119 | self._cache.set(key, value) 120 | 121 | signals.config_updated.send(sender=config, key=key, old_value=old_value, new_value=value) 122 | 123 | def clear(self, sender, instance, created, **kwargs): 124 | if self._cache and not created: 125 | keys = [self.add_prefix(k) for k in settings.CONFIG] 126 | keys.append(self.add_prefix(self._autofill_cachekey)) 127 | self._cache.delete_many(keys) 128 | self.autofill() 129 | -------------------------------------------------------------------------------- /constance/backends/memory.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | 3 | from constance import config 4 | from constance import signals 5 | 6 | from . import Backend 7 | 8 | 9 | class MemoryBackend(Backend): 10 | """Simple in-memory backend that should be mostly used for testing purposes.""" 11 | 12 | _storage = {} 13 | _lock = Lock() 14 | 15 | def __init__(self): 16 | super().__init__() 17 | 18 | def get(self, key): 19 | with self._lock: 20 | return self._storage.get(key) 21 | 22 | def mget(self, keys): 23 | if not keys: 24 | return None 25 | result = [] 26 | with self._lock: 27 | for key in keys: 28 | value = self._storage.get(key) 29 | if value is not None: 30 | result.append((key, value)) 31 | return result 32 | 33 | def set(self, key, value): 34 | with self._lock: 35 | old_value = self._storage.get(key) 36 | self._storage[key] = value 37 | signals.config_updated.send(sender=config, key=key, old_value=old_value, new_value=value) 38 | -------------------------------------------------------------------------------- /constance/backends/redisd.py: -------------------------------------------------------------------------------- 1 | from threading import RLock 2 | from time import monotonic 3 | 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | from constance import config 7 | from constance import settings 8 | from constance import signals 9 | from constance import utils 10 | from constance.backends import Backend 11 | from constance.codecs import dumps 12 | from constance.codecs import loads 13 | 14 | 15 | class RedisBackend(Backend): 16 | def __init__(self): 17 | super().__init__() 18 | self._prefix = settings.REDIS_PREFIX 19 | connection_cls = settings.REDIS_CONNECTION_CLASS 20 | if connection_cls is not None: 21 | self._rd = utils.import_module_attr(connection_cls)() 22 | else: 23 | try: 24 | import redis 25 | except ImportError: 26 | raise ImproperlyConfigured('The Redis backend requires redis-py to be installed.') from None 27 | if isinstance(settings.REDIS_CONNECTION, str): 28 | self._rd = redis.from_url(settings.REDIS_CONNECTION) 29 | else: 30 | self._rd = redis.Redis(**settings.REDIS_CONNECTION) 31 | 32 | def add_prefix(self, key): 33 | return f'{self._prefix}{key}' 34 | 35 | def get(self, key): 36 | value = self._rd.get(self.add_prefix(key)) 37 | if value: 38 | return loads(value) 39 | return None 40 | 41 | def mget(self, keys): 42 | if not keys: 43 | return 44 | prefixed_keys = [self.add_prefix(key) for key in keys] 45 | for key, value in zip(keys, self._rd.mget(prefixed_keys)): 46 | if value: 47 | yield key, loads(value) 48 | 49 | def set(self, key, value): 50 | old_value = self.get(key) 51 | self._rd.set(self.add_prefix(key), dumps(value)) 52 | signals.config_updated.send(sender=config, key=key, old_value=old_value, new_value=value) 53 | 54 | 55 | class CachingRedisBackend(RedisBackend): 56 | _sentinel = object() 57 | _lock = RLock() 58 | 59 | def __init__(self): 60 | super().__init__() 61 | self._timeout = settings.REDIS_CACHE_TIMEOUT 62 | self._cache = {} 63 | self._sentinel = object() 64 | 65 | def _has_expired(self, value): 66 | return value[0] <= monotonic() 67 | 68 | def _cache_value(self, key, new_value): 69 | self._cache[key] = (monotonic() + self._timeout, new_value) 70 | 71 | def get(self, key): 72 | value = self._cache.get(key, self._sentinel) 73 | 74 | if value is self._sentinel or self._has_expired(value): 75 | with self._lock: 76 | new_value = super().get(key) 77 | self._cache_value(key, new_value) 78 | return new_value 79 | 80 | return value[1] 81 | 82 | def set(self, key, value): 83 | with self._lock: 84 | super().set(key, value) 85 | self._cache_value(key, value) 86 | 87 | def mget(self, keys): 88 | if not keys: 89 | return 90 | for key in keys: 91 | value = self.get(key) 92 | if value is not None: 93 | yield key, value 94 | -------------------------------------------------------------------------------- /constance/base.py: -------------------------------------------------------------------------------- 1 | from . import settings 2 | from . import utils 3 | 4 | 5 | class Config: 6 | """The global config wrapper that handles the backend.""" 7 | 8 | def __init__(self): 9 | super().__setattr__('_backend', utils.import_module_attr(settings.BACKEND)()) 10 | 11 | def __getattr__(self, key): 12 | try: 13 | if len(settings.CONFIG[key]) not in (2, 3): 14 | raise AttributeError(key) 15 | default = settings.CONFIG[key][0] 16 | except KeyError as e: 17 | raise AttributeError(key) from e 18 | result = self._backend.get(key) 19 | if result is None: 20 | result = default 21 | setattr(self, key, default) 22 | return result 23 | return result 24 | 25 | def __setattr__(self, key, value): 26 | if key not in settings.CONFIG: 27 | raise AttributeError(key) 28 | self._backend.set(key, value) 29 | 30 | def __dir__(self): 31 | return settings.CONFIG.keys() 32 | -------------------------------------------------------------------------------- /constance/checks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from django.core import checks 4 | from django.core.checks import CheckMessage 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | 8 | def check_fieldsets(*args, **kwargs) -> list[CheckMessage]: 9 | """ 10 | A Django system check to make sure that, if defined, 11 | CONFIG_FIELDSETS is consistent with settings.CONFIG. 12 | """ 13 | from . import settings 14 | 15 | errors = [] 16 | 17 | if hasattr(settings, 'CONFIG_FIELDSETS') and settings.CONFIG_FIELDSETS: 18 | missing_keys, extra_keys = get_inconsistent_fieldnames() 19 | if missing_keys: 20 | check = checks.Warning( 21 | _('CONSTANCE_CONFIG_FIELDSETS is missing field(s) that exists in CONSTANCE_CONFIG.'), 22 | hint=', '.join(sorted(missing_keys)), 23 | obj='settings.CONSTANCE_CONFIG', 24 | id='constance.E001', 25 | ) 26 | errors.append(check) 27 | if extra_keys: 28 | check = checks.Warning( 29 | _('CONSTANCE_CONFIG_FIELDSETS contains extra field(s) that does not exist in CONFIG.'), 30 | hint=', '.join(sorted(extra_keys)), 31 | obj='settings.CONSTANCE_CONFIG', 32 | id='constance.E002', 33 | ) 34 | errors.append(check) 35 | return errors 36 | 37 | 38 | def get_inconsistent_fieldnames() -> tuple[set, set]: 39 | """ 40 | Returns a pair of values: 41 | 1) set of keys from settings.CONFIG that are not accounted for in settings.CONFIG_FIELDSETS 42 | 2) set of keys from settings.CONFIG_FIELDSETS that are not present in settings.CONFIG 43 | If there are no fieldnames in settings.CONFIG_FIELDSETS, returns an empty set. 44 | """ 45 | from . import settings 46 | 47 | if isinstance(settings.CONFIG_FIELDSETS, dict): 48 | fieldset_items = settings.CONFIG_FIELDSETS.items() 49 | else: 50 | fieldset_items = settings.CONFIG_FIELDSETS 51 | 52 | unique_field_names = set() 53 | for _fieldset_title, fields_list in fieldset_items: 54 | # fields_list can be a dictionary, when a fieldset is defined as collapsible 55 | # https://django-constance.readthedocs.io/en/latest/#fieldsets-collapsing 56 | if isinstance(fields_list, dict) and 'fields' in fields_list: 57 | fields_list = fields_list['fields'] 58 | unique_field_names.update(fields_list) 59 | if not unique_field_names: 60 | return unique_field_names, unique_field_names 61 | config_keys = set(settings.CONFIG.keys()) 62 | missing_keys = config_keys - unique_field_names 63 | extra_keys = unique_field_names - config_keys 64 | return missing_keys, extra_keys 65 | -------------------------------------------------------------------------------- /constance/codecs.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import logging 5 | import uuid 6 | from datetime import date 7 | from datetime import datetime 8 | from datetime import time 9 | from datetime import timedelta 10 | from decimal import Decimal 11 | from typing import Any 12 | from typing import Protocol 13 | from typing import TypeVar 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | DEFAULT_DISCRIMINATOR = 'default' 18 | 19 | 20 | class JSONEncoder(json.JSONEncoder): 21 | """Django-constance custom json encoder.""" 22 | 23 | def default(self, o): 24 | for discriminator, (t, _, encoder) in _codecs.items(): 25 | if isinstance(o, t): 26 | return _as(discriminator, encoder(o)) 27 | raise TypeError(f'Object of type {o.__class__.__name__} is not JSON serializable') 28 | 29 | 30 | def _as(discriminator: str, v: Any) -> dict[str, Any]: 31 | return {'__type__': discriminator, '__value__': v} 32 | 33 | 34 | def dumps(obj, _dumps=json.dumps, cls=JSONEncoder, default_kwargs=None, **kwargs): 35 | """Serialize object to json string.""" 36 | default_kwargs = default_kwargs or {} 37 | is_default_type = isinstance(obj, (list, dict, str, int, bool, float, type(None))) 38 | return _dumps( 39 | _as(DEFAULT_DISCRIMINATOR, obj) if is_default_type else obj, cls=cls, **dict(default_kwargs, **kwargs) 40 | ) 41 | 42 | 43 | def loads(s, _loads=json.loads, *, first_level=True, **kwargs): 44 | """Deserialize json string to object.""" 45 | if first_level: 46 | return _loads(s, object_hook=object_hook, **kwargs) 47 | if isinstance(s, dict) and '__type__' not in s and '__value__' not in s: 48 | return {k: loads(v, first_level=False) for k, v in s.items()} 49 | if isinstance(s, list): 50 | return list(loads(v, first_level=False) for v in s) 51 | return _loads(s, object_hook=object_hook, **kwargs) 52 | 53 | 54 | def object_hook(o: dict) -> Any: 55 | """Hook function to perform custom deserialization.""" 56 | if o.keys() == {'__type__', '__value__'}: 57 | if o['__type__'] == DEFAULT_DISCRIMINATOR: 58 | return o['__value__'] 59 | codec = _codecs.get(o['__type__']) 60 | if not codec: 61 | raise ValueError(f'Unsupported type: {o["__type__"]}') 62 | return codec[1](o['__value__']) 63 | if '__type__' not in o and '__value__' not in o: 64 | return o 65 | logger.error('Cannot deserialize object: %s', o) 66 | raise ValueError(f'Invalid object: {o}') 67 | 68 | 69 | T = TypeVar('T') 70 | 71 | 72 | class Encoder(Protocol[T]): 73 | def __call__(self, value: T, /) -> str: ... # pragma: no cover 74 | 75 | 76 | class Decoder(Protocol[T]): 77 | def __call__(self, value: str, /) -> T: ... # pragma: no cover 78 | 79 | 80 | def register_type(t: type[T], discriminator: str, encoder: Encoder[T], decoder: Decoder[T]): 81 | if not discriminator: 82 | raise ValueError('Discriminator must be specified') 83 | if _codecs.get(discriminator) or discriminator == DEFAULT_DISCRIMINATOR: 84 | raise ValueError(f'Type with discriminator {discriminator} is already registered') 85 | _codecs[discriminator] = (t, decoder, encoder) 86 | 87 | 88 | _codecs: dict[str, tuple[type, Decoder, Encoder]] = {} 89 | 90 | 91 | def _register_default_types(): 92 | # NOTE: datetime should be registered before date, because datetime is also instance of date. 93 | register_type(datetime, 'datetime', datetime.isoformat, datetime.fromisoformat) 94 | register_type(date, 'date', lambda o: o.isoformat(), lambda o: datetime.fromisoformat(o).date()) 95 | register_type(time, 'time', lambda o: o.isoformat(), time.fromisoformat) 96 | register_type(Decimal, 'decimal', str, Decimal) 97 | register_type(uuid.UUID, 'uuid', lambda o: o.hex, uuid.UUID) 98 | register_type(timedelta, 'timedelta', lambda o: o.total_seconds(), lambda o: timedelta(seconds=o)) 99 | 100 | 101 | _register_default_types() 102 | -------------------------------------------------------------------------------- /constance/context_processors.py: -------------------------------------------------------------------------------- 1 | import constance 2 | 3 | 4 | def config(request): 5 | """ 6 | Simple context processor that puts the config into every 7 | RequestContext. Just make sure you have a setting like this: 8 | 9 | TEMPLATE_CONTEXT_PROCESSORS = ( 10 | # ... 11 | 'constance.context_processors.config', 12 | ) 13 | 14 | """ 15 | return {'config': constance.config} 16 | -------------------------------------------------------------------------------- /constance/forms.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from datetime import date 3 | from datetime import datetime 4 | from datetime import time 5 | from datetime import timedelta 6 | from decimal import Decimal 7 | from os.path import join 8 | 9 | from django import conf 10 | from django import forms 11 | from django.contrib import messages 12 | from django.contrib.admin import widgets 13 | from django.core.exceptions import ImproperlyConfigured 14 | from django.core.files.storage import default_storage 15 | from django.forms import fields 16 | from django.utils import timezone 17 | from django.utils.encoding import smart_bytes 18 | from django.utils.module_loading import import_string 19 | from django.utils.text import normalize_newlines 20 | from django.utils.translation import gettext_lazy as _ 21 | 22 | from . import LazyConfig 23 | from . import settings 24 | from .checks import get_inconsistent_fieldnames 25 | 26 | config = LazyConfig() 27 | 28 | NUMERIC_WIDGET = forms.TextInput(attrs={'size': 10}) 29 | 30 | INTEGER_LIKE = (fields.IntegerField, {'widget': NUMERIC_WIDGET}) 31 | STRING_LIKE = ( 32 | fields.CharField, 33 | { 34 | 'widget': forms.Textarea(attrs={'rows': 3}), 35 | 'required': False, 36 | }, 37 | ) 38 | 39 | FIELDS = { 40 | bool: (fields.BooleanField, {'required': False}), 41 | int: INTEGER_LIKE, 42 | Decimal: (fields.DecimalField, {'widget': NUMERIC_WIDGET}), 43 | str: STRING_LIKE, 44 | datetime: (fields.SplitDateTimeField, {'widget': widgets.AdminSplitDateTime}), 45 | timedelta: (fields.DurationField, {'widget': widgets.AdminTextInputWidget}), 46 | date: (fields.DateField, {'widget': widgets.AdminDateWidget}), 47 | time: (fields.TimeField, {'widget': widgets.AdminTimeWidget}), 48 | float: (fields.FloatField, {'widget': NUMERIC_WIDGET}), 49 | } 50 | 51 | 52 | def parse_additional_fields(fields): 53 | for key in fields: 54 | field = list(fields[key]) 55 | 56 | if len(field) == 1: 57 | field.append({}) 58 | 59 | field[0] = import_string(field[0]) 60 | 61 | if 'widget' in field[1]: 62 | klass = import_string(field[1]['widget']) 63 | field[1]['widget'] = klass(**(field[1].get('widget_kwargs', {}) or {})) 64 | 65 | if 'widget_kwargs' in field[1]: 66 | del field[1]['widget_kwargs'] 67 | 68 | fields[key] = field 69 | 70 | return fields 71 | 72 | 73 | FIELDS.update(parse_additional_fields(settings.ADDITIONAL_FIELDS)) 74 | 75 | 76 | class ConstanceForm(forms.Form): 77 | version = forms.CharField(widget=forms.HiddenInput) 78 | 79 | def __init__(self, initial, request=None, *args, **kwargs): 80 | super().__init__(*args, initial=initial, **kwargs) 81 | version_hash = hashlib.sha256() 82 | 83 | only_view = request and not request.user.has_perm('constance.change_config') 84 | if only_view: 85 | messages.warning( 86 | request, 87 | _("You don't have permission to change these values"), 88 | ) 89 | 90 | for name, options in settings.CONFIG.items(): 91 | default = options[0] 92 | if len(options) == 3: 93 | config_type = options[2] 94 | if config_type not in settings.ADDITIONAL_FIELDS and not isinstance(default, config_type): 95 | raise ImproperlyConfigured( 96 | _( 97 | 'Default value type must be ' 98 | 'equal to declared config ' 99 | 'parameter type. Please fix ' 100 | 'the default value of ' 101 | "'%(name)s'." 102 | ) 103 | % {'name': name} 104 | ) 105 | else: 106 | config_type = type(default) 107 | 108 | if config_type not in FIELDS: 109 | raise ImproperlyConfigured( 110 | _( 111 | "Constance doesn't support " 112 | 'config values of the type ' 113 | '%(config_type)s. Please fix ' 114 | "the value of '%(name)s'." 115 | ) 116 | % {'config_type': config_type, 'name': name} 117 | ) 118 | field_class, kwargs = FIELDS[config_type] 119 | if only_view: 120 | kwargs['disabled'] = True 121 | self.fields[name] = field_class(label=name, **kwargs) 122 | 123 | version_hash.update(smart_bytes(initial.get(name, ''))) 124 | self.initial['version'] = version_hash.hexdigest() 125 | 126 | def save(self): 127 | for file_field in self.files: 128 | file = self.cleaned_data[file_field] 129 | self.cleaned_data[file_field] = default_storage.save(join(settings.FILE_ROOT, file.name), file) 130 | 131 | for name in settings.CONFIG: 132 | current = getattr(config, name) 133 | new = self.cleaned_data[name] 134 | 135 | if isinstance(new, str): 136 | new = normalize_newlines(new) 137 | 138 | if conf.settings.USE_TZ and isinstance(current, datetime) and not timezone.is_aware(current): 139 | current = timezone.make_aware(current) 140 | 141 | if current != new: 142 | setattr(config, name, new) 143 | 144 | def clean_version(self): 145 | value = self.cleaned_data['version'] 146 | 147 | if settings.IGNORE_ADMIN_VERSION_CHECK: 148 | return value 149 | 150 | if value != self.initial['version']: 151 | raise forms.ValidationError( 152 | _( 153 | 'The settings have been modified ' 154 | 'by someone else. Please reload the ' 155 | 'form and resubmit your changes.' 156 | ) 157 | ) 158 | return value 159 | 160 | def clean(self): 161 | cleaned_data = super().clean() 162 | 163 | if not settings.CONFIG_FIELDSETS: 164 | return cleaned_data 165 | 166 | missing_keys, extra_keys = get_inconsistent_fieldnames() 167 | if missing_keys or extra_keys: 168 | raise forms.ValidationError( 169 | _('CONSTANCE_CONFIG_FIELDSETS is missing field(s) that exists in CONSTANCE_CONFIG.') 170 | ) 171 | 172 | return cleaned_data 173 | -------------------------------------------------------------------------------- /constance/locale/ar/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/ar/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/ar/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , 2020. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-06-13 19:40+0530\n" 11 | "PO-Revision-Date: 2020-11-30 23:15+0100\n" 12 | "Language: ar\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Last-Translator: \n" 17 | "Language-Team: \n" 18 | "X-Generator: \n" 19 | 20 | #: admin.py:113 21 | #, python-format 22 | msgid "" 23 | "Default value type must be equal to declared config parameter type. Please " 24 | "fix the default value of '%(name)s'." 25 | msgstr "" 26 | "يجب أن يكون نوع القيمة الافتراضي مساوياً لنوع معلمة التكوين المعلن. الرجاء " 27 | "إصلاح القيمة الافتراضية لـ '%(name)s'." 28 | 29 | #: admin.py:123 30 | #, python-format 31 | msgid "" 32 | "Constance doesn't support config values of the type %(config_type)s. Please " 33 | "fix the value of '%(name)s'." 34 | msgstr "" 35 | "لا يعتمد كونستانس قيم التكوين من النوع %(config_type)s. الرجاء إصلاح قيمة " 36 | "'%(name)s'." 37 | 38 | #: admin.py:147 39 | msgid "" 40 | "The settings have been modified by someone else. Please reload the form and " 41 | "resubmit your changes." 42 | msgstr "" 43 | "تم تعديل الإعدادات بواسطة شخص آخر. الرجاء إعادة تحميل النموذج وإعادة إرسال " 44 | "التغييرات." 45 | 46 | #: admin.py:160 47 | msgid "" 48 | "CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in " 49 | "CONSTANCE_CONFIG." 50 | msgstr "" 51 | "لا يحتوي CONSTANCE_CONFIG_FIELDSETS على حقول موجودة في CONSTANCE_CONFIG." 52 | 53 | #: admin.py:224 54 | msgid "Live settings updated successfully." 55 | msgstr "تم تحديث الإعدادات المباشرة بنجاح." 56 | 57 | #: admin.py:285 58 | msgid "config" 59 | msgstr "التكوين" 60 | 61 | #: apps.py:8 62 | msgid "Constance" 63 | msgstr "كونستانس" 64 | 65 | #: backends/database/models.py:19 66 | msgid "constance" 67 | msgstr "كونستانس" 68 | 69 | #: backends/database/models.py:20 70 | msgid "constances" 71 | msgstr "كونستانس" 72 | 73 | #: management/commands/constance.py:30 74 | msgid "Get/Set In-database config settings handled by Constance" 75 | msgstr "" 76 | "الحصول على / تعيين إعدادات التكوين في قاعدة البيانات التي تعالجها كونستانس" 77 | 78 | #: templates/admin/constance/change_list.html:75 79 | msgid "Save" 80 | msgstr "حفظ" 81 | 82 | #: templates/admin/constance/change_list.html:84 83 | msgid "Home" 84 | msgstr "الصفحة الرئيسية" 85 | 86 | #: templates/admin/constance/includes/results_list.html:5 87 | msgid "Name" 88 | msgstr "الإسم" 89 | 90 | #: templates/admin/constance/includes/results_list.html:6 91 | msgid "Default" 92 | msgstr "افتراضي" 93 | 94 | #: templates/admin/constance/includes/results_list.html:7 95 | msgid "Value" 96 | msgstr "القيمة" 97 | 98 | #: templates/admin/constance/includes/results_list.html:8 99 | msgid "Is modified" 100 | msgstr "تم تعديله" 101 | -------------------------------------------------------------------------------- /constance/locale/cs_CZ/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/cs_CZ/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/cs_CZ/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-constance\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-06-13 19:40+0530\n" 11 | "PO-Revision-Date: 2014-11-27 18:13+0000\n" 12 | "Last-Translator: Jannis Leidel \n" 13 | "Language-Team: Czech (Czech Republic) (http://www.transifex.com/projects/p/" 14 | "django-constance/language/cs_CZ/)\n" 15 | "Language: cs_CZ\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" 20 | 21 | #: admin.py:113 22 | #, python-format 23 | msgid "" 24 | "Default value type must be equal to declared config parameter type. Please " 25 | "fix the default value of '%(name)s'." 26 | msgstr "" 27 | 28 | #: admin.py:123 29 | #, python-format 30 | msgid "" 31 | "Constance doesn't support config values of the type %(config_type)s. Please " 32 | "fix the value of '%(name)s'." 33 | msgstr "" 34 | 35 | #: admin.py:147 36 | msgid "" 37 | "The settings have been modified by someone else. Please reload the form and " 38 | "resubmit your changes." 39 | msgstr "" 40 | 41 | #: admin.py:160 42 | msgid "" 43 | "CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in " 44 | "CONSTANCE_CONFIG." 45 | msgstr "" 46 | 47 | #: admin.py:224 48 | msgid "Live settings updated successfully." 49 | msgstr "Nastavení bylo úspěšně uloženo." 50 | 51 | #: admin.py:285 52 | msgid "config" 53 | msgstr "nastavení" 54 | 55 | #: apps.py:8 56 | msgid "Constance" 57 | msgstr "" 58 | 59 | #: backends/database/models.py:19 60 | msgid "constance" 61 | msgstr "konstanta" 62 | 63 | #: backends/database/models.py:20 64 | msgid "constances" 65 | msgstr "konstanty" 66 | 67 | #: management/commands/constance.py:30 68 | msgid "Get/Set In-database config settings handled by Constance" 69 | msgstr "" 70 | 71 | #: templates/admin/constance/change_list.html:75 72 | msgid "Save" 73 | msgstr "Uložit" 74 | 75 | #: templates/admin/constance/change_list.html:84 76 | msgid "Home" 77 | msgstr "Domů" 78 | 79 | #: templates/admin/constance/includes/results_list.html:5 80 | msgid "Name" 81 | msgstr "Název" 82 | 83 | #: templates/admin/constance/includes/results_list.html:6 84 | msgid "Default" 85 | msgstr "Výchozí hodnota" 86 | 87 | #: templates/admin/constance/includes/results_list.html:7 88 | msgid "Value" 89 | msgstr "Hodnota" 90 | 91 | #: templates/admin/constance/includes/results_list.html:8 92 | msgid "Is modified" 93 | msgstr "Je změněna?" 94 | 95 | #~ msgid "Constance config" 96 | #~ msgstr "Nastavení konstant" 97 | -------------------------------------------------------------------------------- /constance/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Jannis Leidel , 2014 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: django-constance\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-06-13 19:40+0530\n" 12 | "PO-Revision-Date: 2014-11-27 18:17+0000\n" 13 | "Last-Translator: Jannis Leidel \n" 14 | "Language-Team: German (http://www.transifex.com/projects/p/django-constance/" 15 | "language/de/)\n" 16 | "Language: de\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=UTF-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 21 | 22 | #: admin.py:113 23 | #, fuzzy, python-format 24 | #| msgid "" 25 | #| "Constance doesn't support config values of the type %(config_type)s. " 26 | #| "Please fix the value of '%(name)s'." 27 | msgid "" 28 | "Default value type must be equal to declared config parameter type. Please " 29 | "fix the default value of '%(name)s'." 30 | msgstr "" 31 | "Konstanze unterstützt die Konfigurationswerte vom Typ %(config_type)s nicht. " 32 | "Bitte den Ausgangswert von '%(name)s' ändern." 33 | 34 | #: admin.py:123 35 | #, python-format 36 | msgid "" 37 | "Constance doesn't support config values of the type %(config_type)s. Please " 38 | "fix the value of '%(name)s'." 39 | msgstr "" 40 | "Konstanze unterstützt die Konfigurationswerte vom Typ %(config_type)s nicht. " 41 | "Bitte den Ausgangswert von '%(name)s' ändern." 42 | 43 | #: admin.py:147 44 | msgid "" 45 | "The settings have been modified by someone else. Please reload the form and " 46 | "resubmit your changes." 47 | msgstr "" 48 | "Die Konfiguration wurde seit Öffnen dieser Seite verändert. Bitte die Seite " 49 | "neuladen und die Änderungen erneut vornehmen." 50 | 51 | #: admin.py:160 52 | msgid "" 53 | "CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in " 54 | "CONSTANCE_CONFIG." 55 | msgstr "" 56 | 57 | #: admin.py:224 58 | msgid "Live settings updated successfully." 59 | msgstr "Die Livekonfiguration wurde erfolgreich aktualisiert." 60 | 61 | #: admin.py:285 62 | msgid "config" 63 | msgstr "Konfiguration" 64 | 65 | #: apps.py:8 66 | msgid "Constance" 67 | msgstr "Konstanze" 68 | 69 | #: backends/database/models.py:19 70 | msgid "constance" 71 | msgstr "Konstanze" 72 | 73 | #: backends/database/models.py:20 74 | msgid "constances" 75 | msgstr "Konstanzes" 76 | 77 | #: management/commands/constance.py:30 78 | msgid "Get/Set In-database config settings handled by Constance" 79 | msgstr "" 80 | 81 | #: templates/admin/constance/change_list.html:75 82 | msgid "Save" 83 | msgstr "Sichern" 84 | 85 | #: templates/admin/constance/change_list.html:84 86 | msgid "Home" 87 | msgstr "Start" 88 | 89 | #: templates/admin/constance/includes/results_list.html:5 90 | msgid "Name" 91 | msgstr "Name" 92 | 93 | #: templates/admin/constance/includes/results_list.html:6 94 | msgid "Default" 95 | msgstr "Voreinstellung" 96 | 97 | #: templates/admin/constance/includes/results_list.html:7 98 | msgid "Value" 99 | msgstr "Wert" 100 | 101 | #: templates/admin/constance/includes/results_list.html:8 102 | msgid "Is modified" 103 | msgstr "Ist modifiziert" 104 | 105 | #~ msgid "Constance config" 106 | #~ msgstr "Constance Konfiguration" 107 | -------------------------------------------------------------------------------- /constance/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-07-19 21:00+0500\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: admin.py:113 21 | #, python-format 22 | msgid "" 23 | "Default value type must be equal to declared config parameter type. Please " 24 | "fix the default value of '%(name)s'." 25 | msgstr "" 26 | 27 | #: admin.py:123 28 | #, python-format 29 | msgid "" 30 | "Constance doesn't support config values of the type %(config_type)s. Please " 31 | "fix the value of '%(name)s'." 32 | msgstr "" 33 | 34 | #: admin.py:147 35 | msgid "" 36 | "The settings have been modified by someone else. Please reload the form and " 37 | "resubmit your changes." 38 | msgstr "" 39 | 40 | #: admin.py:160 41 | msgid "" 42 | "CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in " 43 | "CONSTANCE_CONFIG." 44 | msgstr "" 45 | 46 | #: admin.py:224 47 | msgid "Live settings updated successfully." 48 | msgstr "" 49 | 50 | #: admin.py:267 51 | msgid "Failed to update live settings." 52 | msgstr "" 53 | 54 | #: admin.py:285 55 | msgid "config" 56 | msgstr "" 57 | 58 | #: apps.py:8 59 | msgid "Constance" 60 | msgstr "" 61 | 62 | #: backends/database/models.py:19 63 | msgid "constance" 64 | msgstr "" 65 | 66 | #: backends/database/models.py:20 67 | msgid "constances" 68 | msgstr "" 69 | 70 | #: management/commands/constance.py:30 71 | msgid "Get/Set In-database config settings handled by Constance" 72 | msgstr "" 73 | 74 | #: templates/admin/constance/change_list.html:75 75 | msgid "Save" 76 | msgstr "" 77 | 78 | #: templates/admin/constance/change_list.html:84 79 | msgid "Home" 80 | msgstr "" 81 | 82 | #: templates/admin/constance/includes/results_list.html:5 83 | msgid "Name" 84 | msgstr "" 85 | 86 | #: templates/admin/constance/includes/results_list.html:6 87 | msgid "Default" 88 | msgstr "" 89 | 90 | #: templates/admin/constance/includes/results_list.html:7 91 | msgid "Value" 92 | msgstr "" 93 | 94 | #: templates/admin/constance/includes/results_list.html:8 95 | msgid "Is modified" 96 | msgstr "" 97 | -------------------------------------------------------------------------------- /constance/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/es/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Igor Támara , 2015 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-06-13 19:40+0530\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Igor Támara \n" 14 | "Language-Team: Spanish (http://www.transifex.com/projects/p/django-constance/" 15 | "language/de/\n" 16 | "Language: es\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=UTF-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 21 | 22 | #: admin.py:113 23 | #, fuzzy, python-format 24 | #| msgid "" 25 | #| "Constance doesn't support config values of the type %(config_type)s. " 26 | #| "Please fix the value of '%(name)s'." 27 | msgid "" 28 | "Default value type must be equal to declared config parameter type. Please " 29 | "fix the default value of '%(name)s'." 30 | msgstr "" 31 | "Constance no soporta valores de configuración de los tipos %(config_type)s. " 32 | "Por favor arregle el valor de '%(name)s'." 33 | 34 | #: admin.py:123 35 | #, python-format 36 | msgid "" 37 | "Constance doesn't support config values of the type %(config_type)s. Please " 38 | "fix the value of '%(name)s'." 39 | msgstr "" 40 | "Constance no soporta valores de configuración de los tipos %(config_type)s. " 41 | "Por favor arregle el valor de '%(name)s'." 42 | 43 | #: admin.py:147 44 | msgid "" 45 | "The settings have been modified by someone else. Please reload the form and " 46 | "resubmit your changes." 47 | msgstr "" 48 | "La configuración ha sido modificada por alguien más. Por favor recargue el " 49 | "formulario y reenvíe sus cambios." 50 | 51 | #: admin.py:160 52 | msgid "" 53 | "CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in " 54 | "CONSTANCE_CONFIG." 55 | msgstr "" 56 | 57 | #: admin.py:224 58 | msgid "Live settings updated successfully." 59 | msgstr "Las configuraciones en vivo se actualizaron exitosamente." 60 | 61 | #: admin.py:285 62 | msgid "config" 63 | msgstr "configuración" 64 | 65 | #: apps.py:8 66 | msgid "Constance" 67 | msgstr "Constance" 68 | 69 | #: backends/database/models.py:19 70 | msgid "constance" 71 | msgstr "constance" 72 | 73 | #: backends/database/models.py:20 74 | msgid "constances" 75 | msgstr "constances" 76 | 77 | #: management/commands/constance.py:30 78 | msgid "Get/Set In-database config settings handled by Constance" 79 | msgstr "" 80 | 81 | #: templates/admin/constance/change_list.html:75 82 | msgid "Save" 83 | msgstr "Guardar" 84 | 85 | #: templates/admin/constance/change_list.html:84 86 | msgid "Home" 87 | msgstr "Inicio" 88 | 89 | #: templates/admin/constance/includes/results_list.html:5 90 | msgid "Name" 91 | msgstr "Nombre" 92 | 93 | #: templates/admin/constance/includes/results_list.html:6 94 | msgid "Default" 95 | msgstr "Predeterminado" 96 | 97 | #: templates/admin/constance/includes/results_list.html:7 98 | msgid "Value" 99 | msgstr "Valor" 100 | 101 | #: templates/admin/constance/includes/results_list.html:8 102 | msgid "Is modified" 103 | msgstr "Está modificado" 104 | 105 | #~ msgid "Constance config" 106 | #~ msgstr "Configuración de Constance" 107 | -------------------------------------------------------------------------------- /constance/locale/et/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/et/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/et/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-06-13 19:40+0530\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: admin.py:113 22 | #, python-format 23 | msgid "" 24 | "Default value type must be equal to declared config parameter type. Please " 25 | "fix the default value of '%(name)s'." 26 | msgstr "" 27 | "Vaikimisi väärtus peab olema vastav seade parameetri tüübile. Palun " 28 | "sisestagekorrekne vaikimisi väärtus väljale '%(name)s'" 29 | 30 | #: admin.py:123 31 | #, python-format 32 | msgid "" 33 | "Constance doesn't support config values of the type %(config_type)s. Please " 34 | "fix the value of '%(name)s'." 35 | msgstr "" 36 | "Constance ei toeta seadete seda tüüpi %(config_type) seadete väärtusi. " 37 | "Palunsisestage korrektne väärtus väljale '%(name)s'." 38 | 39 | #: admin.py:147 40 | msgid "" 41 | "The settings have been modified by someone else. Please reload the form and " 42 | "resubmit your changes." 43 | msgstr "" 44 | "Seadeid on vahepeal muudetud, palun esitage oma muudatused peale seda kui " 45 | "olete lehe uuesti laadinud" 46 | 47 | #: admin.py:160 48 | msgid "" 49 | "CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in " 50 | "CONSTANCE_CONFIG." 51 | msgstr "" 52 | 53 | #: admin.py:224 54 | msgid "Live settings updated successfully." 55 | msgstr "Seaded edukalt muudetud." 56 | 57 | #: admin.py:285 58 | msgid "config" 59 | msgstr "konfiguratsioon" 60 | 61 | #: apps.py:8 62 | msgid "Constance" 63 | msgstr "Seaded" 64 | 65 | #: backends/database/models.py:19 66 | msgid "constance" 67 | msgstr "seaded" 68 | 69 | #: backends/database/models.py:20 70 | msgid "constances" 71 | msgstr "seaded" 72 | 73 | #: management/commands/constance.py:30 74 | msgid "Get/Set In-database config settings handled by Constance" 75 | msgstr "Loe/salvesta andmebaasi põhiseid seadeid" 76 | 77 | #: templates/admin/constance/change_list.html:75 78 | msgid "Save" 79 | msgstr "Salvesta" 80 | 81 | #: templates/admin/constance/change_list.html:84 82 | msgid "Home" 83 | msgstr "" 84 | 85 | #: templates/admin/constance/includes/results_list.html:5 86 | msgid "Name" 87 | msgstr "Nimi" 88 | 89 | #: templates/admin/constance/includes/results_list.html:6 90 | msgid "Default" 91 | msgstr "Vaikimisi" 92 | 93 | #: templates/admin/constance/includes/results_list.html:7 94 | msgid "Value" 95 | msgstr "Väärtus" 96 | 97 | #: templates/admin/constance/includes/results_list.html:8 98 | msgid "Is modified" 99 | msgstr "Muudetud" 100 | -------------------------------------------------------------------------------- /constance/locale/fa/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/fa/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/fa/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-06-13 19:40+0530\n" 11 | "PO-Revision-Date: 2020-09-24 17:33+0330\n" 12 | "Language: fa\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Last-Translator: Mehdi Namaki \n" 17 | "Language-Team: \n" 18 | "X-Generator: Poedit 2.3\n" 19 | 20 | #: admin.py:113 21 | #, python-format 22 | msgid "" 23 | "Default value type must be equal to declared config parameter type. Please " 24 | "fix the default value of '%(name)s'." 25 | msgstr "" 26 | "نوع مقدار پیش فرض باید برابر با نوع پارامتر پیکربندی اعلام شده باشد. لطفاً " 27 | "مقدار پیش فرض '%(name)s' را اصلاح کنید." 28 | 29 | #: admin.py:123 30 | #, python-format 31 | msgid "" 32 | "Constance doesn't support config values of the type %(config_type)s. Please " 33 | "fix the value of '%(name)s'." 34 | msgstr "" 35 | "تنظیمات مقادیر پیکربندی از نوع %(config_type)s را پشتیبانی نمی کند. لطفاً " 36 | "مقدار '%(name)s' را اصلاح کنید." 37 | 38 | #: admin.py:147 39 | msgid "" 40 | "The settings have been modified by someone else. Please reload the form and " 41 | "resubmit your changes." 42 | msgstr "" 43 | "تنظیمات توسط شخص دیگری تغییر یافته است. لطفاً فرم را بارگیری کنید و تغییرات " 44 | "خود را دوباره ارسال کنید." 45 | 46 | #: admin.py:160 47 | msgid "" 48 | "CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in " 49 | "CONSTANCE_CONFIG." 50 | msgstr "CONSTANCE_CONFIG_FIELDSETS شامل فیلدهای CONSTANCE_CONFIG نیست." 51 | 52 | #: admin.py:224 53 | msgid "Live settings updated successfully." 54 | msgstr "تنظیمات زنده با موفقیت به روز شد." 55 | 56 | #: admin.py:285 57 | msgid "config" 58 | msgstr "پیکربندی" 59 | 60 | #: apps.py:8 61 | msgid "Constance" 62 | msgstr "تنظیمات" 63 | 64 | #: backends/database/models.py:19 65 | msgid "constance" 66 | msgstr "تنظیمات" 67 | 68 | #: backends/database/models.py:20 69 | msgid "constances" 70 | msgstr "تنظیمات" 71 | 72 | #: management/commands/constance.py:30 73 | msgid "Get/Set In-database config settings handled by Constance" 74 | msgstr "" 75 | "دریافت/تنظیم تنظیمات پیکربندی درون پایگاه داده که توسط تنظیمات بکار برده می " 76 | "شود" 77 | 78 | #: templates/admin/constance/change_list.html:75 79 | msgid "Save" 80 | msgstr "ذخیره" 81 | 82 | #: templates/admin/constance/change_list.html:84 83 | msgid "Home" 84 | msgstr "خانه" 85 | 86 | #: templates/admin/constance/includes/results_list.html:5 87 | msgid "Name" 88 | msgstr "نام" 89 | 90 | #: templates/admin/constance/includes/results_list.html:6 91 | msgid "Default" 92 | msgstr "پیش‌فرض" 93 | 94 | #: templates/admin/constance/includes/results_list.html:7 95 | msgid "Value" 96 | msgstr "مقدار" 97 | 98 | #: templates/admin/constance/includes/results_list.html:8 99 | msgid "Is modified" 100 | msgstr "تغییر یافته" 101 | 102 | #: templates/admin/constance/includes/results_list.html:44 103 | msgid "Reset to default" 104 | msgstr "بازنشانی به پیش‌فرض" 105 | -------------------------------------------------------------------------------- /constance/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-07-02 19:01+0100\n" 11 | "PO-Revision-Date: 2017-07-03 12:35+0100\n" 12 | "Last-Translator: Bruno Alla \n" 13 | "Language: fr\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 18 | "Language-Team: \n" 19 | "X-Generator: Poedit 2.0.2\n" 20 | 21 | #: admin.py:111 22 | #, python-format 23 | msgid "" 24 | "Default value type must be equal to declared config parameter type. Please " 25 | "fix the default value of '%(name)s'." 26 | msgstr "" 27 | "Le type de la valeur par défaut doit être le même que le type déclaré dans " 28 | "la configuration. Veuillez corriger la valeur par défaut de '%(name)s'." 29 | 30 | #: admin.py:121 31 | #, python-format 32 | msgid "" 33 | "Constance doesn't support config values of the type %(config_type)s. Please " 34 | "fix the value of '%(name)s'." 35 | msgstr "" 36 | "Constance ne prend pas en charge les valeurs de configuration du type " 37 | "%(config_type)s. Veuillez corriger la valeur de ‘%(name)s’." 38 | 39 | #: admin.py:145 40 | msgid "" 41 | "The settings have been modified by someone else. Please reload the form and " 42 | "resubmit your changes." 43 | msgstr "" 44 | "Les paramètres ont été modifiés par quelqu'un d'autre. Veuillez rafraichir " 45 | "le formulaire et soumettre de nouveau vos modifications." 46 | 47 | #: admin.py:209 48 | msgid "Live settings updated successfully." 49 | msgstr "Paramètres mis à jour avec succès." 50 | 51 | #: admin.py:271 52 | msgid "config" 53 | msgstr "config" 54 | 55 | #: apps.py:8 56 | msgid "Constance" 57 | msgstr "Constance" 58 | 59 | #: backends/database/models.py:19 60 | msgid "constance" 61 | msgstr "constance" 62 | 63 | #: backends/database/models.py:20 64 | msgid "constances" 65 | msgstr "constances" 66 | 67 | #: management/commands/constance.py:30 68 | msgid "Get/Set In-database config settings handled by Constance" 69 | msgstr "" 70 | "Obtenir/définir les paramètres de configuration de base de données gérées " 71 | "par Constance" 72 | 73 | #: templates/admin/constance/change_list.html:68 74 | msgid "Save" 75 | msgstr "Enregistrer" 76 | 77 | #: templates/admin/constance/change_list.html:77 78 | msgid "Home" 79 | msgstr "Index" 80 | 81 | #: templates/admin/constance/includes/results_list.html:5 82 | msgid "Name" 83 | msgstr "Nom" 84 | 85 | #: templates/admin/constance/includes/results_list.html:6 86 | msgid "Default" 87 | msgstr "Défaut" 88 | 89 | #: templates/admin/constance/includes/results_list.html:7 90 | msgid "Value" 91 | msgstr "Valeur" 92 | 93 | #: templates/admin/constance/includes/results_list.html:8 94 | msgid "Is modified" 95 | msgstr "Est modifié" 96 | -------------------------------------------------------------------------------- /constance/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/it/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-constance\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-06-13 19:40+0530\n" 11 | "PO-Revision-Date: 2018-03-13 15:26+0100\n" 12 | "Last-Translator: Paolo Melchiorre \n" 13 | "Language-Team: Italian (http://www.transifex.com/projects/p/django-constance/" 14 | "language/it/)\n" 15 | "Language: it\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | "X-Generator: Poedit 2.0.4\n" 21 | 22 | #: admin.py:113 23 | #, python-format 24 | msgid "" 25 | "Default value type must be equal to declared config parameter type. Please " 26 | "fix the default value of '%(name)s'." 27 | msgstr "" 28 | "Il tipo dei valori di default deve essere uguale al tipo del parametro. " 29 | "Modifica il valore di default di '%(name)s'." 30 | 31 | #: admin.py:123 32 | #, python-format 33 | msgid "" 34 | "Constance doesn't support config values of the type %(config_type)s. Please " 35 | "fix the value of '%(name)s'." 36 | msgstr "" 37 | "Constance non supporta valori di impostazioni di tipo %(config_type)s. " 38 | "Modifica il valore di '%(name)s'." 39 | 40 | #: admin.py:147 41 | msgid "" 42 | "The settings have been modified by someone else. Please reload the form and " 43 | "resubmit your changes." 44 | msgstr "" 45 | "Le impostazioni sono state modificate da qualcuno. Ricarica la pagina e " 46 | "invia nuovamente le tue modifiche." 47 | 48 | #: admin.py:160 49 | msgid "" 50 | "CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in " 51 | "CONSTANCE_CONFIG." 52 | msgstr "" 53 | "CONSTANCE_CONFIG_FIELDSETS non contiene campi che esistono in " 54 | "CONSTANCE_CONFIG." 55 | 56 | #: admin.py:224 57 | msgid "Live settings updated successfully." 58 | msgstr "Le impostazioni attive sono state aggiornate correttamente." 59 | 60 | #: admin.py:285 61 | msgid "config" 62 | msgstr "configurazioni" 63 | 64 | #: apps.py:8 65 | msgid "Constance" 66 | msgstr "Impostazioni" 67 | 68 | #: backends/database/models.py:19 69 | msgid "constance" 70 | msgstr "impostazione" 71 | 72 | #: backends/database/models.py:20 73 | msgid "constances" 74 | msgstr "impostazioni" 75 | 76 | #: management/commands/constance.py:30 77 | msgid "Get/Set In-database config settings handled by Constance" 78 | msgstr "" 79 | 80 | #: templates/admin/constance/change_list.html:75 81 | msgid "Save" 82 | msgstr "Salva" 83 | 84 | #: templates/admin/constance/change_list.html:84 85 | msgid "Home" 86 | msgstr "Inizio" 87 | 88 | #: templates/admin/constance/includes/results_list.html:5 89 | msgid "Name" 90 | msgstr "Nome" 91 | 92 | #: templates/admin/constance/includes/results_list.html:6 93 | msgid "Default" 94 | msgstr "Default" 95 | 96 | #: templates/admin/constance/includes/results_list.html:7 97 | msgid "Value" 98 | msgstr "Valore" 99 | 100 | #: templates/admin/constance/includes/results_list.html:8 101 | msgid "Is modified" 102 | msgstr "Modificato" 103 | 104 | #~ msgid "Constance config" 105 | #~ msgstr "Configurazione Impostazioni" 106 | -------------------------------------------------------------------------------- /constance/locale/pl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/pl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/pl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-constance\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-06-13 19:40+0530\n" 11 | "PO-Revision-Date: 2014-11-27 18:13+0000\n" 12 | "Last-Translator: Jannis Leidel \n" 13 | "Language-Team: Polish (http://www.transifex.com/projects/p/django-constance/" 14 | "language/pl/)\n" 15 | "Language: pl\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " 20 | "|| n%100>=20) ? 1 : 2);\n" 21 | 22 | #: admin.py:113 23 | #, python-format 24 | msgid "" 25 | "Default value type must be equal to declared config parameter type. Please " 26 | "fix the default value of '%(name)s'." 27 | msgstr "" 28 | 29 | #: admin.py:123 30 | #, python-format 31 | msgid "" 32 | "Constance doesn't support config values of the type %(config_type)s. Please " 33 | "fix the value of '%(name)s'." 34 | msgstr "" 35 | 36 | #: admin.py:147 37 | msgid "" 38 | "The settings have been modified by someone else. Please reload the form and " 39 | "resubmit your changes." 40 | msgstr "" 41 | 42 | #: admin.py:160 43 | msgid "" 44 | "CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in " 45 | "CONSTANCE_CONFIG." 46 | msgstr "" 47 | 48 | #: admin.py:224 49 | msgid "Live settings updated successfully." 50 | msgstr "Parametry zostały zaktualizowane" 51 | 52 | #: admin.py:285 53 | msgid "config" 54 | msgstr "" 55 | 56 | #: apps.py:8 57 | msgid "Constance" 58 | msgstr "" 59 | 60 | #: backends/database/models.py:19 61 | msgid "constance" 62 | msgstr "parametr" 63 | 64 | #: backends/database/models.py:20 65 | msgid "constances" 66 | msgstr "parametry" 67 | 68 | #: management/commands/constance.py:30 69 | msgid "Get/Set In-database config settings handled by Constance" 70 | msgstr "" 71 | 72 | #: templates/admin/constance/change_list.html:75 73 | msgid "Save" 74 | msgstr "Zapisz" 75 | 76 | #: templates/admin/constance/change_list.html:84 77 | msgid "Home" 78 | msgstr "Początek" 79 | 80 | #: templates/admin/constance/includes/results_list.html:5 81 | msgid "Name" 82 | msgstr "Nazwa" 83 | 84 | #: templates/admin/constance/includes/results_list.html:6 85 | msgid "Default" 86 | msgstr "Domyślnie" 87 | 88 | #: templates/admin/constance/includes/results_list.html:7 89 | msgid "Value" 90 | msgstr "Wartość" 91 | 92 | #: templates/admin/constance/includes/results_list.html:8 93 | msgid "Is modified" 94 | msgstr "Zmodyfikowana" 95 | 96 | #~ msgid "Constance config" 97 | #~ msgstr "Konfiguracja Constance" 98 | -------------------------------------------------------------------------------- /constance/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-constance\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-07-19 20:59+0500\n" 11 | "PO-Revision-Date: 2014-11-27 18:13+0000\n" 12 | "Last-Translator: Jannis Leidel \n" 13 | "Language-Team: Russian (http://www.transifex.com/projects/p/django-constance/" 14 | "language/ru/)\n" 15 | "Language: ru\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 21 | 22 | #: admin.py:113 23 | #, python-format 24 | msgid "" 25 | "Default value type must be equal to declared config parameter type. Please " 26 | "fix the default value of '%(name)s'." 27 | msgstr "" 28 | 29 | #: admin.py:123 30 | #, python-format 31 | msgid "" 32 | "Constance doesn't support config values of the type %(config_type)s. Please " 33 | "fix the value of '%(name)s'." 34 | msgstr "" 35 | 36 | #: admin.py:147 37 | msgid "" 38 | "The settings have been modified by someone else. Please reload the form and " 39 | "resubmit your changes." 40 | msgstr "" 41 | 42 | #: admin.py:160 43 | msgid "" 44 | "CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in " 45 | "CONSTANCE_CONFIG." 46 | msgstr "" 47 | 48 | #: admin.py:224 49 | msgid "Live settings updated successfully." 50 | msgstr "Настройки успешно сохранены." 51 | 52 | #: admin.py:267 53 | msgid "Failed to update live settings." 54 | msgstr "Не удалось сохранить настройки." 55 | 56 | #: admin.py:285 57 | msgid "config" 58 | msgstr "настройки" 59 | 60 | #: apps.py:8 61 | msgid "Constance" 62 | msgstr "" 63 | 64 | #: backends/database/models.py:19 65 | msgid "constance" 66 | msgstr "настройки" 67 | 68 | #: backends/database/models.py:20 69 | msgid "constances" 70 | msgstr "настройки" 71 | 72 | #: management/commands/constance.py:30 73 | msgid "Get/Set In-database config settings handled by Constance" 74 | msgstr "" 75 | 76 | #: templates/admin/constance/change_list.html:75 77 | msgid "Save" 78 | msgstr "Сохранить" 79 | 80 | #: templates/admin/constance/change_list.html:84 81 | msgid "Home" 82 | msgstr "Главная" 83 | 84 | #: templates/admin/constance/includes/results_list.html:5 85 | msgid "Name" 86 | msgstr "Название" 87 | 88 | #: templates/admin/constance/includes/results_list.html:6 89 | msgid "Default" 90 | msgstr "По умолчанию" 91 | 92 | #: templates/admin/constance/includes/results_list.html:7 93 | msgid "Value" 94 | msgstr "Текущее значение" 95 | 96 | #: templates/admin/constance/includes/results_list.html:8 97 | msgid "Is modified" 98 | msgstr "Было изменено" 99 | 100 | #~ msgid "Constance config" 101 | #~ msgstr "Настройки" 102 | -------------------------------------------------------------------------------- /constance/locale/tr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/tr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/tr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2019-11-09 19:14+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Ozcan Yarimdunya \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | #: constance/admin.py:116 21 | #, python-format 22 | msgid "" 23 | "Default value type must be equal to declared config parameter type. Please " 24 | "fix the default value of '%(name)s'." 25 | msgstr "" 26 | "Varsayılan değer tipi, tanımlanan ayarlar parametresi tipi ile aynı olmalıdır. Lütfen " 27 | "'%(name)s' in varsayılan değerini düzeltin." 28 | 29 | #: constance/admin.py:126 30 | #, python-format 31 | msgid "" 32 | "Constance doesn't support config values of the type %(config_type)s. Please " 33 | "fix the value of '%(name)s'." 34 | msgstr "" 35 | "Constance %(config_type)s tipinin yapılandırma değerlerini desteklemiyor. Lütfen " 36 | "'%(name)s' in değerini düzeltin." 37 | 38 | #: constance/admin.py:160 39 | msgid "" 40 | "The settings have been modified by someone else. Please reload the form and " 41 | "resubmit your changes." 42 | msgstr "" 43 | "Ayarlar başkası tarafından değiştirildi. Lütfen formu tekrar yükleyin ve " 44 | "değişikliklerinizi tekrar kaydedin." 45 | 46 | #: constance/admin.py:172 constance/checks.py:19 47 | msgid "" 48 | "CONSTANCE_CONFIG_FIELDSETS is missing field(s) that exists in " 49 | "CONSTANCE_CONFIG." 50 | msgstr "" 51 | "CONSTANCE_CONFIG içinde mevcut olan alan(lar) için " 52 | "CONSTANCE_CONFIG_FIELDSETS eksik." 53 | 54 | #: constance/admin.py:240 55 | msgid "Live settings updated successfully." 56 | msgstr "Canlı ayarlar başarıyla güncellendi." 57 | 58 | #: constance/admin.py:305 59 | msgid "config" 60 | msgstr "ayar" 61 | 62 | #: constance/backends/database/models.py:19 63 | msgid "constance" 64 | msgstr "constance" 65 | 66 | #: constance/backends/database/models.py:20 67 | msgid "constances" 68 | msgstr "constances" 69 | 70 | #: constance/management/commands/constance.py:32 71 | msgid "Get/Set In-database config settings handled by Constance" 72 | msgstr "Constance tarafından veritabanında barındırılan ayarları görüntüle/değiştir" 73 | 74 | #: constance/templates/admin/constance/change_list.html:60 75 | msgid "Save" 76 | msgstr "Kaydet" 77 | 78 | #: constance/templates/admin/constance/change_list.html:69 79 | msgid "Home" 80 | msgstr "Anasayfa" 81 | 82 | #: constance/templates/admin/constance/includes/results_list.html:6 83 | msgid "Name" 84 | msgstr "İsim" 85 | 86 | #: constance/templates/admin/constance/includes/results_list.html:7 87 | msgid "Default" 88 | msgstr "Varsayılan" 89 | 90 | #: constance/templates/admin/constance/includes/results_list.html:8 91 | msgid "Value" 92 | msgstr "Değer" 93 | 94 | #: constance/templates/admin/constance/includes/results_list.html:9 95 | msgid "Is modified" 96 | msgstr "Değiştirildi mi" 97 | 98 | #: constance/templates/admin/constance/includes/results_list.html:22 99 | msgid "Current file" 100 | msgstr "Geçerli dosya" 101 | 102 | #: constance/templates/admin/constance/includes/results_list.html:39 103 | msgid "Reset to default" 104 | msgstr "Varsayılana dön" 105 | -------------------------------------------------------------------------------- /constance/locale/uk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/uk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/uk/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-constance\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-07-19 16:00+0000\n" 11 | "PO-Revision-Date: 2014-11-27 18:13+0000\n" 12 | "Last-Translator: Vasyl Dizhak \n" 13 | "Language-Team: (http://www.transifex.com/projects/p/django-constance/" 14 | "language/uk/)\n" 15 | "Language: uk\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 21 | 22 | #: admin.py:109 23 | msgid "You don't have permission to change these values" 24 | msgstr "У вас немає прав для зміни цих значень" 25 | 26 | #: admin.py:117 27 | #, python-format 28 | msgid "" 29 | "Default value type must be equal to declared config parameter type. Please " 30 | "fix the default value of '%(name)s'." 31 | msgstr "" 32 | "Тип значення за замовчуванням повинен співпадати із вказаним типом параметра " 33 | "конфігурації. Будь ласка, виправте значення за замовчуванням для '%(name)s'." 34 | 35 | #: admin.py:127 36 | #, python-format 37 | msgid "" 38 | "Constance doesn't support config values of the type %(config_type)s. Please " 39 | "fix the value of '%(name)s'." 40 | msgstr "" 41 | "Constance не підтрумує значення наступного типу %(config_type)s. Будь ласка, " 42 | "змініть тип для значення '%(name)s'" 43 | 44 | #: admin.py:166 45 | msgid "" 46 | "The settings have been modified by someone else. Please reload the form and " 47 | "resubmit your changes." 48 | msgstr "" 49 | "Налаштування було змінено кимось іншим. Буд ласка, перезавантажте форму та " 50 | "повторно збережіть зміни." 51 | 52 | #: admin.py:178 checks.py:19 53 | msgid "" 54 | "CONSTANCE_CONFIG_FIELDSETS is missing field(s) that exists in " 55 | "CONSTANCE_CONFIG." 56 | msgstr "" 57 | "Одне чи кілька полів з CONSTANCE_CONFIG відсутні в " 58 | "CONSTANCE_CONFIG_FIELDSETS." 59 | 60 | #: admin.py:250 61 | msgid "Live settings updated successfully." 62 | msgstr "Налаштування успішно збережені." 63 | 64 | #: admin.py:267 65 | msgid "Failed to update live settings." 66 | msgstr "Не вдалося зберегти налаштування." 67 | 68 | #: admin.py:326 69 | msgid "config" 70 | msgstr "налаштування" 71 | 72 | #: apps.py:8 73 | msgid "Constance" 74 | msgstr "" 75 | 76 | #: backends/database/models.py:19 77 | msgid "constance" 78 | msgstr "налаштування" 79 | 80 | #: backends/database/models.py:20 81 | msgid "constances" 82 | msgstr "налаштування" 83 | 84 | #: management/commands/constance.py:30 85 | msgid "Get/Set In-database config settings handled by Constance" 86 | msgstr "Отримати/встановити налашування в базі даних, якими керує Constance" 87 | 88 | #: templates/admin/constance/change_list.html:61 89 | msgid "Save" 90 | msgstr "Зберегти" 91 | 92 | #: templates/admin/constance/change_list.html:70 93 | msgid "Home" 94 | msgstr "Головна" 95 | 96 | #: templates/admin/constance/includes/results_list.html:6 97 | msgid "Name" 98 | msgstr "Назва" 99 | 100 | #: templates/admin/constance/includes/results_list.html:7 101 | msgid "Default" 102 | msgstr "За замовчуванням" 103 | 104 | #: templates/admin/constance/includes/results_list.html:8 105 | msgid "Value" 106 | msgstr "Поточне значення" 107 | 108 | #: templates/admin/constance/includes/results_list.html:9 109 | msgid "Is modified" 110 | msgstr "Було змінено" 111 | 112 | #: templates/admin/constance/includes/results_list.html:26 113 | msgid "Current file" 114 | msgstr "Поточний файл" 115 | 116 | #: templates/admin/constance/includes/results_list.html:44 117 | msgid "Reset to default" 118 | msgstr "Скинути до значення за замовчуванням" 119 | 120 | #~ msgid "Constance config" 121 | #~ msgstr "Настройки" 122 | -------------------------------------------------------------------------------- /constance/locale/zh_CN/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/zh_CN/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/zh_CN/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Yifu Yu , 2015 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: django-constance\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-06-13 19:40+0530\n" 12 | "PO-Revision-Date: 2015-03-15 18:40+0000\n" 13 | "Last-Translator: Yifu Yu \n" 14 | "Language-Team: Chinese (China) (http://www.transifex.com/jezdez/django-" 15 | "constance/language/zh_CN/)\n" 16 | "Language: zh_CN\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=UTF-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Plural-Forms: nplurals=1; plural=0;\n" 21 | 22 | #: admin.py:113 23 | #, fuzzy, python-format 24 | #| msgid "" 25 | #| "Constance doesn't support config values of the type %(config_type)s. " 26 | #| "Please fix the value of '%(name)s'." 27 | msgid "" 28 | "Default value type must be equal to declared config parameter type. Please " 29 | "fix the default value of '%(name)s'." 30 | msgstr "Constance不支持保存类型为%(config_type)s的值,请修正%(name)s的值。" 31 | 32 | #: admin.py:123 33 | #, python-format 34 | msgid "" 35 | "Constance doesn't support config values of the type %(config_type)s. Please " 36 | "fix the value of '%(name)s'." 37 | msgstr "Constance不支持保存类型为%(config_type)s的值,请修正%(name)s的值。" 38 | 39 | #: admin.py:147 40 | msgid "" 41 | "The settings have been modified by someone else. Please reload the form and " 42 | "resubmit your changes." 43 | msgstr "设置已经被他人修改过,请刷新页面并重新提交您的更改。" 44 | 45 | #: admin.py:160 46 | msgid "" 47 | "CONSTANCE_CONFIG_FIELDSETS does not contain fields that exist in " 48 | "CONSTANCE_CONFIG." 49 | msgstr "" 50 | 51 | #: admin.py:224 52 | msgid "Live settings updated successfully." 53 | msgstr "成功更新实时配置" 54 | 55 | #: admin.py:285 56 | msgid "config" 57 | msgstr "配置" 58 | 59 | #: apps.py:8 60 | msgid "Constance" 61 | msgstr "Constance模块" 62 | 63 | #: backends/database/models.py:19 64 | msgid "constance" 65 | msgstr "Constance模块" 66 | 67 | #: backends/database/models.py:20 68 | msgid "constances" 69 | msgstr "Constance模块" 70 | 71 | #: management/commands/constance.py:30 72 | msgid "Get/Set In-database config settings handled by Constance" 73 | msgstr "" 74 | 75 | #: templates/admin/constance/change_list.html:75 76 | msgid "Save" 77 | msgstr "保存" 78 | 79 | #: templates/admin/constance/change_list.html:84 80 | msgid "Home" 81 | msgstr "首页" 82 | 83 | #: templates/admin/constance/includes/results_list.html:5 84 | msgid "Name" 85 | msgstr "名称" 86 | 87 | #: templates/admin/constance/includes/results_list.html:6 88 | msgid "Default" 89 | msgstr "默认值" 90 | 91 | #: templates/admin/constance/includes/results_list.html:7 92 | msgid "Value" 93 | msgstr "值" 94 | 95 | #: templates/admin/constance/includes/results_list.html:8 96 | msgid "Is modified" 97 | msgstr "是否修改过" 98 | 99 | #~ msgid "Constance config" 100 | #~ msgstr "Constance 配置页面" 101 | -------------------------------------------------------------------------------- /constance/locale/zh_Hans/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/locale/zh_Hans/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /constance/locale/zh_Hans/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # Yifu Yu , 2015. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2018-04-19 11:31+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: YinKH <614457662@qq.com>\n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=1; plural=0;\n" 20 | 21 | #: .\admin.py:115 22 | #, python-format 23 | msgid "" 24 | "Default value type must be equal to declared config parameter type. Please " 25 | "fix the default value of '%(name)s'." 26 | msgstr "默认值的类型必须与参数声明类型相同,请修正%(name)s的值。" 27 | 28 | #: .\admin.py:125 29 | #, python-format 30 | msgid "" 31 | "Constance doesn't support config values of the type %(config_type)s. Please " 32 | "fix the value of '%(name)s'." 33 | msgstr "Constance不支持保存类型为%(config_type)s的值,请修正%(name)s的值。" 34 | 35 | #: .\admin.py:157 36 | msgid "" 37 | "The settings have been modified by someone else. Please reload the form and " 38 | "resubmit your changes." 39 | msgstr "设置已经被他人修改过,请刷新页面并重新提交您的更改。" 40 | 41 | #: .\admin.py:173 42 | msgid "" 43 | "CONSTANCE_CONFIG_FIELDSETS is missing field(s) that exists in " 44 | "CONSTANCE_CONFIG." 45 | msgstr "CONSTANCE_CONFIG_FIELDSETS中缺少在CONSTANCE_CONFIG中声明的字段。" 46 | 47 | #: .\admin.py:240 48 | msgid "Live settings updated successfully." 49 | msgstr "实时配置更新成功" 50 | 51 | #: .\admin.py:301 52 | msgid "config" 53 | msgstr "配置" 54 | 55 | #: .\backends\database\models.py:19 56 | msgid "constance" 57 | msgstr "Constance模块" 58 | 59 | #: .\backends\database\models.py:20 60 | msgid "constances" 61 | msgstr "Constance模块" 62 | 63 | #: .\management\commands\constance.py:30 64 | msgid "Get/Set In-database config settings handled by Constance" 65 | msgstr "获取或设置由Constance模块处理的数据库配置" 66 | 67 | #: .\templates\admin\constance\change_list.html:60 68 | msgid "Save" 69 | msgstr "保存" 70 | 71 | #: .\templates\admin\constance\change_list.html:69 72 | msgid "Home" 73 | msgstr "首页" 74 | 75 | #: .\templates\admin\constance\includes\results_list.html:5 76 | msgid "Name" 77 | msgstr "名称" 78 | 79 | #: .\templates\admin\constance\includes\results_list.html:6 80 | msgid "Default" 81 | msgstr "默认值" 82 | 83 | #: .\templates\admin\constance\includes\results_list.html:7 84 | msgid "Value" 85 | msgstr "当前值" 86 | 87 | #: .\templates\admin\constance\includes\results_list.html:8 88 | msgid "Is modified" 89 | msgstr "是否修改过" 90 | 91 | #: .\templates\admin\constance\includes\results_list.html:21 92 | msgid "Current file" 93 | msgstr "当前文件" 94 | 95 | #: .\templates\admin\constance\includes\results_list.html:36 96 | msgid "Reset to default" 97 | msgstr "重置至默认值" 98 | -------------------------------------------------------------------------------- /constance/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/management/__init__.py -------------------------------------------------------------------------------- /constance/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/management/commands/__init__.py -------------------------------------------------------------------------------- /constance/management/commands/constance.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ValidationError 3 | from django.core.management import BaseCommand 4 | from django.core.management import CommandError 5 | from django.utils.translation import gettext as _ 6 | 7 | from constance import config 8 | from constance.forms import ConstanceForm 9 | from constance.models import Constance 10 | from constance.utils import get_values 11 | 12 | 13 | def _set_constance_value(key, value): 14 | """ 15 | Parses and sets a Constance value from a string 16 | :param key: 17 | :param value: 18 | :return: 19 | """ 20 | form = ConstanceForm(initial=get_values()) 21 | 22 | field = form.fields[key] 23 | 24 | clean_value = field.clean(field.to_python(value)) 25 | setattr(config, key, clean_value) 26 | 27 | 28 | class Command(BaseCommand): 29 | help = _('Get/Set In-database config settings handled by Constance') 30 | 31 | GET = 'get' 32 | SET = 'set' 33 | LIST = 'list' 34 | REMOVE_STALE_KEYS = 'remove_stale_keys' 35 | 36 | def add_arguments(self, parser): 37 | subparsers = parser.add_subparsers(dest='command') 38 | subparsers.add_parser(self.LIST, help='list all Constance keys and their values') 39 | 40 | parser_get = subparsers.add_parser(self.GET, help='get the value of a Constance key') 41 | parser_get.add_argument('key', help='name of the key to get', metavar='KEY') 42 | 43 | parser_set = subparsers.add_parser(self.SET, help='set the value of a Constance key') 44 | parser_set.add_argument('key', help='name of the key to set', metavar='KEY') 45 | # use nargs='+' so that we pass a list to MultiValueField (eg SplitDateTimeField) 46 | parser_set.add_argument('value', help='value to set', metavar='VALUE', nargs='+') 47 | 48 | subparsers.add_parser( 49 | self.REMOVE_STALE_KEYS, 50 | help='delete all Constance keys and their values if they are not in settings.CONSTANCE_CONFIG (stale keys)', 51 | ) 52 | 53 | def handle(self, command, key=None, value=None, *args, **options): 54 | if command == self.GET: 55 | try: 56 | self.stdout.write(str(getattr(config, key)), ending='\n') 57 | except AttributeError as e: 58 | raise CommandError(f'{key} is not defined in settings.CONSTANCE_CONFIG') from e 59 | elif command == self.SET: 60 | try: 61 | if len(value) == 1: 62 | # assume that if a single argument was passed, the field doesn't expect a list 63 | value = value[0] 64 | _set_constance_value(key, value) 65 | except KeyError as e: 66 | raise CommandError(f'{key} is not defined in settings.CONSTANCE_CONFIG') from e 67 | except ValidationError as e: 68 | raise CommandError(', '.join(e)) from e 69 | elif command == self.LIST: 70 | for k, v in get_values().items(): 71 | self.stdout.write(f'{k}\t{v}', ending='\n') 72 | elif command == self.REMOVE_STALE_KEYS: 73 | actual_keys = settings.CONSTANCE_CONFIG.keys() 74 | stale_records = Constance.objects.exclude(key__in=actual_keys) 75 | if stale_records: 76 | self.stdout.write('The following record will be deleted:', ending='\n') 77 | else: 78 | self.stdout.write('There are no stale records in the database.', ending='\n') 79 | for stale_record in stale_records: 80 | self.stdout.write(f'{stale_record.key}\t{stale_record.value}', ending='\n') 81 | stale_records.delete() 82 | else: 83 | raise CommandError('Invalid command') 84 | -------------------------------------------------------------------------------- /constance/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | from django.db import models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | initial = True 7 | 8 | dependencies = [] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name='Constance', 13 | fields=[ 14 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 15 | ('key', models.CharField(max_length=255, unique=True)), 16 | ('value', models.TextField(blank=True, editable=False, null=True)), 17 | ], 18 | options={ 19 | 'verbose_name': 'constance', 20 | 'verbose_name_plural': 'constances', 21 | 'permissions': [('change_config', 'Can change config'), ('view_config', 'Can view config')], 22 | }, 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /constance/migrations/0002_migrate_from_old_table.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | 3 | from django.core.management.color import no_style 4 | from django.db import migrations 5 | 6 | logger = getLogger(__name__) 7 | 8 | 9 | def _migrate_from_old_table(apps, schema_editor) -> None: 10 | """ 11 | Copies values from old table. 12 | On new installations just ignore error that table does not exist. 13 | """ 14 | connection = schema_editor.connection 15 | quoted_string = ', '.join([connection.ops.quote_name(item) for item in ['id', 'key', 'value']]) 16 | old_table_name = 'constance_config' 17 | with connection.cursor() as cursor: 18 | if old_table_name not in connection.introspection.table_names(): 19 | logger.info('Old table does not exist, skipping') 20 | return 21 | cursor.execute( 22 | f'INSERT INTO constance_constance ( {quoted_string} ) SELECT {quoted_string} FROM {old_table_name}', # noqa: S608 23 | [], 24 | ) 25 | cursor.execute(f'DROP TABLE {old_table_name}', []) 26 | 27 | Constance = apps.get_model('constance', 'Constance') 28 | sequence_sql = connection.ops.sequence_reset_sql(no_style(), [Constance]) 29 | with connection.cursor() as cursor: 30 | for sql in sequence_sql: 31 | cursor.execute(sql) 32 | 33 | 34 | class Migration(migrations.Migration): 35 | dependencies = [('constance', '0001_initial')] 36 | 37 | atomic = False 38 | 39 | operations = [ 40 | migrations.RunPython(_migrate_from_old_table, reverse_code=lambda x, y: None), 41 | ] 42 | -------------------------------------------------------------------------------- /constance/migrations/0003_drop_pickle.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import pickle 4 | from base64 import b64decode 5 | from importlib import import_module 6 | 7 | from django.db import migrations 8 | 9 | from constance import settings 10 | from constance.codecs import dumps 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def is_already_migrated(value): 16 | try: 17 | data = json.loads(value) 18 | if isinstance(data, dict) and set(data.keys()) == {'__type__', '__value__'}: 19 | return True 20 | except (json.JSONDecodeError, TypeError, UnicodeDecodeError): 21 | return False 22 | return False 23 | 24 | 25 | def import_module_attr(path): 26 | package, module = path.rsplit('.', 1) 27 | return getattr(import_module(package), module) 28 | 29 | 30 | def migrate_pickled_data(apps, schema_editor) -> None: # pragma: no cover 31 | Constance = apps.get_model('constance', 'Constance') 32 | 33 | for constance in Constance.objects.exclude(value=None): 34 | if not is_already_migrated(constance.value): 35 | constance.value = dumps(pickle.loads(b64decode(constance.value.encode()))) # noqa: S301 36 | constance.save(update_fields=['value']) 37 | 38 | if settings.BACKEND in ('constance.backends.redisd.RedisBackend', 'constance.backends.redisd.CachingRedisBackend'): 39 | import redis 40 | 41 | _prefix = settings.REDIS_PREFIX 42 | connection_cls = settings.REDIS_CONNECTION_CLASS 43 | if connection_cls is not None: 44 | _rd = import_module_attr(connection_cls)() 45 | else: 46 | if isinstance(settings.REDIS_CONNECTION, str): 47 | _rd = redis.from_url(settings.REDIS_CONNECTION) 48 | else: 49 | _rd = redis.Redis(**settings.REDIS_CONNECTION) 50 | redis_migrated_data = {} 51 | for key in settings.CONFIG: 52 | prefixed_key = f'{_prefix}{key}' 53 | value = _rd.get(prefixed_key) 54 | if value is not None and not is_already_migrated(value): 55 | redis_migrated_data[prefixed_key] = dumps(pickle.loads(value)) # noqa: S301 56 | for prefixed_key, value in redis_migrated_data.items(): 57 | _rd.set(prefixed_key, value) 58 | 59 | 60 | class Migration(migrations.Migration): 61 | dependencies = [('constance', '0002_migrate_from_old_table')] 62 | 63 | operations = [ 64 | migrations.RunPython(migrate_pickled_data), 65 | ] 66 | -------------------------------------------------------------------------------- /constance/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/constance/migrations/__init__.py -------------------------------------------------------------------------------- /constance/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class Constance(models.Model): 6 | key = models.CharField(max_length=255, unique=True) 7 | value = models.TextField(null=True, blank=True, editable=False) 8 | 9 | class Meta: 10 | verbose_name = _('constance') 11 | verbose_name_plural = _('constances') 12 | permissions = [ 13 | ('change_config', 'Can change config'), 14 | ('view_config', 'Can view config'), 15 | ] 16 | 17 | def __str__(self): 18 | return self.key 19 | -------------------------------------------------------------------------------- /constance/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | BACKEND = getattr(settings, 'CONSTANCE_BACKEND', 'constance.backends.redisd.RedisBackend') 4 | 5 | CONFIG = getattr(settings, 'CONSTANCE_CONFIG', {}) 6 | 7 | CONFIG_FIELDSETS = getattr(settings, 'CONSTANCE_CONFIG_FIELDSETS', {}) 8 | 9 | ADDITIONAL_FIELDS = getattr(settings, 'CONSTANCE_ADDITIONAL_FIELDS', {}) 10 | 11 | FILE_ROOT = getattr(settings, 'CONSTANCE_FILE_ROOT', '') 12 | 13 | DATABASE_CACHE_BACKEND = getattr(settings, 'CONSTANCE_DATABASE_CACHE_BACKEND', None) 14 | 15 | DATABASE_CACHE_AUTOFILL_TIMEOUT = getattr(settings, 'CONSTANCE_DATABASE_CACHE_AUTOFILL_TIMEOUT', 60 * 60 * 24) 16 | 17 | DATABASE_PREFIX = getattr(settings, 'CONSTANCE_DATABASE_PREFIX', '') 18 | 19 | REDIS_PREFIX = getattr(settings, 'CONSTANCE_REDIS_PREFIX', 'constance:') 20 | 21 | REDIS_CACHE_TIMEOUT = getattr(settings, 'CONSTANCE_REDIS_CACHE_TIMEOUT', 60) 22 | 23 | REDIS_CONNECTION_CLASS = getattr(settings, 'CONSTANCE_REDIS_CONNECTION_CLASS', None) 24 | 25 | REDIS_CONNECTION = getattr(settings, 'CONSTANCE_REDIS_CONNECTION', {}) 26 | 27 | SUPERUSER_ONLY = getattr(settings, 'CONSTANCE_SUPERUSER_ONLY', True) 28 | 29 | IGNORE_ADMIN_VERSION_CHECK = getattr(settings, 'CONSTANCE_IGNORE_ADMIN_VERSION_CHECK', False) 30 | -------------------------------------------------------------------------------- /constance/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | config_updated = django.dispatch.Signal() 4 | -------------------------------------------------------------------------------- /constance/static/admin/css/constance.css: -------------------------------------------------------------------------------- 1 | #result_list .changed { 2 | background-color: #ffc; 3 | } 4 | #changelist table thead th .text { 5 | padding: 2px 5px; 6 | } 7 | #changelist table tbody td:first-child { 8 | text-align: left; 9 | } 10 | #changelist-form ul.errorlist { 11 | margin: 0 !important; 12 | } 13 | .help { 14 | font-weight: normal !important; 15 | } 16 | #results { 17 | overflow-x: auto; 18 | } 19 | .item-anchor { 20 | visibility: hidden; 21 | margin-left: .1em; 22 | } 23 | .item-name { 24 | white-space: nowrap; 25 | } 26 | .item-name:hover .item-anchor { 27 | visibility: visible; 28 | } 29 | .sticky-footer { 30 | position: sticky; 31 | width: 100%; 32 | left: 0; 33 | bottom: 0; 34 | } 35 | -------------------------------------------------------------------------------- /constance/static/admin/js/constance.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 'use strict'; 3 | 4 | $(function() { 5 | 6 | $('#content-main').on('click', '.reset-link', function(e) { 7 | e.preventDefault(); 8 | 9 | const field_selector = this.dataset.fieldId.replace(/ /g, "\\ ") 10 | const field = $('#' + field_selector); 11 | const fieldType = this.dataset.fieldType; 12 | 13 | if (fieldType === 'checkbox') { 14 | field.prop('checked', this.dataset.default === 'true'); 15 | } else if (fieldType === 'date') { 16 | const defaultDate = new Date(this.dataset.default * 1000); 17 | $('#' + this.dataset.fieldId).val(defaultDate.strftime(get_format('DATE_INPUT_FORMATS')[0])); 18 | } else if (fieldType === 'datetime') { 19 | const defaultDate = new Date(this.dataset.default * 1000); 20 | $('#' + this.dataset.fieldId + '_0').val(defaultDate.strftime(get_format('DATE_INPUT_FORMATS')[0])); 21 | $('#' + this.dataset.fieldId + '_1').val(defaultDate.strftime(get_format('TIME_INPUT_FORMATS')[0])); 22 | } else { 23 | field.val(this.dataset.default); 24 | } 25 | }); 26 | }); 27 | })(django.jQuery); 28 | -------------------------------------------------------------------------------- /constance/templates/admin/constance/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load admin_list static i18n %} 3 | 4 | {% block extrastyle %} 5 | {{ block.super }} 6 | 7 | 8 | {{ media.css }} 9 | 10 | {% endblock %} 11 | 12 | {% block extrahead %} 13 | {% url 'admin:jsi18n' as jsi18nurl %} 14 | 15 | {{ block.super }} 16 | {{ media.js }} 17 | 18 | {% if django_version < "5.1" %} 19 | 20 | {% endif %} 21 | {% endblock %} 22 | 23 | {% block bodyclass %}{{ block.super }} change-list{% endblock %} 24 | 25 | {% block content %} 26 |
27 |
28 |
{% csrf_token %} 29 | {% if form.non_field_errors %} 30 |
    31 | {% for error in form.non_field_errors %} 32 |
  • {{ error }}
  • 33 | {% endfor %} 34 |
35 | {% endif %} 36 | {% if form.errors %} 37 |
    38 | {% endif %} 39 | {% for field in form.hidden_fields %} 40 | {% for error in field.errors %} 41 |
  • {{ error }}
  • 42 | {% endfor %} 43 | {{ field }} 44 | {% endfor %} 45 | {% if form.errors %} 46 |
47 | {% endif %} 48 | 49 | {% if fieldsets %} 50 | {% for fieldset in fieldsets %} 51 |
52 |

{{ fieldset.title }}

53 | {% with config_values=fieldset.config_values %} 54 | {% include "admin/constance/includes/results_list.html" %} 55 | {% endwith %} 56 |
57 | {% endfor %} 58 | {% else %} 59 | {% include "admin/constance/includes/results_list.html" %} 60 | {% endif %} 61 | 62 | 65 |
66 |
67 |
68 | {% endblock %} 69 | 70 | {% block breadcrumbs %} 71 | 76 | {% endblock %} 77 | -------------------------------------------------------------------------------- /constance/templates/admin/constance/includes/results_list.html: -------------------------------------------------------------------------------- 1 | {% load admin_list static i18n %} 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% for item in config_values %} 13 | 14 | 21 | 24 | 47 | 54 | 55 | {% endfor %} 56 |
{% trans "Name" %}
{% trans "Default" %}
{% trans "Value" %}
{% trans "Is modified" %}
15 | 16 | {{ item.name }} 17 | 18 | 19 |
{{ item.help_text|linebreaksbr }}
20 |
22 | {{ item.default|linebreaks }} 23 | 25 | {{ item.form_field.errors }} 26 | {% if item.is_file %}{% trans "Current file" %}: {{ item.value }}{% endif %} 27 | {{ item.form_field }} 28 | {% if not item.is_file %} 29 |
30 | {% trans "Reset to default" %} 45 | {% endif %} 46 |
48 | {% if item.modified %} 49 | {{ item.modified }} 50 | {% else %} 51 | {{ item.modified }} 52 | {% endif %} 53 |
57 |
58 | -------------------------------------------------------------------------------- /constance/test/__init__.py: -------------------------------------------------------------------------------- 1 | from .unittest import override_config # pragma: no cover 2 | 3 | __all__ = ['override_config'] 4 | -------------------------------------------------------------------------------- /constance/test/pytest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pytest constance override config plugin. 3 | 4 | Inspired by https://github.com/pytest-dev/pytest-django/. 5 | """ 6 | 7 | from contextlib import ContextDecorator 8 | 9 | import pytest 10 | 11 | from constance import config as constance_config 12 | 13 | 14 | @pytest.hookimpl(trylast=True) 15 | def pytest_configure(config): # pragma: no cover 16 | """Register override_config marker.""" 17 | config.addinivalue_line('markers', ('override_config(**kwargs): mark test to override django-constance config')) 18 | 19 | 20 | @pytest.hookimpl(hookwrapper=True) 21 | def pytest_runtest_call(item): # pragma: no cover 22 | """Validate constance override marker params. Run test with overridden config.""" 23 | marker = item.get_closest_marker('override_config') 24 | if marker is not None: 25 | if marker.args: 26 | pytest.fail('Constance override can not not accept positional args') 27 | with override_config(**marker.kwargs): 28 | yield 29 | else: 30 | yield 31 | 32 | 33 | class override_config(ContextDecorator): 34 | """ 35 | Override config while running test function. 36 | 37 | Act as context manager and decorator. 38 | """ 39 | 40 | def enable(self): 41 | """Store original config values and set overridden values.""" 42 | for key, value in self._to_override.items(): 43 | self._original_values[key] = getattr(constance_config, key) 44 | setattr(constance_config, key, value) 45 | 46 | def disable(self): 47 | """Set original values to the config.""" 48 | for key, value in self._original_values.items(): 49 | setattr(constance_config, key, value) 50 | 51 | def __init__(self, **kwargs): 52 | self._to_override = kwargs.copy() 53 | self._original_values = {} 54 | 55 | def __enter__(self): 56 | self.enable() 57 | 58 | def __exit__(self, exc_type, exc_val, exc_tb): 59 | self.disable() 60 | 61 | 62 | @pytest.fixture(name='override_config') 63 | def _override_config(): 64 | """Make override_config available as a function fixture.""" 65 | return override_config 66 | -------------------------------------------------------------------------------- /constance/test/unittest.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from django import VERSION as DJANGO_VERSION 4 | from django.test import SimpleTestCase 5 | from django.test.utils import override_settings 6 | 7 | from constance import config 8 | 9 | __all__ = ('override_config',) 10 | 11 | 12 | class override_config(override_settings): 13 | """ 14 | Decorator to modify constance setting for TestCase. 15 | 16 | Based on django.test.utils.override_settings. 17 | """ 18 | 19 | def __init__(self, **kwargs): 20 | super().__init__(**kwargs) 21 | self.original_values = {} 22 | 23 | def __call__(self, test_func): 24 | """Modify the decorated function to override config values.""" 25 | if isinstance(test_func, type): 26 | if not issubclass(test_func, SimpleTestCase): 27 | raise Exception('Only subclasses of Django SimpleTestCase can be decorated with override_config') 28 | return self.modify_test_case(test_func) 29 | 30 | @wraps(test_func) 31 | def inner(*args, **kwargs): 32 | with self: 33 | return test_func(*args, **kwargs) 34 | 35 | return inner 36 | 37 | def modify_test_case(self, test_case): 38 | """ 39 | Override the config by modifying TestCase methods. 40 | 41 | This method follows the Django <= 1.6 method of overriding the 42 | _pre_setup and _post_teardown hooks rather than modifying the TestCase 43 | itself. 44 | """ 45 | original_pre_setup = test_case._pre_setup 46 | original_post_teardown = test_case._post_teardown 47 | 48 | if DJANGO_VERSION < (5, 2): 49 | 50 | def _pre_setup(inner_self): 51 | self.enable() 52 | original_pre_setup(inner_self) 53 | else: 54 | 55 | @classmethod 56 | def _pre_setup(cls): 57 | # NOTE: Django 5.2 turned this as a classmethod 58 | # https://github.com/django/django/pull/18514/files 59 | self.enable() 60 | original_pre_setup() 61 | 62 | def _post_teardown(inner_self): 63 | original_post_teardown(inner_self) 64 | self.disable() 65 | 66 | test_case._pre_setup = _pre_setup 67 | test_case._post_teardown = _post_teardown 68 | 69 | return test_case 70 | 71 | def enable(self): 72 | """Store original config values and set overridden values.""" 73 | # Store the original values to an instance variable 74 | for config_key in self.options: 75 | self.original_values[config_key] = getattr(config, config_key) 76 | 77 | # Update config with the overridden values 78 | self.unpack_values(self.options) 79 | 80 | def disable(self): 81 | """Set original values to the config.""" 82 | self.unpack_values(self.original_values) 83 | 84 | @staticmethod 85 | def unpack_values(options): 86 | """Unpack values from the given dict to config.""" 87 | for name, value in options.items(): 88 | setattr(config, name, value) 89 | -------------------------------------------------------------------------------- /constance/utils.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | from . import LazyConfig 4 | from . import settings 5 | 6 | config = LazyConfig() 7 | 8 | 9 | def import_module_attr(path): 10 | package, module = path.rsplit('.', 1) 11 | return getattr(import_module(package), module) 12 | 13 | 14 | def get_values(): 15 | """ 16 | Get dictionary of values from the backend 17 | :return: 18 | """ 19 | # First load a mapping between config name and default value 20 | default_initial = ((name, options[0]) for name, options in settings.CONFIG.items()) 21 | # Then update the mapping with actually values from the backend 22 | return dict(default_initial, **dict(config._backend.mget(settings.CONFIG))) 23 | 24 | 25 | def get_values_for_keys(keys): 26 | """ 27 | Retrieve values for specified keys from the backend. 28 | 29 | :param keys: List of keys to retrieve. 30 | :return: Dictionary with values for the specified keys. 31 | :raises AttributeError: If any key is not found in the configuration. 32 | """ 33 | if not isinstance(keys, (list, tuple, set)): 34 | raise TypeError('keys must be a list, tuple, or set of strings') 35 | 36 | # Prepare default initial mapping 37 | default_initial = {name: options[0] for name, options in settings.CONFIG.items() if name in keys} 38 | 39 | # Check if all keys are present in the default_initial mapping 40 | missing_keys = [key for key in keys if key not in default_initial] 41 | if missing_keys: 42 | raise AttributeError(f'"{", ".join(missing_keys)}" keys not found in configuration.') 43 | 44 | # Merge default values and backend values, prioritizing backend values 45 | return dict(default_initial, **dict(config._backend.mget(keys))) 46 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/docs/_static/screenshot1.png -------------------------------------------------------------------------------- /docs/_static/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/docs/_static/screenshot2.png -------------------------------------------------------------------------------- /docs/_static/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/docs/_static/screenshot3.png -------------------------------------------------------------------------------- /docs/backends.rst: -------------------------------------------------------------------------------- 1 | .. _backends: 2 | 3 | .. highlight:: python 4 | 5 | Backends 6 | ======== 7 | 8 | Constance ships with a bunch of backends that are used to store the 9 | configuration values. By default it uses the Redis backend. To override 10 | the default please set the :setting:`CONSTANCE_BACKEND` setting to the appropriate 11 | dotted path. 12 | 13 | Configuration values are stored in JSON format and automatically serialized/deserialized 14 | on access. 15 | 16 | Redis 17 | ----- 18 | 19 | The configuration values are stored in a redis store and retrieved using the 20 | `redis-py`_ library. Please install it like this:: 21 | 22 | pip install django-constance[redis] 23 | 24 | Configuration is simple and defaults to the following value, you don't have 25 | to add it to your project settings:: 26 | 27 | CONSTANCE_BACKEND = 'constance.backends.redisd.RedisBackend' 28 | 29 | Default redis backend retrieves values every time. There is another redis backend with local cache. 30 | `CachingRedisBackend` stores the value from a redis to memory at first access and checks a value ttl at next. 31 | Configuration installation is simple:: 32 | 33 | CONSTANCE_BACKEND = 'constance.backends.redisd.CachingRedisBackend' 34 | # optionally set a value ttl 35 | CONSTANCE_REDIS_CACHE_TIMEOUT = 60 36 | 37 | .. _`redis-py`: https://pypi.org/project/redis/ 38 | 39 | Settings 40 | ^^^^^^^^ 41 | 42 | There are a couple of options: 43 | 44 | ``CONSTANCE_REDIS_CONNECTION`` 45 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 46 | 47 | A dictionary of parameters to pass to the to Redis client, e.g.:: 48 | 49 | CONSTANCE_REDIS_CONNECTION = { 50 | 'host': 'localhost', 51 | 'port': 6379, 52 | 'db': 0, 53 | } 54 | 55 | Alternatively you can use a URL to do the same:: 56 | 57 | CONSTANCE_REDIS_CONNECTION = 'redis://username:password@localhost:6379/0' 58 | 59 | ``CONSTANCE_REDIS_CONNECTION_CLASS`` 60 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 61 | 62 | An (optional) dotted import path to a connection to use, e.g.:: 63 | 64 | CONSTANCE_REDIS_CONNECTION_CLASS = 'myproject.myapp.mockup.Connection' 65 | 66 | If you are using `django-redis `_, 67 | feel free to use the ``CONSTANCE_REDIS_CONNECTION_CLASS`` setting to define 68 | a callable that returns a redis connection, e.g.:: 69 | 70 | CONSTANCE_REDIS_CONNECTION_CLASS = 'django_redis.get_redis_connection' 71 | 72 | ``CONSTANCE_REDIS_PREFIX`` 73 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 74 | 75 | The (optional) prefix to be used for the key when storing in the Redis 76 | database. Defaults to ``'constance:'``. E.g.:: 77 | 78 | CONSTANCE_REDIS_PREFIX = 'constance:myproject:' 79 | 80 | 81 | ``CONSTANCE_REDIS_CACHE_TIMEOUT`` 82 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 83 | 84 | The (optional) ttl of values in seconds used by `CachingRedisBackend` for storing in a local cache. 85 | Defaults to `60` seconds. 86 | 87 | Database 88 | -------- 89 | 90 | Database backend stores configuration values in a standard Django model. 91 | 92 | You must set the ``CONSTANCE_BACKEND`` Django setting to:: 93 | 94 | CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' 95 | 96 | Please make sure to apply the database migrations:: 97 | 98 | python manage.py migrate 99 | 100 | .. note:: If you're upgrading Constance to 1.0 and use Django 1.7 or higher 101 | please make sure to let the migration system know that you've 102 | already created the tables for the database backend. 103 | 104 | You can do that using the ``--fake`` option of the migrate command:: 105 | 106 | python manage.py migrate database --fake 107 | 108 | 109 | Just like the Redis backend you can set an optional prefix that is used during 110 | database interactions (it defaults to an empty string, ``''``). To use 111 | something else do this:: 112 | 113 | CONSTANCE_DATABASE_PREFIX = 'constance:myproject:' 114 | 115 | Caching 116 | ^^^^^^^ 117 | 118 | The database backend has the ability to automatically cache the config 119 | values and clear them when saving. Assuming you have a :setting:`CACHES` 120 | setting set you only need to set the the 121 | :setting:`CONSTANCE_DATABASE_CACHE_BACKEND` setting to the name of the 122 | configured cache backend to enable this feature, e.g. "default":: 123 | 124 | CACHES = { 125 | 'default': { 126 | 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 127 | 'LOCATION': '127.0.0.1:11211', 128 | } 129 | } 130 | CONSTANCE_DATABASE_CACHE_BACKEND = 'default' 131 | 132 | .. warning:: The cache feature won't work with a cache backend that is 133 | incompatible with cross-process caching like the local memory 134 | cache backend included in Django because correct cache 135 | invalidation can't be guaranteed. 136 | 137 | If you try this, Constance will throw an error and refuse 138 | to let your application start. You can work around this by 139 | subclassing ``constance.backends.database.DatabaseBackend`` 140 | and overriding `__init__` to remove the check. You'll 141 | want to consult the source code for that function to see 142 | exactly how. 143 | 144 | We're deliberately being vague about this, because it's 145 | dangerous; the behavior is undefined, and could even cause 146 | your app to crash. Nevertheless, there are some limited 147 | circumstances in which this could be useful, but please 148 | think carefully before going down this path. 149 | 150 | .. note:: By default Constance will autofill the cache on startup and after 151 | saving any of the config values. If you want to disable the cache 152 | simply set the :setting:`CONSTANCE_DATABASE_CACHE_AUTOFILL_TIMEOUT` 153 | setting to ``None``. 154 | 155 | Memory 156 | ------ 157 | 158 | The configuration values are stored in a memory and do not persist between process 159 | restarts. In order to use this backend you must set the ``CONSTANCE_BACKEND`` 160 | Django setting to:: 161 | 162 | CONSTANCE_BACKEND = 'constance.backends.memory.MemoryBackend' 163 | 164 | The main purpose of this one is to be used mostly for testing/developing means, 165 | so make sure you intentionally use it on production environments. 166 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | Starting with version 4.0.0, the changelog is maintained at the GitHub releases `GitHub releases`_ 4 | 5 | .. _GitHub releases: https://github.com/jazzband/django-constance/releases 6 | 7 | v4.0.0 (2024/08/21) 8 | ~~~~~~~~~~~~~~~~~~~ 9 | 10 | * Replace `pickle` with JSON for the database backend 11 | * Fix migration on MySQL 12 | * Fix data loss using `DatabaseBackend` when the DB connection is unstable 13 | * Fix typos in the documentation 14 | * Fix small HTML errors 15 | * Drop support for legacy Django versions 16 | * Migrate JavaScript to ES2015 17 | * Fix documentation build 18 | * Add linters and formatters (using `ruff`) 19 | * Prepare for Django 5.1 support 20 | * Migrate from `setup.py` to `pyproject.toml` 21 | * Bump `tox` 22 | * Declare support for Python 3.12 23 | 24 | v3.1.0 (2023/08/21) 25 | ~~~~~~~~~~~~~~~~~~~ 26 | 27 | * Add support for using a subdirectory of `MEDIA_ROOT` for file fields 28 | 29 | * Remove pypy from tox tests 30 | 31 | v3.0.0 (2023/07/27) 32 | ~~~~~~~~~~~~~~~~~~~ 33 | 34 | * Refactor database backend 35 | Backward incompatible changes: 36 | remove ``'constance.backends.database'`` from ``INSTALLED_APPS`` 37 | 38 | * Dropped support for python < 3.7 and django < 3.2 39 | 40 | * Example app now supports django 4.1 41 | 42 | * Add support for django 4.2 43 | 44 | * Forward the request when saving the admin changelist form 45 | 46 | v2.9.1 (2022/08/11) 47 | ~~~~~~~~~~~~~~~~~~~ 48 | 49 | * Add support for gettext in fieldset headers 50 | 51 | * Add support for Django 4.1 52 | 53 | * Fix text format for MultiValueField usage 54 | 55 | v2.9.0 (2022/03/11) 56 | ~~~~~~~~~~~~~~~~~~~ 57 | 58 | * Added arabic translation 59 | 60 | * Add concrete_model class attribute to fake admin model 61 | 62 | * Added tests for django 3.2 63 | 64 | * Fix do not detect datetime fields as date type 65 | 66 | * Added support for python 3.10 67 | 68 | * Fixes for Ukrainian locale 69 | 70 | * Added documentation for constance_dbs config 71 | 72 | * Add caching redis backend 73 | 74 | * Serialize according to widget 75 | 76 | * Add default_auto_field to database backend 77 | 78 | v2.8.0 (2020/11/19) 79 | ~~~~~~~~~~~~~~~~~~~ 80 | 81 | * Prevent reset to default for file field 82 | 83 | * Fields_list can be a dictionary, when a fieldset is defined as collapsible 84 | 85 | * Create and add fa language translations files 86 | 87 | * Respect other classes added by admin templates 88 | 89 | * Removed deprecated url() 90 | 91 | * Use gettext_lazy instead of ugettext_lazy 92 | 93 | * Updated python and django version support 94 | 95 | v2.7.0 (2020/06/22) 96 | ~~~~~~~~~~~~~~~~~~~ 97 | 98 | * Deleted south migrations 99 | 100 | * Improve grammar of documentation index file 101 | 102 | * Simplify documentation installation section 103 | 104 | * Fix IntegrityError after 2.5.0 release 105 | (Allow concurrent calls to `DatabaseBackend.set()` method) 106 | 107 | * Make groups of fieldsets collapsable 108 | 109 | * Allow override_config for pytest 110 | 111 | * Put back wheel generation in travis 112 | 113 | * Fix wrong "is modified" in admin for multi line strings 114 | 115 | * Switch md5 to sha256 116 | 117 | * Fix Attempts to change config values fail silently and 118 | appear to succeed when user does not have change permissions 119 | 120 | * Make constance app verbose name translatable 121 | 122 | * Update example project for Django>2 123 | 124 | * Add anchors in admin for constance settings 125 | 126 | * Added a sticky footer in django constance admin 127 | 128 | * Add memory backend 129 | 130 | * Added Ukrainian locale 131 | 132 | * Added lazy checks for pytest 133 | 134 | v2.6.0 (2020/01/29) 135 | ~~~~~~~~~~~~~~~~~~~ 136 | 137 | * Drop support py<3.5 django<2.2 138 | 139 | * Set pickle protocol version for the Redis backend 140 | 141 | * Add a command to delete stale records 142 | 143 | v2.5.0 (2019/12/23) 144 | ~~~~~~~~~~~~~~~~~~~ 145 | 146 | * Made results table responsive for Django 2 admin 147 | 148 | * Add a Django system check that CONFIG_FIELDSETS accounts for all of CONFIG 149 | 150 | * Rewrite set() method of database backend to reduce number of queries 151 | 152 | * Fixed "can't compare offset-naive and offset-aware datetimes" when USE_TZ = True 153 | 154 | * Fixed compatibility issue with Django 3.0 due to django.utils.six 155 | 156 | * Add Turkish language 157 | 158 | v2.4.0 (2019/03/16) 159 | ~~~~~~~~~~~~~~~~~~~ 160 | 161 | * Show not existing fields in field_list 162 | 163 | * Drop Django<1.11 and 2.0, fix tests vs Django 2.2b 164 | 165 | * Fixed "Reset to default" button with constants whose name contains a space 166 | 167 | * Use default_storage to save file 168 | 169 | * Allow null & blank for PickleField 170 | 171 | * Removed Python 3.4 since is not longer supported 172 | 173 | v2.3.1 (2018/09/20) 174 | ~~~~~~~~~~~~~~~~~~~ 175 | 176 | * Fixes javascript typo. 177 | 178 | v2.3.0 (2018/09/13) 179 | ~~~~~~~~~~~~~~~~~~~ 180 | 181 | * Added zh_Hans translation. 182 | 183 | * Fixed TestAdmin.test_linebreaks() due to linebreaksbr() behavior change 184 | on Django 2.1 185 | 186 | * Improved chinese translation 187 | 188 | * Fix bug of can't change permission chang_config's name 189 | 190 | * Improve consistency of reset value handling for `date` 191 | 192 | * Drop support for Python 3.3 193 | 194 | * Added official Django 2.0 support. 195 | 196 | * Added support for Django 2.1 197 | 198 | v2.2.0 (2018/03/23) 199 | ~~~~~~~~~~~~~~~~~~~ 200 | 201 | * Fix ConstanceForm validation. 202 | 203 | * `CONSTANCE_DBS` setting for directing constance permissions/content_type 204 | settings to certain DBs only. 205 | 206 | * Added config labels. 207 | 208 | * Updated italian translations. 209 | 210 | * Fix `CONSTANCE_CONFIG_FIELDSETS` mismatch issue. 211 | 212 | v2.1.0 (2018/02/07) 213 | ~~~~~~~~~~~~~~~~~~~ 214 | 215 | * Move inline JavaScript to constance.js. 216 | 217 | * Remove translation from the app name. 218 | 219 | * Added file uploads. 220 | 221 | * Update information on template context processors. 222 | 223 | * Allow running set while database is not created. 224 | 225 | * Moved inline css/javascripts out to their own files. 226 | 227 | * Add French translations. 228 | 229 | * Add testing for all supported Python and Django versions. 230 | 231 | * Preserve sorting from fieldset config. 232 | 233 | * Added datetime.timedelta support. 234 | 235 | * Added Estonian translations. 236 | 237 | * Account for server timezone for Date object. 238 | 239 | v2.0.0 (2017/02/17) 240 | ~~~~~~~~~~~~~~~~~~~ 241 | 242 | * **BACKWARD INCOMPATIBLE** Added the old value to the config_updated signal. 243 | 244 | * Added a `get_changelist_form` hook in the ModelAdmin. 245 | 246 | * Fix create_perm in apps.py to use database alias given by the post_migrate 247 | signal. 248 | 249 | * Added tests for django 1.11. 250 | 251 | * Fix Reset to default to work with boolean/checkboxes. 252 | 253 | * Fix handling of MultiValueField's (eg SplitDateTimeField) on the command 254 | line. 255 | 256 | v1.3.4 (2016/12/23) 257 | ~~~~~~~~~~~~~~~~~~~ 258 | 259 | * Fix config ordering issue 260 | 261 | * Added localize to check modified flag 262 | 263 | * Allow to rename Constance in Admin 264 | 265 | * Preserve line breaks in default value 266 | 267 | * Added functionality from django-constance-cli 268 | 269 | * Added "Reset to default" feature 270 | 271 | v1.3.3 (2016/09/17) 272 | ~~~~~~~~~~~~~~~~~~~ 273 | 274 | * Revert broken release 275 | 276 | v1.3.2 (2016/09/17) 277 | ~~~~~~~~~~~~~~~~~~~ 278 | 279 | * Fixes a bug where the signal was sent for fields without changes 280 | 281 | v1.3.1 (2016/09/15) 282 | ~~~~~~~~~~~~~~~~~~~ 283 | 284 | * Improved the signal path to avoid import errors 285 | 286 | * Improved the admin layout when using fieldsets 287 | 288 | v1.3 (2016/09/14) 289 | ~~~~~~~~~~~~~~~~~ 290 | 291 | * **BACKWARD INCOMPATIBLE** Dropped support for Django < 1.8). 292 | 293 | * Added ordering constance fields using OrderedDict 294 | 295 | * Added a signal when updating constance fields 296 | 297 | v1.2.1 (2016/09/1) 298 | ~~~~~~~~~~~~~~~~~~ 299 | 300 | * Added some fixes to small bugs 301 | 302 | * Fix cache when key changes 303 | 304 | * Upgrade django_redis connection string 305 | 306 | * Autofill cache key if key is missing 307 | 308 | * Added support for fieldsets 309 | 310 | v1.2 (2016/05/14) 311 | ~~~~~~~~~~~~~~~~~ 312 | 313 | * Custom Fields were added as a new feature 314 | 315 | * Added documentation on how to use Custom settings form 316 | 317 | * Introduced ``CONSTANCE_IGNORE_ADMIN_VERSION_CHECK`` 318 | 319 | * Improved documentation for ``CONSTANCE_ADDITIONAL_FIELDS`` 320 | 321 | v1.1.2 (2016/02/08) 322 | ~~~~~~~~~~~~~~~~~~~ 323 | 324 | * Moved to Jazzband organization (https://github.com/jazzband/django-constance) 325 | 326 | * Added Custom Fields 327 | 328 | * Added Django 1.9 support to tests 329 | 330 | * Fixes icons for Django 1.9 admin 331 | 332 | v1.1.1 (2015/10/01) 333 | ~~~~~~~~~~~~~~~~~~~ 334 | 335 | * Fixed a regression in the 1.1 release that prevented the rendering of the 336 | admin view with constance values when using the context processor at the 337 | same time. 338 | 339 | v1.1 (2015/09/24) 340 | ~~~~~~~~~~~~~~~~~ 341 | 342 | * **BACKWARD INCOMPATIBLE** Dropped support for Python 2.6 343 | The supported versions are 2.7, 3.3 (on Django < 1.9) and 3.4. 344 | 345 | * **BACKWARD INCOMPATIBLE** Dropped support for Django 1.4, 1.5 and 1.6 346 | The supported versions are 1.7, 1.8 and the upcoming 1.9 release 347 | 348 | * Added compatibility to Django 1.8 and 1.9. 349 | 350 | * Added Spanish and Chinese (``zh_CN``) translations. 351 | 352 | * Added :class:`override_config` decorator/context manager for easy 353 | :doc:`testing `. 354 | 355 | * Added the ability to use linebreaks in config value help texts. 356 | 357 | * Various testing fixes. 358 | 359 | v1.0.1 (2015/01/07) 360 | ~~~~~~~~~~~~~~~~~~~ 361 | 362 | * Fixed issue with import time side effect on Django >= 1.7. 363 | 364 | v1.0 (2014/12/04) 365 | ~~~~~~~~~~~~~~~~~ 366 | 367 | * Added docs and set up Read The Docs project: 368 | 369 | https://django-constance.readthedocs.io/ 370 | 371 | * Set up Transifex project for easier translations: 372 | 373 | https://www.transifex.com/projects/p/django-constance 374 | 375 | * Added autofill feature for the database backend cache which is enabled 376 | by default. 377 | 378 | * Added Django>=1.7 migrations and moved South migrations to own folder. 379 | Please upgrade to South>=1.0 to use the new South migration location. 380 | 381 | For Django 1.7 users that means running the following to fake the migration:: 382 | 383 | django-admin.py migrate database --fake 384 | 385 | * Added consistency check when saving config values in the admin to prevent 386 | accidentally overwriting other users' changes. 387 | 388 | * Fixed issue with South migration that would break on MySQL. 389 | 390 | * Fix compatibility with Django 1.6 and 1.7 and current master (to be 1.8). 391 | 392 | * Fixed clearing database cache en masse by applying prefix correctly. 393 | 394 | * Fixed a few translation related issues. 395 | 396 | * Switched to tox as test script. 397 | 398 | * Fixed a few minor cosmetic frontend issues 399 | (e.g. padding in admin table header). 400 | 401 | * Deprecated a few old settings: 402 | 403 | ============================== =================================== 404 | deprecated replacement 405 | ============================== =================================== 406 | ``CONSTANCE_CONNECTION_CLASS`` ``CONSTANCE_REDIS_CONNECTION_CLASS`` 407 | ``CONSTANCE_CONNECTION`` ``CONSTANCE_REDIS_CONNECTION`` 408 | ``CONSTANCE_PREFIX`` ``CONSTANCE_REDIS_PREFIX`` 409 | ============================== =================================== 410 | 411 | * The undocumented feature to use an environment variable called 412 | ``CONSTANCE_SETTINGS_MODULE`` to define which module to load 413 | settings from has been removed. 414 | 415 | v0.6 (2013/04/12) 416 | ~~~~~~~~~~~~~~~~~ 417 | 418 | * Added Python 3 support. Supported versions: 2.6, 2.7, 3.2 and 3.3. 419 | For Python 3.x the use of Django > 1.5.x is required. 420 | 421 | * Fixed a serious issue with ordering in the admin when using the database 422 | backend. Thanks, Bouke Haarsma. 423 | 424 | * Switch to django-discover-runner as test runner to be able to run on 425 | Python 3. 426 | 427 | * Fixed an issue with refering to static files in the admin interface 428 | when using Django < 1.4. 429 | 430 | v0.5 (2013/03/02) 431 | ~~~~~~~~~~~~~~~~~ 432 | 433 | * Fixed compatibility with Django 1.5's swappable model backends. 434 | 435 | * Converted the ``key`` field of the database backend to use a ``CharField`` 436 | with uniqueness instead of just ``TextField``. 437 | 438 | For South users we provide a migration for that change. First you 439 | have to "fake" the initial migration we've also added to this release:: 440 | 441 | django-admin.py migrate database --fake 0001 442 | 443 | After that you can run the rest of the migrations:: 444 | 445 | django-admin.py migrate database 446 | 447 | * Fixed compatibility with Django>1.4's way of refering to static files in 448 | the admin. 449 | 450 | * Added ability to add custom authorization checks via the new 451 | ``CONSTANCE_SUPERUSER_ONLY`` setting. 452 | 453 | * Added Polish translation. Thanks, Janusz Harkot. 454 | 455 | * Allow ``CONSTANCE_REDIS_CONNECTION`` being an URL instead of a dict. 456 | 457 | * Added ``CONSTANCE_DATABASE_PREFIX`` setting allow setting a key prefix. 458 | 459 | * Switched test runner to use django-nose. 460 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | import os 7 | import re 8 | import sys 9 | from datetime import datetime 10 | 11 | 12 | def get_version(): 13 | with open('../pyproject.toml') as f: 14 | for line in f: 15 | match = re.match(r'version = "(.*)"', line) 16 | if match: 17 | return match.group(1) 18 | return '0.0.0' 19 | 20 | 21 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') 22 | 23 | # If extensions (or modules to document with autodoc) are in another directory, 24 | # add these directories to sys.path here. If the directory is relative to the 25 | # documentation root, use os.path.abspath to make it absolute, like shown here. 26 | sys.path.insert(0, os.path.abspath('extensions')) 27 | sys.path.insert(0, os.path.abspath('..')) 28 | 29 | # -- Project information ----------------------------------------------------- 30 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 31 | 32 | project = 'django-constance' 33 | project_copyright = datetime.now().year.__str__() + ', Jazzband' 34 | 35 | # The full version, including alpha/beta/rc tags 36 | release = get_version() 37 | # The short X.Y version 38 | version = '.'.join(release.split('.')[:3]) 39 | 40 | # -- General configuration ------------------------------------------------ 41 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 42 | 43 | extensions = [ 44 | 'sphinx.ext.intersphinx', 45 | 'sphinx.ext.todo', 46 | 'sphinx_search.extension', 47 | 'settings', 48 | ] 49 | 50 | templates_path = ['_templates'] 51 | source_suffix = '.rst' 52 | root_doc = 'index' 53 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 54 | pygments_style = 'sphinx' 55 | html_last_updated_fmt = '' 56 | 57 | # -- Options for HTML output ------------------------------------------------- 58 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 59 | 60 | html_theme = 'sphinx_rtd_theme' 61 | html_static_path = ['_static'] 62 | htmlhelp_basename = 'django-constancedoc' 63 | 64 | # -- Options for LaTeX output --------------------------------------------- 65 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-latex-output 66 | 67 | latex_elements = {} 68 | 69 | latex_documents = [ 70 | ('index', 'django-constance.tex', 'django-constance Documentation', 'Jazzband', 'manual'), 71 | ] 72 | 73 | # -- Options for manual page output --------------------------------------- 74 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-manual-page-output 75 | 76 | man_pages = [('index', 'django-constance', 'django-constance Documentation', ['Jazzband'], 1)] 77 | 78 | # -- Options for Texinfo output ------------------------------------------- 79 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-texinfo-output 80 | 81 | texinfo_documents = [ 82 | ( 83 | 'index', 84 | 'django-constance', 85 | 'django-constance Documentation', 86 | 'Jazzband', 87 | 'django-constance', 88 | 'One line description of project.', 89 | 'Miscellaneous', 90 | ), 91 | ] 92 | 93 | # Example configuration for intersphinx: refer to the Python standard library. 94 | intersphinx_mapping = { 95 | 'python': ('https://docs.python.org/3', None), 96 | 'django': ('https://docs.djangoproject.com/en/dev/', 'https://docs.djangoproject.com/en/dev/_objects/'), 97 | } 98 | -------------------------------------------------------------------------------- /docs/extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/docs/extensions/__init__.py -------------------------------------------------------------------------------- /docs/extensions/settings.py: -------------------------------------------------------------------------------- 1 | def setup(app): 2 | app.add_crossref_type( 3 | directivename='setting', 4 | rolename='setting', 5 | indextemplate='pair: %s; setting', 6 | ) 7 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | readthedocs-sphinx-search==0.3.2 2 | sphinx==7.3.7 3 | sphinx-rtd-theme==2.0.0 4 | -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | 4 | Testing how your app behaves with different config values is achieved with the 5 | :class:`override_config` class. This intentionally mirrors the use of Django's 6 | :class:`~django.test.override_setting`. 7 | 8 | .. py:class:: override_config(**kwargs) 9 | 10 | Replaces key-value pairs in the config. 11 | Use as decorator or context manager. 12 | 13 | Usage 14 | ~~~~~ 15 | 16 | It can be used as a decorator at the :class:`~django.test.TestCase` level, the 17 | method level and also as a 18 | `context manager `_. 19 | 20 | .. code-block:: python 21 | 22 | from constance import config 23 | from constance.test import override_config 24 | 25 | from django.test import TestCase 26 | 27 | 28 | @override_config(YOUR_NAME="Arthur of Camelot") 29 | class ExampleTestCase(TestCase): 30 | 31 | def test_what_is_your_name(self): 32 | self.assertEqual(config.YOUR_NAME, "Arthur of Camelot") 33 | 34 | @override_config(YOUR_QUEST="To find the Holy Grail") 35 | def test_what_is_your_quest(self): 36 | self.assertEqual(config.YOUR_QUEST, "To find the Holy Grail") 37 | 38 | def test_what_is_your_favourite_color(self): 39 | with override_config(YOUR_FAVOURITE_COLOR="Blue?"): 40 | self.assertEqual(config.YOUR_FAVOURITE_COLOR, "Blue?") 41 | 42 | 43 | Pytest usage 44 | ~~~~~~~~~~~~ 45 | 46 | Django-constance provides pytest plugin that adds marker 47 | :class:`@pytest.mark.override_config()`. It handles config override for 48 | module/class/function, and automatically revert any changes made to the 49 | constance config values when test is completed. 50 | 51 | .. py:function:: pytest.mark.override_config(**kwargs) 52 | 53 | Specify different config values for the marked tests in kwargs. 54 | 55 | Module scope override 56 | 57 | .. code-block:: python 58 | 59 | pytestmark = pytest.mark.override_config(API_URL="/awesome/url/") 60 | 61 | def test_api_url_is_awesome(): 62 | ... 63 | 64 | Class/function scope 65 | 66 | .. code-block:: python 67 | 68 | from constance import config 69 | 70 | @pytest.mark.override_config(API_URL="/awesome/url/") 71 | class SomeClassTest: 72 | def test_is_awesome_url(self): 73 | assert config.API_URL == "/awesome/url/" 74 | 75 | @pytest.mark.override_config(API_URL="/another/awesome/url/") 76 | def test_another_awesome_url(self): 77 | assert config.API_URL == "/another/awesome/url/" 78 | 79 | If you want to use override as a context manager or decorator, consider using 80 | 81 | .. code-block:: python 82 | 83 | from constance.test.pytest import override_config 84 | 85 | def test_override_context_manager(): 86 | with override_config(BOOL_VALUE=False): 87 | ... 88 | # or 89 | @override_config(BOOL_VALUE=False) 90 | def test_override_context_manager(): 91 | ... 92 | 93 | Pytest fixture as function or method parameter. 94 | 95 | .. note:: No import needed as fixture is available globally. 96 | 97 | .. code-block:: python 98 | 99 | def test_api_url_is_awesome(override_config): 100 | with override_config(API_URL="/awesome/url/"): 101 | ... 102 | 103 | Any scope, auto-used fixture alternative can also be implemented like this 104 | 105 | .. code-block:: python 106 | 107 | @pytest.fixture(scope='module', autouse=True) # e.g. module scope 108 | def api_url(override_config): 109 | with override_config(API_URL="/awesome/url/"): 110 | yield 111 | 112 | 113 | Memory backend 114 | ~~~~~~~~~~~~~~ 115 | 116 | If you don't want to rely on any external services such as Redis or database when 117 | running your unittests you can select :class:`MemoryBackend` for a test Django settings file 118 | 119 | .. code-block:: python 120 | 121 | CONSTANCE_BACKEND = 'constance.backends.memory.MemoryBackend' 122 | 123 | It will provide simple thread-safe backend which will reset to default values after each 124 | test run. 125 | -------------------------------------------------------------------------------- /example/cheeseshop/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/example/cheeseshop/__init__.py -------------------------------------------------------------------------------- /example/cheeseshop/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/example/cheeseshop/apps/__init__.py -------------------------------------------------------------------------------- /example/cheeseshop/apps/catalog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/example/cheeseshop/apps/catalog/__init__.py -------------------------------------------------------------------------------- /example/cheeseshop/apps/catalog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from cheeseshop.apps.catalog.models import Brand 4 | 5 | admin.site.register(Brand) 6 | -------------------------------------------------------------------------------- /example/cheeseshop/apps/catalog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | from django.db import models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | dependencies = [] 7 | 8 | operations = [ 9 | migrations.CreateModel( 10 | name='Brand', 11 | fields=[ 12 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 13 | ('name', models.CharField(max_length=75)), 14 | ], 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /example/cheeseshop/apps/catalog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/example/cheeseshop/apps/catalog/migrations/__init__.py -------------------------------------------------------------------------------- /example/cheeseshop/apps/catalog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Brand(models.Model): 5 | name = models.CharField(max_length=75) 6 | -------------------------------------------------------------------------------- /example/cheeseshop/apps/storage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/example/cheeseshop/apps/storage/__init__.py -------------------------------------------------------------------------------- /example/cheeseshop/apps/storage/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from cheeseshop.apps.storage.models import Shelf 4 | from cheeseshop.apps.storage.models import Supply 5 | 6 | admin.site.register(Shelf) 7 | admin.site.register(Supply) 8 | -------------------------------------------------------------------------------- /example/cheeseshop/apps/storage/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | from django.db import models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | dependencies = [] 7 | 8 | operations = [ 9 | migrations.CreateModel( 10 | name='Shelf', 11 | fields=[ 12 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 13 | ('name', models.CharField(max_length=75)), 14 | ], 15 | options={ 16 | 'verbose_name_plural': 'shelves', 17 | }, 18 | ), 19 | migrations.CreateModel( 20 | name='Supply', 21 | fields=[ 22 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 23 | ('name', models.CharField(max_length=75)), 24 | ], 25 | options={ 26 | 'verbose_name_plural': 'supplies', 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /example/cheeseshop/apps/storage/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/example/cheeseshop/apps/storage/migrations/__init__.py -------------------------------------------------------------------------------- /example/cheeseshop/apps/storage/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Shelf(models.Model): 5 | name = models.CharField(max_length=75) 6 | 7 | class Meta: 8 | verbose_name_plural = 'shelves' 9 | 10 | 11 | class Supply(models.Model): 12 | name = models.CharField(max_length=75) 13 | 14 | class Meta: 15 | verbose_name_plural = 'supplies' 16 | -------------------------------------------------------------------------------- /example/cheeseshop/fields.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.forms import fields 4 | from django.forms import widgets 5 | 6 | 7 | class JsonField(fields.CharField): 8 | widget = widgets.Textarea 9 | 10 | def __init__(self, rows: int = 5, **kwargs): 11 | self.rows = rows 12 | super().__init__(**kwargs) 13 | 14 | def widget_attrs(self, widget: widgets.Widget): 15 | attrs = super().widget_attrs(widget) 16 | attrs['rows'] = self.rows 17 | return attrs 18 | 19 | def to_python(self, value): 20 | if value: 21 | return json.loads(value) 22 | return {} 23 | 24 | def prepare_value(self, value): 25 | return json.dumps(value) 26 | -------------------------------------------------------------------------------- /example/cheeseshop/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for cheeseshop project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/4.1/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/4.1/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | from datetime import date 14 | 15 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 16 | 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ 20 | 21 | SITE_ID = 1 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'hdx64#m+lnc_0ffoyehbk&7gk1&*9uar$pcfcm-%$km#p0$k=6' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = ( 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.sites', 40 | 'django.contrib.messages', 41 | 'django.contrib.staticfiles', 42 | 'cheeseshop.apps.catalog', 43 | 'cheeseshop.apps.storage', 44 | 'constance', 45 | ) 46 | 47 | MIDDLEWARE = ( 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | ) 54 | 55 | ROOT_URLCONF = 'cheeseshop.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'cheeseshop.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/4.1/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': '/tmp/cheeseshop.db', 83 | } 84 | } 85 | 86 | CONSTANCE_REDIS_CONNECTION = { 87 | 'host': 'localhost', 88 | 'port': 6379, 89 | 'db': 0, 90 | } 91 | 92 | CONSTANCE_ADDITIONAL_FIELDS = { 93 | 'yes_no_null_select': [ 94 | 'django.forms.fields.ChoiceField', 95 | {'widget': 'django.forms.Select', 'choices': ((None, '-----'), ('yes', 'Yes'), ('no', 'No'))}, 96 | ], 97 | 'email': ('django.forms.fields.EmailField',), 98 | 'json_field': ['cheeseshop.fields.JsonField'], 99 | 'image_field': ['django.forms.ImageField', {}], 100 | } 101 | 102 | CONSTANCE_CONFIG = { 103 | 'BANNER': ('The National Cheese Emporium', 'name of the shop'), 104 | 'OWNER': ('Mr. Henry Wensleydale', 'owner of the shop'), 105 | 'OWNER_EMAIL': ('henry@example.com', 'contact email for owner', 'email'), 106 | 'MUSICIANS': (4, 'number of musicians inside the shop'), 107 | 'DATE_ESTABLISHED': (date(1972, 11, 30), "the shop's first opening"), 108 | 'MY_SELECT_KEY': ('yes', 'select yes or no', 'yes_no_null_select'), 109 | 'MULTILINE': ('Line one\nLine two', 'multiline string'), 110 | 'JSON_DATA': ( 111 | {'a': 1_000, 'b': 'test', 'max': 30_000_000}, 112 | 'Some test data for json', 113 | 'json_field', 114 | ), 115 | 'LOGO': ( 116 | '', 117 | 'Logo image file', 118 | 'image_field', 119 | ), 120 | } 121 | 122 | CONSTANCE_CONFIG_FIELDSETS = { 123 | 'Cheese shop general info': [ 124 | 'BANNER', 125 | 'OWNER', 126 | 'OWNER_EMAIL', 127 | 'MUSICIANS', 128 | 'DATE_ESTABLISHED', 129 | 'LOGO', 130 | ], 131 | 'Awkward test settings': ['MY_SELECT_KEY', 'MULTILINE', 'JSON_DATA'], 132 | } 133 | 134 | CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' 135 | 136 | 137 | CACHES = { 138 | 'default': { 139 | 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 140 | 'LOCATION': '127.0.0.1:11211', 141 | } 142 | } 143 | CONSTANCE_DATABASE_CACHE_BACKEND = 'default' 144 | 145 | # Internationalization 146 | # https://docs.djangoproject.com/en/4.1/topics/i18n/ 147 | 148 | LANGUAGE_CODE = 'en-us' 149 | 150 | TIME_ZONE = 'America/Chicago' 151 | 152 | USE_I18N = True 153 | 154 | USE_L10N = True 155 | 156 | USE_TZ = True 157 | 158 | 159 | # Static files (CSS, JavaScript, Images) 160 | # https://docs.djangoproject.com/en/4.1/howto/static-files/ 161 | 162 | STATIC_URL = '/static/' 163 | 164 | MEDIA_URL = '/media/' 165 | 166 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 167 | 168 | CONSTANCE_FILE_ROOT = 'constance' 169 | 170 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 171 | -------------------------------------------------------------------------------- /example/cheeseshop/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import admin 3 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 4 | from django.urls import re_path 5 | 6 | admin.autodiscover() 7 | 8 | urlpatterns = [ 9 | re_path('admin/', admin.site.urls), 10 | ] 11 | 12 | if settings.DEBUG: 13 | urlpatterns += staticfiles_urlpatterns() 14 | -------------------------------------------------------------------------------- /example/cheeseshop/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cheeseshop.settings') 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cheeseshop.settings') 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=3.2 2 | Pillow 3 | pymemcache 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel", "setuptools_scm>=8"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "django-constance" 7 | dynamic = ["version"] 8 | description = "Django live settings with pluggable backends, including Redis." 9 | readme = "README.rst" 10 | license = { text = "BSD" } 11 | requires-python = ">=3.8" 12 | authors = [ 13 | { name = "Jannis Leidel", email = "jannis@leidel.info" }, 14 | ] 15 | keywords = ["django", "libraries", "redis", "settings"] 16 | classifiers = [ 17 | "Development Status :: 5 - Production/Stable", 18 | "Environment :: Web Environment", 19 | "Framework :: Django", 20 | "Framework :: Django :: 4.2", 21 | "Framework :: Django :: 5.0", 22 | "Intended Audience :: Developers", 23 | "License :: OSI Approved :: BSD License", 24 | "Natural Language :: English", 25 | "Operating System :: OS Independent", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3 :: Only", 29 | "Programming Language :: Python :: 3.8", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | "Programming Language :: Python :: 3.12", 34 | "Programming Language :: Python :: 3.13", 35 | "Programming Language :: Python :: Implementation :: CPython", 36 | "Topic :: Utilities", 37 | ] 38 | 39 | [project.optional-dependencies] 40 | redis = [ 41 | "redis", 42 | ] 43 | 44 | [project.entry-points.pytest11] 45 | pytest-django-constance = "constance.test.pytest" 46 | 47 | [project.urls] 48 | homepage = "https://github.com/jazzband/django-constance/" 49 | documentation = "https://django-constance.readthedocs.io/en/latest/" 50 | repository = "https://github.com/jazzband/django-constance/" 51 | changelog = "https://github.com/jazzband/django-constance/releases/" 52 | 53 | [tool.setuptools] 54 | license-files = [] # see https://github.com/pypa/twine/issues/1216#issuecomment-2609745412 55 | 56 | [tool.setuptools.packages.find] 57 | include = ["constance*"] 58 | 59 | [tool.setuptools_scm] 60 | version_file = "constance/_version.py" 61 | 62 | [tool.ruff] 63 | line-length = 120 64 | indent-width = 4 65 | 66 | [tool.ruff.format] 67 | quote-style = "single" 68 | indent-style = "space" 69 | skip-magic-trailing-comma = false 70 | line-ending = "auto" 71 | 72 | [tool.ruff.lint] 73 | select = [ 74 | "B", 75 | "D", 76 | "E", 77 | "ERA", 78 | "EXE", 79 | "F", 80 | "FBT", 81 | "FURB", 82 | "G", 83 | "FA", 84 | "I", 85 | "ICN", 86 | "INP", 87 | "LOG", 88 | "PGH", 89 | "RET", 90 | "RUF", 91 | "S", 92 | "SIM", 93 | "TID", 94 | "UP", 95 | "W", 96 | ] 97 | ignore = ["D1", "D203", "D205", "D415", "D212", "RUF012", "D400", "D401"] 98 | 99 | [tool.ruff.lint.per-file-ignores] 100 | "docs/*" = ["INP"] 101 | "example/*" = ["S"] 102 | "tests/*" = ["S"] 103 | 104 | [tool.ruff.lint.isort] 105 | force-single-line = true 106 | 107 | [tool.ruff.lint.flake8-boolean-trap] 108 | extend-allowed-calls = ["unittest.mock.patch", "django.db.models.Value"] 109 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/tests/__init__.py -------------------------------------------------------------------------------- /tests/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-constance/6970708e05d769b2e424c4c22790eb2fd778ca88/tests/backends/__init__.py -------------------------------------------------------------------------------- /tests/backends/test_database.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from constance import settings 4 | from tests.storage import StorageTestsMixin 5 | 6 | 7 | class TestDatabase(StorageTestsMixin, TestCase): 8 | def setUp(self): 9 | self.old_backend = settings.BACKEND 10 | settings.BACKEND = 'constance.backends.database.DatabaseBackend' 11 | super().setUp() 12 | 13 | def test_database_queries(self): 14 | # Read and set to default value 15 | with self.assertNumQueries(5): 16 | self.assertEqual(self.config.INT_VALUE, 1) 17 | 18 | # Read again 19 | with self.assertNumQueries(1): 20 | self.assertEqual(self.config.INT_VALUE, 1) 21 | 22 | # Set value 23 | with self.assertNumQueries(2): 24 | self.config.INT_VALUE = 15 25 | 26 | def tearDown(self): 27 | settings.BACKEND = self.old_backend 28 | -------------------------------------------------------------------------------- /tests/backends/test_memory.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from constance import settings 4 | from tests.storage import StorageTestsMixin 5 | 6 | 7 | class TestMemory(StorageTestsMixin, TestCase): 8 | def setUp(self): 9 | self.old_backend = settings.BACKEND 10 | settings.BACKEND = 'constance.backends.memory.MemoryBackend' 11 | super().setUp() 12 | self.config._backend._storage = {} 13 | 14 | def tearDown(self): 15 | self.config._backend._storage = {} 16 | settings.BACKEND = self.old_backend 17 | -------------------------------------------------------------------------------- /tests/backends/test_redis.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from constance import settings 4 | from tests.storage import StorageTestsMixin 5 | 6 | 7 | class TestRedis(StorageTestsMixin, TestCase): 8 | _BACKEND = 'constance.backends.redisd.RedisBackend' 9 | 10 | def setUp(self): 11 | self.old_backend = settings.BACKEND 12 | settings.BACKEND = self._BACKEND 13 | super().setUp() 14 | self.config._backend._rd.clear() 15 | 16 | def tearDown(self): 17 | self.config._backend._rd.clear() 18 | settings.BACKEND = self.old_backend 19 | 20 | 21 | class TestCachingRedis(TestRedis): 22 | _BACKEND = 'constance.backends.redisd.CachingRedisBackend' 23 | -------------------------------------------------------------------------------- /tests/redis_mockup.py: -------------------------------------------------------------------------------- 1 | class Connection(dict): 2 | def set(self, key, value): 3 | self[key] = value 4 | 5 | def mget(self, keys): 6 | return [self.get(key) for key in keys] 7 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from datetime import datetime 3 | from datetime import time 4 | from datetime import timedelta 5 | from decimal import Decimal 6 | 7 | SECRET_KEY = 'cheese' 8 | 9 | MIDDLEWARE = ( 10 | 'django.contrib.sessions.middleware.SessionMiddleware', 11 | 'django.middleware.common.CommonMiddleware', 12 | 'django.middleware.csrf.CsrfViewMiddleware', 13 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 14 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 15 | 'django.contrib.messages.middleware.MessageMiddleware', 16 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 17 | ) 18 | 19 | DATABASE_ENGINE = 'sqlite3' 20 | 21 | DATABASES = { 22 | 'default': { 23 | 'ENGINE': 'django.db.backends.sqlite3', 24 | 'NAME': ':memory:', 25 | }, 26 | 'secondary': { 27 | 'ENGINE': 'django.db.backends.sqlite3', 28 | 'NAME': ':memory:', 29 | }, 30 | } 31 | 32 | INSTALLED_APPS = ( 33 | 'django.contrib.admin', 34 | 'django.contrib.staticfiles', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'constance', 40 | 'constance.backends.database', 41 | ) 42 | 43 | ROOT_URLCONF = 'tests.urls' 44 | 45 | CONSTANCE_REDIS_CONNECTION_CLASS = 'tests.redis_mockup.Connection' 46 | 47 | CONSTANCE_ADDITIONAL_FIELDS = { 48 | 'yes_no_null_select': [ 49 | 'django.forms.fields.ChoiceField', 50 | {'widget': 'django.forms.Select', 'choices': ((None, '-----'), ('yes', 'Yes'), ('no', 'No'))}, 51 | ], 52 | # note this intentionally uses a tuple so that we can test immutable 53 | 'email': ('django.forms.fields.EmailField',), 54 | 'array': ['django.forms.fields.CharField', {'widget': 'django.forms.Textarea'}], 55 | 'json': ['django.forms.fields.CharField', {'widget': 'django.forms.Textarea'}], 56 | } 57 | 58 | USE_TZ = True 59 | 60 | CONSTANCE_CONFIG = { 61 | 'INT_VALUE': (1, 'some int'), 62 | 'BOOL_VALUE': (True, 'true or false'), 63 | 'STRING_VALUE': ('Hello world', 'greetings'), 64 | 'DECIMAL_VALUE': (Decimal('0.1'), 'the first release version'), 65 | 'DATETIME_VALUE': (datetime(2010, 8, 23, 11, 29, 24), 'time of the first commit'), 66 | 'FLOAT_VALUE': (3.1415926536, 'PI'), 67 | 'DATE_VALUE': (date(2010, 12, 24), 'Merry Chrismas'), 68 | 'TIME_VALUE': (time(23, 59, 59), 'And happy New Year'), 69 | 'TIMEDELTA_VALUE': (timedelta(days=1, hours=2, minutes=3), 'Interval'), 70 | 'CHOICE_VALUE': ('yes', 'select yes or no', 'yes_no_null_select'), 71 | 'LINEBREAK_VALUE': ('Spam spam', 'eggs\neggs'), 72 | 'EMAIL_VALUE': ('test@example.com', 'An email', 'email'), 73 | 'LIST_VALUE': ([1, '1', date(2019, 1, 1)], 'A list', 'array'), 74 | 'JSON_VALUE': ( 75 | { 76 | 'key': 'value', 77 | 'key2': 2, 78 | 'key3': [1, 2, 3], 79 | 'key4': {'key': 'value'}, 80 | 'key5': date(2019, 1, 1), 81 | 'key6': None, 82 | }, 83 | 'A JSON object', 84 | 'json', 85 | ), 86 | } 87 | 88 | DEBUG = True 89 | 90 | STATIC_ROOT = './static/' 91 | 92 | STATIC_URL = '/static/' 93 | 94 | TEMPLATES = [ 95 | { 96 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 97 | 'DIRS': [], 98 | 'APP_DIRS': True, 99 | 'OPTIONS': { 100 | 'context_processors': [ 101 | 'django.template.context_processors.debug', 102 | 'django.template.context_processors.i18n', 103 | 'django.template.context_processors.request', 104 | 'django.template.context_processors.static', 105 | 'django.contrib.auth.context_processors.auth', 106 | 'django.contrib.messages.context_processors.messages', 107 | 'constance.context_processors.config', 108 | ], 109 | }, 110 | }, 111 | ] 112 | -------------------------------------------------------------------------------- /tests/storage.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from datetime import datetime 3 | from datetime import time 4 | from datetime import timedelta 5 | from decimal import Decimal 6 | 7 | from constance import settings 8 | from constance.base import Config 9 | 10 | 11 | class StorageTestsMixin: 12 | def setUp(self): 13 | self.config = Config() 14 | super().setUp() 15 | 16 | def test_store(self): 17 | self.assertEqual(self.config.INT_VALUE, 1) 18 | self.assertEqual(self.config.BOOL_VALUE, True) 19 | self.assertEqual(self.config.STRING_VALUE, 'Hello world') 20 | self.assertEqual(self.config.DECIMAL_VALUE, Decimal('0.1')) 21 | self.assertEqual(self.config.DATETIME_VALUE, datetime(2010, 8, 23, 11, 29, 24)) 22 | self.assertEqual(self.config.FLOAT_VALUE, 3.1415926536) 23 | self.assertEqual(self.config.DATE_VALUE, date(2010, 12, 24)) 24 | self.assertEqual(self.config.TIME_VALUE, time(23, 59, 59)) 25 | self.assertEqual(self.config.TIMEDELTA_VALUE, timedelta(days=1, hours=2, minutes=3)) 26 | self.assertEqual(self.config.CHOICE_VALUE, 'yes') 27 | self.assertEqual(self.config.EMAIL_VALUE, 'test@example.com') 28 | self.assertEqual(self.config.LIST_VALUE, [1, '1', date(2019, 1, 1)]) 29 | self.assertEqual( 30 | self.config.JSON_VALUE, 31 | { 32 | 'key': 'value', 33 | 'key2': 2, 34 | 'key3': [1, 2, 3], 35 | 'key4': {'key': 'value'}, 36 | 'key5': date(2019, 1, 1), 37 | 'key6': None, 38 | }, 39 | ) 40 | 41 | # set values 42 | self.config.INT_VALUE = 100 43 | self.config.BOOL_VALUE = False 44 | self.config.STRING_VALUE = 'Beware the weeping angel' 45 | self.config.DECIMAL_VALUE = Decimal('1.2') 46 | self.config.DATETIME_VALUE = datetime(1977, 10, 2) 47 | self.config.FLOAT_VALUE = 2.718281845905 48 | self.config.DATE_VALUE = date(2001, 12, 20) 49 | self.config.TIME_VALUE = time(1, 59, 0) 50 | self.config.TIMEDELTA_VALUE = timedelta(days=2, hours=3, minutes=4) 51 | self.config.CHOICE_VALUE = 'no' 52 | self.config.EMAIL_VALUE = 'foo@bar.com' 53 | self.config.LIST_VALUE = [1, date(2020, 2, 2)] 54 | self.config.JSON_VALUE = {'key': 'OK'} 55 | 56 | # read again 57 | self.assertEqual(self.config.INT_VALUE, 100) 58 | self.assertEqual(self.config.BOOL_VALUE, False) 59 | self.assertEqual(self.config.STRING_VALUE, 'Beware the weeping angel') 60 | self.assertEqual(self.config.DECIMAL_VALUE, Decimal('1.2')) 61 | self.assertEqual(self.config.DATETIME_VALUE, datetime(1977, 10, 2)) 62 | self.assertEqual(self.config.FLOAT_VALUE, 2.718281845905) 63 | self.assertEqual(self.config.DATE_VALUE, date(2001, 12, 20)) 64 | self.assertEqual(self.config.TIME_VALUE, time(1, 59, 0)) 65 | self.assertEqual(self.config.TIMEDELTA_VALUE, timedelta(days=2, hours=3, minutes=4)) 66 | self.assertEqual(self.config.CHOICE_VALUE, 'no') 67 | self.assertEqual(self.config.EMAIL_VALUE, 'foo@bar.com') 68 | self.assertEqual(self.config.LIST_VALUE, [1, date(2020, 2, 2)]) 69 | self.assertEqual(self.config.JSON_VALUE, {'key': 'OK'}) 70 | 71 | def test_nonexistent(self): 72 | self.assertRaises(AttributeError, getattr, self.config, 'NON_EXISTENT') 73 | 74 | with self.assertRaises(AttributeError): 75 | self.config.NON_EXISTENT = 1 76 | 77 | def test_missing_values(self): 78 | # set some values and leave out others 79 | self.config.BOOL_VALUE = False 80 | self.config.DECIMAL_VALUE = Decimal('1.2') 81 | self.config.DATETIME_VALUE = datetime(1977, 10, 2) 82 | self.config.DATE_VALUE = date(2001, 12, 20) 83 | self.config.TIME_VALUE = time(1, 59, 0) 84 | 85 | self.assertEqual(self.config.INT_VALUE, 1) # this should be the default value 86 | self.assertEqual(self.config.BOOL_VALUE, False) 87 | self.assertEqual(self.config.STRING_VALUE, 'Hello world') # this should be the default value 88 | self.assertEqual(self.config.DECIMAL_VALUE, Decimal('1.2')) 89 | self.assertEqual(self.config.DATETIME_VALUE, datetime(1977, 10, 2)) 90 | self.assertEqual(self.config.FLOAT_VALUE, 3.1415926536) # this should be the default value 91 | self.assertEqual(self.config.DATE_VALUE, date(2001, 12, 20)) 92 | self.assertEqual(self.config.TIME_VALUE, time(1, 59, 0)) 93 | self.assertEqual(self.config.TIMEDELTA_VALUE, timedelta(days=1, hours=2, minutes=3)) 94 | 95 | def test_backend_retrieves_multiple_values(self): 96 | # Check corner cases such as falsy values 97 | self.config.INT_VALUE = 0 98 | self.config.BOOL_VALUE = False 99 | self.config.STRING_VALUE = '' 100 | 101 | values = dict(self.config._backend.mget(settings.CONFIG)) 102 | self.assertEqual(values['INT_VALUE'], 0) 103 | self.assertEqual(values['BOOL_VALUE'], False) 104 | self.assertEqual(values['STRING_VALUE'], '') 105 | 106 | def test_backend_does_not_return_none_values(self): 107 | result = dict(self.config._backend.mget(settings.CONFIG)) 108 | self.assertEqual(result, {}) 109 | -------------------------------------------------------------------------------- /tests/test_admin.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from unittest import mock 3 | 4 | from django.contrib import admin 5 | from django.contrib.auth.models import Permission 6 | from django.contrib.auth.models import User 7 | from django.core.exceptions import PermissionDenied 8 | from django.http import HttpResponseRedirect 9 | from django.template.defaultfilters import linebreaksbr 10 | from django.test import RequestFactory 11 | from django.test import TestCase 12 | from django.utils.translation import gettext_lazy as _ 13 | 14 | from constance import settings 15 | from constance.admin import Config 16 | from constance.forms import ConstanceForm 17 | from constance.utils import get_values 18 | 19 | 20 | class TestAdmin(TestCase): 21 | model = Config 22 | 23 | def setUp(self): 24 | super().setUp() 25 | self.rf = RequestFactory() 26 | self.superuser = User.objects.create_superuser('admin', 'nimda', 'a@a.cz') 27 | self.normaluser = User.objects.create_user('normal', 'nimda', 'b@b.cz') 28 | self.normaluser.is_staff = True 29 | self.normaluser.save() 30 | self.options = admin.site._registry[self.model] 31 | 32 | def test_changelist(self): 33 | self.client.login(username='admin', password='nimda') 34 | request = self.rf.get('/admin/constance/config/') 35 | request.user = self.superuser 36 | response = self.options.changelist_view(request, {}) 37 | self.assertEqual(response.status_code, 200) 38 | 39 | def test_custom_auth(self): 40 | settings.SUPERUSER_ONLY = False 41 | self.client.login(username='normal', password='nimda') 42 | request = self.rf.get('/admin/constance/config/') 43 | request.user = self.normaluser 44 | self.assertRaises(PermissionDenied, self.options.changelist_view, request, {}) 45 | self.assertFalse(request.user.has_perm('constance.change_config')) 46 | 47 | # reload user to reset permission cache 48 | request = self.rf.get('/admin/constance/config/') 49 | request.user = User.objects.get(pk=self.normaluser.pk) 50 | 51 | request.user.user_permissions.add(Permission.objects.get(codename='change_config')) 52 | self.assertTrue(request.user.has_perm('constance.change_config')) 53 | 54 | response = self.options.changelist_view(request, {}) 55 | self.assertEqual(response.status_code, 200) 56 | 57 | def test_linebreaks(self): 58 | self.client.login(username='admin', password='nimda') 59 | request = self.rf.get('/admin/constance/config/') 60 | request.user = self.superuser 61 | response = self.options.changelist_view(request, {}) 62 | self.assertContains(response, 'LINEBREAK_VALUE') 63 | self.assertContains(response, linebreaksbr('eggs\neggs')) 64 | 65 | @mock.patch( 66 | 'constance.settings.CONFIG_FIELDSETS', 67 | { 68 | 'Numbers': ('INT_VALUE',), 69 | 'Text': ('STRING_VALUE',), 70 | }, 71 | ) 72 | def test_fieldset_headers(self): 73 | self.client.login(username='admin', password='nimda') 74 | request = self.rf.get('/admin/constance/config/') 75 | request.user = self.superuser 76 | response = self.options.changelist_view(request, {}) 77 | self.assertContains(response, '

Numbers

') 78 | self.assertContains(response, '

Text

') 79 | 80 | @mock.patch( 81 | 'constance.settings.CONFIG_FIELDSETS', 82 | ( 83 | ('Numbers', ('INT_VALUE',)), 84 | ('Text', ('STRING_VALUE',)), 85 | ), 86 | ) 87 | def test_fieldset_tuple(self): 88 | self.client.login(username='admin', password='nimda') 89 | request = self.rf.get('/admin/constance/config/') 90 | request.user = self.superuser 91 | response = self.options.changelist_view(request, {}) 92 | self.assertContains(response, '

Numbers

') 93 | self.assertContains(response, '

Text

') 94 | 95 | @mock.patch( 96 | 'constance.settings.CONFIG_FIELDSETS', 97 | { 98 | 'Numbers': { 99 | 'fields': ( 100 | 'INT_VALUE', 101 | 'DECIMAL_VALUE', 102 | ), 103 | 'collapse': True, 104 | }, 105 | 'Text': { 106 | 'fields': ( 107 | 'STRING_VALUE', 108 | 'LINEBREAK_VALUE', 109 | ), 110 | 'collapse': True, 111 | }, 112 | }, 113 | ) 114 | def test_collapsed_fieldsets(self): 115 | self.client.login(username='admin', password='nimda') 116 | request = self.rf.get('/admin/constance/config/') 117 | request.user = self.superuser 118 | response = self.options.changelist_view(request, {}) 119 | self.assertContains(response, 'module collapse') 120 | 121 | @mock.patch('constance.settings.CONFIG_FIELDSETS', {'FieldSetOne': ('INT_VALUE',)}) 122 | @mock.patch( 123 | 'constance.settings.CONFIG', 124 | { 125 | 'INT_VALUE': (1, 'some int'), 126 | }, 127 | ) 128 | @mock.patch('constance.settings.IGNORE_ADMIN_VERSION_CHECK', True) 129 | @mock.patch('constance.forms.ConstanceForm.save', lambda _: None) 130 | @mock.patch('constance.forms.ConstanceForm.is_valid', lambda _: True) 131 | def test_submit(self): 132 | """ 133 | Test that submitting the admin page results in an http redirect when 134 | everything is in order. 135 | """ 136 | initial_value = {'INT_VALUE': settings.CONFIG['INT_VALUE'][0]} 137 | 138 | self.client.login(username='admin', password='nimda') 139 | 140 | request = self.rf.post( 141 | '/admin/constance/config/', 142 | data={ 143 | **initial_value, 144 | 'version': '123', 145 | }, 146 | ) 147 | 148 | request.user = self.superuser 149 | request._dont_enforce_csrf_checks = True 150 | 151 | with mock.patch('django.contrib.messages.add_message') as mock_message, mock.patch.object( 152 | ConstanceForm, '__init__', **initial_value, return_value=None 153 | ) as mock_form: 154 | response = self.options.changelist_view(request, {}) 155 | mock_form.assert_called_with(data=request.POST, files=request.FILES, initial=initial_value, request=request) 156 | mock_message.assert_called_with(request, 25, _('Live settings updated successfully.')) 157 | 158 | self.assertIsInstance(response, HttpResponseRedirect) 159 | 160 | @mock.patch('constance.settings.CONFIG_FIELDSETS', {'FieldSetOne': ('MULTILINE',)}) 161 | @mock.patch( 162 | 'constance.settings.CONFIG', 163 | { 164 | 'MULTILINE': ('Hello\nWorld', 'multiline value'), 165 | }, 166 | ) 167 | @mock.patch('constance.settings.IGNORE_ADMIN_VERSION_CHECK', True) 168 | def test_newlines_normalization(self): 169 | self.client.login(username='admin', password='nimda') 170 | request = self.rf.post( 171 | '/admin/constance/config/', 172 | data={ 173 | 'MULTILINE': 'Hello\r\nWorld', 174 | 'version': '123', 175 | }, 176 | ) 177 | request.user = self.superuser 178 | request._dont_enforce_csrf_checks = True 179 | with mock.patch('django.contrib.messages.add_message'): 180 | response = self.options.changelist_view(request, {}) 181 | self.assertIsInstance(response, HttpResponseRedirect) 182 | self.assertEqual(get_values()['MULTILINE'], 'Hello\nWorld') 183 | 184 | @mock.patch( 185 | 'constance.settings.CONFIG', 186 | { 187 | 'DATETIME_VALUE': (datetime(2019, 8, 7, 18, 40, 0), 'some naive datetime'), 188 | }, 189 | ) 190 | @mock.patch('constance.settings.IGNORE_ADMIN_VERSION_CHECK', True) 191 | @mock.patch('tests.redis_mockup.Connection.set', mock.MagicMock()) 192 | def test_submit_aware_datetime(self): 193 | """ 194 | Test that submitting the admin page results in an http redirect when 195 | everything is in order. 196 | """ 197 | request = self.rf.post( 198 | '/admin/constance/config/', 199 | data={ 200 | 'DATETIME_VALUE_0': '2019-08-07', 201 | 'DATETIME_VALUE_1': '19:17:01', 202 | 'version': '123', 203 | }, 204 | ) 205 | request.user = self.superuser 206 | request._dont_enforce_csrf_checks = True 207 | with mock.patch('django.contrib.messages.add_message'): 208 | response = self.options.changelist_view(request, {}) 209 | self.assertIsInstance(response, HttpResponseRedirect) 210 | 211 | @mock.patch( 212 | 'constance.settings.CONFIG_FIELDSETS', 213 | { 214 | 'Numbers': ('INT_VALUE',), 215 | 'Text': ('STRING_VALUE',), 216 | }, 217 | ) 218 | def test_inconsistent_fieldset_submit(self): 219 | """ 220 | Test that the admin page warns users if the CONFIG_FIELDSETS setting 221 | doesn't account for every field in CONFIG. 222 | """ 223 | self.client.login(username='admin', password='nimda') 224 | request = self.rf.post('/admin/constance/config/', data=None) 225 | request.user = self.superuser 226 | request._dont_enforce_csrf_checks = True 227 | with mock.patch('django.contrib.messages.add_message'): 228 | response = self.options.changelist_view(request, {}) 229 | self.assertContains(response, 'is missing field(s)') 230 | 231 | @mock.patch( 232 | 'constance.settings.CONFIG_FIELDSETS', 233 | { 234 | 'Fieldsets': ( 235 | 'STRING_VALUE', 236 | 'INT_VALUE', 237 | ), 238 | }, 239 | ) 240 | def test_fieldset_ordering_1(self): 241 | """Ordering of inner list should be preserved.""" 242 | self.client.login(username='admin', password='nimda') 243 | request = self.rf.get('/admin/constance/config/') 244 | request.user = self.superuser 245 | response = self.options.changelist_view(request, {}) 246 | response.render() 247 | content_str = response.content.decode() 248 | self.assertGreater(content_str.find('INT_VALUE'), content_str.find('STRING_VALUE')) 249 | 250 | @mock.patch( 251 | 'constance.settings.CONFIG_FIELDSETS', 252 | { 253 | 'Fieldsets': ( 254 | 'INT_VALUE', 255 | 'STRING_VALUE', 256 | ), 257 | }, 258 | ) 259 | def test_fieldset_ordering_2(self): 260 | """Ordering of inner list should be preserved.""" 261 | self.client.login(username='admin', password='nimda') 262 | request = self.rf.get('/admin/constance/config/') 263 | request.user = self.superuser 264 | response = self.options.changelist_view(request, {}) 265 | response.render() 266 | content_str = response.content.decode() 267 | self.assertGreater(content_str.find('STRING_VALUE'), content_str.find('INT_VALUE')) 268 | 269 | def test_labels(self): 270 | self.assertEqual(type(self.model._meta.label), str) 271 | self.assertEqual(type(self.model._meta.label_lower), str) 272 | -------------------------------------------------------------------------------- /tests/test_checks.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from django.test import TestCase 4 | 5 | from constance import settings 6 | from constance.checks import check_fieldsets 7 | from constance.checks import get_inconsistent_fieldnames 8 | 9 | 10 | class ChecksTestCase(TestCase): 11 | @mock.patch('constance.settings.CONFIG_FIELDSETS', {'Set1': settings.CONFIG.keys()}) 12 | def test_get_inconsistent_fieldnames_none(self): 13 | """ 14 | Test that get_inconsistent_fieldnames returns an empty data and no checks fail 15 | if CONFIG_FIELDSETS accounts for every key in settings.CONFIG. 16 | """ 17 | missing_keys, extra_keys = get_inconsistent_fieldnames() 18 | self.assertFalse(missing_keys) 19 | self.assertFalse(extra_keys) 20 | 21 | @mock.patch( 22 | 'constance.settings.CONFIG_FIELDSETS', 23 | {'Set1': list(settings.CONFIG.keys())[:-1]}, 24 | ) 25 | def test_get_inconsistent_fieldnames_for_missing_keys(self): 26 | """ 27 | Test that get_inconsistent_fieldnames returns data and the check fails 28 | if CONFIG_FIELDSETS does not account for every key in settings.CONFIG. 29 | """ 30 | missing_keys, extra_keys = get_inconsistent_fieldnames() 31 | self.assertTrue(missing_keys) 32 | self.assertFalse(extra_keys) 33 | self.assertEqual(1, len(check_fieldsets())) 34 | 35 | @mock.patch( 36 | 'constance.settings.CONFIG_FIELDSETS', 37 | {'Set1': [*settings.CONFIG.keys(), 'FORGOTTEN_KEY']}, 38 | ) 39 | def test_get_inconsistent_fieldnames_for_extra_keys(self): 40 | """ 41 | Test that get_inconsistent_fieldnames returns data and the check fails 42 | if CONFIG_FIELDSETS contains extra key that is absent in settings.CONFIG. 43 | """ 44 | missing_keys, extra_keys = get_inconsistent_fieldnames() 45 | self.assertFalse(missing_keys) 46 | self.assertTrue(extra_keys) 47 | self.assertEqual(1, len(check_fieldsets())) 48 | 49 | @mock.patch('constance.settings.CONFIG_FIELDSETS', {}) 50 | def test_check_fieldsets(self): 51 | """check_fieldsets should not output warning if CONFIG_FIELDSETS is not defined.""" 52 | del settings.CONFIG_FIELDSETS 53 | self.assertEqual(0, len(check_fieldsets())) 54 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | from datetime import datetime 3 | from io import StringIO 4 | from textwrap import dedent 5 | 6 | from django.conf import settings 7 | from django.core.management import CommandError 8 | from django.core.management import call_command 9 | from django.test import TransactionTestCase 10 | from django.utils import timezone 11 | from django.utils.encoding import smart_str 12 | 13 | from constance import config 14 | from constance.models import Constance 15 | 16 | 17 | class CliTestCase(TransactionTestCase): 18 | def setUp(self): 19 | self.out = StringIO() 20 | 21 | def test_help(self): 22 | with contextlib.suppress(SystemExit): 23 | call_command('constance', '--help') 24 | 25 | def test_list(self): 26 | call_command('constance', 'list', stdout=self.out) 27 | 28 | self.assertEqual( 29 | set(self.out.getvalue().splitlines()), 30 | set( 31 | dedent( 32 | smart_str( 33 | """ BOOL_VALUE\tTrue 34 | EMAIL_VALUE\ttest@example.com 35 | INT_VALUE\t1 36 | LINEBREAK_VALUE\tSpam spam 37 | DATE_VALUE\t2010-12-24 38 | TIME_VALUE\t23:59:59 39 | TIMEDELTA_VALUE\t1 day, 2:03:00 40 | STRING_VALUE\tHello world 41 | CHOICE_VALUE\tyes 42 | DECIMAL_VALUE\t0.1 43 | DATETIME_VALUE\t2010-08-23 11:29:24 44 | FLOAT_VALUE\t3.1415926536 45 | JSON_VALUE\t{'key': 'value', 'key2': 2, 'key3': [1, 2, 3], 'key4': {'key': 'value'}, 'key5': datetime.date(2019, 1, 1), 'key6': None} 46 | LIST_VALUE\t[1, '1', datetime.date(2019, 1, 1)] 47 | """ # noqa: E501 48 | ) 49 | ).splitlines() 50 | ), 51 | ) 52 | 53 | def test_get(self): 54 | call_command('constance', *('get EMAIL_VALUE'.split()), stdout=self.out) 55 | 56 | self.assertEqual(self.out.getvalue().strip(), 'test@example.com') 57 | 58 | def test_set(self): 59 | call_command('constance', *('set EMAIL_VALUE blah@example.com'.split()), stdout=self.out) 60 | 61 | self.assertEqual(config.EMAIL_VALUE, 'blah@example.com') 62 | 63 | call_command('constance', *('set', 'DATETIME_VALUE', '2011-09-24', '12:30:25'), stdout=self.out) 64 | 65 | expected = datetime(2011, 9, 24, 12, 30, 25) 66 | if settings.USE_TZ: 67 | expected = timezone.make_aware(expected) 68 | self.assertEqual(config.DATETIME_VALUE, expected) 69 | 70 | def test_get_invalid_name(self): 71 | self.assertRaisesMessage( 72 | CommandError, 73 | 'NOT_A_REAL_CONFIG is not defined in settings.CONSTANCE_CONFIG', 74 | call_command, 75 | 'constance', 76 | 'get', 77 | 'NOT_A_REAL_CONFIG', 78 | ) 79 | 80 | def test_set_invalid_name(self): 81 | self.assertRaisesMessage( 82 | CommandError, 83 | 'NOT_A_REAL_CONFIG is not defined in settings.CONSTANCE_CONFIG', 84 | call_command, 85 | 'constance', 86 | 'set', 87 | 'NOT_A_REAL_CONFIG', 88 | 'foo', 89 | ) 90 | 91 | def test_set_invalid_value(self): 92 | self.assertRaisesMessage( 93 | CommandError, 94 | 'Enter a valid email address.', 95 | call_command, 96 | 'constance', 97 | 'set', 98 | 'EMAIL_VALUE', 99 | 'not a valid email', 100 | ) 101 | 102 | def test_set_invalid_multi_value(self): 103 | self.assertRaisesMessage( 104 | CommandError, 105 | 'Enter a list of values.', 106 | call_command, 107 | 'constance', 108 | 'set', 109 | 'DATETIME_VALUE', 110 | '2011-09-24 12:30:25', 111 | ) 112 | 113 | def test_delete_stale_records(self): 114 | initial_count = Constance.objects.count() 115 | 116 | Constance.objects.create(key='STALE_KEY', value=None) 117 | call_command('constance', 'remove_stale_keys', stdout=self.out) 118 | 119 | self.assertEqual(Constance.objects.count(), initial_count, msg=self.out) 120 | -------------------------------------------------------------------------------- /tests/test_codecs.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from datetime import date 3 | from datetime import datetime 4 | from datetime import time 5 | from datetime import timedelta 6 | from decimal import Decimal 7 | from unittest import TestCase 8 | 9 | from constance.codecs import dumps 10 | from constance.codecs import loads 11 | from constance.codecs import register_type 12 | 13 | 14 | class TestJSONSerialization(TestCase): 15 | def setUp(self): 16 | self.datetime = datetime(2023, 10, 5, 15, 30, 0) 17 | self.date = date(2023, 10, 5) 18 | self.time = time(15, 30, 0) 19 | self.decimal = Decimal('10.5') 20 | self.uuid = uuid.UUID('12345678123456781234567812345678') 21 | self.string = 'test' 22 | self.integer = 42 23 | self.float = 3.14 24 | self.boolean = True 25 | self.none = None 26 | self.timedelta = timedelta(days=1, hours=2, minutes=3) 27 | self.list = [1, 2, self.date] 28 | self.dict = {'key': self.date, 'key2': 1} 29 | 30 | def test_serializes_and_deserializes_default_types(self): 31 | self.assertEqual(dumps(self.datetime), '{"__type__": "datetime", "__value__": "2023-10-05T15:30:00"}') 32 | self.assertEqual(dumps(self.date), '{"__type__": "date", "__value__": "2023-10-05"}') 33 | self.assertEqual(dumps(self.time), '{"__type__": "time", "__value__": "15:30:00"}') 34 | self.assertEqual(dumps(self.decimal), '{"__type__": "decimal", "__value__": "10.5"}') 35 | self.assertEqual(dumps(self.uuid), '{"__type__": "uuid", "__value__": "12345678123456781234567812345678"}') 36 | self.assertEqual(dumps(self.string), '{"__type__": "default", "__value__": "test"}') 37 | self.assertEqual(dumps(self.integer), '{"__type__": "default", "__value__": 42}') 38 | self.assertEqual(dumps(self.float), '{"__type__": "default", "__value__": 3.14}') 39 | self.assertEqual(dumps(self.boolean), '{"__type__": "default", "__value__": true}') 40 | self.assertEqual(dumps(self.none), '{"__type__": "default", "__value__": null}') 41 | self.assertEqual(dumps(self.timedelta), '{"__type__": "timedelta", "__value__": 93780.0}') 42 | self.assertEqual( 43 | dumps(self.list), 44 | '{"__type__": "default", "__value__": [1, 2, {"__type__": "date", "__value__": "2023-10-05"}]}', 45 | ) 46 | self.assertEqual( 47 | dumps(self.dict), 48 | '{"__type__": "default", "__value__": {"key": {"__type__": "date", "__value__": "2023-10-05"}, "key2": 1}}', 49 | ) 50 | for t in ( 51 | self.datetime, 52 | self.date, 53 | self.time, 54 | self.decimal, 55 | self.uuid, 56 | self.string, 57 | self.integer, 58 | self.float, 59 | self.boolean, 60 | self.none, 61 | self.timedelta, 62 | self.dict, 63 | self.list, 64 | ): 65 | self.assertEqual(t, loads(dumps(t))) 66 | 67 | def test_invalid_deserialization(self): 68 | with self.assertRaisesRegex(ValueError, 'Expecting value'): 69 | loads('THIS_IS_NOT_RIGHT') 70 | with self.assertRaisesRegex(ValueError, 'Invalid object'): 71 | loads('{"__type__": "THIS_IS_NOT_RIGHT", "__value__": "test", "THIS_IS_NOT_RIGHT": "THIS_IS_NOT_RIGHT"}') 72 | with self.assertRaisesRegex(ValueError, 'Unsupported type'): 73 | loads('{"__type__": "THIS_IS_NOT_RIGHT", "__value__": "test"}') 74 | 75 | def test_handles_unknown_type(self): 76 | class UnknownType: 77 | pass 78 | 79 | with self.assertRaisesRegex(TypeError, 'Object of type UnknownType is not JSON serializable'): 80 | dumps(UnknownType()) 81 | 82 | def test_custom_type_serialization(self): 83 | class CustomType: 84 | def __init__(self, value): 85 | self.value = value 86 | 87 | register_type(CustomType, 'custom', lambda o: o.value, lambda o: CustomType(o)) 88 | custom_data = CustomType('test') 89 | json_data = dumps(custom_data) 90 | self.assertEqual(json_data, '{"__type__": "custom", "__value__": "test"}') 91 | deserialized_data = loads(json_data) 92 | self.assertTrue(isinstance(deserialized_data, CustomType)) 93 | self.assertEqual(deserialized_data.value, 'test') 94 | 95 | def test_register_known_type(self): 96 | with self.assertRaisesRegex(ValueError, 'Discriminator must be specified'): 97 | register_type(int, '', lambda o: o.value, lambda o: int(o)) 98 | with self.assertRaisesRegex(ValueError, 'Type with discriminator default is already registered'): 99 | register_type(int, 'default', lambda o: o.value, lambda o: int(o)) 100 | register_type(int, 'new_custom_type', lambda o: o.value, lambda o: int(o)) 101 | with self.assertRaisesRegex(ValueError, 'Type with discriminator new_custom_type is already registered'): 102 | register_type(int, 'new_custom_type', lambda o: o.value, lambda o: int(o)) 103 | 104 | def test_nested_collections(self): 105 | data = {'key': [[[[{'key': self.date}]]]]} 106 | self.assertEqual( 107 | dumps(data), 108 | ( 109 | '{"__type__": "default", ' 110 | '"__value__": {"key": [[[[{"key": {"__type__": "date", "__value__": "2023-10-05"}}]]]]}}' 111 | ), 112 | ) 113 | self.assertEqual(data, loads(dumps(data))) 114 | -------------------------------------------------------------------------------- /tests/test_form.py: -------------------------------------------------------------------------------- 1 | from django.forms import fields 2 | from django.test import TestCase 3 | 4 | from constance.forms import ConstanceForm 5 | 6 | 7 | class TestForm(TestCase): 8 | def test_form_field_types(self): 9 | f = ConstanceForm({}) 10 | 11 | self.assertIsInstance(f.fields['INT_VALUE'], fields.IntegerField) 12 | self.assertIsInstance(f.fields['BOOL_VALUE'], fields.BooleanField) 13 | self.assertIsInstance(f.fields['STRING_VALUE'], fields.CharField) 14 | self.assertIsInstance(f.fields['DECIMAL_VALUE'], fields.DecimalField) 15 | self.assertIsInstance(f.fields['DATETIME_VALUE'], fields.SplitDateTimeField) 16 | self.assertIsInstance(f.fields['TIMEDELTA_VALUE'], fields.DurationField) 17 | self.assertIsInstance(f.fields['FLOAT_VALUE'], fields.FloatField) 18 | self.assertIsInstance(f.fields['DATE_VALUE'], fields.DateField) 19 | self.assertIsInstance(f.fields['TIME_VALUE'], fields.TimeField) 20 | 21 | # from CONSTANCE_ADDITIONAL_FIELDS 22 | self.assertIsInstance(f.fields['CHOICE_VALUE'], fields.ChoiceField) 23 | self.assertIsInstance(f.fields['EMAIL_VALUE'], fields.EmailField) 24 | -------------------------------------------------------------------------------- /tests/test_pytest_overrides.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | try: 4 | import pytest 5 | 6 | from constance import config 7 | from constance.test.pytest import override_config 8 | 9 | class TestPytestOverrideConfigFunctionDecorator: 10 | """ 11 | Test that the override_config decorator works correctly for Pytest classes. 12 | 13 | Test usage of override_config on test method and as context manager. 14 | """ 15 | 16 | def test_default_value_is_true(self): 17 | """Assert that the default value of config.BOOL_VALUE is True.""" 18 | assert config.BOOL_VALUE 19 | 20 | @pytest.mark.override_config(BOOL_VALUE=False) 21 | def test_override_config_on_method_changes_config_value(self): 22 | """Assert that the pytest mark decorator changes config.BOOL_VALUE.""" 23 | assert not config.BOOL_VALUE 24 | 25 | def test_override_config_as_context_manager_changes_config_value(self): 26 | """Assert that the context manager changes config.BOOL_VALUE.""" 27 | with override_config(BOOL_VALUE=False): 28 | assert not config.BOOL_VALUE 29 | 30 | assert config.BOOL_VALUE 31 | 32 | @override_config(BOOL_VALUE=False) 33 | def test_method_decorator(self): 34 | """Ensure `override_config` can be used as test method decorator.""" 35 | assert not config.BOOL_VALUE 36 | 37 | @pytest.mark.override_config(BOOL_VALUE=False) 38 | class TestPytestOverrideConfigDecorator: 39 | """Test that the override_config decorator works on classes.""" 40 | 41 | def test_override_config_on_class_changes_config_value(self): 42 | """Assert that the class decorator changes config.BOOL_VALUE.""" 43 | assert not config.BOOL_VALUE 44 | 45 | @pytest.mark.override_config(BOOL_VALUE='True') 46 | def test_override_config_on_overridden_value(self): 47 | """Ensure that method mark decorator changes already overridden value for class.""" 48 | assert config.BOOL_VALUE == 'True' 49 | 50 | def test_fixture_override_config(override_config): 51 | """ 52 | Ensure `override_config` fixture is available globally 53 | and can be used in test functions. 54 | """ 55 | with override_config(BOOL_VALUE=False): 56 | assert not config.BOOL_VALUE 57 | 58 | @override_config(BOOL_VALUE=False) 59 | def test_func_decorator(): 60 | """Ensure `override_config` can be used as test function decorator.""" 61 | assert not config.BOOL_VALUE 62 | 63 | except ImportError: 64 | pass 65 | 66 | 67 | class PytestTests(unittest.TestCase): 68 | def setUp(self): 69 | self.skipTest('Skip all pytest tests when using unittest') 70 | 71 | def test_do_not_skip_silently(self): 72 | """If no at least one test present, unittest silently skips module.""" 73 | pass 74 | -------------------------------------------------------------------------------- /tests/test_test_overrides.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from constance import config 4 | from constance.test import override_config 5 | 6 | 7 | class OverrideConfigFunctionDecoratorTestCase(TestCase): 8 | """ 9 | Test that the override_config decorator works correctly. 10 | 11 | Test usage of override_config on test method and as context manager. 12 | """ 13 | 14 | def test_default_value_is_true(self): 15 | """Assert that the default value of config.BOOL_VALUE is True.""" 16 | self.assertTrue(config.BOOL_VALUE) 17 | 18 | @override_config(BOOL_VALUE=False) 19 | def test_override_config_on_method_changes_config_value(self): 20 | """Assert that the method decorator changes config.BOOL_VALUE.""" 21 | self.assertFalse(config.BOOL_VALUE) 22 | 23 | def test_override_config_as_context_manager_changes_config_value(self): 24 | """Assert that the context manager changes config.BOOL_VALUE.""" 25 | with override_config(BOOL_VALUE=False): 26 | self.assertFalse(config.BOOL_VALUE) 27 | 28 | self.assertTrue(config.BOOL_VALUE) 29 | 30 | 31 | @override_config(BOOL_VALUE=False) 32 | class OverrideConfigClassDecoratorTestCase(TestCase): 33 | """Test that the override_config decorator works on classes.""" 34 | 35 | def test_override_config_on_class_changes_config_value(self): 36 | """Assert that the class decorator changes config.BOOL_VALUE.""" 37 | self.assertFalse(config.BOOL_VALUE) 38 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from decimal import Decimal 3 | 4 | from django.core.exceptions import ValidationError 5 | from django.test import TestCase 6 | 7 | from constance.management.commands.constance import _set_constance_value 8 | from constance.utils import get_values 9 | from constance.utils import get_values_for_keys 10 | 11 | 12 | class UtilsTestCase(TestCase): 13 | def test_set_value_validation(self): 14 | self.assertRaisesMessage(ValidationError, 'Enter a whole number.', _set_constance_value, 'INT_VALUE', 'foo') 15 | self.assertRaisesMessage( 16 | ValidationError, 'Enter a valid email address.', _set_constance_value, 'EMAIL_VALUE', 'not a valid email' 17 | ) 18 | self.assertRaisesMessage( 19 | ValidationError, 20 | 'Enter a valid date.', 21 | _set_constance_value, 22 | 'DATETIME_VALUE', 23 | ( 24 | '2000-00-00', 25 | '99:99:99', 26 | ), 27 | ) 28 | self.assertRaisesMessage( 29 | ValidationError, 30 | 'Enter a valid time.', 31 | _set_constance_value, 32 | 'DATETIME_VALUE', 33 | ( 34 | '2016-01-01', 35 | '99:99:99', 36 | ), 37 | ) 38 | 39 | def test_get_values(self): 40 | self.assertEqual( 41 | get_values(), 42 | { 43 | 'FLOAT_VALUE': 3.1415926536, 44 | 'BOOL_VALUE': True, 45 | 'EMAIL_VALUE': 'test@example.com', 46 | 'INT_VALUE': 1, 47 | 'CHOICE_VALUE': 'yes', 48 | 'TIME_VALUE': datetime.time(23, 59, 59), 49 | 'DATE_VALUE': datetime.date(2010, 12, 24), 50 | 'TIMEDELTA_VALUE': datetime.timedelta(days=1, hours=2, minutes=3), 51 | 'LINEBREAK_VALUE': 'Spam spam', 52 | 'DECIMAL_VALUE': Decimal('0.1'), 53 | 'STRING_VALUE': 'Hello world', 54 | 'DATETIME_VALUE': datetime.datetime(2010, 8, 23, 11, 29, 24), 55 | 'LIST_VALUE': [1, '1', datetime.date(2019, 1, 1)], 56 | 'JSON_VALUE': { 57 | 'key': 'value', 58 | 'key2': 2, 59 | 'key3': [1, 2, 3], 60 | 'key4': {'key': 'value'}, 61 | 'key5': datetime.date(2019, 1, 1), 62 | 'key6': None, 63 | }, 64 | }, 65 | ) 66 | 67 | def test_get_values_for_keys(self): 68 | self.assertEqual( 69 | get_values_for_keys(['BOOL_VALUE', 'CHOICE_VALUE', 'LINEBREAK_VALUE']), 70 | { 71 | 'BOOL_VALUE': True, 72 | 'CHOICE_VALUE': 'yes', 73 | 'LINEBREAK_VALUE': 'Spam spam', 74 | }, 75 | ) 76 | 77 | def test_get_values_for_keys_empty_keys(self): 78 | result = get_values_for_keys([]) 79 | self.assertEqual(result, {}) 80 | 81 | def test_get_values_for_keys_throw_error_if_no_key(self): 82 | self.assertRaisesMessage( 83 | AttributeError, 84 | '"OLD_VALUE, BOLD_VALUE" keys not found in configuration.', 85 | get_values_for_keys, 86 | ['BOOL_VALUE', 'OLD_VALUE', 'BOLD_VALUE'], 87 | ) 88 | 89 | def test_get_values_for_keys_invalid_input_type(self): 90 | with self.assertRaises(TypeError): 91 | get_values_for_keys('key1') 92 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | ] 7 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build = true 3 | envlist = 4 | py{38,39,310,311,312}-dj{42}-{unittest,pytest,checkmigrations} 5 | py{310,311,312}-dj{50}-{unittest,pytest,checkmigrations} 6 | py{310,311,312,313}-dj{51}-{unittest,pytest,checkmigrations} 7 | py{310,311,312,313}-dj{52}-{unittest,pytest,checkmigrations} 8 | py{310,311,312,313}-dj{main}-{unittest,pytest,checkmigrations} 9 | skip_missing_interpreters = True 10 | 11 | [testenv] 12 | deps = 13 | redis 14 | coverage 15 | dj42: django>=4.2,<4.3 16 | dj50: django>=5.0,<5.1 17 | dj51: django>=5.1,<5.2 18 | dj52: django>=5.2,<5.3 19 | djmain: https://github.com/django/django/archive/main.tar.gz 20 | pytest: pytest 21 | pytest: pytest-cov 22 | pytest: pytest-django 23 | usedevelop = True 24 | ignore_outcome = 25 | djmain: True 26 | commands = 27 | unittest: coverage run {envbindir}/django-admin test -v2 28 | unittest: coverage report 29 | unittest: coverage xml 30 | pytest: pytest --cov=. --ignore=.tox --disable-pytest-warnings --cov-report=xml --cov-append {toxinidir} 31 | checkmigrations: django-admin makemigrations --check --dry-run 32 | setenv = 33 | PYTHONPATH = {toxinidir} 34 | PYTHONDONTWRITEBYTECODE = 1 35 | DJANGO_SETTINGS_MODULE = tests.settings 36 | 37 | [gh-actions] 38 | python = 39 | 3.8: py38 40 | 3.9: py39 41 | 3.10: py310 42 | 3.11: py311 43 | 3.12: py312 44 | 3.13: py313 45 | --------------------------------------------------------------------------------