I`m using a GenericStackedInline in my django admin. I would like to have the option to duplicate the inline object when editing the parent object.
I can think of two ways of doing this:
- Using django-inline-actions to add a “clone” button. This did not work, because it does not show when using a fieldset in the GenericStackedInline
- Adding another checkbox next to “delete” checkbox with label “clone”. When activating the checkbox and saving parent object, it should clone the inline object with new id. Is there an easy way to add another checkbox and add a action to handle cloning?
Background:
I built some kind of a content editor with GenericStackedInline. The inline data has some fields. It is lot`s of work to duplicate entries with copy and pasting the fields. Would be great to just be able to clone entries. I think this should be easy in a function, where I just remove the ID of the object and then save it as new. I just don`t know how to implement it into the django admin iterface as easy as possible.
I’m not sure I understand exactly what you want the UI/UX to be here.
My interpretation is that you have an inline widget with multiple entries, and you want the user to be able to select an individual entry and replicate it, without causing a submit with the corresponding page refresh. Is that an accurate phrasing?
@KenWhitesell Thank you. A submit with the parent object would be fine. I would just like to be able to clone inline entries, without copy and pasting every field. Like the default “delete” checkbox, but with “clone” instead of “delete”
Ok, I’m with you so far - but there are two different ways in which this can generally be done. It can either be with a submit button that submits to a view to clone the selected entry, or a button that replicates the current entry as one of the extra rows within the current formset. The first would require a custom view to handle that request, the second is done entirely within JavaScript inside the browser.
Either situation is going to require that you create a subclass of the GenericStackedInline class where you create a custom formset. You can add a widget in that formset to trigger the actions you wish to perform.
As one idea, see Adding additional fields to a formset, where in this case, this new field would be the button to add for each row.
Thank you! I will try that, should be not that complicated
@KenWhitesell - Ok, it`s more complicated then I throught:
I added this code:
class RgCloneInlineFormset(BaseGenericInlineFormSet):
def add_fields(self, form, index):
super().add_fields(form, index)
form.fields["clone_field"] = forms.BooleanField()
class RgGenericStackedInlineWithClone(GenericStackedInline):
template = "admin/edit_inline/stacked_with_clone.html"
formset = RgCloneInlineFormset
I expect to be able to use the field in the template now like this:
<span class="clone">{{ inline_admin_form.clone_field.field }} Clone</span>
But it`s just empty, field is not rendered (label is shown). What am I doing wrong?
See Rendering fields manually
A field is rendered as {{ form_name.field_name }}
.
Ok, found a solution. I don`t really like it. Biggest flaw in my oppinion is, that I did not find a way to do clone in InlineClass, Mixin or formset. It's a bit clumsy that two classes / Mixins need to be combined to handle it.
Improvements or other solutions are very welcome. Thank you @KenWhitesell for pointing me to the right drection.
Inline classes
class RgCloneInlineFormset(BaseGenericInlineFormSet):
def add_fields(self, form, index):
super().add_fields(form, index)
form.fields["CLONE"] = forms.BooleanField(required=False)
class RgGenericStackedInlineWithClone(GenericStackedInline):
template = "admin/edit_inline/stacked_with_clone.html"
formset = RgCloneInlineFormset
class DetailpageElementInline(RgGenericStackedInlineWithClone, RgImagePreviewMixin):
model = DetailpageElement
extra = 0
Admin class
class CloneDetailpageElementInlineMixin():
def save_formset(self, request, form, formset, change):
# Save formset
ret = super().save_formset(request, form, formset, change)
# Do clone if nessesary
detailpage_ids_to_clone = []
for data in formset.cleaned_data:
if isinstance(data.get("id"), DetailpageElement):
if data.get("CLONE"):
detailpage_ids_to_clone.append(data.get("id").id)
for detailpage_element in DetailpageElement.objects.filter(id__in=detailpage_ids_to_clone):
detailpage_element.id = None
detailpage_element.save()
# Make sure function works like super function
return ret
@admin.register(ArticleInt)
class ArticleIntAdmin(CloneDetailpageElementInlineMixin, ContentAdminBase):
inlines = [
inlines.DetailpageElementInline,
]
Template admin/edit_inline/stacked_with_clone.html
Just copied the stacked.html from django source code and added
{% if inline_admin_formset.has_add_permission and inline_admin_form.original %}<span class="clone">{{ inline_admin_form.form.CLONE }} Clone</span>{% endif %}
Your solution looks fine to me.
Depends upon your perspective I guess.
I will add some cautionary commentary though.
I will frequently point out to people the first two paragraphs of the Django Admin docs. In this case, it’s the second paragraph that I would reference:
The admin has many hooks for customization, but beware of trying to use those hooks exclusively. If you need to provide a more process-centric interface that abstracts away the implementation details of database tables and fields, then it’s probably time to write your own views.
I have seen too many cases where people keep trying to extend the Admin to areas far beyond what it was ever designed or intended to do. There comes a point where it would have been easier just to create a custom view instead of wrangling the admin to force those features into place.
I am not saying that you have reached this point. Just recommending that you maintain the perspective that there is a point of diminishing value with extending the admin, and if you keep getting this feeling like things are “awkward” or “clumsy”, then you may be approaching this line.