django-postgres-denormalize 1.0.0__tar.gz → 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/CHANGELOG.rst +5 -0
- {django_postgres_denormalize-1.0.0/src/django_postgres_denormalize.egg-info → django_postgres_denormalize-1.0.1}/PKG-INFO +1 -1
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/guides/drift.rst +3 -4
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/guides/exclusions.rst +8 -3
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/integrations/djanquiltdb.rst +3 -3
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/reference/checks.rst +6 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/reference/settings.rst +32 -1
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1/src/django_postgres_denormalize.egg-info}/PKG-INFO +1 -1
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/postgres_denormalize/__init__.py +1 -1
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/postgres_denormalize/checks.py +3 -1
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/postgres_denormalize/conf.py +32 -4
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/postgres_denormalize/fields.py +62 -32
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/isolated/test_fields.py +171 -1
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/test_conf.py +43 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/LICENSE +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/MANIFEST.in +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/README.rst +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/icon-128.png +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/icon-16.png +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/icon-256.png +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/icon-32.png +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/icon-48.png +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/icon-512.png +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/icon-64.png +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/icon-small.svg +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/icon.svg +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/social-preview.png +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/social-preview.svg +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/Makefile +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/_static/custom.css +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/_templates/layout.html +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/conf.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/getting_started/alternatives.rst +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/getting_started/installation.rst +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/getting_started/overview.rst +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/getting_started/when_to_use.rst +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/guides/batch_updates.rst +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/guides/fields.rst +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/guides/indexing.rst +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/guides/migrations.rst +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/guides/triggers.rst +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/index.rst +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/reference/api.rst +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/reference/changelog.rst +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/pyproject.toml +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/setup.cfg +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/django_postgres_denormalize.egg-info/SOURCES.txt +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/django_postgres_denormalize.egg-info/dependency_links.txt +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/django_postgres_denormalize.egg-info/requires.txt +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/django_postgres_denormalize.egg-info/top_level.txt +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/postgres_denormalize/apps.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/postgres_denormalize/batch.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/postgres_denormalize/chains.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/postgres_denormalize/operations.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/postgres_denormalize/py.typed +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/src/postgres_denormalize/triggers.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/catalog/__init__.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/catalog/apps.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/catalog/exclusions.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/catalog/migrations/0001_initial.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/catalog/migrations/__init__.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/catalog/models.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/config/__init__.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/config/settings.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/isolated/__init__.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/isolated/apps.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/isolated/test_checks.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/isolated/test_operations.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/isolated/test_triggers.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/manage.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/tenancy/__init__.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/tenancy/apps.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/tenancy/migrations/0001_initial.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/tenancy/migrations/__init__.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/tenancy/models.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/test_batch.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/test_chains.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/test_checks.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/test_fields.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/test_operations.py +0 -0
- {django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/test_triggers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-postgres-denormalize
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Trigger-maintained copies of ancestor columns for Django on PostgreSQL.
|
|
5
5
|
Author-email: DjanQuiltDB Project <djanquiltdb@portal42.net>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/guides/drift.rst
RENAMED
|
@@ -76,10 +76,9 @@ Before you run it on a large table, note the following:
|
|
|
76
76
|
leaf, gets a history row.
|
|
77
77
|
* **Unique constraints.** When a copy is part of a unique constraint, the correction can make two rows collide, and the
|
|
78
78
|
PostgreSQL error names only the first collision (see :ref:`collisions <migration_collisions>`).
|
|
79
|
-
* **Excluded models.** The call skips
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<excluded_model_copies>`).
|
|
79
|
+
* **Excluded models.** The call skips an excluded model, such as an audit table, because its rows record past values.
|
|
80
|
+
``also_fill`` writes the copies of such a model, but it overwrites every row with the current value. It is meant for
|
|
81
|
+
filling a new column, not for repairs (see :ref:`copies on excluded models <excluded_model_copies>`).
|
|
83
82
|
|
|
84
83
|
|
|
85
84
|
In a migration
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/guides/exclusions.rst
RENAMED
|
@@ -12,9 +12,14 @@ This package skips an excluded model entirely:
|
|
|
12
12
|
* it gets no triggers, so nothing ever rewrites its copies;
|
|
13
13
|
* :func:`~postgres_denormalize.fields.get_denormalized_fields` returns nothing for it, so the drift filter and the
|
|
14
14
|
backfill skip it (unless ``also_fill`` names it explicitly, see :doc:`/guides/migrations`);
|
|
15
|
-
* the project-wide system checks do not look at it
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
* the project-wide system checks do not look at it;
|
|
16
|
+
* its fields skip the field checks that compare the declaration with the lookup path, such as ``W009`` for an
|
|
17
|
+
``on_delete`` rule that differs from the path. ``EXCLUDE_MODELS_SILENCED_CHECKS`` chooses which ones (see
|
|
18
|
+
:doc:`/reference/settings`).
|
|
19
|
+
|
|
20
|
+
Its denormalized columns still get their type, so the table is created and migrated like any other. The type, and the
|
|
21
|
+
target of a foreign key copy, come from the lookup path, so the path must resolve on the excluded model too. The field
|
|
22
|
+
checks that report a path that does not resolve therefore still run by default.
|
|
18
23
|
|
|
19
24
|
Without either key, no model is excluded. An ``EXCLUDE_MODELS`` entry that names no installed model is reported
|
|
20
25
|
(``E401``, see :doc:`/reference/checks`).
|
|
@@ -7,9 +7,9 @@ Django project across PostgreSQL schemas and nodes. Both libraries work on their
|
|
|
7
7
|
does not need DjanQuiltDB, or any familiarity with it.
|
|
8
8
|
|
|
9
9
|
In a sharded project, the copies have to be checked and corrected in each shard, and a lookup path has to stay inside
|
|
10
|
-
the shard. DjanQuiltDB's
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
the shard. DjanQuiltDB's own system checks report a lookup path whose cascade trigger or fill trigger would write across
|
|
11
|
+
shards. DjanQuiltDB's ``postgres-denormalize`` extra installs a plugin with helpers that find and correct out-of-date
|
|
12
|
+
copies in one shard or in every shard::
|
|
13
13
|
|
|
14
14
|
pip install djanquiltdb[postgres-denormalize]
|
|
15
15
|
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/reference/checks.rst
RENAMED
|
@@ -36,6 +36,12 @@ Each denormalized field checks its own declaration:
|
|
|
36
36
|
See :doc:`/guides/fields`.
|
|
37
37
|
================================ =======================================================================================
|
|
38
38
|
|
|
39
|
+
A field on an excluded model (see :doc:`/guides/exclusions`) skips the checks that ``EXCLUDE_MODELS_SILENCED_CHECKS``
|
|
40
|
+
lists. By default, these are ``E002``, ``E003``, ``E004`` and ``W009``, which compare the declaration with the lookup
|
|
41
|
+
path. No trigger writes the column of an excluded model, so they say nothing useful there. The field still infers its
|
|
42
|
+
target, column type and ``on_delete`` rule from the path, so the other checks still run. See
|
|
43
|
+
:doc:`/reference/settings`.
|
|
44
|
+
|
|
39
45
|
|
|
40
46
|
Project checks
|
|
41
47
|
--------------
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/reference/settings.rst
RENAMED
|
@@ -22,7 +22,8 @@ EXCLUDE_MODELS
|
|
|
22
22
|
--------------
|
|
23
23
|
|
|
24
24
|
A list of model labels, ``'app_label.ModelName'``, matched case-insensitively. This package skips the models that it
|
|
25
|
-
lists: they get no triggers, no drift filter, no backfill and no project checks
|
|
25
|
+
lists: they get no triggers, no drift filter, no backfill and no project checks, and their fields skip the checks that
|
|
26
|
+
``EXCLUDE_MODELS_SILENCED_CHECKS`` lists. By default it lists nothing. An entry
|
|
26
27
|
that is not a string with exactly one dot raises ``ImproperlyConfigured``, and an entry that names no installed model is
|
|
27
28
|
reported by ``postgres_denormalize.E401``. See :doc:`/guides/exclusions`.
|
|
28
29
|
|
|
@@ -36,6 +37,36 @@ Use it when the models to exclude follow a rule, and not a list. A model is excl
|
|
|
36
37
|
callable, raise ``ImproperlyConfigured``. See :doc:`/guides/exclusions` for what the callback is called with.
|
|
37
38
|
|
|
38
39
|
|
|
40
|
+
EXCLUDE_MODELS_SILENCED_CHECKS
|
|
41
|
+
------------------------------
|
|
42
|
+
|
|
43
|
+
A list of the field checks (see :doc:`/reference/checks`) that a field on an excluded model skips, as check ids such as
|
|
44
|
+
``'postgres_denormalize.W009'``, the way ``SILENCED_SYSTEM_CHECKS`` names them. It does not change the checks of a
|
|
45
|
+
field on any other model. The default skips the checks that compare the declaration with the lookup path:
|
|
46
|
+
|
|
47
|
+
.. code-block:: python
|
|
48
|
+
|
|
49
|
+
# settings.py
|
|
50
|
+
POSTGRES_DENORMALIZE = {
|
|
51
|
+
'EXCLUDE_MODELS_SILENCED_CHECKS': [
|
|
52
|
+
'postgres_denormalize.E002',
|
|
53
|
+
'postgres_denormalize.E003',
|
|
54
|
+
'postgres_denormalize.E004',
|
|
55
|
+
'postgres_denormalize.W009',
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
No trigger writes the column of an excluded model, so these checks say nothing useful there. An audit package, for
|
|
60
|
+
example, declares ``on_delete=DO_NOTHING`` on every foreign key that it copies, and W009 would report each copy. The
|
|
61
|
+
field still infers its target, its column type and its ``on_delete`` rule from the lookup path, so by default it still
|
|
62
|
+
reports a path that cannot give them: ``E001`` and ``E005`` to ``E008``. An empty list runs every field check on
|
|
63
|
+
excluded models, and a list of all nine skips them all.
|
|
64
|
+
|
|
65
|
+
Only the ids of the field checks, ``E001`` to ``E008`` and ``W009``, can be listed, because the project checks skip
|
|
66
|
+
excluded models altogether. Any other entry, and a value that is not a list, raise ``ImproperlyConfigured``. Changing
|
|
67
|
+
the list changes no trigger, so ``makemigrations`` writes no migration for it.
|
|
68
|
+
|
|
69
|
+
|
|
39
70
|
REDUNDANT_COPY_CHECK
|
|
40
71
|
--------------------
|
|
41
72
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-postgres-denormalize
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Trigger-maintained copies of ancestor columns for Django on PostgreSQL.
|
|
5
5
|
Author-email: DjanQuiltDB Project <djanquiltdb@portal42.net>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
System checks that look at the project as a whole rather than at one field.
|
|
3
3
|
|
|
4
4
|
The field-level checks (a lookup path that does not resolve, a nullable hop, a target that differs from the path) are
|
|
5
|
-
part of each field's own ``check()``.
|
|
5
|
+
part of each field's own ``check()``. A field on an excluded model skips the field checks that
|
|
6
|
+
``EXCLUDE_MODELS_SILENCED_CHECKS`` lists. The checks here skip excluded models altogether. They are registered under the
|
|
7
|
+
``postgres_denormalize`` tag:
|
|
6
8
|
|
|
7
9
|
- ``E301``: pgtrigger neither writes triggers into migrations nor installs them after ``migrate``, so the triggers that
|
|
8
10
|
this package derives would never reach the database.
|
|
@@ -5,11 +5,15 @@ package reads it.
|
|
|
5
5
|
Every key is optional::
|
|
6
6
|
|
|
7
7
|
POSTGRES_DENORMALIZE = {
|
|
8
|
-
# Models that this package skips: no triggers, no drift filter, no backfill, no project checks
|
|
9
|
-
# models that have denormalized fields on purpose without keeping them in sync, such
|
|
10
|
-
# excluded when either key says so.
|
|
8
|
+
# Models that this package skips: no triggers, no drift filter, no backfill, no project checks, and only some
|
|
9
|
+
# field checks. This is for models that have denormalized fields on purpose without keeping them in sync, such
|
|
10
|
+
# as audit tables. A model is excluded when either key says so.
|
|
11
11
|
'EXCLUDE_MODELS': ['tenancy.TaskAudit'], # model labels, matched case-insensitively
|
|
12
12
|
'EXCLUDE_MODELS_CALLBACK': 'myproject.audit.is_audit_model', # a callable, or a dotted path to one
|
|
13
|
+
# The field checks that the fields of excluded models skip, as check ids. By default, these are E002, E003,
|
|
14
|
+
# E004 and W009, which compare the declaration with the lookup path. That matters only for a field that a
|
|
15
|
+
# trigger writes. Changing this list changes no trigger, so makemigrations writes no migration for it.
|
|
16
|
+
'EXCLUDE_MODELS_SILENCED_CHECKS': ['postgres_denormalize.W009'],
|
|
13
17
|
'REDUNDANT_COPY_CHECK': {
|
|
14
18
|
'LEVEL': 'warning', # or 'error', or 'off'
|
|
15
19
|
'APPS': None, # app labels to check; None checks every app outside site-packages
|
|
@@ -36,6 +40,12 @@ SETTING_NAME = 'POSTGRES_DENORMALIZE'
|
|
|
36
40
|
|
|
37
41
|
LEVELS = ('off', 'warning', 'error')
|
|
38
42
|
|
|
43
|
+
# The ids of the checks that each denormalized field runs on its own declaration. EXCLUDE_MODELS_SILENCED_CHECKS can
|
|
44
|
+
# list only these, because the project checks skip excluded models altogether.
|
|
45
|
+
FIELD_CHECKS = tuple(
|
|
46
|
+
f'postgres_denormalize.{code}' for code in ('E001', 'E002', 'E003', 'E004', 'E005', 'E006', 'E007', 'E008', 'W009')
|
|
47
|
+
)
|
|
48
|
+
|
|
39
49
|
DEFAULT_REDUNDANT_COPY_CHECK = {
|
|
40
50
|
'LEVEL': 'warning',
|
|
41
51
|
'APPS': None,
|
|
@@ -46,6 +56,14 @@ DEFAULT_REDUNDANT_COPY_CHECK = {
|
|
|
46
56
|
DEFAULTS = {
|
|
47
57
|
'EXCLUDE_MODELS': (),
|
|
48
58
|
'EXCLUDE_MODELS_CALLBACK': None,
|
|
59
|
+
# The checks that compare the declaration with the lookup path. The others find a path that gives the field no
|
|
60
|
+
# target, column type or on_delete rule, which a field on an excluded model needs too.
|
|
61
|
+
'EXCLUDE_MODELS_SILENCED_CHECKS': (
|
|
62
|
+
'postgres_denormalize.E002',
|
|
63
|
+
'postgres_denormalize.E003',
|
|
64
|
+
'postgres_denormalize.E004',
|
|
65
|
+
'postgres_denormalize.W009',
|
|
66
|
+
),
|
|
49
67
|
'REDUNDANT_COPY_CHECK': DEFAULT_REDUNDANT_COPY_CHECK,
|
|
50
68
|
'ORDERING_CHECK_LEVEL': 'warning',
|
|
51
69
|
'ISOLATION_CHECK_LEVEL': 'warning',
|
|
@@ -137,6 +155,16 @@ def validate_settings():
|
|
|
137
155
|
f'{SETTING_NAME}["EXCLUDE_MODELS_CALLBACK"] must be a callable or a dotted path to one.'
|
|
138
156
|
)
|
|
139
157
|
|
|
158
|
+
silenced = get_setting('EXCLUDE_MODELS_SILENCED_CHECKS')
|
|
159
|
+
if isinstance(silenced, str) or not isinstance(silenced, (list, tuple, set, frozenset)):
|
|
160
|
+
raise ImproperlyConfigured(f'{SETTING_NAME}["EXCLUDE_MODELS_SILENCED_CHECKS"] must be a list of check ids.')
|
|
161
|
+
unknown = [check_id for check_id in silenced if check_id not in FIELD_CHECKS]
|
|
162
|
+
if unknown:
|
|
163
|
+
raise ImproperlyConfigured(
|
|
164
|
+
f'{SETTING_NAME}["EXCLUDE_MODELS_SILENCED_CHECKS"] contains {", ".join(map(repr, unknown))}, but it can '
|
|
165
|
+
f'list only the field checks: {", ".join(FIELD_CHECKS)}.'
|
|
166
|
+
)
|
|
167
|
+
|
|
140
168
|
redundant = configured.get('REDUNDANT_COPY_CHECK', {})
|
|
141
169
|
if not isinstance(redundant, dict):
|
|
142
170
|
raise ImproperlyConfigured(f'{SETTING_NAME}["REDUNDANT_COPY_CHECK"] must be a dict.')
|
|
@@ -167,4 +195,4 @@ def _validate_level(key, value):
|
|
|
167
195
|
raise ImproperlyConfigured(f'{SETTING_NAME}["{key}"] is {value!r}; use one of {", ".join(LEVELS)}.')
|
|
168
196
|
|
|
169
197
|
|
|
170
|
-
__all__ = ['DEFAULTS', 'LEVELS', 'get_setting', 'is_excluded', 'validate_settings']
|
|
198
|
+
__all__ = ['DEFAULTS', 'FIELD_CHECKS', 'LEVELS', 'get_setting', 'is_excluded', 'validate_settings']
|
|
@@ -216,8 +216,26 @@ class DenormalizedMixin:
|
|
|
216
216
|
def check(self, **kwargs):
|
|
217
217
|
return [*super().check(**kwargs), *self._check_denormalize_from()]
|
|
218
218
|
|
|
219
|
+
def _silenced_checks(self):
|
|
220
|
+
"""
|
|
221
|
+
Return the ids of the checks that this field skips.
|
|
222
|
+
|
|
223
|
+
A field on an excluded model skips the checks that ``EXCLUDE_MODELS_SILENCED_CHECKS`` lists. No trigger writes
|
|
224
|
+
its column, so by default it skips the checks that compare the declaration with the lookup path. Its target,
|
|
225
|
+
column type and ``on_delete`` rule still come from the path, so the checks that find a path that cannot give
|
|
226
|
+
them still run.
|
|
227
|
+
"""
|
|
228
|
+
if conf.is_excluded(self.model):
|
|
229
|
+
return frozenset(conf.get_setting('EXCLUDE_MODELS_SILENCED_CHECKS'))
|
|
230
|
+
return frozenset()
|
|
231
|
+
|
|
219
232
|
def _check_denormalize_from(self):
|
|
233
|
+
# Each check reports the first problem that it finds. A silenced check is skipped, rather than its message
|
|
234
|
+
# dropped, so that the checks after it still run.
|
|
235
|
+
silenced = self._silenced_checks()
|
|
220
236
|
if '__' not in self.denormalize_from:
|
|
237
|
+
if 'postgres_denormalize.E001' in silenced:
|
|
238
|
+
return []
|
|
221
239
|
return self._error(
|
|
222
240
|
'postgres_denormalize.E001',
|
|
223
241
|
f'`denormalize_from` is `{self.denormalize_from}`, which is a column on the same row, not a column '
|
|
@@ -227,25 +245,30 @@ class DenormalizedMixin:
|
|
|
227
245
|
try:
|
|
228
246
|
chain = self.resolve_chain()
|
|
229
247
|
except PathRefused as refusal:
|
|
248
|
+
if refusal.code in silenced:
|
|
249
|
+
return []
|
|
230
250
|
return self._error(refusal.code, f'{refusal}.', refusal.hint)
|
|
231
251
|
except (FieldDoesNotExist, ValueError) as exception:
|
|
252
|
+
if 'postgres_denormalize.E001' in silenced:
|
|
253
|
+
return []
|
|
232
254
|
return self._error(
|
|
233
255
|
'postgres_denormalize.E001',
|
|
234
256
|
f'`denormalize_from` is `{self.denormalize_from}`, which does not resolve: {exception}.',
|
|
235
257
|
)
|
|
236
258
|
|
|
237
|
-
if chain[0].null:
|
|
259
|
+
if chain[0].null and 'postgres_denormalize.E002' not in silenced:
|
|
238
260
|
return self._error(
|
|
239
261
|
'postgres_denormalize.E002',
|
|
240
262
|
f'`denormalize_from` is `{self.denormalize_from}`, which starts at the nullable foreign key '
|
|
241
263
|
f'`{chain[0].name}`, so the trigger computes NULL for every row where `{chain[0].name}` is empty.',
|
|
242
264
|
)
|
|
243
265
|
|
|
244
|
-
return self._check_end_of_chain(chain)
|
|
266
|
+
return self._check_end_of_chain(chain, silenced)
|
|
245
267
|
|
|
246
|
-
def _check_end_of_chain(self, chain):
|
|
268
|
+
def _check_end_of_chain(self, chain, silenced):
|
|
247
269
|
"""
|
|
248
|
-
Check the field at the end of the lookup path
|
|
270
|
+
Check the field at the end of the lookup path, skipping the checks in ``silenced``. Each concrete class allows
|
|
271
|
+
different fields there.
|
|
249
272
|
"""
|
|
250
273
|
raise NotImplementedError
|
|
251
274
|
|
|
@@ -428,39 +451,42 @@ class DenormalizedForeignKey(DenormalizedMixin, models.ForeignKey):
|
|
|
428
451
|
return self._check_denormalize_from()
|
|
429
452
|
return super().check(**kwargs)
|
|
430
453
|
|
|
431
|
-
def _check_end_of_chain(self, chain):
|
|
454
|
+
def _check_end_of_chain(self, chain, silenced):
|
|
432
455
|
end = chain[-1].related_model._meta.label
|
|
433
|
-
if not
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
456
|
+
if 'postgres_denormalize.E003' not in silenced:
|
|
457
|
+
if not self.target_is_known:
|
|
458
|
+
return self._error(
|
|
459
|
+
'postgres_denormalize.E003',
|
|
460
|
+
f'The target of the field is not known, but `{self.denormalize_from}` ends at {end}.',
|
|
461
|
+
)
|
|
462
|
+
if chain[-1].related_model is not self.related_model:
|
|
463
|
+
return self._error(
|
|
464
|
+
'postgres_denormalize.E003',
|
|
465
|
+
f'The field points at {self.related_model._meta.label}, but `{self.denormalize_from}` ends at '
|
|
466
|
+
f'{end}.',
|
|
467
|
+
)
|
|
468
|
+
if chain[-1].target_field != self.target_field:
|
|
469
|
+
return self._error(
|
|
470
|
+
'postgres_denormalize.E003',
|
|
471
|
+
f'The field points at {end}.{self.target_field.name}, but `{self.denormalize_from}` ends at '
|
|
472
|
+
f'{end}.{chain[-1].target_field.name}. Declare the field with the same to_field as the last hop, '
|
|
473
|
+
f'or without one.',
|
|
474
|
+
)
|
|
450
475
|
|
|
451
476
|
nullable = [hop.name for hop in chain[1:] if hop.null]
|
|
452
|
-
if nullable and not self.null:
|
|
477
|
+
if nullable and not self.null and 'postgres_denormalize.E004' not in silenced:
|
|
453
478
|
return self._error(
|
|
454
479
|
'postgres_denormalize.E004',
|
|
455
480
|
f'`{self.denormalize_from}` crosses the nullable foreign key `{nullable[0]}`, so the trigger writes '
|
|
456
481
|
'NULL for every row where it is empty. Declare the field with null=True, or without null.',
|
|
457
482
|
)
|
|
458
483
|
|
|
459
|
-
return self._check_on_delete_rule(chain)
|
|
484
|
+
return self._check_on_delete_rule(chain, silenced)
|
|
460
485
|
|
|
461
|
-
def _check_on_delete_rule(self, chain):
|
|
486
|
+
def _check_on_delete_rule(self, chain, silenced):
|
|
462
487
|
"""
|
|
463
|
-
Check the ``on_delete`` rule against the rule that the lookup path implies (see ``infer_on_delete``)
|
|
488
|
+
Check the ``on_delete`` rule against the rule that the lookup path implies (see ``infer_on_delete``), skipping
|
|
489
|
+
the checks in ``silenced``.
|
|
464
490
|
"""
|
|
465
491
|
inferred, deciding_hop = infer_on_delete(chain)
|
|
466
492
|
hop_label = f'{deciding_hop.model._meta.label}.{deciding_hop.name}'
|
|
@@ -468,6 +494,8 @@ class DenormalizedForeignKey(DenormalizedMixin, models.ForeignKey):
|
|
|
468
494
|
if self.on_delete_is_known:
|
|
469
495
|
# A declared rule cannot be compared with a custom rule along the path.
|
|
470
496
|
return []
|
|
497
|
+
if 'postgres_denormalize.E008' in silenced:
|
|
498
|
+
return []
|
|
471
499
|
return self._error(
|
|
472
500
|
'postgres_denormalize.E008',
|
|
473
501
|
f'`{self.denormalize_from}` crosses {hop_label}, which has the custom on_delete rule '
|
|
@@ -475,7 +503,7 @@ class DenormalizedForeignKey(DenormalizedMixin, models.ForeignKey):
|
|
|
475
503
|
hint='Declare on_delete on the field.',
|
|
476
504
|
)
|
|
477
505
|
declared = self.remote_field.on_delete
|
|
478
|
-
if declared is not inferred:
|
|
506
|
+
if declared is not inferred and 'postgres_denormalize.W009' not in silenced:
|
|
479
507
|
return self._warning(
|
|
480
508
|
'postgres_denormalize.W009',
|
|
481
509
|
f'The field declares on_delete={describe_rule(declared)}, but `{self.denormalize_from}` implies '
|
|
@@ -575,7 +603,9 @@ class DenormalizedField(DenormalizedMixin, models.Field):
|
|
|
575
603
|
kwargs['base_field'] = self._base_field
|
|
576
604
|
return name, path, args, kwargs
|
|
577
605
|
|
|
578
|
-
def _check_end_of_chain(self, chain):
|
|
606
|
+
def _check_end_of_chain(self, chain, silenced):
|
|
607
|
+
if 'postgres_denormalize.E005' in silenced:
|
|
608
|
+
return []
|
|
579
609
|
source = chain[-1]
|
|
580
610
|
if source.is_relation:
|
|
581
611
|
return self._error(
|
|
@@ -681,10 +711,10 @@ def get_denormalized_fields(model):
|
|
|
681
711
|
proxy model, therefore get no triggers, no drift filter and no backfill for the fields of the parent, because the
|
|
682
712
|
triggers of the parent already keep those columns correct.
|
|
683
713
|
|
|
684
|
-
For an excluded model (see ``EXCLUDE_MODELS``) this returns no fields, so the model
|
|
685
|
-
filter and no backfill. The usual reason to exclude a model is an audit table that copies
|
|
686
|
-
model, including the denormalized ones. Its rows record the value that a copy had at the
|
|
687
|
-
rewrite them.
|
|
714
|
+
For an excluded model (see ``EXCLUDE_MODELS`` and ``EXCLUDE_MODELS_CALLBACK``) this returns no fields, so the model
|
|
715
|
+
gets no triggers, no drift filter and no backfill. The usual reason to exclude a model is an audit table that copies
|
|
716
|
+
the fields of a tracked model, including the denormalized ones. Its rows record the value that a copy had at the
|
|
717
|
+
time, so no trigger may rewrite them.
|
|
688
718
|
"""
|
|
689
719
|
if conf.is_excluded(model):
|
|
690
720
|
return []
|
|
@@ -8,9 +8,10 @@ from django.db.migrations.autodetector import MigrationAutodetector
|
|
|
8
8
|
from django.db.migrations.operations import AlterField
|
|
9
9
|
from django.db.migrations.state import ProjectState
|
|
10
10
|
from django.db.models.functions import Length
|
|
11
|
-
from django.test import SimpleTestCase
|
|
11
|
+
from django.test import SimpleTestCase, override_settings
|
|
12
12
|
from django.test.utils import isolate_apps
|
|
13
13
|
|
|
14
|
+
from postgres_denormalize import conf
|
|
14
15
|
from postgres_denormalize.fields import (
|
|
15
16
|
INFERRED_TARGET,
|
|
16
17
|
DenormalizedField,
|
|
@@ -868,3 +869,172 @@ class OnDeleteInferenceTestCase(SimpleTestCase):
|
|
|
868
869
|
chain = field.resolve_chain()
|
|
869
870
|
|
|
870
871
|
self.assertEqual(infer_on_delete(chain), (models.PROTECT, chain[0]))
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
AUDIT_CALLBACK = 'catalog.exclusions.is_audit_model'
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
@isolate_apps('isolated')
|
|
878
|
+
@override_settings(POSTGRES_DENORMALIZE={'EXCLUDE_MODELS_CALLBACK': AUDIT_CALLBACK})
|
|
879
|
+
class ExcludedModelChecksTestCase(SimpleTestCase):
|
|
880
|
+
"""
|
|
881
|
+
The callback excludes every model whose name ends in ``audit``. Each test declares the same field on a leaf and on
|
|
882
|
+
an audit model of that leaf, and compares the checks of the two copies.
|
|
883
|
+
"""
|
|
884
|
+
|
|
885
|
+
def declare_copies(self, suffix, make_field, owner_rule=models.PROTECT, owner_null=False, first_hop_null=False):
|
|
886
|
+
"""
|
|
887
|
+
Declare Owner, Parent, Leaf and LeafAudit, each with ``suffix`` in its name, and return the field ``copy`` of
|
|
888
|
+
Leaf and of LeafAudit.
|
|
889
|
+
|
|
890
|
+
Parent.owner has the rule ``owner_rule``, and is nullable when ``owner_null`` is true. Leaf and LeafAudit each
|
|
891
|
+
have the foreign key ``parent``, which is nullable when ``first_hop_null`` is true, and the field that
|
|
892
|
+
``make_field`` returns.
|
|
893
|
+
"""
|
|
894
|
+
owner = type(f'Owner{suffix}', (models.Model,), {'__module__': __name__})
|
|
895
|
+
parent = type(
|
|
896
|
+
f'Parent{suffix}',
|
|
897
|
+
(models.Model,),
|
|
898
|
+
{'__module__': __name__, 'owner': models.ForeignKey(owner, on_delete=owner_rule, null=owner_null)},
|
|
899
|
+
)
|
|
900
|
+
copies = []
|
|
901
|
+
for name in (f'Leaf{suffix}', f'Leaf{suffix}Audit'):
|
|
902
|
+
leaf = type(
|
|
903
|
+
name,
|
|
904
|
+
(models.Model,),
|
|
905
|
+
{
|
|
906
|
+
'__module__': __name__,
|
|
907
|
+
'parent': models.ForeignKey(parent, on_delete=models.CASCADE, null=first_hop_null),
|
|
908
|
+
'copy': make_field(),
|
|
909
|
+
},
|
|
910
|
+
)
|
|
911
|
+
copies.append(leaf._meta.get_field('copy'))
|
|
912
|
+
return copies
|
|
913
|
+
|
|
914
|
+
def check_ids(self, field):
|
|
915
|
+
return [message.id for message in field.check()]
|
|
916
|
+
|
|
917
|
+
def test_a_declared_rule_that_differs_from_the_path_is_muted_on_an_excluded_model(self):
|
|
918
|
+
"""
|
|
919
|
+
Case: Parent.owner is PROTECT. Leaf and LeafAudit copy parent__owner with on_delete=DO_NOTHING, which an audit
|
|
920
|
+
package declares on every foreign key that it copies.
|
|
921
|
+
Expected: The copy on Leaf reports warning W009. The copy on LeafAudit reports nothing.
|
|
922
|
+
"""
|
|
923
|
+
leaf, audit = self.declare_copies(
|
|
924
|
+
'', lambda: DenormalizedForeignKey('parent__owner', on_delete=models.DO_NOTHING, related_name='+')
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
self.assertEqual(self.check_ids(leaf), ['postgres_denormalize.W009'])
|
|
928
|
+
self.assertEqual(self.check_ids(audit), [])
|
|
929
|
+
|
|
930
|
+
def test_the_checks_that_compare_the_declaration_with_the_path_are_muted_on_an_excluded_model(self):
|
|
931
|
+
"""
|
|
932
|
+
Case: Leaf and LeafAudit copy parent__owner three times: with a nullable first hop, with to=Parent, and with
|
|
933
|
+
null=False across the nullable hop Parent.owner.
|
|
934
|
+
Expected: The copy on Leaf reports E002, E003 and E004 in turn. The copy on LeafAudit reports nothing.
|
|
935
|
+
"""
|
|
936
|
+
cases = [
|
|
937
|
+
(
|
|
938
|
+
'postgres_denormalize.E002',
|
|
939
|
+
{'first_hop_null': True},
|
|
940
|
+
lambda: DenormalizedForeignKey('parent__owner', on_delete=models.PROTECT, null=True, related_name='+'),
|
|
941
|
+
),
|
|
942
|
+
(
|
|
943
|
+
'postgres_denormalize.E003',
|
|
944
|
+
{},
|
|
945
|
+
lambda: DenormalizedForeignKey(
|
|
946
|
+
'parent__owner', on_delete=models.PROTECT, to='isolated.Parent1', related_name='+'
|
|
947
|
+
),
|
|
948
|
+
),
|
|
949
|
+
(
|
|
950
|
+
'postgres_denormalize.E004',
|
|
951
|
+
{'owner_null': True},
|
|
952
|
+
lambda: DenormalizedForeignKey('parent__owner', on_delete=models.PROTECT, null=False, related_name='+'),
|
|
953
|
+
),
|
|
954
|
+
]
|
|
955
|
+
for index, (check_id, options, make_field) in enumerate(cases):
|
|
956
|
+
with self.subTest(check_id=check_id):
|
|
957
|
+
leaf, audit = self.declare_copies(index, make_field, **options)
|
|
958
|
+
|
|
959
|
+
self.assertEqual(self.check_ids(leaf), [check_id])
|
|
960
|
+
self.assertEqual(self.check_ids(audit), [])
|
|
961
|
+
|
|
962
|
+
def test_the_checks_that_find_no_target_type_or_rule_are_still_reported_on_an_excluded_model(self):
|
|
963
|
+
"""
|
|
964
|
+
Case: Leaf and LeafAudit copy parent__missing as a DenormalizedForeignKey and as a DenormalizedField. Then they
|
|
965
|
+
copy parent__owner without on_delete, while Parent.owner has a custom on_delete rule.
|
|
966
|
+
Expected: Both copies report E001, E001 and E008 in turn.
|
|
967
|
+
"""
|
|
968
|
+
cases = [
|
|
969
|
+
('postgres_denormalize.E001', {}, lambda: DenormalizedForeignKey('parent__missing', related_name='+')),
|
|
970
|
+
('postgres_denormalize.E001', {}, lambda: DenormalizedField('parent__missing')),
|
|
971
|
+
(
|
|
972
|
+
'postgres_denormalize.E008',
|
|
973
|
+
{'owner_rule': custom_rule},
|
|
974
|
+
lambda: DenormalizedForeignKey('parent__owner', related_name='+'),
|
|
975
|
+
),
|
|
976
|
+
]
|
|
977
|
+
for index, (check_id, options, make_field) in enumerate(cases):
|
|
978
|
+
with self.subTest(index=index, check_id=check_id):
|
|
979
|
+
for field in self.declare_copies(index, make_field, **options):
|
|
980
|
+
self.assertEqual(self.check_ids(field), [check_id])
|
|
981
|
+
|
|
982
|
+
def test_a_muted_check_does_not_hide_the_checks_after_it(self):
|
|
983
|
+
"""
|
|
984
|
+
Case: Leaf and LeafAudit copy parent__owner as a DenormalizedField, through a nullable first hop. The path
|
|
985
|
+
ends at a foreign key, which a DenormalizedField cannot copy.
|
|
986
|
+
Expected: The copy on Leaf reports E002, the first problem. The copy on LeafAudit skips the muted E002 and
|
|
987
|
+
reports E005.
|
|
988
|
+
"""
|
|
989
|
+
leaf, audit = self.declare_copies('', lambda: DenormalizedField('parent__owner'), first_hop_null=True)
|
|
990
|
+
|
|
991
|
+
self.assertEqual(self.check_ids(leaf), ['postgres_denormalize.E002'])
|
|
992
|
+
self.assertEqual(self.check_ids(audit), ['postgres_denormalize.E005'])
|
|
993
|
+
|
|
994
|
+
def test_the_setting_chooses_the_checks_that_are_muted(self):
|
|
995
|
+
"""
|
|
996
|
+
Case: EXCLUDE_MODELS_SILENCED_CHECKS is empty, and then lists every field check. Leaf and LeafAudit copy
|
|
997
|
+
parent__owner with on_delete=DO_NOTHING while Parent.owner is PROTECT, and copy parent__missing.
|
|
998
|
+
Expected: With the empty list, the copy on LeafAudit reports W009 and E001, as the copy on Leaf does. With every
|
|
999
|
+
field check listed, the copies on LeafAudit report nothing, and the copies on Leaf still report W009
|
|
1000
|
+
and E001.
|
|
1001
|
+
"""
|
|
1002
|
+
rule_copies = self.declare_copies(
|
|
1003
|
+
'Rule', lambda: DenormalizedForeignKey('parent__owner', on_delete=models.DO_NOTHING, related_name='+')
|
|
1004
|
+
)
|
|
1005
|
+
path_copies = self.declare_copies('Path', lambda: DenormalizedForeignKey('parent__missing', related_name='+'))
|
|
1006
|
+
|
|
1007
|
+
def setting(silenced):
|
|
1008
|
+
return override_settings(
|
|
1009
|
+
POSTGRES_DENORMALIZE={
|
|
1010
|
+
'EXCLUDE_MODELS_CALLBACK': AUDIT_CALLBACK,
|
|
1011
|
+
'EXCLUDE_MODELS_SILENCED_CHECKS': silenced,
|
|
1012
|
+
}
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
with setting([]):
|
|
1016
|
+
for field in rule_copies:
|
|
1017
|
+
self.assertEqual(self.check_ids(field), ['postgres_denormalize.W009'])
|
|
1018
|
+
for field in path_copies:
|
|
1019
|
+
self.assertEqual(self.check_ids(field), ['postgres_denormalize.E001'])
|
|
1020
|
+
|
|
1021
|
+
with setting(list(conf.FIELD_CHECKS)):
|
|
1022
|
+
self.assertEqual(self.check_ids(rule_copies[0]), ['postgres_denormalize.W009'])
|
|
1023
|
+
self.assertEqual(self.check_ids(path_copies[0]), ['postgres_denormalize.E001'])
|
|
1024
|
+
self.assertEqual(self.check_ids(rule_copies[1]), [])
|
|
1025
|
+
self.assertEqual(self.check_ids(path_copies[1]), [])
|
|
1026
|
+
|
|
1027
|
+
def test_the_checks_of_django_still_run_on_an_excluded_model(self):
|
|
1028
|
+
"""
|
|
1029
|
+
Case: Leaf and LeafAudit copy parent__owner with on_delete=SET_NULL and null=False while Parent.owner is
|
|
1030
|
+
PROTECT. Django's own checks refuse SET_NULL on a column that cannot be NULL.
|
|
1031
|
+
Expected: The copy on Leaf reports Django's fields.E320 and W009. The copy on LeafAudit reports only
|
|
1032
|
+
fields.E320.
|
|
1033
|
+
"""
|
|
1034
|
+
leaf, audit = self.declare_copies(
|
|
1035
|
+
'',
|
|
1036
|
+
lambda: DenormalizedForeignKey('parent__owner', on_delete=models.SET_NULL, null=False, related_name='+'),
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
self.assertEqual(self.check_ids(leaf), ['fields.E320', 'postgres_denormalize.W009'])
|
|
1040
|
+
self.assertEqual(self.check_ids(audit), ['fields.E320'])
|
|
@@ -15,6 +15,15 @@ class GetSettingTestCase(SimpleTestCase):
|
|
|
15
15
|
"""
|
|
16
16
|
self.assertEqual(conf.get_setting('EXCLUDE_MODELS'), ())
|
|
17
17
|
self.assertIsNone(conf.get_setting('EXCLUDE_MODELS_CALLBACK'))
|
|
18
|
+
self.assertEqual(
|
|
19
|
+
conf.get_setting('EXCLUDE_MODELS_SILENCED_CHECKS'),
|
|
20
|
+
(
|
|
21
|
+
'postgres_denormalize.E002',
|
|
22
|
+
'postgres_denormalize.E003',
|
|
23
|
+
'postgres_denormalize.E004',
|
|
24
|
+
'postgres_denormalize.W009',
|
|
25
|
+
),
|
|
26
|
+
)
|
|
18
27
|
self.assertEqual(conf.get_setting('ORDERING_CHECK_LEVEL'), 'warning')
|
|
19
28
|
self.assertEqual(conf.get_setting('ISOLATION_CHECK_LEVEL'), 'warning')
|
|
20
29
|
self.assertEqual(conf.get_setting('REDUNDANT_COPY_CHECK'), conf.DEFAULT_REDUNDANT_COPY_CHECK)
|
|
@@ -157,6 +166,40 @@ class ValidateSettingsTestCase(SimpleTestCase):
|
|
|
157
166
|
"""
|
|
158
167
|
conf.validate_settings()
|
|
159
168
|
|
|
169
|
+
def test_refuses_silenced_checks_that_are_not_a_list_of_field_checks(self):
|
|
170
|
+
"""
|
|
171
|
+
Case: EXCLUDE_MODELS_SILENCED_CHECKS is a string, and then a list with the project check W101 and the
|
|
172
|
+
misspelt 'postgres_denormalize.W9'.
|
|
173
|
+
Expected: For the string, an ImproperlyConfigured error that asks for a list of check ids. For the list, an
|
|
174
|
+
ImproperlyConfigured error that names both entries and the field checks that can be listed.
|
|
175
|
+
"""
|
|
176
|
+
self.assertRefused(
|
|
177
|
+
{'EXCLUDE_MODELS_SILENCED_CHECKS': 'postgres_denormalize.W009'}, 'must be a list of check ids'
|
|
178
|
+
)
|
|
179
|
+
with override_settings(
|
|
180
|
+
POSTGRES_DENORMALIZE={
|
|
181
|
+
'EXCLUDE_MODELS_SILENCED_CHECKS': ['postgres_denormalize.W101', 'postgres_denormalize.W9']
|
|
182
|
+
}
|
|
183
|
+
):
|
|
184
|
+
with self.assertRaises(ImproperlyConfigured) as raised:
|
|
185
|
+
conf.validate_settings()
|
|
186
|
+
message = str(raised.exception)
|
|
187
|
+
self.assertIn("'postgres_denormalize.W101', 'postgres_denormalize.W9'", message)
|
|
188
|
+
self.assertIn('postgres_denormalize.E001', message)
|
|
189
|
+
self.assertIn('postgres_denormalize.W009', message)
|
|
190
|
+
|
|
191
|
+
def test_accepts_silenced_checks_from_none_to_every_field_check(self):
|
|
192
|
+
"""
|
|
193
|
+
Case: EXCLUDE_MODELS_SILENCED_CHECKS is an empty list, and then a list of every field check.
|
|
194
|
+
Expected: No error for either.
|
|
195
|
+
"""
|
|
196
|
+
for silenced in ([], list(conf.FIELD_CHECKS)):
|
|
197
|
+
with (
|
|
198
|
+
self.subTest(silenced=silenced),
|
|
199
|
+
override_settings(POSTGRES_DENORMALIZE={'EXCLUDE_MODELS_SILENCED_CHECKS': silenced}),
|
|
200
|
+
):
|
|
201
|
+
conf.validate_settings()
|
|
202
|
+
|
|
160
203
|
def test_refuses_a_redundant_copy_check_that_is_not_a_dict(self):
|
|
161
204
|
"""
|
|
162
205
|
Case: REDUNDANT_COPY_CHECK is the string 'error'.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/icon-small.svg
RENAMED
|
File without changes
|
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/social-preview.png
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/assets/social-preview.svg
RENAMED
|
File without changes
|
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/_static/custom.css
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/_templates/layout.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/guides/fields.rst
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/guides/indexing.rst
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/guides/migrations.rst
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/guides/triggers.rst
RENAMED
|
File without changes
|
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/reference/api.rst
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/docs/reference/changelog.rst
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/catalog/__init__.py
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/catalog/apps.py
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/catalog/exclusions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/catalog/models.py
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/config/__init__.py
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/config/settings.py
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/isolated/__init__.py
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/isolated/apps.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/tenancy/__init__.py
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/tenancy/apps.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/tenancy/models.py
RENAMED
|
File without changes
|
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/test_chains.py
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/test_checks.py
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/test_fields.py
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/test_operations.py
RENAMED
|
File without changes
|
{django_postgres_denormalize-1.0.0 → django_postgres_denormalize-1.0.1}/tests/test_triggers.py
RENAMED
|
File without changes
|