Package: django4-inline-actions
django4-inline-actionsdjango4-inline-actions adds actions to each row of the ModelAdmin or InlineModelAdmin. Add support for Django 4.RequirementsPython 3.8 or newerScreenshotInstallationInstall django4-inline-actionspipinstalldjango4-inline-actionsAddinline_actionsto yourINSTALLED_APPS.IntegrationAdd theInlineActionsModelAdminMixinto yourModelAdmin.
If you want to have actions on your inlines, add theInlineActionsMixinto yourInlineModelAdmin.
Each action is implemented as a method on theModelAdmin/InlineModelAdminandmust havethe following signature.defaction_name(self,request,obj,parent_obj=None):ArgumentDescriptionrequestcurrent requestobjinstance on which the action was triggeredparent_objinstance of the parent model, only set on inlinesand should returnNoneto return to the current changeform or aHttpResponse.
Finally, add your method name to list of actionsinline_actionsdefined on the correspondingModelAdmin.
If you want to disable theactionscolumn, you have to explicitly setinline_actions = None.
To add your actions dynamically, you can use the methodget_inline_actions(self, request, obj=None)instead.This module is bundled with two actions for viewing (inline_actions.actions.ViewAction) and deleting (inline_actions.actions.DeleteAction).
Just add these classes to your admin and you're done.Additionally, you can add methods to generate a custom label and CSS classes per object.
If you have an inline action calledaction_namethen you can definedefget_action_name_label(self,obj):return'some string'defget_action_name_css(self,obj):return'some string'ArgumentDescriptionobjinstance on which the action was triggeredEach defined method has to return a string.Example 1Imagine a simple news application with the followingadmin.py.fromdjango.contribimportadminfrominline_actions.adminimportInlineActionsMixinfrominline_actions.adminimportInlineActionsModelAdminMixinfrom.modelsimportArticle,AuthorclassArticleInline(InlineActionsMixin,admin.TabularInline):model=Articleinline_actions=[]defhas_add_permission(self,request,obj=None):returnFalse@admin.register(Author)classAuthorAdmin(InlineActionsModelAdminMixin,admin.ModelAdmin):inlines=[ArticleInline]list_display=('name',)@admin.register(Article)classAuthorAdmin(admin.ModelAdmin):list_display=('title','status','author')We now want to add two simple actions (view,unpublish) to each article within theAuthorAdmin.
Theviewaction redirects to the changeform of the selected instance.fromdjango.core.urlresolversimportreversefromdjango.shortcutsimportredirectclassArticleInline(InlineActionsMixin,admin.TabularInline):# ...inline_actions=['view']# ...defview(self,request,obj,parent_obj=None):url=reverse('admin:{}_{}_change'.format(obj._meta.app_label,obj._meta.model_name,),args=(obj.pk,))returnredirect(url)view.short_description=_("View")Sinceunpublishdepends onarticle.statuswe must useget_inline_actionsto add this action dynamically.fromdjango.contribimportadmin,messagesfromdjango.utils.translationimportgettext_lazyas_classArticleInline(InlineActionsMixin,admin.TabularInline):# ...defget_inline_actions(self,request,obj=None):actions=super(ArticleInline,self).get_inline_actions(request,obj)ifobj:ifobj.status==Article.PUBLISHED:actions.append('unpublish')returnactionsdefunpublish(self,request,obj,parent_obj=None):obj.status=Article.DRAFTobj.save()messages.info(request,_("Article unpublished"))unpublish.short_description=_("Unpublish")Addinginline_actionsto the changelist works similar. See the sample project for further details (test_proj/blog/admin.py).Example 2Instead of creating separate actions for publishing and unpublishing, we might prefer an action, which toggles between those two states.toggle_publishimplements the behaviour described above.deftoggle_publish(self,request,obj,parent_obj=None):ifobj.status==Article.DRAFT:obj.status=Article.PUBLISHEDelse:obj.status=Article.DRAFTobj.save()ifobj.status==Article.DRAFT:messages.info(request,_("Article unpublished."))else:messages.info(request,_("Article published."))This might leave the user with an ambiguous button label as it will be calledToggle publishregardless of the internal state.
We can specify a dynamic label by adding a special methodget_ACTIONNAME_label.defget_toggle_publish_label(self,obj):ifobj.status==Article.DRAFT:return'Publish'return'Unpublish'So assuming an object in a row hasDRAFTstatus, then the button label will beToggle publishandToggle unpublishotherwise.We can go even fancier when we create a method that will add css classes for each object depending on a status like:defget_toggle_publish_css(self,obj):ifobj.status==Article.DRAFT:return'btn-red'return'btn-green'You can make it more eye-candy by usingbtn-greenthat makes your button green andbtn-redthat makes your button red.
Or you can use those classes to add some javascript logic (i.e. confirmation box).Tip on confirmation alertsWhen performing a certain critical action or ones which may not be easily reversible it's good to have a confirmation prompt before submitting the action form. To achieve this, one way would be to overridetemplates/admin/change_list.htmlwith the following.{% extends "admin/change_list.html" %}

