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

Add support for related ManyToMany fields #190

Merged
merged 17 commits into from
Nov 5, 2020
48 changes: 36 additions & 12 deletions model_clone/mixins/clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,25 @@ def make_clone(self, attrs=None, sub_clone=False):
):
many_to_one_or_one_to_many_fields.append(f)

elif all(
[
f.many_to_many,
f.name in self._clone_many_to_many_fields,
]
):
many_to_many_fields.append(f)

elif all(
[
f.many_to_many,
not self._clone_many_to_many_fields,
self._clone_excluded_many_to_many_fields,
f not in many_to_many_fields,
f.name not in self._clone_excluded_many_to_many_fields,
]
):
many_to_many_fields.append(f)

for f in self._meta.many_to_many:
if not sub_clone:
if f.name in self._clone_many_to_many_fields:
Expand Down Expand Up @@ -352,27 +371,32 @@ def make_clone(self, attrs=None, sub_clone=False):

# Clone many to many fields
for field in many_to_many_fields:
if hasattr(field, "field"):
# ManyToManyRel
field_name = field.field.m2m_reverse_field_name()
through = field.through
source = getattr(self, field.related_name)
destination = getattr(duplicate, field.related_name)
else:
through = field.remote_field.through
field_name = field.m2m_field_name()
source = getattr(self, field.attname)
destination = getattr(duplicate, field.attname)
if all(
[
field.remote_field.through,
not field.remote_field.through._meta.auto_created,
through,
not through._meta.auto_created,
]
):
objs = field.remote_field.through.objects.filter(
**{field.m2m_field_name(): self.pk}
)
objs = through.objects.filter(**{field_name: self.pk})
for item in objs:
if hasattr(field.remote_field.through, "make_clone"):
item.make_clone(
attrs={field.m2m_field_name(): duplicate}, sub_clone=True
)
if hasattr(through, "make_clone"):
item.make_clone(attrs={field_name: duplicate}, sub_clone=True)
else:
item.pk = None
setattr(item, field.m2m_field_name(), duplicate)
setattr(item, field_name, duplicate)
item.save()
else:
source = getattr(self, field.attname)
destination = getattr(duplicate, field.attname)
destination.set(source.all())
return duplicate

Expand Down
22 changes: 22 additions & 0 deletions model_clone/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,28 @@ def test_cloning_with_explicit__clone_many_to_many_fields(
list(book.authors.values_list("first_name", "last_name")),
list(book_clone.authors.values_list("first_name", "last_name")),
)

@patch("sample.models.Author._clone_many_to_many_fields", new_callable=PropertyMock)
def test_cloning_with_explicit_related__clone_many_to_many_fields(
self,
_clone_many_to_many_fields_mock,
):
author = Author.objects.create(
first_name="Opubo", last_name="Jack", age=24, sex="F", created_by=self.user
)

_clone_many_to_many_fields_mock.return_value = ["books"]

book_1 = Book.objects.create(name="New Book 1", created_by=self.user)
book_2 = Book.objects.create(name="New Book 2", created_by=self.user)
author.books.set([book_1, book_2])

author_clone = author.make_clone()

self.assertEqual(
list(author.books.values_list("name")),
list(author_clone.books.values_list("name")),
)
_clone_many_to_many_fields_mock.assert_called_once()

def test_cloning_unique_fields_is_valid(self):
Expand Down