{"id":13468483,"url":"https://github.com/escaped/django-inline-actions","last_synced_at":"2025-04-04T13:13:31.911Z","repository":{"id":45438261,"uuid":"58007279","full_name":"escaped/django-inline-actions","owner":"escaped","description":"django-inline-actions adds actions to each row of the ModelAdmin or InlineModelAdmin.","archived":false,"fork":false,"pushed_at":"2023-04-21T21:39:12.000Z","size":289,"stargazers_count":215,"open_issues_count":16,"forks_count":62,"subscribers_count":11,"default_branch":"master","last_synced_at":"2024-10-30T06:54:56.940Z","etag":null,"topics":["django","django-admin","hacktoberfest","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/escaped.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2016-05-03T23:09:59.000Z","updated_at":"2024-10-07T08:14:41.000Z","dependencies_parsed_at":"2024-01-18T20:05:02.753Z","dependency_job_id":null,"html_url":"https://github.com/escaped/django-inline-actions","commit_stats":{"total_commits":77,"total_committers":11,"mean_commits":7.0,"dds":"0.19480519480519476","last_synced_commit":"d02441339a1b5ec3d34c10a29da41157afe9df2c"},"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escaped%2Fdjango-inline-actions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escaped%2Fdjango-inline-actions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escaped%2Fdjango-inline-actions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escaped%2Fdjango-inline-actions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/escaped","download_url":"https://codeload.github.com/escaped/django-inline-actions/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247182378,"owners_count":20897380,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["django","django-admin","hacktoberfest","python"],"created_at":"2024-07-31T15:01:11.905Z","updated_at":"2025-04-04T13:13:31.891Z","avatar_url":"https://github.com/escaped.png","language":"Python","funding_links":[],"categories":["Python"],"sub_categories":[],"readme":"# django-inline-actions\n\n![PyPI](https://img.shields.io/pypi/v/django-inline-actions?style=flat-square)\n![GitHub Workflow Status (master)](https://img.shields.io/github/workflow/status/escaped/django-inline-actions/Test%20\u0026%20Lint/master?style=flat-square)\n![Coveralls github branch](https://img.shields.io/coveralls/github/escaped/django-inline-actions/master?style=flat-square)\n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-inline-actions?style=flat-square)\n![PyPI - License](https://img.shields.io/pypi/l/django-inline-actions?style=flat-square)\n\ndjango-inline-actions adds actions to each row of the ModelAdmin or InlineModelAdmin.\n\n## Requirements\n\n* Python 3.6.1 or newer\n\n## Screenshot\n\n![Changelist example](https://raw.githubusercontent.com/escaped/django-inline-actions/master/example_changelist.png)\n![Inline example](https://raw.githubusercontent.com/escaped/django-inline-actions/master/example_inline.png)\n\n## Installation\n\n1. Install django-inline-actions\n\n   ```sh\n   pip install django-inline-actions\n   ```\n\n2. Add `inline_actions` to your `INSTALLED_APPS`.\n\n## Integration\n\nAdd the `InlineActionsModelAdminMixin` to your `ModelAdmin`.\nIf you want to have actions on your inlines, add the `InlineActionsMixin` to your `InlineModelAdmin`.\nEach action is implemented as a method on the `ModelAdmin`/`InlineModelAdmin` and **must have** the following signature.\n\n```python\ndef action_name(self, request, obj, parent_obj=None):\n```\n\n| Argument     | Description                                       |\n|--------------|---------------------------------------------------|\n| `request`    | current request                                   |\n| `obj`        | instance on which the action was triggered        |\n| `parent_obj` | instance of the parent model, only set on inlines |\n\nand should return `None` to return to the current changeform or a `HttpResponse`.\nFinally, add your method name to list of actions `inline_actions` defined on the corresponding `ModelAdmin`.\nIf you want to disable the *actions* column, you have to explicitly set `inline_actions = None`.\nTo add your actions dynamically, you can use the method `get_inline_actions(self, request, obj=None)` instead.\n\nThis module is bundled with two actions for viewing (`inline_actions.actions.ViewAction`) and deleting (`inline_actions.actions.DeleteAction`).\nJust add these classes to your admin and you're done.\n\nAdditionally, you can add methods to generate a custom label and CSS classes per object.\nIf you have an inline action called `action_name` then you can define\n\n```python\ndef get_action_name_label(self, obj):\n    return 'some string'\n\ndef get_action_name_css(self, obj):\n    return 'some string'\n```\n\n| Argument | Description                                |\n|----------|--------------------------------------------|\n| `obj`    | instance on which the action was triggered |\n\nEach defined method has to return a string.\n\n### Example 1\n\nImagine a simple news application with the following `admin.py`.\n\n```python\nfrom django.contrib import admin\nfrom inline_actions.admin import InlineActionsMixin\nfrom inline_actions.admin import InlineActionsModelAdminMixin\n\nfrom .models import Article, Author\n\n\nclass ArticleInline(InlineActionsMixin,\n                    admin.TabularInline):\n    model = Article\n    inline_actions = []\n\n    def has_add_permission(self, request, obj=None):\n        return False\n\n\n@admin.register(Author)\nclass AuthorAdmin(InlineActionsModelAdminMixin,\n                  admin.ModelAdmin):\n    inlines = [ArticleInline]\n    list_display = ('name',)\n\n\n@admin.register(Article)\nclass AuthorAdmin(admin.ModelAdmin):\n    list_display = ('title', 'status', 'author')\n```\n\nWe now want to add two simple actions (`view`, `unpublish`) to each article within the `AuthorAdmin`.\nThe `view` action redirects to the changeform of the selected instance.\n\n```python\nfrom django.core.urlresolvers import reverse\nfrom django.shortcuts import redirect\n\n\nclass ArticleInline(InlineActionsMixin,\n                    admin.TabularInline):\n    # ...\n    inline_actions = ['view']\n    # ...\n\n    def view(self, request, obj, parent_obj=None):\n        url = reverse(\n            'admin:{}_{}_change'.format(\n                obj._meta.app_label,\n                obj._meta.model_name,\n            ),\n            args=(obj.pk,)\n        )\n        return redirect(url)\n    view.short_description = _(\"View\")\n```\n\nSince `unpublish` depends on `article.status` we must use `get_inline_actions` to add this action dynamically.\n\n```python\nfrom django.contrib import admin, messages\nfrom django.utils.translation import gettext_lazy as _\n\n\nclass ArticleInline(InlineActionsMixin,\n                    admin.TabularInline):\n    # ...\n    def get_inline_actions(self, request, obj=None):\n        actions = super(ArticleInline, self).get_inline_actions(request, obj)\n        if obj:\n            if obj.status == Article.PUBLISHED:\n                actions.append('unpublish')\n        return actions\n\n    def unpublish(self, request, obj, parent_obj=None):\n        obj.status = Article.DRAFT\n        obj.save()\n        messages.info(request, _(\"Article unpublished\"))\n    unpublish.short_description = _(\"Unpublish\")\n```\n\nAdding `inline_actions` to the changelist works similar. See the sample project for further details (`test_proj/blog/admin.py`).\n\n### Example 2\n\nInstead of creating separate actions for publishing and unpublishing, we might prefer an action, which toggles between those two states.\n`toggle_publish` implements the behaviour described above.\n\n```python\ndef toggle_publish(self, request, obj, parent_obj=None):\n    if obj.status == Article.DRAFT:\n        obj.status = Article.PUBLISHED\n    else:\n        obj.status = Article.DRAFT\n\n    obj.save()\n\n    if obj.status == Article.DRAFT:\n        messages.info(request, _(\"Article unpublished.\"))\n    else:\n        messages.info(request, _(\"Article published.\"))\n```\n\nThis might leave the user with an ambiguous button label as it will be called `Toggle publish` regardless of the internal state.\nWe can specify a dynamic label by adding a special method `get_ACTIONNAME_label`.\n\n```python\ndef get_toggle_publish_label(self, obj):\n    if obj.status == Article.DRAFT:\n        return 'Publish'\n    return 'Unpublish'\n```\n\nSo assuming an object in a row has `DRAFT` status, then the button label will be `Toggle publish` and `Toggle unpublish` otherwise.\n\nWe can go even fancier when we create a method that will add css classes for each object depending on a status like:\n\n```python\ndef get_toggle_publish_css(self, obj):\n    if obj.status == Article.DRAFT:\n        return 'btn-red'\n    return 'btn-green'\n```\n\nYou can make it more eye-candy by using `btn-green` that makes your button green and `btn-red` that makes your button red.\nOr you can use those classes to add some javascript logic (i.e. confirmation box).\n\n### Tip on confirmation alerts\n\nWhen 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.\n\n```html\n{% extends \"admin/change_list.html\" %}\n\n{% block extrahead %}\n    {{ block.super }}\n    \u003cscript\u003e\n        (function() {\n            document.addEventListener(\"DOMContentLoaded\", function(event) {\n                let inline_actions = document.querySelectorAll(\".inline_actions input\");\n                for (var i=0; i \u003c inline_actions.length; i++) {\n                    inline_actions[i].addEventListener(\"click\", function(e) {\n                        if(!confirm(\"Do you really want to \" + e.target.value + \"?\")) {\n                            e.preventDefault();\n                        }\n                    });\n                }\n            });\n        })();\n    \u003c/script\u003e\n{% endblock %}\n```\n\nIf a staff user has clicked any inline action accidentally, they can safely click no in the confirmation prompt \u0026 the inline action form would not be submitted.\n\n## Intermediate forms\n\nThe current implementation for using intermediate forms involves some manual handling.\nThis will be simplified in the next major release!\n\n\nIn order to have an intermediate form, you must add some information about the triggered action.\n`django-inline-actions` provides a handy templatetag `render_inline_action_fields`,\nwhich adds these information as hidden fields to a form.\n\n```html\n{% extends \"admin/base_site.html\" %}\n{% load inline_action_tags %}\n\n{% block content %}\n  \u003cform action=\"\" method=\"post\"\u003e\n    {% csrf_token %}\n    {% render_inline_action_fields %}\n\n    {{ form.as_p }}\n\n    \u003cinput type=\"submit\" name=\"_back\" value=\"Cancel\"/\u003e\n    \u003cinput type=\"submit\" name=\"_save\" value=\"Update\"/\u003e\n  \u003c/form\u003e\n{% endblock %}\n```\n\nAs the action does not know that an intermediate form is used, we have to include some special handling.\nIn the case above we have to consider 3 cases:\n\n1. The form has been submitted and we want to redirect to the previous view.\n2. Back button has been clicked.\n3. Initial access to the intermediate page/form.\n\nThe corresponding action could look like\n\n```python\n    def change_title(self, request, obj, parent_obj=None):\n\n        # 1. has the form been submitted?\n        if '_save' in request.POST:\n            form = forms.ChangeTitleForm(request.POST, instance=obj)\n            form.save()\n            return None  # return back to list view\n        # 2. has the back button been pressed?\n        elif '_back' in request.POST:\n            return None  # return back to list view\n        # 3. simply display the form\n        else:\n            form = forms.ChangeTitleForm(instance=obj)\n\n        return render(\n            request,\n            'change_title.html',\n            context={'form': form}\n        )\n```\n\n## Example Application\n\nYou can see `django-inline-actions` in action using the bundled test application `test_proj`.\nUse [`poetry`](https://poetry.eustace.io/) to run it.\n\n```bash\ngit clone https://github.com/escaped/django-inline-actions.git\ncd django-inline-actions/\npoetry install\npoetry run pip install Django\ncd test_proj\npoetry run ./manage.py migrate\npoetry run ./manage.py createsuperuser\npoetry run ./manage.py runserver\n```\n\nOpen [`http://localhost:8000/admin/`](http://localhost:8000/admin/) in your browser and create an author and some articles.\n\n## How to test your actions?\n\nThere are two ways on how to write tests for your actions.\nWe will use [pytest](https://docs.pytest.org/en/latest/) for the following examples.\n\n### Test the action itself\n\nBefore 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.\nTherefore, we implement a fixture called `admin_site`, which is used on each test.\n\n```python\nimport pytest\nfrom django.contrib.admin import AdminSite\n\nfrom yourapp.module.admin import MyAdmin\n\n\n@pytest.fixture\ndef admin_site():\n    return AdminSite()\n\n@pytest.mark.django_db\ndef test_action_XXX(admin_site):\n    \"\"\"Test action XXX\"\"\"\n    fake_request = {}  # you might need to use a RequestFactory here\n    obj = ...  # create an instance\n\n    admin = MyAdmin(obj, admin_site)\n\n    admin.render_inline_actions(article)\n    response = admin.action_XXX(fake_request, obj)\n    # assert the state of the application\n```\n\n### Test the admin integration\n\nAlternatively, you can test your actions on the real Django admin page.\nYou will have to log in, navigate to the corresponding admin and trigger a click on the action.\nTo simplify this process you can use [django-webtest](https://github.com/django-webtest/django-webtest).\nExample can be found [here](https://github.com/escaped/django-inline-actions/blob/76b6f6b83c6d1830c2ad71512cd1e85362936dbd/test_proj/blog/tests/test_inline_admin.py#L146).\n\n## Development\n\nThis project uses [poetry](https://poetry.eustace.io/) for packaging and\nmanaging all dependencies and [pre-commit](https://pre-commit.com/) to run\n[flake8](http://flake8.pycqa.org/), [isort](https://pycqa.github.io/isort/),\n[mypy](http://mypy-lang.org/) and [black](https://github.com/python/black).\n\nAdditionally, [pdbpp](https://github.com/pdbpp/pdbpp) and [better-exceptions](https://github.com/qix-/better-exceptions) are installed to provide a better debugging experience.\nTo enable `better-exceptions` you have to run `export BETTER_EXCEPTIONS=1` in your current session/terminal.\n\nClone this repository and run\n\n```bash\npoetry install\npoetry run pre-commit install\n```\n\nto create a virtual enviroment containing all dependencies.\nAfterwards, You can run the test suite using\n\n```bash\npoetry run pytest\n```\n\nThis repository follows the [Conventional Commits](https://www.conventionalcommits.org/)\nstyle.\n\n### Cookiecutter template\n\nThis project was created using [cruft](https://github.com/cruft/cruft) and the\n[cookiecutter-pyproject](https://github.com/escaped/cookiecutter-pypackage) template.\nIn order to update this repository to the latest template version run\n\n```sh\ncruft update\n```\n\nin the root of this repository.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fescaped%2Fdjango-inline-actions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fescaped%2Fdjango-inline-actions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fescaped%2Fdjango-inline-actions/lists"}