Skip to content

Commit

Permalink
Added support for adding duplicate suffix 'copy' to non unique fields. (
Browse files Browse the repository at this point in the history
#445)

* Added support for adding duplicate suffix 'copy' to non unique fields.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update clone.py

* Updated README.

* Updated test.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update migrations

* Fix code style issues with Black

* Update README.md

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Lint Action <github-action[bot]@github.com>
  • Loading branch information
4 people authored Aug 12, 2021
1 parent bf0c1ea commit 419e636
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 22 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,24 +217,34 @@ Out[10]: 'replica'

### CloneMixin attributes

#### Explicit
| Attribute | Description |
|:------------------------------:|:------------:|
| `DUPLICATE_SUFFIX` | Suffix to append to duplicates <br> (NOTE: This requires `USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS` <br> to be enabled and supports string fields). |
`USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS` | Enable appending the `DUPLICATE_SUFFIX` to new cloned instances. |
`UNIQUE_DUPLICATE_SUFFIX` | Suffix to append to unique fields |
`USE_UNIQUE_DUPLICATE_SUFFIX` | Enable appending the `UNIQUE_DUPLICATE_SUFFIX` to new cloned instances. |
`MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS` | The max query attempt while generating unique values for a case of unique conflicts. |


| Field Names | Description |
#### Explicit (Cloneable fields)

| Attribute | Description |
|:------------------------------:|:------------:|
| `_clone_fields` | Restrict the list of fields to copy from the instance (By default: Copies all fields excluding auto-created/non editable model fields) |
`_clone_m2m_fields` | Restricted Many to many fields (i.e Test.tags) |
`_clone_m2o_or_o2m_fields` | Restricted Many to One/One to Many fields |
`_clone_o2o_fields` | Restricted One to One fields |

#### Implicit
#### Implicit (Cloneable fields)

| Field Names (include all except these fields.) | Description |
| Attribute (include all except these fields.) | Description |
|:--------------------:|:-----------:|
| `_clone_excluded_fields` | Excluded model fields. |
`_clone_excluded_m2m_fields` | Excluded many to many fields. |
`_clone_excluded_m2o_or_o2m_fields` | Excluded Many to One/One to Many fields. |
`_clone_excluded_o2o_fields` | Excluded one to one fields. |


> **NOTE:** :warning:
>
> * Ensure to either set `_clone_excluded_*` or `_clone_*`. Using both would raise errors.
Expand Down
33 changes: 32 additions & 1 deletion model_clone/mixins/clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
get_fields_and_unique_fields_from_cls,
get_unique_default,
get_unique_value,
get_value,
transaction_autocommit,
)

Expand Down Expand Up @@ -90,6 +91,8 @@ class CloneMixin(object):
_clone_excluded_m2o_or_o2m_fields = [] # type: List[str]
_clone_excluded_o2o_fields = [] # type: List[str]

DUPLICATE_SUFFIX = "copy" # type: str
USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS = False # type: bool
UNIQUE_DUPLICATE_SUFFIX = "copy" # type: str
USE_UNIQUE_DUPLICATE_SUFFIX = True # type: bool
MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS = 100 # type: int
Expand Down Expand Up @@ -309,9 +312,19 @@ def _create_copy_of_instance(instance, using=None, force=False, sub_clone=False)
"MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS",
CloneMixin.MAX_UNIQUE_DUPLICATE_QUERY_ATTEMPTS,
)
duplicate_suffix = getattr(
cls,
"DUPLICATE_SUFFIX",
CloneMixin.DUPLICATE_SUFFIX,
)
use_duplicate_suffix_for_non_unique_fields = getattr(
cls,
"USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS",
CloneMixin.USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS,
)

fields, unique_fields = get_fields_and_unique_fields_from_cls(
cls=cls,
model=cls,
force=force,
clone_fields=clone_fields,
clone_excluded_fields=clone_excluded_fields,
Expand Down Expand Up @@ -381,6 +394,24 @@ def _create_copy_of_instance(instance, using=None, force=False, sub_clone=False)
)
sub_instance.save(using=using)
value = sub_instance.pk
elif all(
[
use_duplicate_suffix_for_non_unique_fields,
f.concrete,
f.editable,
f.name not in unique_fields,
]
):
if (
isinstance(f, (models.CharField, models.TextField))
and not f.choices
):
value = get_value(
value=value,
transform=(slugify if isinstance(f, SlugField) else str),
suffix=duplicate_suffix,
max_length=f.max_length,
)

