From 6116787bafbec8e20d2e0682fc3681b461021c5d Mon Sep 17 00:00:00 2001 From: jackton12 Date: Sat, 17 Jul 2021 19:01:48 -0400 Subject: [PATCH 01/19] Resolved bug cloning models with UniqueConstraint. --- django_clone/settings.py | 2 +- model_clone/mixins/clone.py | 28 ++++++-- model_clone/tests/test_clone_mixin.py | 49 +++++++++++-- model_clone/utils.py | 72 +++++++++++++++++-- .../0019_saletag_sale_tag_unique_name.py | 18 +++++ sample/migrations/0020_auto_20210717_2230.py | 23 ++++++ sample/models.py | 27 ++++++- 7 files changed, 203 insertions(+), 16 deletions(-) create mode 100644 sample/migrations/0019_saletag_sale_tag_unique_name.py create mode 100644 sample/migrations/0020_auto_20210717_2230.py diff --git a/django_clone/settings.py b/django_clone/settings.py index 669353c4..84977610 100644 --- a/django_clone/settings.py +++ b/django_clone/settings.py @@ -47,7 +47,7 @@ "sample_driver", ] -if sys.version_info >= (3, 6): +if sys.version_info >= (3, 6) and '--fix' in sys.argv: INSTALLED_APPS += ["migration_fixer"] diff --git a/model_clone/mixins/clone.py b/model_clone/mixins/clone.py index f6924d99..c12b1586 100644 --- a/model_clone/mixins/clone.py +++ b/model_clone/mixins/clone.py @@ -1,4 +1,5 @@ import itertools +import warnings from itertools import repeat from typing import Dict, List, Optional @@ -15,7 +16,7 @@ context_mutable_attribute, get_fields_and_unique_fields_from_cls, get_unique_value, - transaction_autocommit, + transaction_autocommit, get_unique_default, ) @@ -341,12 +342,31 @@ def _create_copy_of_instance(instance, using=None, force=False, sub_clone=False) and not f.choices ): value = clean_value(value, unique_duplicate_suffix) - if use_unique_duplicate_suffix: + + if f.has_default(): + value = f.get_default() + + if not callable(f.default): + value = get_unique_default( + model=cls, + fname=f.attname, + value=value, + transform=( + slugify if isinstance(f, SlugField) else str + ), + suffix=unique_duplicate_suffix, + max_length=f.max_length, + max_attempts=max_unique_duplicate_query_attempts, + ) + + elif use_unique_duplicate_suffix: value = get_unique_value( - obj=instance, + model=cls, fname=f.attname, value=value, - transform=(slugify if isinstance(f, SlugField) else str), + transform=( + slugify if isinstance(f, SlugField) else str + ), suffix=unique_duplicate_suffix, max_length=f.max_length, max_attempts=max_unique_duplicate_query_attempts, diff --git a/model_clone/tests/test_clone_mixin.py b/model_clone/tests/test_clone_mixin.py index 7477eca7..10d02a0d 100644 --- a/model_clone/tests/test_clone_mixin.py +++ b/model_clone/tests/test_clone_mixin.py @@ -233,6 +233,26 @@ def test_cloning_without_explicit_clone_m2m_fields(self): list(book_clone.authors.values_list("first_name", "last_name")), ) + def test_cloning_with_unique_constraint_is_valid(self): + sale_tag = SaleTag.objects.create(name="test-sale-tag") + clone_sale_tag = sale_tag.make_clone() + + self.assertNotEqual(sale_tag.pk, clone_sale_tag.pk) + self.assertRegexpMatches( + clone_sale_tag.name, + r"{}\s[\d]".format(SaleTag.UNIQUE_DUPLICATE_SUFFIX), + ) + + def test_cloning_with_unique_constraint_uses_field_default(self): + tag = Tag.objects.create(name="test-tag") + clone_tag = tag.make_clone() + + self.assertNotEqual(tag.pk, clone_tag.pk) + self.assertRegexpMatches( + clone_tag.name, + r"\s[\d]", + ) + @patch("sample.models.Book._clone_m2m_fields", new_callable=PropertyMock) def test_cloning_with_explicit_clone_m2m_fields( self, @@ -416,17 +436,34 @@ def test_cloning_unique_together_fields_with_enum_field(self): created_by=self.user1, ) - author_clone = author.make_clone() + author_clone_1 = author.make_clone() - self.assertNotEqual(author.pk, author_clone.pk) - self.assertEqual(author.sex, author_clone.sex) + self.assertNotEqual(author.pk, author_clone_1.pk) + self.assertEqual(author.sex, author_clone_1.sex) self.assertEqual( - author_clone.first_name, + author_clone_1.first_name, "{} {} {}".format(first_name, Author.UNIQUE_DUPLICATE_SUFFIX, 1), ) self.assertEqual( - author_clone.last_name, - "{} {} {}".format(last_name, Author.UNIQUE_DUPLICATE_SUFFIX, 1), + author_clone_1.last_name, + Author._meta.get_field('last_name').get_default(), + ) + + author_clone_2 = author.make_clone() + + self.assertNotEqual(author.pk, author_clone_2.pk) + self.assertEqual(author.sex, author_clone_2.sex) + self.assertEqual( + author_clone_2.first_name, + "{} {} {}".format(first_name, Author.UNIQUE_DUPLICATE_SUFFIX, 2), + ) + self.assertEqual( + author_clone_2.last_name, + "{} {} {}".format( + Author._meta.get_field('last_name').get_default(), + Author.UNIQUE_DUPLICATE_SUFFIX, + 1 + ), ) def test_cloning_unique_slug_field(self): diff --git a/model_clone/utils.py b/model_clone/utils.py index 81ef7ab1..44c77ed2 100644 --- a/model_clone/utils.py +++ b/model_clone/utils.py @@ -99,6 +99,22 @@ def create_copy_of_instance( return new_obj +def unpack_unique_constraints(opts, only_fields=()): + """ + Unpack unique constraint fields. + + :param opts: Model options + :type opts: `django.db.models.options.Options` + :param only_fields: Fields that should be considered. + :type only_fields: `collections.Iterable` + :return: Flat list of fields. + """ + fields = [] + for constraint in opts.total_unique_constraints: + fields.extend([f for f in constraint.fields if f in only_fields]) + return fields + + def unpack_unique_together(opts, only_fields=()): """ Unpack unique together fields. @@ -163,7 +179,7 @@ def get_value(value, suffix, transform, max_length, index): Append a suffix to a string value and apply a pass directly to a transformation function. """ - duplicate_suffix = " {} {}".format(suffix, index) + duplicate_suffix = " " + "{} {}".format(suffix, index).strip() total_length = len(value + duplicate_suffix) if max_length is not None and total_length > max_length: @@ -189,12 +205,20 @@ def generate_value(value, suffix, transform, max_length, max_attempts): ) -def get_unique_value(obj, fname, value, transform, suffix, max_length, max_attempts): +def get_unique_value( + model, + fname, + value='', + transform=lambda v: v, + suffix='', + max_length=None, + max_attempts=100, +): """ Generate a unique value using current value and query the model for existing objects with the new value. """ - qs = obj.__class__._default_manager.all() + qs = model._default_manager.all() it = generate_value(value, suffix, transform, max_length, max_attempts) new = six.next(it) @@ -251,10 +275,50 @@ def get_fields_and_unique_fields_from_cls( only_fields=[f.attname for f in fields], ) + unique_constraint_field_names = unpack_unique_constraints( + opts=cls._meta, + only_fields=[f.attname for f in fields], + ) + unique_fields = [ f.name for f in fields - if not f.auto_created and (f.unique or f.name in unique_field_names) + if not f.auto_created and (f.unique or f.name in unique_field_names + or f.name in unique_constraint_field_names) ] return fields, unique_fields + + +def get_unique_default( + model, + fname, + value, + transform=lambda v: v, + suffix='', + max_length=None, + max_attempts=100, +): + """Get a unique value using the value and adding a suffix if needed.""" + + qs = model._default_manager.all() + + if not qs.filter(**{fname: value}).exists(): + return value + + it = generate_value( + value, + suffix, + transform, + max_length, + max_attempts, + ) + + new = six.next(it) + kwargs = {fname: new} + + while qs.filter(**kwargs).exists(): + new = six.next(it) + kwargs[fname] = new + + return new diff --git a/sample/migrations/0019_saletag_sale_tag_unique_name.py b/sample/migrations/0019_saletag_sale_tag_unique_name.py new file mode 100644 index 00000000..a1290dd4 --- /dev/null +++ b/sample/migrations/0019_saletag_sale_tag_unique_name.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.5 on 2021-07-17 20:26 + +import django +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sample', '0018_auto_20210628_2301'), + ] + + operations = [ + migrations.AddConstraint( + model_name='saletag', + constraint=models.UniqueConstraint(fields=('name',), name='sale_tag_unique_name'), + ), + ] if django.VERSION >= (2, 2) else [] diff --git a/sample/migrations/0020_auto_20210717_2230.py b/sample/migrations/0020_auto_20210717_2230.py new file mode 100644 index 00000000..20c32557 --- /dev/null +++ b/sample/migrations/0020_auto_20210717_2230.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.5 on 2021-07-17 22:30 +import django +from django.db import migrations, models +import sample.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sample', '0019_saletag_sale_tag_unique_name'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='name', + field=models.CharField(default=sample.models.get_unique_tag_name, max_length=255), + ), + migrations.AddConstraint( + model_name='tag', + constraint=models.UniqueConstraint(fields=('name',), name='tag_unique_name'), + ), + ] if django.VERSION >= (2, 2) else [] diff --git a/sample/models.py b/sample/models.py index 622bef05..b2c5d428 100644 --- a/sample/models.py +++ b/sample/models.py @@ -1,10 +1,17 @@ +from itertools import count from uuid import uuid4 +import django from django.conf import settings from django.db import models from django.utils import timezone from django.utils.translation import gettext as _ +from model_clone.utils import get_unique_default + +if django.VERSION >= (2, 2): + from django.db.models import UniqueConstraint + from model_clone import CloneMixin from model_clone.models import CloneModel @@ -38,8 +45,21 @@ class Meta: unique_together = (("first_name", "last_name", "sex"),) +def get_unique_tag_name(): + return get_unique_default( + model=Tag, + fname='name', + value='test-tag', + ) + + class Tag(CloneModel): - name = models.CharField(max_length=255) + name = models.CharField(max_length=255, default=get_unique_tag_name) + + class Meta: + constraints = [ + UniqueConstraint(fields=['name'], name='tag_unique_name'), + ] if django.VERSION >= (2, 2) else [] def __str__(self): return _(self.name) @@ -48,6 +68,11 @@ def __str__(self): class SaleTag(CloneModel): name = models.CharField(max_length=255) + class Meta: + constraints = [ + UniqueConstraint(fields=['name'], name='sale_tag_unique_name'), + ] if django.VERSION >= (2, 2) else [] + def __str__(self): return _(self.name) From 61d06d7a03952d97e743c07e13b55d1968f834b0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 17 Jul 2021 23:04:09 +0000 Subject: [PATCH 02/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- django_clone/settings.py | 2 +- model_clone/mixins/clone.py | 8 ++--- model_clone/tests/test_clone_mixin.py | 6 ++-- model_clone/utils.py | 14 +++++--- .../0019_saletag_sale_tag_unique_name.py | 20 +++++++---- sample/migrations/0020_auto_20210717_2230.py | 33 ++++++++++++------- sample/models.py | 25 +++++++++----- 7 files changed, 66 insertions(+), 42 deletions(-) diff --git a/django_clone/settings.py b/django_clone/settings.py index 84977610..0b7b4852 100644 --- a/django_clone/settings.py +++ b/django_clone/settings.py @@ -47,7 +47,7 @@ "sample_driver", ] -if sys.version_info >= (3, 6) and '--fix' in sys.argv: +if sys.version_info >= (3, 6) and "--fix" in sys.argv: INSTALLED_APPS += ["migration_fixer"] diff --git a/model_clone/mixins/clone.py b/model_clone/mixins/clone.py index c12b1586..7d8d3e69 100644 --- a/model_clone/mixins/clone.py +++ b/model_clone/mixins/clone.py @@ -1,5 +1,4 @@ import itertools -import warnings from itertools import repeat from typing import Dict, List, Optional @@ -15,8 +14,9 @@ clean_value, context_mutable_attribute, get_fields_and_unique_fields_from_cls, + get_unique_default, get_unique_value, - transaction_autocommit, get_unique_default, + transaction_autocommit, ) @@ -364,9 +364,7 @@ def _create_copy_of_instance(instance, using=None, force=False, sub_clone=False) model=cls, fname=f.attname, value=value, - transform=( - slugify if isinstance(f, SlugField) else str - ), + transform=(slugify if isinstance(f, SlugField) else str), suffix=unique_duplicate_suffix, max_length=f.max_length, max_attempts=max_unique_duplicate_query_attempts, diff --git a/model_clone/tests/test_clone_mixin.py b/model_clone/tests/test_clone_mixin.py index 10d02a0d..f4d56519 100644 --- a/model_clone/tests/test_clone_mixin.py +++ b/model_clone/tests/test_clone_mixin.py @@ -446,7 +446,7 @@ def test_cloning_unique_together_fields_with_enum_field(self): ) self.assertEqual( author_clone_1.last_name, - Author._meta.get_field('last_name').get_default(), + Author._meta.get_field("last_name").get_default(), ) author_clone_2 = author.make_clone() @@ -460,9 +460,9 @@ def test_cloning_unique_together_fields_with_enum_field(self): self.assertEqual( author_clone_2.last_name, "{} {} {}".format( - Author._meta.get_field('last_name').get_default(), + Author._meta.get_field("last_name").get_default(), Author.UNIQUE_DUPLICATE_SUFFIX, - 1 + 1, ), ) diff --git a/model_clone/utils.py b/model_clone/utils.py index 44c77ed2..b81f546a 100644 --- a/model_clone/utils.py +++ b/model_clone/utils.py @@ -208,9 +208,9 @@ def generate_value(value, suffix, transform, max_length, max_attempts): def get_unique_value( model, fname, - value='', + value="", transform=lambda v: v, - suffix='', + suffix="", max_length=None, max_attempts=100, ): @@ -283,8 +283,12 @@ def get_fields_and_unique_fields_from_cls( unique_fields = [ f.name for f in fields - if not f.auto_created and (f.unique or f.name in unique_field_names - or f.name in unique_constraint_field_names) + if not f.auto_created + and ( + f.unique + or f.name in unique_field_names + or f.name in unique_constraint_field_names + ) ] return fields, unique_fields @@ -295,7 +299,7 @@ def get_unique_default( fname, value, transform=lambda v: v, - suffix='', + suffix="", max_length=None, max_attempts=100, ): diff --git a/sample/migrations/0019_saletag_sale_tag_unique_name.py b/sample/migrations/0019_saletag_sale_tag_unique_name.py index a1290dd4..4afbb05e 100644 --- a/sample/migrations/0019_saletag_sale_tag_unique_name.py +++ b/sample/migrations/0019_saletag_sale_tag_unique_name.py @@ -7,12 +7,18 @@ class Migration(migrations.Migration): dependencies = [ - ('sample', '0018_auto_20210628_2301'), + ("sample", "0018_auto_20210628_2301"), ] - operations = [ - migrations.AddConstraint( - model_name='saletag', - constraint=models.UniqueConstraint(fields=('name',), name='sale_tag_unique_name'), - ), - ] if django.VERSION >= (2, 2) else [] + operations = ( + [ + migrations.AddConstraint( + model_name="saletag", + constraint=models.UniqueConstraint( + fields=("name",), name="sale_tag_unique_name" + ), + ), + ] + if django.VERSION >= (2, 2) + else [] + ) diff --git a/sample/migrations/0020_auto_20210717_2230.py b/sample/migrations/0020_auto_20210717_2230.py index 20c32557..371b7342 100644 --- a/sample/migrations/0020_auto_20210717_2230.py +++ b/sample/migrations/0020_auto_20210717_2230.py @@ -1,23 +1,32 @@ # Generated by Django 3.2.5 on 2021-07-17 22:30 import django from django.db import migrations, models + import sample.models class Migration(migrations.Migration): dependencies = [ - ('sample', '0019_saletag_sale_tag_unique_name'), + ("sample", "0019_saletag_sale_tag_unique_name"), ] - operations = [ - migrations.AlterField( - model_name='tag', - name='name', - field=models.CharField(default=sample.models.get_unique_tag_name, max_length=255), - ), - migrations.AddConstraint( - model_name='tag', - constraint=models.UniqueConstraint(fields=('name',), name='tag_unique_name'), - ), - ] if django.VERSION >= (2, 2) else [] + operations = ( + [ + migrations.AlterField( + model_name="tag", + name="name", + field=models.CharField( + default=sample.models.get_unique_tag_name, max_length=255 + ), + ), + migrations.AddConstraint( + model_name="tag", + constraint=models.UniqueConstraint( + fields=("name",), name="tag_unique_name" + ), + ), + ] + if django.VERSION >= (2, 2) + else [] + ) diff --git a/sample/models.py b/sample/models.py index b2c5d428..3d9648f1 100644 --- a/sample/models.py +++ b/sample/models.py @@ -1,4 +1,3 @@ -from itertools import count from uuid import uuid4 import django @@ -48,8 +47,8 @@ class Meta: def get_unique_tag_name(): return get_unique_default( model=Tag, - fname='name', - value='test-tag', + fname="name", + value="test-tag", ) @@ -57,9 +56,13 @@ class Tag(CloneModel): name = models.CharField(max_length=255, default=get_unique_tag_name) class Meta: - constraints = [ - UniqueConstraint(fields=['name'], name='tag_unique_name'), - ] if django.VERSION >= (2, 2) else [] + constraints = ( + [ + UniqueConstraint(fields=["name"], name="tag_unique_name"), + ] + if django.VERSION >= (2, 2) + else [] + ) def __str__(self): return _(self.name) @@ -69,9 +72,13 @@ class SaleTag(CloneModel): name = models.CharField(max_length=255) class Meta: - constraints = [ - UniqueConstraint(fields=['name'], name='sale_tag_unique_name'), - ] if django.VERSION >= (2, 2) else [] + constraints = ( + [ + UniqueConstraint(fields=["name"], name="sale_tag_unique_name"), + ] + if django.VERSION >= (2, 2) + else [] + ) def __str__(self): return _(self.name) From b0d55cbd2594b40f4cc29d21cdc10f04682b2421 Mon Sep 17 00:00:00 2001 From: jackton12 Date: Sat, 17 Jul 2021 19:13:47 -0400 Subject: [PATCH 03/19] Fixed test. --- model_clone/utils.py | 5 +++-- sample/migrations/0020_auto_20210717_2230.py | 9 ++++++++- sample/models.py | 18 ++++++++++-------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/model_clone/utils.py b/model_clone/utils.py index 44c77ed2..4256c7d0 100644 --- a/model_clone/utils.py +++ b/model_clone/utils.py @@ -110,8 +110,9 @@ def unpack_unique_constraints(opts, only_fields=()): :return: Flat list of fields. """ fields = [] - for constraint in opts.total_unique_constraints: - fields.extend([f for f in constraint.fields if f in only_fields]) + if hasattr(opts, 'total_unique_constraints'): + for constraint in opts.total_unique_constraints: + fields.extend([f for f in constraint.fields if f in only_fields]) return fields diff --git a/sample/migrations/0020_auto_20210717_2230.py b/sample/migrations/0020_auto_20210717_2230.py index 20c32557..f904683c 100644 --- a/sample/migrations/0020_auto_20210717_2230.py +++ b/sample/migrations/0020_auto_20210717_2230.py @@ -20,4 +20,11 @@ class Migration(migrations.Migration): model_name='tag', constraint=models.UniqueConstraint(fields=('name',), name='tag_unique_name'), ), - ] if django.VERSION >= (2, 2) else [] + ] if django.VERSION >= (2, 2) else [ + migrations.AlterField( + model_name='tag', + name='name', + field=models.CharField(default=sample.models.get_unique_tag_name, + max_length=255), + ), + ] diff --git a/sample/models.py b/sample/models.py index b2c5d428..e6d09f72 100644 --- a/sample/models.py +++ b/sample/models.py @@ -56,10 +56,11 @@ def get_unique_tag_name(): class Tag(CloneModel): name = models.CharField(max_length=255, default=get_unique_tag_name) - class Meta: - constraints = [ - UniqueConstraint(fields=['name'], name='tag_unique_name'), - ] if django.VERSION >= (2, 2) else [] + if django.VERSION >= (2, 2): + class Meta: + constraints = [ + UniqueConstraint(fields=['name'], name='tag_unique_name'), + ] def __str__(self): return _(self.name) @@ -68,10 +69,11 @@ def __str__(self): class SaleTag(CloneModel): name = models.CharField(max_length=255) - class Meta: - constraints = [ - UniqueConstraint(fields=['name'], name='sale_tag_unique_name'), - ] if django.VERSION >= (2, 2) else [] + if django.VERSION >= (2, 2): + class Meta: + constraints = [ + UniqueConstraint(fields=['name'], name='sale_tag_unique_name'), + ] def __str__(self): return _(self.name) From 1c94712fdf0a2bfcf4a2d5c2071080710d3373eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 17 Jul 2021 23:22:29 +0000 Subject: [PATCH 04/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- model_clone/utils.py | 2 +- sample/migrations/0020_auto_20210717_2230.py | 45 ++++++++++++-------- sample/models.py | 6 ++- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/model_clone/utils.py b/model_clone/utils.py index bb3440d2..a1e493fd 100644 --- a/model_clone/utils.py +++ b/model_clone/utils.py @@ -110,7 +110,7 @@ def unpack_unique_constraints(opts, only_fields=()): :return: Flat list of fields. """ fields = [] - if hasattr(opts, 'total_unique_constraints'): + if hasattr(opts, "total_unique_constraints"): for constraint in opts.total_unique_constraints: fields.extend([f for f in constraint.fields if f in only_fields]) return fields diff --git a/sample/migrations/0020_auto_20210717_2230.py b/sample/migrations/0020_auto_20210717_2230.py index 119cc606..ea1babeb 100644 --- a/sample/migrations/0020_auto_20210717_2230.py +++ b/sample/migrations/0020_auto_20210717_2230.py @@ -11,21 +11,30 @@ class Migration(migrations.Migration): ("sample", "0019_saletag_sale_tag_unique_name"), ] - operations = [ - migrations.AlterField( - model_name='tag', - name='name', - field=models.CharField(default=sample.models.get_unique_tag_name, max_length=255), - ), - migrations.AddConstraint( - model_name='tag', - constraint=models.UniqueConstraint(fields=('name',), name='tag_unique_name'), - ), - ] if django.VERSION >= (2, 2) else [ - migrations.AlterField( - model_name='tag', - name='name', - field=models.CharField(default=sample.models.get_unique_tag_name, - max_length=255), - ), - ] + operations = ( + [ + migrations.AlterField( + model_name="tag", + name="name", + field=models.CharField( + default=sample.models.get_unique_tag_name, max_length=255 + ), + ), + migrations.AddConstraint( + model_name="tag", + constraint=models.UniqueConstraint( + fields=("name",), name="tag_unique_name" + ), + ), + ] + if django.VERSION >= (2, 2) + else [ + migrations.AlterField( + model_name="tag", + name="name", + field=models.CharField( + default=sample.models.get_unique_tag_name, max_length=255 + ), + ), + ] + ) diff --git a/sample/models.py b/sample/models.py index d20390e0..fabea7fb 100644 --- a/sample/models.py +++ b/sample/models.py @@ -56,9 +56,10 @@ class Tag(CloneModel): name = models.CharField(max_length=255, default=get_unique_tag_name) if django.VERSION >= (2, 2): + class Meta: constraints = [ - UniqueConstraint(fields=['name'], name='tag_unique_name'), + UniqueConstraint(fields=["name"], name="tag_unique_name"), ] def __str__(self): @@ -69,9 +70,10 @@ class SaleTag(CloneModel): name = models.CharField(max_length=255) if django.VERSION >= (2, 2): + class Meta: constraints = [ - UniqueConstraint(fields=['name'], name='sale_tag_unique_name'), + UniqueConstraint(fields=["name"], name="sale_tag_unique_name"), ] def __str__(self): From 558b7482b39b783fa5fe240afde8a55f58113892 Mon Sep 17 00:00:00 2001 From: jackton12 Date: Sat, 17 Jul 2021 19:28:33 -0400 Subject: [PATCH 05/19] Fixed test. --- sample/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sample/models.py b/sample/models.py index d20390e0..3ea24d13 100644 --- a/sample/models.py +++ b/sample/models.py @@ -53,7 +53,8 @@ def get_unique_tag_name(): class Tag(CloneModel): - name = models.CharField(max_length=255, default=get_unique_tag_name) + name = models.CharField(max_length=255, default=get_unique_tag_name, + unique=django.VERSION < (2, 2)) if django.VERSION >= (2, 2): class Meta: @@ -66,7 +67,7 @@ def __str__(self): class SaleTag(CloneModel): - name = models.CharField(max_length=255) + name = models.CharField(max_length=255, unique=django.VERSION < (2, 2)) if django.VERSION >= (2, 2): class Meta: From a2c0757f86f3af60e11ac6ed983cc237c58299ac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 17 Jul 2021 23:28:54 +0000 Subject: [PATCH 06/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- sample/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sample/models.py b/sample/models.py index 9ceeca2b..ea37cd63 100644 --- a/sample/models.py +++ b/sample/models.py @@ -53,8 +53,9 @@ def get_unique_tag_name(): class Tag(CloneModel): - name = models.CharField(max_length=255, default=get_unique_tag_name, - unique=django.VERSION < (2, 2)) + name = models.CharField( + max_length=255, default=get_unique_tag_name, unique=django.VERSION < (2, 2) + ) if django.VERSION >= (2, 2): From 69654e168c4d88e7101623e8cd8f30c2cc493876 Mon Sep 17 00:00:00 2001 From: jackton12 Date: Sat, 17 Jul 2021 19:39:48 -0400 Subject: [PATCH 07/19] Fixed test. --- sample/migrations/0019_saletag_sale_tag_unique_name.py | 8 +++++++- sample/migrations/0020_auto_20210717_2230.py | 9 +++++---- sample/models.py | 1 - 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/sample/migrations/0019_saletag_sale_tag_unique_name.py b/sample/migrations/0019_saletag_sale_tag_unique_name.py index 4afbb05e..5e2a6b1c 100644 --- a/sample/migrations/0019_saletag_sale_tag_unique_name.py +++ b/sample/migrations/0019_saletag_sale_tag_unique_name.py @@ -20,5 +20,11 @@ class Migration(migrations.Migration): ), ] if django.VERSION >= (2, 2) - else [] + else [ + migrations.AlterField( + model_name='saletag', + name='name', + field=models.CharField(max_length=255, unique=True), + ), + ] ) diff --git a/sample/migrations/0020_auto_20210717_2230.py b/sample/migrations/0020_auto_20210717_2230.py index ea1babeb..9e090b7b 100644 --- a/sample/migrations/0020_auto_20210717_2230.py +++ b/sample/migrations/0020_auto_20210717_2230.py @@ -30,11 +30,12 @@ class Migration(migrations.Migration): if django.VERSION >= (2, 2) else [ migrations.AlterField( - model_name="tag", - name="name", + model_name='tag', + name='name', field=models.CharField( - default=sample.models.get_unique_tag_name, max_length=255 - ), + default=sample.models.get_unique_tag_name, + max_length=255, + unique=True), ), ] ) diff --git a/sample/models.py b/sample/models.py index 9ceeca2b..5f080b7d 100644 --- a/sample/models.py +++ b/sample/models.py @@ -57,7 +57,6 @@ class Tag(CloneModel): unique=django.VERSION < (2, 2)) if django.VERSION >= (2, 2): - class Meta: constraints = [ UniqueConstraint(fields=["name"], name="tag_unique_name"), From a6fb5f7f62c7e6454b25ffa8205cc50be9351857 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 17 Jul 2021 23:40:16 +0000 Subject: [PATCH 08/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- sample/migrations/0019_saletag_sale_tag_unique_name.py | 4 ++-- sample/migrations/0020_auto_20210717_2230.py | 7 ++++--- sample/models.py | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/sample/migrations/0019_saletag_sale_tag_unique_name.py b/sample/migrations/0019_saletag_sale_tag_unique_name.py index 5e2a6b1c..d6de938c 100644 --- a/sample/migrations/0019_saletag_sale_tag_unique_name.py +++ b/sample/migrations/0019_saletag_sale_tag_unique_name.py @@ -22,8 +22,8 @@ class Migration(migrations.Migration): if django.VERSION >= (2, 2) else [ migrations.AlterField( - model_name='saletag', - name='name', + model_name="saletag", + name="name", field=models.CharField(max_length=255, unique=True), ), ] diff --git a/sample/migrations/0020_auto_20210717_2230.py b/sample/migrations/0020_auto_20210717_2230.py index 9e090b7b..dda72035 100644 --- a/sample/migrations/0020_auto_20210717_2230.py +++ b/sample/migrations/0020_auto_20210717_2230.py @@ -30,12 +30,13 @@ class Migration(migrations.Migration): if django.VERSION >= (2, 2) else [ migrations.AlterField( - model_name='tag', - name='name', + model_name="tag", + name="name", field=models.CharField( default=sample.models.get_unique_tag_name, max_length=255, - unique=True), + unique=True, + ), ), ] ) diff --git a/sample/models.py b/sample/models.py index ca47de6e..ea37cd63 100644 --- a/sample/models.py +++ b/sample/models.py @@ -58,6 +58,7 @@ class Tag(CloneModel): ) if django.VERSION >= (2, 2): + class Meta: constraints = [ UniqueConstraint(fields=["name"], name="tag_unique_name"), From 74a6343f539bf524b7cb73c6d600380239112602 Mon Sep 17 00:00:00 2001 From: jackton12 Date: Sat, 17 Jul 2021 19:53:43 -0400 Subject: [PATCH 09/19] Fixed test. --- model_clone/utils.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/model_clone/utils.py b/model_clone/utils.py index a1e493fd..e5c9cf21 100644 --- a/model_clone/utils.py +++ b/model_clone/utils.py @@ -110,9 +110,13 @@ def unpack_unique_constraints(opts, only_fields=()): :return: Flat list of fields. """ fields = [] - if hasattr(opts, "total_unique_constraints"): - for constraint in opts.total_unique_constraints: - fields.extend([f for f in constraint.fields if f in only_fields]) + constraints = getattr( + opts, + "total_unique_constraints", + getattr(opts, "constraints", []) + ) + for constraint in constraints: + fields.extend([f for f in constraint.fields if f in only_fields]) return fields From 0c58ed27cde0df4437a0395bdfcbb29a506ad72a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 17 Jul 2021 23:54:13 +0000 Subject: [PATCH 10/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- model_clone/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/model_clone/utils.py b/model_clone/utils.py index e5c9cf21..344af5fa 100644 --- a/model_clone/utils.py +++ b/model_clone/utils.py @@ -111,9 +111,7 @@ def unpack_unique_constraints(opts, only_fields=()): """ fields = [] constraints = getattr( - opts, - "total_unique_constraints", - getattr(opts, "constraints", []) + opts, "total_unique_constraints", getattr(opts, "constraints", []) ) for constraint in constraints: fields.extend([f for f in constraint.fields if f in only_fields]) From e50d4fa3c7ac5f0fb1e5fe50c2de6785933d24f3 Mon Sep 17 00:00:00 2001 From: jackton12 Date: Sat, 17 Jul 2021 19:59:21 -0400 Subject: [PATCH 11/19] Updated test. --- model_clone/tests/test_clone_mixin.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/model_clone/tests/test_clone_mixin.py b/model_clone/tests/test_clone_mixin.py index f4d56519..562939d7 100644 --- a/model_clone/tests/test_clone_mixin.py +++ b/model_clone/tests/test_clone_mixin.py @@ -235,11 +235,19 @@ def test_cloning_without_explicit_clone_m2m_fields(self): def test_cloning_with_unique_constraint_is_valid(self): sale_tag = SaleTag.objects.create(name="test-sale-tag") - clone_sale_tag = sale_tag.make_clone() + clone_sale_tag_1 = sale_tag.make_clone() - self.assertNotEqual(sale_tag.pk, clone_sale_tag.pk) + self.assertNotEqual(sale_tag.pk, clone_sale_tag_1.pk) self.assertRegexpMatches( - clone_sale_tag.name, + clone_sale_tag_1.name, + r"{}\s[\d]".format(SaleTag.UNIQUE_DUPLICATE_SUFFIX), + ) + + clone_sale_tag_2 = sale_tag.make_clone() + + self.assertNotEqual(clone_sale_tag_1.pk, clone_sale_tag_2.pk) + self.assertRegexpMatches( + clone_sale_tag_2.name, r"{}\s[\d]".format(SaleTag.UNIQUE_DUPLICATE_SUFFIX), ) From 43aceea3ec3149d090699cefec4ba49ffe14f5b4 Mon Sep 17 00:00:00 2001 From: jackton12 Date: Sat, 17 Jul 2021 20:05:45 -0400 Subject: [PATCH 12/19] Updated test. --- model_clone/tests/test_clone_mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_clone/tests/test_clone_mixin.py b/model_clone/tests/test_clone_mixin.py index 562939d7..aeb6fc0c 100644 --- a/model_clone/tests/test_clone_mixin.py +++ b/model_clone/tests/test_clone_mixin.py @@ -243,7 +243,7 @@ def test_cloning_with_unique_constraint_is_valid(self): r"{}\s[\d]".format(SaleTag.UNIQUE_DUPLICATE_SUFFIX), ) - clone_sale_tag_2 = sale_tag.make_clone() + clone_sale_tag_2 = clone_sale_tag_1.make_clone() self.assertNotEqual(clone_sale_tag_1.pk, clone_sale_tag_2.pk) self.assertRegexpMatches( From d418ab2a6da9603011bb7286786c288d7e6f7581 Mon Sep 17 00:00:00 2001 From: jackton12 Date: Sat, 17 Jul 2021 20:10:51 -0400 Subject: [PATCH 13/19] Updated test. --- model_clone/tests/test_clone_mixin.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/model_clone/tests/test_clone_mixin.py b/model_clone/tests/test_clone_mixin.py index aeb6fc0c..9aa3a93f 100644 --- a/model_clone/tests/test_clone_mixin.py +++ b/model_clone/tests/test_clone_mixin.py @@ -474,6 +474,23 @@ def test_cloning_unique_together_fields_with_enum_field(self): ), ) + author_clone_3 = author.make_clone() + + self.assertNotEqual(author.pk, author_clone_3.pk) + self.assertEqual(author.sex, author_clone_3.sex) + self.assertEqual( + author_clone_3.first_name, + "{} {} {}".format(first_name, Author.UNIQUE_DUPLICATE_SUFFIX, 2), + ) + self.assertEqual( + author_clone_3.last_name, + "{} {} {}".format( + Author._meta.get_field("last_name").get_default(), + Author.UNIQUE_DUPLICATE_SUFFIX, + 1, + ), + ) + def test_cloning_unique_slug_field(self): name = "New Book" book = Book.objects.create(name=name, created_by=self.user1, slug=slugify(name)) From 7e15f898a07fecf01797e51a547c39e08f5ab226 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sat, 17 Jul 2021 20:40:07 -0400 Subject: [PATCH 14/19] Update utils.py --- model_clone/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_clone/utils.py b/model_clone/utils.py index 344af5fa..7ac5ce38 100644 --- a/model_clone/utils.py +++ b/model_clone/utils.py @@ -227,7 +227,7 @@ def get_unique_value( new = six.next(it) kwargs = {fname: new} - while qs.filter(**kwargs).exists(): + while qs.filter(**kwargs).exists(): # pragma: no cover new = six.next(it) kwargs[fname] = new From 9910189909e4f49e406a9f4bd8a1bb83e2886190 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sat, 17 Jul 2021 20:51:37 -0400 Subject: [PATCH 15/19] Update test_clone_mixin.py --- model_clone/tests/test_clone_mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_clone/tests/test_clone_mixin.py b/model_clone/tests/test_clone_mixin.py index 9aa3a93f..cead6be3 100644 --- a/model_clone/tests/test_clone_mixin.py +++ b/model_clone/tests/test_clone_mixin.py @@ -487,7 +487,7 @@ def test_cloning_unique_together_fields_with_enum_field(self): "{} {} {}".format( Author._meta.get_field("last_name").get_default(), Author.UNIQUE_DUPLICATE_SUFFIX, - 1, + 2, ), ) From ea7e07a7a5e943fcb151e45242539844405ad0d6 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sat, 17 Jul 2021 20:53:52 -0400 Subject: [PATCH 16/19] Update test_clone_mixin.py --- model_clone/tests/test_clone_mixin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model_clone/tests/test_clone_mixin.py b/model_clone/tests/test_clone_mixin.py index cead6be3..a7d22b28 100644 --- a/model_clone/tests/test_clone_mixin.py +++ b/model_clone/tests/test_clone_mixin.py @@ -463,14 +463,14 @@ def test_cloning_unique_together_fields_with_enum_field(self): self.assertEqual(author.sex, author_clone_2.sex) self.assertEqual( author_clone_2.first_name, - "{} {} {}".format(first_name, Author.UNIQUE_DUPLICATE_SUFFIX, 2), + "{} {} {}".format(first_name, Author.UNIQUE_DUPLICATE_SUFFIX, 3), ) self.assertEqual( author_clone_2.last_name, "{} {} {}".format( Author._meta.get_field("last_name").get_default(), Author.UNIQUE_DUPLICATE_SUFFIX, - 1, + 3, ), ) From 4e22321b5dcddaedcc526f09654eae12f5a1fc57 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sat, 17 Jul 2021 20:57:59 -0400 Subject: [PATCH 17/19] Update test_clone_mixin.py --- model_clone/tests/test_clone_mixin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/model_clone/tests/test_clone_mixin.py b/model_clone/tests/test_clone_mixin.py index a7d22b28..e188b768 100644 --- a/model_clone/tests/test_clone_mixin.py +++ b/model_clone/tests/test_clone_mixin.py @@ -463,14 +463,14 @@ def test_cloning_unique_together_fields_with_enum_field(self): self.assertEqual(author.sex, author_clone_2.sex) self.assertEqual( author_clone_2.first_name, - "{} {} {}".format(first_name, Author.UNIQUE_DUPLICATE_SUFFIX, 3), + "{} {} {}".format(first_name, Author.UNIQUE_DUPLICATE_SUFFIX, 2), ) self.assertEqual( author_clone_2.last_name, "{} {} {}".format( Author._meta.get_field("last_name").get_default(), Author.UNIQUE_DUPLICATE_SUFFIX, - 3, + 2, ), ) @@ -480,14 +480,14 @@ def test_cloning_unique_together_fields_with_enum_field(self): self.assertEqual(author.sex, author_clone_3.sex) self.assertEqual( author_clone_3.first_name, - "{} {} {}".format(first_name, Author.UNIQUE_DUPLICATE_SUFFIX, 2), + "{} {} {}".format(first_name, Author.UNIQUE_DUPLICATE_SUFFIX, 3), ) self.assertEqual( author_clone_3.last_name, "{} {} {}".format( Author._meta.get_field("last_name").get_default(), Author.UNIQUE_DUPLICATE_SUFFIX, - 2, + 3, ), ) From 20d24c911e6faf139c13e50061ff20afab1e1204 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sat, 17 Jul 2021 21:05:13 -0400 Subject: [PATCH 18/19] Update test_clone_mixin.py --- model_clone/tests/test_clone_mixin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model_clone/tests/test_clone_mixin.py b/model_clone/tests/test_clone_mixin.py index e188b768..efe6f627 100644 --- a/model_clone/tests/test_clone_mixin.py +++ b/model_clone/tests/test_clone_mixin.py @@ -470,7 +470,7 @@ def test_cloning_unique_together_fields_with_enum_field(self): "{} {} {}".format( Author._meta.get_field("last_name").get_default(), Author.UNIQUE_DUPLICATE_SUFFIX, - 2, + 1, ), ) @@ -487,7 +487,7 @@ def test_cloning_unique_together_fields_with_enum_field(self): "{} {} {}".format( Author._meta.get_field("last_name").get_default(), Author.UNIQUE_DUPLICATE_SUFFIX, - 3, + 2, ), ) From 20619ef937508164043daa0b7061e1505ae95d9f Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sat, 17 Jul 2021 21:12:16 -0400 Subject: [PATCH 19/19] Update utils.py --- model_clone/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_clone/utils.py b/model_clone/utils.py index 7ac5ce38..344af5fa 100644 --- a/model_clone/utils.py +++ b/model_clone/utils.py @@ -227,7 +227,7 @@ def get_unique_value( new = six.next(it) kwargs = {fname: new} - while qs.filter(**kwargs).exists(): # pragma: no cover + while qs.filter(**kwargs).exists(): new = six.next(it) kwargs[fname] = new