django-duplicator 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ferdina Kusumah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-duplicator
3
+ Version: 0.1.0
4
+ Summary: A reusable Django app to easily duplicate model records.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: django,admin,mixin,djang-duplicator,bulk-action
8
+ Author: Ferdina Kusumah
9
+ Author-email: ferdina.kusumah@gmail.com
10
+ Requires-Python: >=3.8,<4.0
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 3.2
13
+ Classifier: Framework :: Django :: 4.2
14
+ Classifier: Framework :: Django :: 5.0
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Internet :: WWW/HTTP
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Requires-Dist: Django (>=3.2)
28
+ Project-URL: Homepage, https://github.com/FerdinaKusumah/duplicator
29
+ Project-URL: Repository, https://github.com/FerdinaKusumah/duplicator
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Django Duplicator 🔁
33
+
34
+ A lightweight mixin for Django Admin that adds object duplication functionality, both in bulk (via an Admin Action) and
35
+ for single objects (via a "Duplicate and continue editing" button on the change form).
36
+
37
+ ---
38
+
39
+ ## ✨ Key Features
40
+
41
+ * **Single Object Duplication**: A "Duplicate and continue editing" button appears on the object detail page.
42
+ * **Bulk Duplication (Admin Action)**: An Admin Action is provided to duplicate selected objects from the changelist
43
+ page.
44
+ * **DRY and Flexible**: Uses simple model and Admin mixins that are easy to integrate into any Django model.
45
+
46
+ ---
47
+
48
+ ## 💻 Installation
49
+
50
+ ```shell
51
+ pip install django-duplicator
52
+ ```
53
+
54
+ Add `duplicator` to your `INSTALLED_APPS` in settings.py:
55
+
56
+ ```shell
57
+ # settings.py
58
+ INSTALLED_APPS = [
59
+ # ...
60
+ 'django.contrib.admin',
61
+ # ...
62
+ 'duplicator',
63
+ ]
64
+ ```
65
+
66
+ ## 🚀 Usage
67
+
68
+ 1. Model (Enabling Duplication)
69
+
70
+ To make your model duplicatable, you must inherit from DuplicatorMixin.
71
+
72
+ ```shell
73
+ # models.py
74
+ from django.db import models
75
+ from duplicator import DuplicatorMixin
76
+
77
+ class Customer(DuplicatorMixin, models.Model):
78
+ name = models.CharField(max_length=255)
79
+ # ... other fields ...
80
+
81
+ def __str__(self):
82
+ return self.name
83
+ ```
84
+
85
+ 2. Admin (Enabling Buttons and Actions)
86
+
87
+ To enable both the single duplication button and the bulk duplication Admin Action, inherit from
88
+ DuplicatorAdminMixin.
89
+
90
+ ```shell
91
+ # admin.py
92
+ from django.contrib import admin
93
+ from duplicator import DuplicatorAdminMixin
94
+ from .models import Customer
95
+
96
+ @admin.register(Customer)
97
+ class CustomerAdmin(DuplicatorAdminMixin, admin.ModelAdmin):
98
+ list_display = ('name', 'id')
99
+ # ... other admin configurations ...
100
+ ```
101
+
102
+ ## 📸 Feature Demonstration
103
+
104
+ ### A. Bulk Duplication (Admin Action)
105
+
106
+ The duplication option appears in the Actions dropdown menu on the changelist page.
107
+
108
+ ![Action](./images/action.png)
109
+
110
+ ### B. Single Duplication (Change Form Button)
111
+
112
+ The "Duplicate and continue editing" button is prominently placed on the object detail page.
113
+
114
+ ![Detail](./images/detail.png)
115
+
116
+ ## 🤝 Contributing
117
+
118
+ We welcome all contributions! If you find a bug or have a feature suggestion, please open an Issue or submit a Pull
119
+ Request.
120
+
121
+ ## 📄 License
122
+
123
+ This project is licensed under the [Your License Type, e.g., MIT License]. See the LICENSE file for full details.
@@ -0,0 +1,92 @@
1
+ # Django Duplicator 🔁
2
+
3
+ A lightweight mixin for Django Admin that adds object duplication functionality, both in bulk (via an Admin Action) and
4
+ for single objects (via a "Duplicate and continue editing" button on the change form).
5
+
6
+ ---
7
+
8
+ ## ✨ Key Features
9
+
10
+ * **Single Object Duplication**: A "Duplicate and continue editing" button appears on the object detail page.
11
+ * **Bulk Duplication (Admin Action)**: An Admin Action is provided to duplicate selected objects from the changelist
12
+ page.
13
+ * **DRY and Flexible**: Uses simple model and Admin mixins that are easy to integrate into any Django model.
14
+
15
+ ---
16
+
17
+ ## 💻 Installation
18
+
19
+ ```shell
20
+ pip install django-duplicator
21
+ ```
22
+
23
+ Add `duplicator` to your `INSTALLED_APPS` in settings.py:
24
+
25
+ ```shell
26
+ # settings.py
27
+ INSTALLED_APPS = [
28
+ # ...
29
+ 'django.contrib.admin',
30
+ # ...
31
+ 'duplicator',
32
+ ]
33
+ ```
34
+
35
+ ## 🚀 Usage
36
+
37
+ 1. Model (Enabling Duplication)
38
+
39
+ To make your model duplicatable, you must inherit from DuplicatorMixin.
40
+
41
+ ```shell
42
+ # models.py
43
+ from django.db import models
44
+ from duplicator import DuplicatorMixin
45
+
46
+ class Customer(DuplicatorMixin, models.Model):
47
+ name = models.CharField(max_length=255)
48
+ # ... other fields ...
49
+
50
+ def __str__(self):
51
+ return self.name
52
+ ```
53
+
54
+ 2. Admin (Enabling Buttons and Actions)
55
+
56
+ To enable both the single duplication button and the bulk duplication Admin Action, inherit from
57
+ DuplicatorAdminMixin.
58
+
59
+ ```shell
60
+ # admin.py
61
+ from django.contrib import admin
62
+ from duplicator import DuplicatorAdminMixin
63
+ from .models import Customer
64
+
65
+ @admin.register(Customer)
66
+ class CustomerAdmin(DuplicatorAdminMixin, admin.ModelAdmin):
67
+ list_display = ('name', 'id')
68
+ # ... other admin configurations ...
69
+ ```
70
+
71
+ ## 📸 Feature Demonstration
72
+
73
+ ### A. Bulk Duplication (Admin Action)
74
+
75
+ The duplication option appears in the Actions dropdown menu on the changelist page.
76
+
77
+ ![Action](./images/action.png)
78
+
79
+ ### B. Single Duplication (Change Form Button)
80
+
81
+ The "Duplicate and continue editing" button is prominently placed on the object detail page.
82
+
83
+ ![Detail](./images/detail.png)
84
+
85
+ ## 🤝 Contributing
86
+
87
+ We welcome all contributions! If you find a bug or have a feature suggestion, please open an Issue or submit a Pull
88
+ Request.
89
+
90
+ ## 📄 License
91
+
92
+ This project is licensed under the [Your License Type, e.g., MIT License]. See the LICENSE file for full details.
@@ -0,0 +1,2 @@
1
+ from .admin import DuplicatorAdminMixin
2
+ from .models import DuplicatorMixin
@@ -0,0 +1,89 @@
1
+ from copy import copy
2
+
3
+ from django.contrib import admin
4
+ from django.contrib import messages
5
+ from django.shortcuts import redirect
6
+ from django.urls import path
7
+ from django.utils.translation import gettext_lazy as _
8
+
9
+
10
+ @admin.action(description=_("Duplicate selected %(verbose_name_plural)s"))
11
+ def duplicate_selected_objects(model_admin, request, queryset):
12
+ if not model_admin.has_add_permission(request):
13
+ model_admin.message_user(
14
+ request,
15
+ _("You do not have permission to duplicate these objects."),
16
+ level=messages.ERROR,
17
+ )
18
+ return
19
+
20
+ cnt = 0
21
+ for obj in queryset:
22
+ model_admin._duplicate(obj)
23
+ cnt += 1
24
+
25
+ model_admin.message_user(
26
+ request, _("Successfully duplicated %d records" % cnt), level=messages.SUCCESS
27
+ )
28
+
29
+
30
+ class DuplicatorAdminMixin(admin.ModelAdmin):
31
+ change_form_template = "admin/duplicator/change_form.html"
32
+ actions = [duplicate_selected_objects]
33
+
34
+ def _duplicate(self, original_object):
35
+ if hasattr(original_object, "clone"):
36
+ new_object = original_object.clone()
37
+ else:
38
+ new_object = copy(original_object)
39
+ new_object.pk = None
40
+ if hasattr(new_object, "name"):
41
+ new_object.name = "{} (Copy)".format(new_object.name)
42
+
43
+ new_object.save()
44
+
45
+ return new_object
46
+
47
+ def get_urls(self):
48
+ urls = super(DuplicatorAdminMixin, self).get_urls()
49
+ info = self.model._meta.app_label, self.model._meta.model_name
50
+
51
+ custom_urls = [
52
+ path(
53
+ "<path:object_id>/duplicate/",
54
+ self.admin_site.admin_view(self.duplicate_view),
55
+ name="%s_%s_duplicate" % info,
56
+ ),
57
+ ]
58
+
59
+ return custom_urls + urls
60
+
61
+ def duplicate_view(self, request, object_id):
62
+ if not self.has_add_permission(request):
63
+ return self.message_user(
64
+ request,
65
+ _("You do not have permission to duplicate this object."),
66
+ level=messages.ERROR,
67
+ )
68
+
69
+ original_object = self.get_object(request, object_id)
70
+
71
+ # not found
72
+ if not original_object:
73
+ return redirect("..")
74
+
75
+ # clone object
76
+ new_object = self._duplicate(original_object)
77
+
78
+ self.message_user(
79
+ request,
80
+ _('Successfully duplicated object "%(obj)s".')
81
+ % {"obj": str(original_object)},
82
+ level=messages.SUCCESS,
83
+ )
84
+
85
+ return redirect(
86
+ "admin:%s_%s_change"
87
+ % (new_object._meta.app_label, new_object._meta.model_name),
88
+ new_object.pk,
89
+ )
@@ -0,0 +1,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DuplicatorConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "duplicator"
7
+ verbose_name = "Django duplicator utility"
@@ -0,0 +1,25 @@
1
+ from copy import copy
2
+
3
+
4
+ class DuplicatorMixin:
5
+
6
+ def clone(self, commit=True, **kwargs):
7
+ # clone instance
8
+ new_instance = copy(self)
9
+
10
+ # remove id
11
+ new_instance.pk = None
12
+
13
+ # flag if record have name
14
+ if hasattr(new_instance, "name"):
15
+ new_instance.name = "{} (Copy)".format(new_instance.name)
16
+
17
+ # add kwargs if any
18
+ for key, value in kwargs.items():
19
+ setattr(new_instance, key, value)
20
+
21
+ if commit:
22
+ # save new instance
23
+ new_instance.save()
24
+
25
+ return new_instance
@@ -0,0 +1,42 @@
1
+ {% extends "admin/change_form.html" %}
2
+ {% load i18n admin_urls admin_modify static %}
3
+
4
+ {% block submit_buttons_bottom %}
5
+ {{ block.super }}
6
+
7
+ {% if change and original.pk %}
8
+ {% url opts|admin_urlname:'duplicate' original.pk|admin_urlquote as duplicate_url %}
9
+
10
+ <div id="dup-btn-wrap" style="display: none;">
11
+ <a href="{% add_preserved_filters duplicate_url %}"
12
+ id="dup-btn-link"
13
+ class="button"
14
+ style="background: #417690;
15
+ color: white;
16
+ margin-left: 5px;
17
+ float: left;
18
+ border-radius: 4px;
19
+ display: inline-block;
20
+ text-decoration: none;
21
+ padding: 10px 15px;">
22
+ {% translate 'Duplicate and continue editing' %}
23
+ </a>
24
+ </div>
25
+ {% endif %}
26
+
27
+ {% block admin_change_form_document_ready %}
28
+ {{ block.super }}
29
+
30
+ {% if change and original.pk %}
31
+ <script>document.addEventListener("DOMContentLoaded", (function () {
32
+ const e = document.getElementById("dup-btn-link"), n = document.querySelector('input[name="_continue"]');
33
+ if (n && e) {
34
+ n.after(e);
35
+ const t = document.getElementById("dup-btn-wrap");
36
+ t && t.remove()
37
+ }
38
+ }));</script>
39
+ {% endif %}
40
+ {% endblock %}
41
+
42
+ {% endblock %}
@@ -0,0 +1,37 @@
1
+ [tool.poetry]
2
+ name = "django-duplicator"
3
+ version = "0.1.0"
4
+ description = "A reusable Django app to easily duplicate model records."
5
+ authors = ["Ferdina Kusumah <ferdina.kusumah@gmail.com>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ packages = [{ include = "duplicator" }]
9
+
10
+ homepage = "https://github.com/FerdinaKusumah/duplicator"
11
+ repository = "https://github.com/FerdinaKusumah/duplicator"
12
+ keywords = ["django", "admin", "mixin", "djang-duplicator", "bulk-action"]
13
+
14
+ classifiers = [
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Framework :: Django",
24
+ "Framework :: Django :: 3.2",
25
+ "Framework :: Django :: 4.2",
26
+ "Framework :: Django :: 5.0",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ "Topic :: Internet :: WWW/HTTP",
29
+ ]
30
+
31
+ [tool.poetry.dependencies]
32
+ python = ">=3.8, <4.0"
33
+ Django = ">=3.2"
34
+
35
+ [build-system]
36
+ requires = ["poetry-core"]
37
+ build-backend = "poetry.core.masonry.api"