Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Truncate with Cascade #73

Merged
merged 4 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion dj_anonymizer/anonymizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@
for queryset in clean_list:
print(f'Cleaning {self.key(queryset.model)}')
if getattr(queryset, 'truncate') is True:
truncate_table(queryset.model)
if getattr(queryset, 'cascade') is True:
truncate_table(queryset.model, True)

Check warning on line 90 in dj_anonymizer/anonymizer.py

View check run for this annotation

Codecov / codecov/patch

dj_anonymizer/anonymizer.py#L89-L90

Added lines #L89 - L90 were not covered by tests
else:
truncate_table(queryset.model, False)

Check warning on line 92 in dj_anonymizer/anonymizer.py

View check run for this annotation

Codecov / codecov/patch

dj_anonymizer/anonymizer.py#L92

Added line #L92 was not covered by tests
else:
queryset.delete()
5 changes: 4 additions & 1 deletion dj_anonymizer/register_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@

class AnonymBase:
truncate = False
cascade = False

def __init__(self, truncate=False):
def __init__(self, truncate=False, cascade=False):
self.truncate = truncate
self.cascade = cascade

@classmethod
def get_fields_names(cls):
Expand Down Expand Up @@ -122,6 +124,7 @@ def register_clean(models):

queryset = model.objects.all()
queryset.truncate = cls_anonym.truncate
queryset.cascade = cls_anonym.cascade
if Anonymizer.key(model) in Anonymizer.clean_models.keys():
raise ValueError(
f'Model {Anonymizer.key(model)} '
Expand Down
21 changes: 18 additions & 3 deletions dj_anonymizer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
'oracle': 'TRUNCATE TABLE',
}

VENDOR_TO_CASCADE = {
'postgresql': 'CASCADE',
'oracle': 'CASCADE',
}