{% block extrahead %}
    {{ block.super }}<script>(function(){document.addEventListener("DOMContentLoaded",function(event){letinline_actions=document.querySelectorAll(".inline_actions input");for(vari=0;i<inline_actions.length;i++){inline_actions[i].addEventListener("click",function(e){if(!confirm("Do you really want to "+e.target.value+"?")){e.preventDefault();}});}});})();</script>{% endblock %}If a staff user has clicked any inline action accidentally, they can safely click no in the confirmation prompt & the inline action form would not be submitted.Intermediate formsThe current implementation for using intermediate forms involves some manual handling.
This will be simplified in the next major release!In order to have an intermediate form, you must add some information about the triggered action.django4-inline-actionsprovides a handy templatetagrender_inline_action_fields,
which adds these information as hidden fields to a form.{% extends "admin/base_site.html" %}
{% load inline_action_tags %}

{% block content %}<formaction=""method="post">{% csrf_token %}
    {% render_inline_action_fields %}

    {{ form.as_p }}<inputtype="submit"name="_back"value="Cancel"/><inputtype="submit"name="_save"value="Update"/></form>{% endblock %}As the action does not know that an intermediate form is used, we have to include some special handling.
In the case above we have to consider 3 cases:The form has been submitted and we want to redirect to the previous view.Back button has been clicked.Initial access to the intermediate page/form.The corresponding action could look likedefchange_title(self,request,obj,parent_obj=None):# 1. has the form been submitted?if'_save'inrequest.POST:form=forms.ChangeTitleForm(request.POST,instance=obj)form.save()returnNone# return back to list view# 2. has the back button been pressed?elif'_back'inrequest.POST:returnNone# return back to list view# 3. simply display the formelse:form=forms.ChangeTitleForm(instance=obj)returnrender(request,'change_title.html',context={'form':form})Example ApplicationYou can seedjango4-inline-actionsin action using the bundled test applicationtest_proj.
Usepoetryto run it.gitclonehttps://github.com/HiroshiFuu/django4-inline-actions.gitcddjango4-inline-actions/
poetryinstall
poetryrunpipinstallDjangocdtest_proj
poetryrun./manage.pymigrate
poetryrun./manage.pycreatesuperuser
poetryrun./manage.pyrunserverOpenhttp://localhost:8000/admin/in your browser and create an author and some articles.How to test your actions?There are two ways on how to write tests for your actions.
We will usepytestfor the following examples.Test the action itselfBefore we can call our action on the admin class itself, we have to instantiate the admin environment and pass it to theModelAdmintogether with an instance of our model.
Therefore, we implement a fixture calledadmin_site, which is used on each test.importpytestfromdjango.contrib.adminimportAdminSitefromyourapp.module.adminimportMyAdmin@pytest.fixturedefadmin_site():returnAdminSite()@pytest.mark.django_dbdeftest_action_XXX(admin_site):"""Test action XXX"""fake_request={}# you might need to use a RequestFactory hereobj=...# create an instanceadmin=MyAdmin(obj,admin_site)admin.render_inline_actions(article)response=admin.action_XXX(fake_request,obj)# assert the state of the applicationTest the admin integrationAlternatively, you can test your actions on the real Django admin page.
You will have to log in, navigate to the corresponding admin and trigger a click on the action.
To simplify this process you can usedjango-webtest.
Example can be foundhere.DevelopmentThis project usespoetryfor packaging and
managing all dependencies andpre-committo runflake8,isort,mypyandblack.Additionally,pdbppandbetter-exceptionsare installed to provide a better debugging experience.
To enablebetter-exceptionsyou have to runexport BETTER_EXCEPTIONS=1in your current session/terminal.Clone this repository and runpoetryinstall
poetryrunpre-commitinstallto create a virtual enviroment containing all dependencies.
Afterwards, You can run the test suite usingpoetryrunpytestThis repository follows theConventional Commitsstyle.DeploymentUsesetup.pyto build and upload to PyPI usingtwine.Cookiecutter templateThis project was created usingcruftand thecookiecutter-pyprojecttemplate.
In order to update this repository to the latest template version runcruftupdatein the root of this repository.