django-admin-inlines
django-admin-inlines adds actions to each row of the ModelAdmin or InlineModelAdmin. (A fork of django-inline-actions)
This is a fork of django-inline-actions. As this package has gone unmaintained for several years, we have decided to maintain the package indefinitely in order to maintain compatibility with future Django versions, starting with Django 4.x.
Requirements
- Python 3.10 or newer
- Django 4.2 or newer
Screenshot
Installation
-
Install django-admin-inlines
pip install django-admin-inlines
-
Add
inline_actionsto yourINSTALLED_APPS.
Integration
Add the InlineActionsModelAdminMixin to your ModelAdmin.
If you want to have actions on your inlines, add the InlineActionsMixin to your InlineModelAdmin.
Each action is implemented as a method on the ModelAdmin/InlineModelAdmin and must have the following signature.
def action_name(self, request, obj, parent_obj=None):
| Argument | Description |
|---|---|
request |
current request |
obj |
instance on which the action was triggered |
parent_obj |
instance of the parent model, only set on inlines |
and should return None to return to the current changeform or a HttpResponse.
Finally, add your method name to list of actions inline_actions defined on the corresponding ModelAdmin.
If you want to disable the actions column, you have to explicitly set inline_actions = None.
To add your actions dynamically, you can use the method get_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 called action_name then you can define
def get_action_name_label(self, obj):
return 'some string'
def get_action_name_css(self, obj):
return 'some string'
| Argument | Description |
|---|---|
obj |
instance on which the action was triggered |
Each defined method has to return a string.
Example 1
Imagine a simple news application with the following admin.py.
from django.contrib import admin
from inline_actions.admin import InlineActionsMixin
from inline_actions.admin import InlineActionsModelAdminMixin
from .models import Article, Author
class ArticleInline(InlineActionsMixin,
admin.TabularInline):
model = Article
inline_actions = []
def has_add_permission(self, request, obj=None):
return False
@admin.register(Author)
class AuthorAdmin(InlineActionsModelAdminMixin,
admin.ModelAdmin):
inlines = [ArticleInline]
list_display = ('name',)
@admin.register(Article)
class AuthorAdmin(admin.ModelAdmin):
list_display = ('title', 'status', 'author')
We now want to add two simple actions (view, unpublish) to each article within the AuthorAdmin.
The view action redirects to the changeform of the selected instance.
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
class ArticleInline(InlineActionsMixin,
admin.TabularInline):
# ...
inline_actions = ['view']
# ...
def view(self, request, obj, parent_obj=None):
url = reverse(
'admin:{}_{}_change'.format(
obj._meta.app_label,
obj._meta.model_name,
),
args=(obj.pk,)
)
return redirect(url)
view.short_description = _("View")
Since unpublish depends on article.status we must use get_inline_actions to add this action dynamically.
from django.contrib import admin, messages
from django.utils.translation import gettext_lazy as _
class ArticleInline(InlineActionsMixin,
admin.TabularInline):
# ...
def get_inline_actions(self, request, obj=None):
actions = super(ArticleInline, self).get_inline_actions(request, obj)
if obj:
if obj.status == Article.PUBLISHED:
actions.append('unpublish')
return actions
def unpublish(self, request, obj, parent_obj=None):
obj.status = Article.DRAFT
obj.save()
messages.info(request, _("Article unpublished"))
unpublish.short_description = _("Unpublish")
Adding inline_actions to the changelist works similar. See the sample project for further details (test_proj/blog/admin.py).
Example 2
Instead of creating separate actions for publishing and unpublishing, we might prefer an action, which toggles between those two states.
toggle_publish implements the behaviour described above.
def toggle_publish(self, request, obj, parent_obj=None):
if obj.status == Article.DRAFT:
obj.status = Article.PUBLISHED
else:
obj.status = Article.DRAFT
obj.save()
if obj.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 called Toggle publish regardless of the internal state.
We can specify a dynamic label by adding a special method get_ACTIONNAME_label.
def get_toggle_publish_label(self, obj):
if obj.status == Article.DRAFT:
return 'Publish'
return 'Unpublish'
So assuming an object in a row has DRAFT status, then the button label will be Toggle publish and Toggle unpublish otherwise.
We can go even fancier when we create a method that will add css classes for each object depending on a status like:
def get_toggle_publish_css(self, obj):
if obj.status == Article.DRAFT:
return 'btn-red'
return 'btn-green'
You can make it more eye-candy by using btn-green that makes your button green and btn-red that makes your button red.
Or you can use those classes to add some javascript logic (i.e. confirmation box).
Tip on confirmation alerts
When 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 override templates/admin/change_list.html with the following.
{% extends "admin/change_list.html" %}
{% block extrahead %}
{{ block.super }}
<script>
(function() {
document.addEventListener("DOMContentLoaded", function(event) {
let inline_actions = document.querySelectorAll(".inline_actions input");
for (var i=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 forms
The 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.
django-admin-inlines provides a handy templatetag render_inline_action_fields,
which adds these information as hidden fields to a form.
{% extends "admin/base_site.html" %}
{% load inline_action_tags %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
{% render_inline_action_fields %}
{{ form.as_p }}
<input type="submit" name="_back" value="Cancel"/>
<input type="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 like
def change_title(self, request, obj, parent_obj=None):
# 1. has the form been submitted?
if '_save' in request.POST:
form = forms.ChangeTitleForm(request.POST, instance=obj)
form.save()
return None # return back to list view
# 2. has the back button been pressed?
elif '_back' in request.POST:
return None # return back to list view
# 3. simply display the form
else:
form = forms.ChangeTitleForm(instance=obj)
return render(
request,
'change_title.html',
context={'form': form}
)
Example Application
You can see django-admin-inlines in action using the bundled test application test_proj.
git clone https://github.com/panevo/django-admin-inlines.git
cd django-admin-inlines/
pip install -e . --group dev
cd test_proj
./manage.py migrate
./manage.py createsuperuser
./manage.py runserver
Open http://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 use pytest for the following examples.
Test the action itself
Before we can call our action on the admin class itself, we have to instantiate the admin environment and pass it to the ModelAdmin together with an instance of our model.
Therefore, we implement a fixture called admin_site, which is used on each test.
import pytest
from django.contrib.admin import AdminSite
from yourapp.module.admin import MyAdmin
@pytest.fixture
def admin_site():
return AdminSite()
@pytest.mark.django_db
def test_action_XXX(admin_site):
"""Test action XXX"""
fake_request = {} # you might need to use a RequestFactory here
obj = ... # create an instance
admin = MyAdmin(obj, admin_site)
admin.render_inline_actions(article)
response = admin.action_XXX(fake_request, obj)
# assert the state of the application
Test the admin integration
Alternatively, 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 use django-webtest. Example can be found here.
Development
This project uses pre-commit to run black, isort, autoflake and mypy. Linting in CI also runs ruff.
Clone this repository and, in a virtual environment, run
pip install -e . --group dev
pre-commit install
to install the package together with all development dependencies (--group requires pip 25.1 or newer).
Afterwards, you can run the test suite using
pytest
or run it against all supported Python and Django versions using tox.
This repository follows the Conventional Commits style.
Deployment
Releases are published to PyPI by the publish.yml workflow when a release is created on GitHub.
Before creating a release, set the new version in pyproject.toml and add a dated entry for it at the top of CHANGELOG.md.
Cookiecutter template
This project was created using cruft and the cookiecutter-pyproject template. In order to update this repository to the latest template version run
cruft update
in the root of this repository.
Metadata
Release files for django-admin-inlines 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| django_admin_inlines-1.2.0.tar.gz | 12.1 kB | With provenance | Details for django_admin_inlines-1.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| django_admin_inlines-1.2.0-py3-none-any.whl | Python 3 | none | any | Details for django_admin_inlines-1.2.0-py3-none-any.whl |
Total release size: 24.3 kB
Release files / django_admin_inlines-1.2.0.tar.gz
| Download URL | django_admin_inlines-1.2.0.tar.gz |
|---|---|
| Size | 12.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5153e5cf1c2b85056be630a70bb8fc2e008f40e374461a00c49441a93012abb0
|
|
BLAKE2b-256 checksum How to use checksums |
8bfc797b3e2bdec1fbe56713d5a8b39e14331e7e9f3111cea8c0526aa96d929d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / django_admin_inlines-1.2.0-py3-none-any.whl
| Download URL | django_admin_inlines-1.2.0-py3-none-any.whl |
|---|---|
| Size | 12.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c67f1185fecb73fcfa5603bf2ed95c89f324499316e4832ee07a8d8d29bad607
|
|
BLAKE2b-256 checksum How to use checksums |
93d3edb131c2e975f72253f669bb2a228361f31f11fc4f89db5dc8c6ebe054df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logSecurity for django-admin-inlines 1.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for django_admin_inlines-1.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.