def import_if_exist(filename):
"""
Expand All @@ -27,7 +32,7 @@ def import_if_exist(filename):
spec.loader.exec_module(mod)


def truncate_table(model):
def truncate_table(model, cascade=False):
"""
Generate and execute via Django ORM proper SQL to truncate table
"""
Expand All @@ -42,11 +47,21 @@ def truncate_table(model):
"Database vendor %s is not supported" % vendor
)

cascade_op = ''
try:
if cascade:
cascade_op = VENDOR_TO_CASCADE[vendor]
except KeyError:
raise NotImplementedError(
"DB vendor %s does not support TRUNCATE with CASCADE" % vendor
)

dbtable = '"{}"'.format(model._meta.db_table)

sql = '{operation} {dbtable}'.format(
sql = '{operation} {dbtable} {cascade}'.format(
operation=operation,
dbtable=dbtable,
)
cascade=cascade_op,
).strip()
with connection.cursor() as c:
c.execute(sql)
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Changelog
=============

0.6.0
----------
* Feature: adds possibility to truncate with cascade option (`#73 <https://github.com/preply/dj_anonymizer/pull/73>`__)

0.5.1
----------
* Bugfix: fix issue when model manager is overridden (`#71 <https://github.com/preply/dj_anonymizer/pull/71>`__)
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
release = '0.5.1'
release = '0.6.0'


# -- General configuration ---------------------------------------------------
Expand Down
16 changes: 14 additions & 2 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Model registration

* `models` - list of tuples `(model, cls_anonym)`, where `model` is a model class and `cls_anonym` - anonymization class, inherited form `AnonymBase` with specified queryset for deletion or just `AnonymBase`.

If `AnonymBase` class have `truncate=True`, parameter table will be truncated instead of performing an SQL delete query.
If `AnonymBase` class have `truncate=True`, parameter table will be truncated instead of performing an SQL delete query. Additionally, `cascade=True` may be set to truncate foreign-key related tables (supported for postgresql and oracle).

.. function:: register_skip(models)

Expand Down Expand Up @@ -186,7 +186,19 @@ Example 2 - truncate all data from model `User`::
(User, AnonymBase(truncate=True)),
])

Example 3 - delete all data from model `User`, except user with id=1::
Example 3 - truncate all data from model `User` with cascade option::

from django.contrib.auth.models import User

from dj_anonymizer.register_models import AnonymBase
from dj_anonymizer.register_models import register_clean


register_clean([
(User, AnonymBase(truncate=True, cascade=True)),
])

Example 4 - delete all data from model `User`, except user with id=1::

from django.contrib.auth.models import User

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def read(*rnames):
name='dj_anonymizer',
packages=['dj_anonymizer'],
include_package_data=True,
version='0.5.1',
version='0.6.0-dev',
description='This project helps anonymize production database '
+ 'with fake data of any kind.',
long_description=(read('README.md')),
Expand Down
29 changes: 27 additions & 2 deletions tests/test_register_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import pytest
from django.contrib.auth.models import Group, Permission, User
from django.contrib.contenttypes.models import ContentType
from django.db.models.query import QuerySet

from dj_anonymizer import fields, register_models
Expand Down Expand Up @@ -216,10 +217,11 @@ def test_register_clean():
register_models.register_clean([
(User, register_models.AnonymBase),
(Permission, register_models.AnonymBase(truncate=True)),
(Group, register_models.AnonymBase())
(Group, register_models.AnonymBase()),
(ContentType, register_models.AnonymBase(truncate=True, cascade=True)),
])

assert len(Anonymizer.clean_models) == 3
assert len(Anonymizer.clean_models) == 4
assert len(Anonymizer.skip_models) == 0
assert len(Anonymizer.anonym_models) == 0

Expand All @@ -229,6 +231,8 @@ def test_register_clean():
Anonymizer.clean_models.keys()
assert 'django.contrib.auth.models.Group' in \
Anonymizer.clean_models.keys()
assert 'django.contrib.contenttypes.models.ContentType' in \
Anonymizer.clean_models.keys()

assert isinstance(
Anonymizer.clean_models['django.contrib.auth.models.User'],
Expand All @@ -242,16 +246,37 @@ def test_register_clean():
Anonymizer.clean_models['django.contrib.auth.models.Group'],
QuerySet
)
assert isinstance(
Anonymizer.clean_models[
'django.contrib.contenttypes.models.ContentType'
],
QuerySet
)

assert Anonymizer.clean_models[
"django.contrib.auth.models.User"
].model is User
assert Anonymizer.clean_models[
"django.contrib.auth.models.Permission"
].model is Permission
assert Anonymizer.clean_models[
"django.contrib.auth.models.Permission"
].truncate is True
assert Anonymizer.clean_models[
"django.contrib.auth.models.Permission"
].cascade is False
assert Anonymizer.clean_models[
"django.contrib.auth.models.Group"
].model is Group
assert Anonymizer.clean_models[
"django.contrib.contenttypes.models.ContentType"
].model is ContentType
assert Anonymizer.clean_models[
"django.contrib.contenttypes.models.ContentType"
].truncate is True
assert Anonymizer.clean_models[
"django.contrib.contenttypes.models.ContentType"
].cascade is True


@pytest.mark.django_db
Expand Down
18 changes: 18 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,21 @@ def test_truncate_table(mock_connections):

with pytest.raises(NotImplementedError):
truncate_table(User)


@mock.patch('dj_anonymizer.utils.connections')
def test_truncate_table_with_cascade(mock_connections):
mock_cursor = mock_connections.\
__getitem__(DEFAULT_DB_ALIAS).\
cursor.return_value.__enter__.return_value
mock_connections.__getitem__(DEFAULT_DB_ALIAS).vendor = 'postgresql'

truncate_table(User, True)
mock_cursor.execute.assert_called_once_with(
'TRUNCATE TABLE "auth_user" CASCADE'
)

mock_connections.__getitem__(DEFAULT_DB_ALIAS).vendor = 'sqlite'

with pytest.raises(NotImplementedError):
truncate_table(User, True)
Loading