setattr(new_instance, f.attname, value)

Expand Down
25 changes: 25 additions & 0 deletions model_clone/tests/test_clone_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,31 @@ def test_making_sub_clones_of_a_unique_slug_field(self):
slugify("{} {} {}".format(book.slug, Book.UNIQUE_DUPLICATE_SUFFIX, i)),
)

@patch(
"sample.models.Book.USE_DUPLICATE_SUFFIX_FOR_NON_UNIQUE_FIELDS",
new_callable=PropertyMock,
)
def test_making_sub_clones_of_a_non_unique_slug_field_appends_copy(
self,
use_duplicate_suffix_for_non_unique_fields_mock,
):
name = "New Book"
book = Book.objects.create(
name=name,
created_by=self.user1,
slug=slugify(name),
custom_slug=slugify(name),
)

use_duplicate_suffix_for_non_unique_fields_mock.return_value = True

book_clone = book.make_clone()

self.assertEqual(
book_clone.custom_slug,
slugify("{} {}".format(book.custom_slug, Book.DUPLICATE_SUFFIX)),
)

def test_cloning_unique_fields_max_length(self):
"""
Max unique field length handling.
Expand Down
41 changes: 24 additions & 17 deletions model_clone/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,20 @@ def create_copy_of_instance(

new_obj = instance.__class__(**defaults)

exclude = exclude or [
f.name
for f in instance._meta.fields
if any(
[
all([f.name not in defaults, f.attname not in defaults]),
f.has_default(),
f.null,
]
)
]
exclude = set(
[
f.name
for f in instance._meta.fields
if any(
[
all([f.name not in defaults, f.attname not in defaults]),
f.has_default(),
f.null,
]
)
]
+ list(exclude)
)

# Bug with django using full_clean on a different db
if using == default_db_alias:
Expand Down Expand Up @@ -177,12 +180,16 @@ def context_mutable_attribute(obj, key, value):
delattr(obj, key) # pragma: no cover


def get_value(value, suffix, transform, max_length, index):
def get_value(value, suffix, transform, max_length, index=None):
"""
Append a suffix to a string value and apply a pass directly to a
transformation function.
"""
duplicate_suffix = " " + "{} {}".format(suffix, index).strip()
if index is None:
duplicate_suffix = " {}".format(suffix.strip())
else:
duplicate_suffix = " {} {}".format(suffix.strip(), index)

total_length = len(value + duplicate_suffix)

if max_length is not None and total_length > max_length:
Expand Down Expand Up @@ -235,7 +242,7 @@ def get_unique_value(


def get_fields_and_unique_fields_from_cls(
cls,
model,
force,
clone_fields,
clone_excluded_fields,
Expand All @@ -248,7 +255,7 @@ def get_fields_and_unique_fields_from_cls(
"""
fields = []

for f in cls._meta.concrete_fields:
for f in model._meta.concrete_fields:
valid = False
if not getattr(f, "primary_key", False):
if clone_fields and not force and not getattr(f, "one_to_one", False):
Expand All @@ -274,12 +281,12 @@ def get_fields_and_unique_fields_from_cls(
fields.append(f)

unique_field_names = unpack_unique_together(
opts=cls._meta,
opts=model._meta,
only_fields=[f.attname for f in fields],
)

unique_constraint_field_names = unpack_unique_constraints(
opts=cls._meta,
opts=model._meta,
only_fields=[f.attname for f in fields],
)

Expand Down
18 changes: 18 additions & 0 deletions sample/migrations/0021_book_custom_slug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2021-08-12 01:39

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("sample", "0020_auto_20210717_2230"),
]

operations = [
migrations.AddField(
model_name="book",
name="custom_slug",
field=models.SlugField(default=""),
),
]
1 change: 1 addition & 0 deletions sample/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def __str__(self):
class Book(CloneModel):
name = models.CharField(max_length=2000)
slug = models.SlugField(unique=True)
custom_slug = models.SlugField(default="")
authors = models.ManyToManyField(Author, related_name="books")
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
Expand Down

0 comments on commit 419e636

Please sign in to comment.