Skip to content

Commit

Permalink
Merge pull request #8 from jackton1/feature/add-create-copy-of-instan…
Browse files Browse the repository at this point in the history
…ce-utility

Added create_copy_of_instance utility.
  • Loading branch information
jackton1 authored Sep 28, 2019
2 parents e806e97 + 697cdc6 commit c0c4914
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 1 deletion.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,39 @@ In [10]: clone.tags.all()
Out[10]: <QuerySet [<Tag: men>, <Tag: women>]>
```

### 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]: <QuerySet [<Tag: men>, <Tag: women>]>

In [9]: clone.pk
Out[9]: 2

In [10]: clone.title
Out[10]: 'Updated title'

In [11]: clone.tags.all()
Out[11]: <QuerySet []>

### Duplicating Models from Django Admin view.

Expand Down
3 changes: 2 additions & 1 deletion model_clone/__init__.py
Original file line number Diff line number Diff line change
@@ -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']
76 changes: 76 additions & 0 deletions model_clone/utils.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit c0c4914

Please sign in to comment.