diff --git a/model_clone/__init__.py b/model_clone/__init__.py index a8dee106..cdcbbb49 100644 --- a/model_clone/__init__.py +++ b/model_clone/__init__.py @@ -4,5 +4,11 @@ from model_clone.admin import CloneModelAdmin, CloneModelAdminMixin from model_clone.mixins.clone import CloneMixin +from model_clone.utils import create_copy_of_instance -__all__ = ["CloneMixin", "CloneModelAdmin", "CloneModelAdminMixin"] +__all__ = [ + "CloneMixin", + "CloneModelAdmin", + "CloneModelAdminMixin", + "create_copy_of_instance", +] diff --git a/model_clone/tests/__init__.py b/model_clone/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/model_clone/tests.py b/model_clone/tests/test_clone_mixin.py similarity index 100% rename from model_clone/tests.py rename to model_clone/tests/test_clone_mixin.py diff --git a/model_clone/tests/test_create_copy_of_instance.py b/model_clone/tests/test_create_copy_of_instance.py new file mode 100644 index 00000000..df98ccbc --- /dev/null +++ b/model_clone/tests/test_create_copy_of_instance.py @@ -0,0 +1,42 @@ +from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError +from django.db import IntegrityError +from django.test import TestCase +from django.utils.text import slugify + +from model_clone import create_copy_of_instance +from sample.models import Library, Book + +User = get_user_model() + + +class CreateCopyOfInstanceTestCase(TestCase): + @classmethod + def setUpTestData(cls): + cls.user1 = User.objects.create(username="user 1") + cls.user2 = User.objects.create(username="user 2") + + def test_cloning_model_with_custom_id(self): + instance = Library.objects.create(name="First library", user=self.user1) + clone = create_copy_of_instance(instance, attrs={"user": self.user2}) + + self.assertNotEqual(instance.pk, clone.pk) + self.assertEqual(clone.user, self.user2) + + def test_cloning_unique_fk_field_without_a_fallback_value_is_invalid(self): + name = "New Library" + instance = Library.objects.create(name=name, user=self.user1) + + with self.assertRaises(ValidationError): + create_copy_of_instance(instance) + + def test_cloning_excluded_field_without_a_fallback_value_is_invalid(self): + name = "New Library" + instance = Book.objects.create( + name=name, created_by=self.user1, slug=slugify(name) + ) + + with self.assertRaises(IntegrityError): + create_copy_of_instance( + instance, exclude={"slug"}, attrs={"created_by": self.user2} + ) diff --git a/model_clone/utils.py b/model_clone/utils.py index 1713f0d9..5500c985 100644 --- a/model_clone/utils.py +++ b/model_clone/utils.py @@ -11,28 +11,30 @@ def create_copy_of_instance(instance, exclude=(), save_new=True, attrs=None): """ Clone an instance of `django.db.models.Model`. - Args: - instance(django.db.models.Model): The model instance to clone. - exclude(list|set): List or set of fields to exclude from unique validation. - save_new(bool): Save the model instance after duplication calling .save(). - attrs(dict): Kwargs of field and value to set on the duplicated instance. - - Returns: - (django.db.models.Model): The new duplicated instance. - - Examples: - >>> from django.contrib.auth import get_user_model - >>> from sample.models import Book - >>> instance = Book.objects.create(name='The Beautiful Life') - >>> instance.pk - 1 - >>> instance.name - "The Beautiful Life" - >>> duplicate = instance.make_clone(attrs={'name': 'Duplicate Book 2'}) - >>> duplicate.pk - 2 - >>> duplicate.name - "Duplicate Book 2" + :param instance: The model instance to clone. + :type instance: django.db.models.Model + :param exclude: List or set of fields to exclude from unique validation. + :type exclude: list|set + :param save_new: Save the model instance after duplication calling .save(). + :type save_new: bool + :param attrs: Kwargs of field and value to set on the duplicated instance. + :type attrs: dict + :return: The new duplicated instance. + :rtype: django.db.models.Model + + :example: + >>> from django.contrib.auth import get_user_model + >>> from sample.models import Book + >>> instance = Book.objects.create(name='The Beautiful Life') + >>> instance.pk + 1 + >>> instance.name + "The Beautiful Life" + >>> duplicate = instance.make_clone(attrs={'name': 'Duplicate Book 2'}) + >>> duplicate.pk + 2 + >>> duplicate.name + "Duplicate Book 2" """ defaults = {} @@ -52,13 +54,17 @@ def create_copy_of_instance(instance, exclude=(), save_new=True, attrs=None): if all( [ not f.auto_created, + not f.primary_key, f.concrete, f.editable, f not in instance.__class__._meta.related_objects, f not in instance.__class__._meta.many_to_many, ] ): - defaults[f.attname] = getattr(instance, f.attname, f.get_default()) + # Prevent duplicates + if f.name not in attrs: + defaults[f.attname] = getattr(instance, f.attname, f.get_default()) + defaults.update(attrs) new_obj = instance.__class__(**defaults) @@ -66,7 +72,13 @@ def create_copy_of_instance(instance, exclude=(), save_new=True, attrs=None): exclude = exclude or [ f.name for f in instance._meta.fields - if any([f.name not in defaults, f.has_default(), f.null]) + if any( + [ + all([f.name not in defaults, f.attname not in defaults]), + f.has_default(), + f.null, + ] + ) ] try: