Skip to content

Commit

Permalink
Fix manager migration (fixes #1)
Browse files Browse the repository at this point in the history
  • Loading branch information
radiac committed Oct 23, 2018
1 parent ff2d7c7 commit 3ed2023
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 21 deletions.
9 changes: 9 additions & 0 deletions docs/upgrading.rst
Expand Up @@ -17,6 +17,15 @@ There are no upgrade instructions yet.
Changelog
=========

1.0.1, 2018-10-
-----------------

Bugfix:

* Managers on registered models which set ``use_in_migrations`` can now be
serialised for migrations.


1.0.0, 2018-09-16
-----------------

Expand Down
22 changes: 20 additions & 2 deletions gdpr_assist/models.py
Expand Up @@ -56,9 +56,11 @@ def _cast_class(cls, queryset):
class PrivacyManager(models.Manager):
"""
A manager with support for anonymising data
Don't subclass this directly - write your manager as normal, this will be
applied automatically.
"""
# The class of a privacy queryset. Here so a custom PrivacyManager can
# replace it with a custom PrivacyQuerySet subclass.
# The class of a privacy queryset.
privacy_queryset = PrivacyQuerySet

def _enhance_queryset(self, qs):
Expand Down Expand Up @@ -91,6 +93,22 @@ def _cast_class(cls, manager):
)
return manager

def deconstruct(self):
"""
Deconstruct the original manager - it will be cast again next time.
"""
# Check bases are as expected from _cast_class
bases = self.__class__.__bases__
if len(bases) != 2:
raise ValueError('Unexpected base classes for CastPrivacyManager')

# Original is second - instatiate and deconstruct it
orig_cls = bases[1]
orig_args = self._constructor_args[0]
orig_kwargs = self._constructor_args[1]
orig_manager = orig_cls(*orig_args, **orig_kwargs)
return orig_manager.deconstruct()


class PrivacyMeta(object):
fields = None
Expand Down
35 changes: 35 additions & 0 deletions tests/base.py
@@ -0,0 +1,35 @@
"""
Base classes for tests
"""
from django.db.migrations.writer import MigrationWriter
from django.test import TestCase


class MigrationTestCase(TestCase):
"""
Based on django.tests.migrations.test_+writer.WriterTests
"""
def safe_exec(self, string, value=None):
d = {}
try:
exec(string, globals(), d)
except Exception as e:
if value:
self.fail("Could not exec %r (from value %r): %s" % (string.strip(), value, e))
else:
self.fail("Could not exec %r: %s" % (string.strip(), e))
return d

def serialize(self, value):
"""
Test a value can be serialised by the MigrationWriter
"""
string, imports = MigrationWriter.serialize(value)
return string, imports

def serialize_round_trip(self, value):
"""
Test a value can be serialised and deserialised
"""
string, imports = self.serialize(value)
return self.safe_exec("%s\ntest_value_result = %s" % ("\n".join(imports), string), value)['test_value_result']
23 changes: 4 additions & 19 deletions tests/test_anonymisation.py
Expand Up @@ -9,7 +9,6 @@
import uuid

from django.db import models
from django.db.migrations.writer import MigrationWriter
from django.test import TestCase

from model_mommy import mommy
Expand All @@ -28,6 +27,7 @@
ForeignKeyModel,
ForeignKeyToUnregisteredModel,
)
from .base import MigrationTestCase


class TestAnonymisationBase(TestCase):
Expand Down Expand Up @@ -63,27 +63,12 @@ def test_anonymise_action__protect__raises_error(self):
self.assertEqual('Cannot ANONYMISE(PROTECT)', str(cm.exception))


class TestOnDeleteAnonymiseDeconstruct(TestCase):
class TestOnDeleteAnonymiseDeconstruct(MigrationTestCase):
"""
Based on django.tests.migrations.test_+writer.WriterTests
Test on_delete=ANONYMISE can be deconstructed
"""
def safe_exec(self, string, value=None):
d = {}
try:
exec(string, globals(), d)
except Exception as e:
if value:
self.fail("Could not exec %r (from value %r): %s" % (string.strip(), value, e))
else:
self.fail("Could not exec %r: %s" % (string.strip(), e))
return d

def serialize_round_trip(self, value):
string, imports = MigrationWriter.serialize(value)
return self.safe_exec("%s\ntest_value_result = %s" % ("\n".join(imports), string), value)['test_value_result']

def test_anonymise_deconstruct__deconstructs(self):
string, imports = MigrationWriter.serialize(
string, imports = self.serialize(
gdpr_assist.ANONYMISE(models.SET_NULL),
)
self.assertEqual(
Expand Down
15 changes: 15 additions & 0 deletions tests/test_model_meta.py
Expand Up @@ -24,6 +24,7 @@
ModelWithPrivacyMeta,
ModelWithoutPrivacyMeta,
)
from .base import MigrationTestCase


class TestRegistry(TestCase):
Expand Down Expand Up @@ -210,3 +211,17 @@ def test_validate_on_delete_anonymise__not_registered__raises_exception(self):
),
str(cm.exception),
)


class TestRegisteredModelMigration(MigrationTestCase):
"""
Check registered models can be migratated
"""
def test_manager_deconstruct__deconstructs(self):
# This should serialise to the original manager
string, imports = self.serialize(ModelWithPrivacyMeta.objects)
self.assertEqual(string, 'django.db.models.manager.Manager()')

# And check it serialises back
obj = self.serialize_round_trip(ModelWithPrivacyMeta.objects)
self.assertIsInstance(obj, models.Manager)

0 comments on commit 3ed2023

Please sign in to comment.