diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..32c8f3a
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "python.formatting.provider": "black",
+ "editor.formatOnSave": true
+}
\ No newline at end of file
diff --git a/Makefile b/Makefile
index e770a5c..c541d1b 100644
--- a/Makefile
+++ b/Makefile
@@ -7,12 +7,18 @@ test:
coverage-badge -f -o coverage.svg
python -m readme_renderer README.md -o /tmp/README.html
+check-readme:
+ python -m readme_renderer README.md -o /tmp/README.html
+
migrate:
./tests/manage.py makemigrations
./tests/manage.py migrate
+shell:
+ ./tests/manage.py shell
+
package:
python3 setup.py sdist bdist_wheel
upload-testpypi:
- python3 -m twine upload --repository testpypi dist/*
+ python3 -m twine upload --repository testpypi dist/django_admin_confirm-$(VERSION)*
diff --git a/README.md b/README.md
index fb9e4e8..23123c0 100644
--- a/README.md
+++ b/README.md
@@ -2,11 +2,15 @@

-AdminConfirmMixin is a mixin for ModelAdmin to add confirmations to changes and additions.
+AdminConfirmMixin is a mixin for ModelAdmin to add confirmations to change, add and actions.
-
+
-It can be configured to add a confirmation page upon saving changes and/or additions on ModelAdmin.
+It can be configured to add a confirmation page on ModelAdmin upon:
+
+- saving changes
+- adding new instances
+- performing actions
Typical Usage:
@@ -39,12 +43,74 @@ To override a template, your app should be listed before `admin_confirm` in INST
## Configuration Options
+**Attributes:**
+
- `confirm_change` _Optional[bool]_ - decides if changes should trigger confirmation
- `confirm_add` _Optional[bool]_ - decides if additions should trigger confirmation
-- `confirmation_fields` _Optional[Array[string]]_ - sets which fields changes should trigger confirmation
-- `change_confirmation_template` _Optional[string]_ - path to custom html template to use
+- `confirmation_fields` _Optional[Array[string]]_ - sets which fields should trigger confirmation for add/change. For adding new instances, the field would only trigger a confirmation if it's set to a value that's not its default.
+- `change_confirmation_template` _Optional[string]_ - path to custom html template to use for change/add
+- `action_confirmation_template` _Optional[string]_ - path to custom html template to use for actions
-Note that setting `confirmation_fields` without setting `confirm_change` or `confirm_add` would not trigger confirmation.
+Note that setting `confirmation_fields` without setting `confirm_change` or `confirm_add` would not trigger confirmation for change/add. Confirmations for actions does not use the `confirmation_fields` option.
+
+**Method Overrides:**
+If you want even more control over the confirmation, these methods can be overridden:
+
+- `get_confirmation_fields(self, request: HttpRequest, obj: Optional[Object]) -> List[str]`
+- `render_change_confirmation(self, request: HttpRequest, context: dict) -> TemplateResponse`
+- `render_action_confirmation(self, request: HttpRequest, context: dict) -> TemplateResponse`
+
+## Usage
+
+**Confirm Change:**
+
+```py
+ from admin_confirm import AdminConfirmMixin
+
+ class MyModelAdmin(AdminConfirmMixin, ModelAdmin):
+ confirm_change = True
+ confirmation_fields = ['field1', 'field2']
+```
+
+This would confirm changes on changes that include modifications on`field1` and/or `field2`.
+
+**Confirm Add:**
+
+```py
+ from admin_confirm import AdminConfirmMixin
+
+ class MyModelAdmin(AdminConfirmMixin, ModelAdmin):
+ confirm_add = True
+ confirmation_fields = ['field1', 'field2']
+```
+
+This would confirm add on adds that set `field1` and/or `field2` to a non default value.
+
+Note: `confirmation_fields` apply to both add/change confirmations.
+
+**Confirm Action:**
+
+```py
+ from admin_confirm import AdminConfirmMixin
+
+ class MyModelAdmin(AdminConfirmMixin, ModelAdmin):
+ actions = ["action1", "action2"]
+
+ def action1(modeladmin, request, queryset):
+ # Do something with the queryset
+
+ @confirm_action
+ def action2(modeladmin, request, queryset):
+ # Do something with the queryset
+
+ action2.allowed_permissions = ('change',)
+```
+
+This would confirm `action2` but not `action1`.
+
+
+
+Action confirmation will respect `allowed_permissions` and the `has_xxx_permission` methods.
## Contribution & Appreciation
@@ -64,7 +130,7 @@ Your appreciation is also very welcome :) Feel free to:
This is a list of features which could potentially be added in the future. Some of which might make more sense in their own package.
-- [ ] confirmations on changelist actions
+- [x] confirmations on changelist actions
- [ ] global actions on changelist page
- [ ] instance actions on change/view page
- [ ] action logs (adding actions to history of instances)
diff --git a/admin_confirm/admin.py b/admin_confirm/admin.py
index c70ada6..83f0bef 100644
--- a/admin_confirm/admin.py
+++ b/admin_confirm/admin.py
@@ -4,6 +4,8 @@ from django.core.exceptions import PermissionDenied
from django.template.response import TemplateResponse
from django.contrib.admin.options import TO_FIELD_VAR
from django.utils.translation import gettext as _
+from django.contrib.admin import helpers
+from admin_confirm.utils import snake_to_title_case
class AdminConfirmMixin:
@@ -17,7 +19,8 @@ class AdminConfirmMixin:
confirmation_fields = None
# Custom templates (designed to be over-ridden in subclasses)
- confirmation_template = None
+ change_confirmation_template = None
+ action_confirmation_template = None
def get_confirmation_fields(self, request, obj=None):
"""
@@ -39,7 +42,7 @@ class AdminConfirmMixin:
return TemplateResponse(
request,
- self.confirmation_template
+ self.change_confirmation_template
or [
"admin/{}/{}/change_confirmation.html".format(
app_label, opts.model_name
@@ -50,6 +53,29 @@ class AdminConfirmMixin:
context,
)
+ def render_action_confirmation(self, request, context):
+ opts = self.model._meta
+ app_label = opts.app_label
+
+ request.current_app = self.admin_site.name
+ context.update(
+ media=self.media,
+ opts=opts,
+ )
+
+ return TemplateResponse(
+ request,
+ self.action_confirmation_template
+ or [
+ "admin/{}/{}/action_confirmation.html".format(
+ app_label, opts.model_name
+ ),
+ "admin/{}/action_confirmation.html".format(app_label),
+ "admin/action_confirmation.html",
+ ],
+ context,
+ )
+
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
if request.method == "POST":
if (not object_id and "_confirm_add" in request.POST) or (
@@ -168,3 +194,40 @@ class AdminConfirmMixin:
**(extra_context or {}),
}
return self.render_change_confirmation(request, context)
+
+
+def confirm_action(func):
+ """
+ @confirm_action function wrapper for Django ModelAdmin actions
+ Will redirect to a confirmation page to ask for confirmation
+
+ Next, it would call the action if confirmed. Otherwise, it would
+ return to the changelist without performing action.
+ """
+
+ def func_wrapper(modeladmin, request, queryset):
+ # First called by `Go` which would not have confirm_action in params
+ if request.POST.get("_confirm_action"):
+ return func(modeladmin, request, queryset)
+
+ # get_actions will only return the actions that are allowed
+ has_perm = modeladmin.get_actions(request).get(func.__name__) is not None
+
+ action_display_name = snake_to_title_case(func.__name__)
+ title = f"Confirm Action: {action_display_name}"
+
+ context = {
+ **modeladmin.admin_site.each_context(request),
+ "title": title,
+ "queryset": queryset,
+ "has_perm": has_perm,
+ "action": func.__name__,
+ "action_display_name": action_display_name,
+ "action_checkbox_name": helpers.ACTION_CHECKBOX_NAME,
+ "submit_name": "confirm_action",
+ }
+
+ # Display confirmation page
+ return modeladmin.render_action_confirmation(request, context)
+
+ return func_wrapper
diff --git a/admin_confirm/templates/admin/action_confirmation.html b/admin_confirm/templates/admin/action_confirmation.html
new file mode 100644
index 0000000..b3a24b7
--- /dev/null
+++ b/admin_confirm/templates/admin/action_confirmation.html
@@ -0,0 +1,56 @@
+{% extends "admin/base_site.html" %}
+{% load i18n l10n admin_urls static %}
+
+{% block extrahead %}
+ {{ block.super }}
+ {{ media }}
+
+{% endblock %}
+
+{% block extrastyle %}
+ {{ block.super }}
+
+
+{% endblock %}
+
+{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-confirmation{% endblock %}
+
+{% block breadcrumbs %}
+
{% trans 'Are you sure you want to perform action' %} {{ action_display_name }} {% trans 'on the following' %} {{ opts.verbose_name_plural|capfirst }}?
+
+ {% for obj in queryset %}
+
{{ obj }}
+ {% endfor %}
+
+
+{% else %}
+
{% trans "You don't have permissions to perform action" %} {{ action_display_name }} {% trans 'on' %} {{ opts.verbose_name_plural|capfirst }}