diff --git a/.gitignore b/.gitignore index d5f90e3..99840f0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ dist/ test.db .tox .coverage +coverage.xml docs/_build .idea diff --git a/constance/apps.py b/constance/apps.py index a96eea4..895467e 100644 --- a/constance/apps.py +++ b/constance/apps.py @@ -1,45 +1,9 @@ -from django.db.models import signals -from django.apps import apps, AppConfig +from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ class ConstanceConfig(AppConfig): name = 'constance' verbose_name = _('Constance') + default_auto_field = 'django.db.models.AutoField' - def ready(self): - super().ready() - signals.post_migrate.connect(self.create_perm, - dispatch_uid='constance.create_perm') - - def create_perm(self, using=None, *args, **kwargs): - """ - Creates a fake content type and permission - to be able to check for permissions - """ - from django.conf import settings - - constance_dbs = getattr(settings, 'CONSTANCE_DBS', None) - if constance_dbs is not None and using not in constance_dbs: - return - if ( - apps.is_installed('django.contrib.contenttypes') and - apps.is_installed('django.contrib.auth') - ): - ContentType = apps.get_model('contenttypes.ContentType') - Permission = apps.get_model('auth.Permission') - content_type, created = ContentType.objects.using(using).get_or_create( - app_label='constance', - model='config', - ) - - Permission.objects.using(using).get_or_create( - content_type=content_type, - codename='change_config', - defaults={'name': 'Can change config'}, - ) - Permission.objects.using(using).get_or_create( - content_type=content_type, - codename='view_config', - defaults={'name': 'Can view config'}, - ) diff --git a/constance/backends/database/__init__.py b/constance/backends/database.py similarity index 97% rename from constance/backends/database/__init__.py rename to constance/backends/database.py index 1c725af..3b1ce30 100644 --- a/constance/backends/database/__init__.py +++ b/constance/backends/database.py @@ -9,13 +9,13 @@ from django.db import ( ) from django.db.models.signals import post_save -from .. import Backend -from ... import settings, signals, config +from constance.backends import Backend +from constance import settings, signals, config class DatabaseBackend(Backend): def __init__(self): - from .models import Constance + from constance.models import Constance self._model = Constance self._prefix = settings.DATABASE_PREFIX self._autofill_timeout = settings.DATABASE_CACHE_AUTOFILL_TIMEOUT diff --git a/constance/backends/database/apps.py b/constance/backends/database/apps.py deleted file mode 100644 index b92778b..0000000 --- a/constance/backends/database/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class ConstanceConfig(AppConfig): - name = 'constance.backends.database' - default_auto_field = 'django.db.models.AutoField' diff --git a/constance/backends/database/migrations/0002_auto_20190129_2304.py b/constance/backends/database/migrations/0002_auto_20190129_2304.py deleted file mode 100644 index 736798b..0000000 --- a/constance/backends/database/migrations/0002_auto_20190129_2304.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-30 04:04 - -from django.db import migrations -import picklefield.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('database', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='constance', - name='value', - field=picklefield.fields.PickledObjectField(blank=True, editable=False, null=True), - ), - ] diff --git a/constance/management/commands/constance.py b/constance/management/commands/constance.py index dd02793..7d44000 100644 --- a/constance/management/commands/constance.py +++ b/constance/management/commands/constance.py @@ -1,4 +1,3 @@ -from django.apps import apps from django.conf import settings from django.core.exceptions import ValidationError from django.core.management import BaseCommand, CommandError @@ -8,6 +7,7 @@ from django import VERSION from ... import config from ...admin import ConstanceForm, get_values +from ...models import Constance def _set_constance_value(key, value): @@ -49,7 +49,6 @@ class Command(BaseCommand): help='delete all Constance keys and their values if they are not in settings.CONSTANCE_CONFIG (stale keys)', ) - def _subparsers_add_parser(self, subparsers, name, **kwargs): # API in Django >= 2.1 changed and removed cmd parameter from add_parser if VERSION >= (2, 1) and 'cmd' in kwargs: @@ -82,23 +81,16 @@ class Command(BaseCommand): self.stdout.write("{}\t{}".format(k, v), ending="\n") elif command == 'remove_stale_keys': - try: - Constance = apps.get_model('database.Constance') - except LookupError: - Constance = None - if Constance: - actual_keys = settings.CONSTANCE_CONFIG.keys() + actual_keys = settings.CONSTANCE_CONFIG.keys() - stale_records = Constance.objects.exclude(key__in=actual_keys) - if stale_records: - self.stdout.write("The following record will be deleted:", ending="\n") - else: - self.stdout.write("There are no stale records in database.", ending="\n") - - for stale_record in stale_records: - self.stdout.write("{}\t{}".format(stale_record.key, stale_record.value), ending="\n") - - stale_records.delete() + stale_records = Constance.objects.exclude(key__in=actual_keys) + if stale_records: + self.stdout.write("The following record will be deleted:", ending="\n") else: - self.stdout.write("Database backend is not set. Nothing is deleted", ending="\n") + self.stdout.write("There are no stale records in database.", ending="\n") + + for stale_record in stale_records: + self.stdout.write("{}\t{}".format(stale_record.key, stale_record.value), ending="\n") + + stale_records.delete() diff --git a/constance/backends/database/migrations/0001_initial.py b/constance/migrations/0001_initial.py similarity index 51% rename from constance/backends/database/migrations/0001_initial.py rename to constance/migrations/0001_initial.py index 6a0400d..6eb0bc6 100644 --- a/constance/backends/database/migrations/0001_initial.py +++ b/constance/migrations/0001_initial.py @@ -1,24 +1,25 @@ -from django.db import models, migrations +from django.db import migrations, models import picklefield.fields class Migration(migrations.Migration): + + initial = True + dependencies = [] operations = [ migrations.CreateModel( name='Constance', fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, - auto_created=True, serialize=False)), - ('key', models.CharField(unique=True, max_length=255)), - ('value', picklefield.fields.PickledObjectField(editable=False)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(max_length=255, unique=True)), + ('value', picklefield.fields.PickledObjectField(blank=True, editable=False, null=True)), ], options={ 'verbose_name': 'constance', 'verbose_name_plural': 'constances', - 'db_table': 'constance_config', + 'permissions': [('change_config', 'Can change config'), ('view_config', 'Can view config')], }, - bases=(models.Model,), ), ] diff --git a/constance/migrations/0002_migrate_from_old_table.py b/constance/migrations/0002_migrate_from_old_table.py new file mode 100644 index 0000000..c3adabc --- /dev/null +++ b/constance/migrations/0002_migrate_from_old_table.py @@ -0,0 +1,33 @@ +from django.core.management.color import no_style +from django.db import migrations, connection, DatabaseError + + +def _migrate_from_old_table(apps, schema_editor) -> None: + """ + Copies values from old table. + On new installations just ignore error that table does not exist. + """ + try: + with connection.cursor() as cursor: + cursor.execute('INSERT INTO constance_constance ( id, key, value ) SELECT id, key, value FROM constance_config', []) + cursor.execute('DROP TABLE constance_config', []) + + except DatabaseError: + pass + + Constance = apps.get_model('constance', 'Constance') + sequence_sql = connection.ops.sequence_reset_sql(no_style(), [Constance]) + with connection.cursor() as cursor: + for sql in sequence_sql: + cursor.execute(sql) + + +class Migration(migrations.Migration): + + dependencies = [('constance', '0001_initial')] + + atomic = False + + operations = [ + migrations.RunPython(_migrate_from_old_table, reverse_code=lambda x, y: None), + ] diff --git a/constance/backends/database/migrations/__init__.py b/constance/migrations/__init__.py similarity index 100% rename from constance/backends/database/migrations/__init__.py rename to constance/migrations/__init__.py diff --git a/constance/backends/database/models.py b/constance/models.py similarity index 84% rename from constance/backends/database/models.py rename to constance/models.py index a28a0dc..e04c4ae 100644 --- a/constance/backends/database/models.py +++ b/constance/models.py @@ -18,7 +18,10 @@ class Constance(models.Model): class Meta: verbose_name = _('constance') verbose_name_plural = _('constances') - db_table = 'constance_config' + permissions = [ + ('change_config', 'Can change config'), + ('view_config', 'Can view config'), + ] def __str__(self): return self.key diff --git a/docs/backends.rst b/docs/backends.rst index 6f1df3a..4ebb9af 100644 --- a/docs/backends.rst +++ b/docs/backends.rst @@ -94,27 +94,17 @@ Defaults to `60` seconds. Database -------- -The database backend is optional and stores the configuration values in a +Database backend stores configuration values in a standard Django model. It requires the package `django-picklefield`_ for -storing those values. Please install it like so:: - - pip install django-constance[database] +storing those values. You must set the ``CONSTANCE_BACKEND`` Django setting to:: CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' -Then add the database backend app to your :setting:`INSTALLED_APPS` setting to -make sure the data model is correctly created:: - - INSTALLED_APPS = ( - # other apps - 'constance.backends.database', - ) - Please make sure to apply the database migrations:: - python manage.py migrate database + python manage.py migrate .. note:: If you're upgrading Constance to 1.0 and use Django 1.7 or higher please make sure to let the migration system know that you've @@ -124,10 +114,6 @@ Please make sure to apply the database migrations:: python manage.py migrate database --fake -.. note:: If you have multiple databases you can set what databases - will be used with ``CONSTANCE_DBS`` - - CONSTANCE_DBS = "default" Just like the Redis backend you can set an optional prefix that is used during database interactions (it defaults to an empty string, ``''``). To use diff --git a/docs/changes.rst b/docs/changes.rst index ed34de3..72a3452 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,6 +1,13 @@ Changelog --------- +v3.0.0 (future) +~~~~~~~~~~~~~~~~~~~ + +* Refactor database backend +Backward incompatible changes: +remove 'constance.backends.database' from INSTALLED_APPS + v2.10.0 (unreleased) ~~~~~~~~~~~~~~~~~~ diff --git a/example/cheeseshop/settings.py b/example/cheeseshop/settings.py index 76308e6..e71fd47 100644 --- a/example/cheeseshop/settings.py +++ b/example/cheeseshop/settings.py @@ -44,7 +44,6 @@ INSTALLED_APPS = ( 'cheeseshop.apps.catalog', 'cheeseshop.apps.storage', 'constance', - 'constance.backends.database', ) MIDDLEWARE = ( @@ -149,3 +148,5 @@ USE_TZ = True # https://docs.djangoproject.com/en/1.8/howto/static-files/ STATIC_URL = '/static/' + +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' diff --git a/setup.py b/setup.py index 872e32b..0472042 100644 --- a/setup.py +++ b/setup.py @@ -60,8 +60,10 @@ setup( include_package_data=True, zip_safe=False, python_requires='>=3.6', + install_requires=[ + 'django-picklefield', + ], extras_require={ - 'database': ['django-picklefield'], 'redis': ['redis'], }, entry_points={ diff --git a/tests/test_admin.py b/tests/test_admin.py index c80ce55..14b5047 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1,9 +1,8 @@ from datetime import datetime -import mock +from unittest import mock from django.contrib import admin from django.contrib.auth.models import User, Permission -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied from django.http import HttpResponseRedirect from django.template.defaultfilters import linebreaksbr @@ -53,10 +52,6 @@ class TestAdmin(TestCase): response = self.options.changelist_view(request, {}) self.assertEqual(response.status_code, 200) - def test_str(self): - ct = ContentType.objects.get(app_label='constance', model='config') - self.assertEqual(str(ct), 'config') - def test_linebreaks(self): self.client.login(username='admin', password='nimda') request = self.rf.get('/admin/constance/config/') diff --git a/tests/test_app.py b/tests/test_app.py deleted file mode 100644 index c2d305d..0000000 --- a/tests/test_app.py +++ /dev/null @@ -1,56 +0,0 @@ -from django.apps import apps -from django.contrib.auth.models import Permission -from django.contrib.contenttypes.models import ContentType -from django.db.models import signals -from django.test import TestCase, override_settings - - -class TestApp(TestCase): - databases = ["default", "secondary"] - - def setUp(self): - self.app_config = apps.get_app_config('constance') - - def test_post_migrate_signal_creates_content_type_and_permission_in_default_database(self): - self.assert_uses_correct_database('default') - - def test_post_migrate_signal_creates_content_type_and_permission_in_secondary_database(self): - self.assert_uses_correct_database('secondary') - - def test_uses_default_db_even_without_giving_using_keyword(self): - self.call_post_migrate(None) - - self.assert_content_type_and_permission_created('default') - - @override_settings(CONSTANCE_DBS=['default']) - def test_only_use_databases_in_constance_dbs(self): - Permission.objects.using('default').delete() - Permission.objects.using('secondary').delete() - self.assert_uses_correct_database('default') - with self.assertRaises(AssertionError): - self.assert_uses_correct_database('secondary') - - def assert_uses_correct_database(self, database_name): - self.call_post_migrate(database_name) - - self.assert_content_type_and_permission_created(database_name) - - def assert_content_type_and_permission_created(self, database_name): - content_type_queryset = ContentType.objects.filter(app_label=self.app_config.name) \ - .using(database_name) - - self.assertTrue(content_type_queryset.exists()) - - permission_queryset = Permission.objects.filter(content_type=content_type_queryset.get()) \ - .using(database_name).exists() - - self.assertTrue(permission_queryset) - - def call_post_migrate(self, database_name): - signals.post_migrate.send( - sender=self.app_config, - app_config=self.app_config, - verbosity=None, - interactive=None, - using=database_name - ) diff --git a/tests/test_checks.py b/tests/test_checks.py index 7883a71..1600a0c 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -1,6 +1,6 @@ import datetime from decimal import Decimal -import mock +from unittest import mock from constance.admin import get_values from constance.checks import check_fieldsets, get_inconsistent_fieldnames diff --git a/tests/test_cli.py b/tests/test_cli.py index 1a17187..65ff436 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,6 @@ from datetime import datetime from textwrap import dedent -from django.apps import apps from django.conf import settings from django.core.management import call_command, CommandError from django.test import TransactionTestCase @@ -10,6 +9,7 @@ from django.utils.encoding import smart_str from io import StringIO from constance import config +from constance.models import Constance class CliTestCase(TransactionTestCase): @@ -74,11 +74,10 @@ class CliTestCase(TransactionTestCase): call_command, 'constance', 'set', 'DATETIME_VALUE', '2011-09-24 12:30:25') def test_delete_stale_records(self): - Constance = apps.get_model('database.Constance') initial_count = Constance.objects.count() Constance.objects.create(key='STALE_KEY', value=None) call_command('constance', 'remove_stale_keys', stdout=self.out) - self.assertEqual(Constance.objects.count(), initial_count) + self.assertEqual(Constance.objects.count(), initial_count, msg=self.out) diff --git a/tox.ini b/tox.ini index 8d48431..6101b30 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,6 @@ envlist = deps = redis coverage - mock django-picklefield dj22: Django>=2.2,<3.0 dj30: Django>=3.0,<3.1