diff --git a/README.md b/README.md index 20d1f050..1688373f 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,39 @@ In [10]: clone.tags.all() Out[10]: , ]> ``` +### Creative clones without using `CloneMixin`. + +> NOTE: This method won't copy over related objects like Many to Many/One to Many relationships. + +```python + +In [1]: from model_clone import create_copy_of_instance + +In [2]: test_obj = TestModel.objects.create(title='New') + +In [3]: test_obj.tags.create(name='men') + +In [4]: test_obj.tags.create(name='women') + +In [5]: clone = create_copy_of_instance(test_obj, attrs={'title': 'Updated title'}) + +In [6]: test_obj.pk +Out[6]: 1 + +In [7]: test_obj.title +Out[7]: 'New' + +In [8]: test_obj.tags.all() +Out[8]: , ]> + +In [9]: clone.pk +Out[9]: 2 + +In [10]: clone.title +Out[10]: 'Updated title' + +In [11]: clone.tags.all() +Out[11]: ### Duplicating Models from Django Admin view. diff --git a/model_clone/__init__.py b/model_clone/__init__.py index caea4ee1..6d76b531 100644 --- a/model_clone/__init__.py +++ b/model_clone/__init__.py @@ -1,4 +1,5 @@ from .mixins import CloneMixin # noqa +from .utils import create_copy_of_instance # noqa from .admin import ClonableModelAdmin # noqa -__all__ = ['CloneMixin', 'ClonableModelAdmin'] +__all__ = ['CloneMixin', 'ClonableModelAdmin', 'create_copy_of_instance'] diff --git a/model_clone/utils.py b/model_clone/utils.py new file mode 100644 index 00000000..7c23bf5e --- /dev/null +++ b/model_clone/utils.py @@ -0,0 +1,76 @@ +from django.core.exceptions import ValidationError +from django.db import models + + +def create_copy_of_instance(instance, exclude=(), save_new=True, attrs=()): + """ + 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. + **attrs: 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 + >>> user = get_user_model().objects.create() + >>> instance = Book.objects.get(pk=1) + >>> instance.pk + 1 + >>> instance.name + "The Beautiful Life" + >>> duplicate.pk + 2 + >>> duplicate.name + "Duplicate Book 2" + """ + + defaults = {} + attrs = attrs or {} + fields = instance.__class__._meta.concrete_fields + + if not isinstance(instance, models.Model): + raise ValueError('Invalid: Expected an instance of django.db.models.Model') + + if not isinstance(attrs, dict): + try: + attrs = dict(attrs) + except (TypeError, ValueError): + raise ValueError('Invalid: Expected attrs to be a dict or iterable.') + + for f in fields: + if all([ + not f.auto_created, + 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()) + defaults.update(attrs) + + new_obj = instance.__class__(**defaults) + + exclude = exclude or [ + f.name for f in instance._meta.fields + if any([ + f.name not in defaults, + f.has_default(), + f.null, + ]) + ] + + try: + # Run the unique validation before creating the instance. + new_obj.full_clean(exclude=exclude) + except ValidationError as e: + raise ValidationError(', '.join(e.messages)) + + if save_new: + new_obj.save() + + return new_obj