Feat/confirm actions (#2)

* Working wrapper for actions

* checking permissions for action

* Refactor/clean change_confirmation template a bit

* Update README

* Update README

* Adding unit tests for confirm_action decorator

* Updated tests/readme

* Update after testing upload to test pypi

* Clean up and format code

Co-authored-by: Thu Trang Pham <thu@joinmodernhealth.com>
main
Thu Trang Pham 2020-11-29 13:31:27 -08:00 committed by GitHub
parent 9edc66f31a
commit 1b617170bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 535 additions and 47 deletions

4
.vscode/settings.json vendored 100644
View File

@ -0,0 +1,4 @@
{
"python.formatting.provider": "black",
"editor.formatOnSave": true
}

View File

@ -7,12 +7,18 @@ test:
coverage-badge -f -o coverage.svg coverage-badge -f -o coverage.svg
python -m readme_renderer README.md -o /tmp/README.html python -m readme_renderer README.md -o /tmp/README.html
check-readme:
python -m readme_renderer README.md -o /tmp/README.html
migrate: migrate:
./tests/manage.py makemigrations ./tests/manage.py makemigrations
./tests/manage.py migrate ./tests/manage.py migrate
shell:
./tests/manage.py shell
package: package:
python3 setup.py sdist bdist_wheel python3 setup.py sdist bdist_wheel
upload-testpypi: upload-testpypi:
python3 -m twine upload --repository testpypi dist/* python3 -m twine upload --repository testpypi dist/django_admin_confirm-$(VERSION)*

View File

@ -2,11 +2,15 @@
![coverage](https://raw.githubusercontent.com/TrangPham/django-admin-confirm/main/coverage.svg) ![coverage](https://raw.githubusercontent.com/TrangPham/django-admin-confirm/main/coverage.svg)
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.
![Screenshot of Confirmation Page](https://raw.githubusercontent.com/TrangPham/django-admin-confirm/main/screenshot.png) ![Screenshot of Change Confirmation Page](https://raw.githubusercontent.com/TrangPham/django-admin-confirm/main/screenshot_confirm_change.png)
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: Typical Usage:
@ -39,12 +43,74 @@ To override a template, your app should be listed before `admin_confirm` in INST
## Configuration Options ## Configuration Options
**Attributes:**
- `confirm_change` _Optional[bool]_ - decides if changes should trigger confirmation - `confirm_change` _Optional[bool]_ - decides if changes should trigger confirmation
- `confirm_add` _Optional[bool]_ - decides if additions 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 - `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 - `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`.
![Screenshot of Action Confirmation Page](https://raw.githubusercontent.com/TrangPham/django-admin-confirm/main/screenshot_confirm_action.png)
Action confirmation will respect `allowed_permissions` and the `has_xxx_permission` methods.
## Contribution & Appreciation ## 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. 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 - [ ] global actions on changelist page
- [ ] instance actions on change/view page - [ ] instance actions on change/view page
- [ ] action logs (adding actions to history of instances) - [ ] action logs (adding actions to history of instances)

View File

@ -4,6 +4,8 @@ from django.core.exceptions import PermissionDenied
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.contrib.admin.options import TO_FIELD_VAR from django.contrib.admin.options import TO_FIELD_VAR
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.contrib.admin import helpers
from admin_confirm.utils import snake_to_title_case
class AdminConfirmMixin: class AdminConfirmMixin:
@ -17,7 +19,8 @@ class AdminConfirmMixin:
confirmation_fields = None confirmation_fields = None
# Custom templates (designed to be over-ridden in subclasses) # 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): def get_confirmation_fields(self, request, obj=None):
""" """
@ -39,7 +42,7 @@ class AdminConfirmMixin:
return TemplateResponse( return TemplateResponse(
request, request,
self.confirmation_template self.change_confirmation_template
or [ or [
"admin/{}/{}/change_confirmation.html".format( "admin/{}/{}/change_confirmation.html".format(
app_label, opts.model_name app_label, opts.model_name
@ -50,6 +53,29 @@ class AdminConfirmMixin:
context, 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): def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
if request.method == "POST": if request.method == "POST":
if (not object_id and "_confirm_add" in request.POST) or ( if (not object_id and "_confirm_add" in request.POST) or (
@ -168,3 +194,40 @@ class AdminConfirmMixin:
**(extra_context or {}), **(extra_context or {}),
} }
return self.render_change_confirmation(request, context) 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

View File

@ -0,0 +1,56 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n admin_urls static %}
{% block extrahead %}
{{ block.super }}
{{ media }}
<script src="{% static 'admin/js/cancel.js' %}" async></script>
{% endblock %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}">
<link rel="stylesheet" type="text/css" href="{% static "admin/css/confirmation.css" %}">
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-confirmation{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; {% trans 'Confirm Action' %}
</div>
{% endblock %}
{% block content %}
{% if has_perm %}
<p>{% trans 'Are you sure you want to perform action' %} {{ action_display_name }} {% trans 'on the following' %} {{ opts.verbose_name_plural|capfirst }}?</p>
<ul>
{% for obj in queryset %}
<li>{{ obj }}</li>
{% endfor %}
</ul>
<form method="post">{% csrf_token %}
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}">
{% endfor %}
<input type="hidden" name="action" value="{{ action }}">
<div class="submit-row">
<input type="submit" value="{% trans 'Yes, Im sure' %}" name="_confirm_action">
<p class="deletelink-box">
<a href="{% url opts|admin_urlname:'changelist' %}" class="button cancel-link">{% trans "No, go back" %}</a>
</p>
</div>
</form>
{% else %}
<p>{% trans "You don't have permissions to perform action" %} {{ action_display_name }} {% trans 'on' %} {{ opts.verbose_name_plural|capfirst }}</p>
<br/>
<div class="submit-row">
<p class="deletelink-box">
<a href="{% url opts|admin_urlname:'changelist' %}" class="button cancel-link">{% trans "Go back" %}</a>
</p>
</div>
{% endif %}
{% endblock %}

View File

@ -30,35 +30,19 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if add %}
{% if add %}
<p>{% blocktrans with escaped_object=object %}Are you sure you want to add the {{ model_name }}?{% endblocktrans %}</p> <p>{% blocktrans with escaped_object=object %}Are you sure you want to add the {{ model_name }}?{% endblocktrans %}</p>
{% if changed_data %} {% include "admin/change_data.html" %}
<div class="changed-data">
<p><b>Confirm Values:</b></p>
<table>
{% for field, values in changed_data.items %}
<tr><th style="text-align: right">{{ field }}:</th><td>{{ values.1 }}</td></tr>
{% endfor %}
</table>
</div>
<form method="post" action="{% url opts|admin_urlname:'add'%}">{% csrf_token %} <form method="post" action="{% url opts|admin_urlname:'add'%}">{% csrf_token %}
{% endif %}
{% else %} {% else %}
<p>{% blocktrans with escaped_object=object %}Are you sure you want to change the {{ model_name }} "{{ object_name }}"?{% endblocktrans %}</p> <p>{% blocktrans with escaped_object=object %}Are you sure you want to change the {{ model_name }} "{{ object_name }}"?{% endblocktrans %}</p>
{% if changed_data %} {% include "admin/change_data.html" %}
<div class="changed-data">
<p><b>Confirm Values:</b></p>
<table>
<tr><th>Field</th><th>Current Value</th><th>New Value</th></tr>
{% for field, values in changed_data.items %}
<tr><td>{{ field }}</td><td>{{ values.0 }}</td><td>{{ values.1 }}</td></tr>
{% endfor %}
</table>
</div>
{% endif %}
<form method="post" action="{% url opts|admin_urlname:'change' object_id|admin_urlquote %}">{% csrf_token %} <form method="post" action="{% url opts|admin_urlname:'change' object_id|admin_urlquote %}">{% csrf_token %}
{% endif %} {% endif %}
<div>
{% for key, value in form_data.items %} {% for key, value in form_data.items %}
<input type="hidden" name="{{ key }}" value="{{ value }}"> <input type="hidden" name="{{ key }}" value="{{ value }}">
{% endfor %} {% endfor %}
@ -70,6 +54,5 @@
<a href="#" class="button cancel-link">{% trans "No, continue to edit" %}</a> <a href="#" class="button cancel-link">{% trans "No, continue to edit" %}</a>
</p> </p>
</div> </div>
</div>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,11 @@
{% if changed_data %}
<div class="changed-data">
<p><b>Confirm Values:</b></p>
<table>
<tr><th>Field</th><th>Current Value</th><th>New Value</th></tr>
{% for field, values in changed_data.items %}
<tr><td>{{ field }}</td><td>{{ values.0 }}</td><td>{{ values.1 }}</td></tr>
{% endfor %}
</table>
</div>
{% endif %}

View File

@ -0,0 +1,281 @@
from django.test import TestCase, RequestFactory
from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import Permission, User
from django.contrib.admin.options import TO_FIELD_VAR
from django.urls import reverse
from tests.market.admin import ShopAdmin
from tests.market.models import Shop
class TestConfirmActions(TestCase):
@classmethod
def setUpTestData(cls):
cls.superuser = User.objects.create_superuser(
username="super", email="super@email.org", password="pass"
)
def setUp(self):
self.client.force_login(self.superuser)
self.factory = RequestFactory()
def test_get_changelist_should_not_be_affected(self):
response = self.client.get(reverse("admin:market_shop_changelist"))
self.assertIsNotNone(response)
self.assertNotIn("Confirm Action", response.rendered_content)
def test_action_without_confirmation(self):
post_params = {
"action": ["show_message_no_confirmation"],
"select_across": ["0"],
"index": ["0"],
"_selected_action": ["3", "2", "1"],
}
response = self.client.post(
reverse("admin:market_shop_changelist"),
data=post_params,
follow=True, # Follow the redirect to get content
)
self.assertIsNotNone(response)
self.assertEqual(response.status_code, 200)
# Should not use confirmaiton page
self.assertNotIn("action_confirmation", response.template_name)
# The action was to show user a message
self.assertIn("You selected without confirmation", response.rendered_content)
def test_action_with_confirmation_should_show_confirmation_page(self):
post_params = {
"action": ["show_message"],
"select_across": ["0"],
"index": ["0"],
"_selected_action": ["3", "2", "1"],
}
response = self.client.post(
reverse("admin:market_shop_changelist"),
data=post_params,
follow=True, # Follow the redirect to get content
)
self.assertIsNotNone(response)
self.assertEqual(response.status_code, 200)
# Should use confirmaiton page
self.assertEqual(
response.template_name,
[
"admin/market/shop/action_confirmation.html",
"admin/market/action_confirmation.html",
"admin/action_confirmation.html",
],
)
# The action was to show user a message, and should not happen yet
self.assertNotIn("You selected", response.rendered_content)
def test_no_permissions_in_database_for_action_with_confirmation(self):
"""
Django would not show the action in changelist action selector
If the user doesn't have permissions, but this doesn't prevent
user from calling post with the params to perform the action.
If the permissions are denied because of Permission in the database,
Django would redirect to the changelist.
"""
# Create a user without permissions for action
user = User.objects.create_user(
username="user",
email="user@email.org",
password="pass",
is_active=True,
is_staff=True,
is_superuser=False,
)
# Give user permissions to ShopAdmin change, add, view but not delete
for permission in Permission.objects.filter(
codename__in=["change_shop", "view_shop", "add_shop"]
):
user.user_permissions.add(permission)
self.client.force_login(user)
post_params = {
"action": ["show_message"],
"select_across": ["0"],
"index": ["0"],
"_selected_action": ["3", "2", "1"],
}
response = self.client.post(
reverse("admin:market_shop_changelist"),
data=post_params,
follow=True, # Follow the redirect to get content
)
self.assertIsNotNone(response)
self.assertEqual(response.status_code, 200)
# Should not use confirmaiton page
self.assertEqual(
response.template_name,
[
"admin/market/shop/change_list.html",
"admin/market/change_list.html",
"admin/change_list.html",
],
)
# The action was to show user a message, and should not happen
self.assertNotIn("You selected", response.rendered_content)
# Django won't show the action as an option to you
self.assertIn("No action selected", response.rendered_content)
def test_no_permissions_in_code_non_superuser_for_action_with_confirmation(self):
"""
Django would not show the action in changelist action selector
If the user doesn't have permissions, but this doesn't prevent
user from calling post with the params to perform the action.
If the permissions are denied because of Permission in the database,
Django would redirect to the changelist.
It should also respect the has_xxx_permission methods
"""
# Create a user without permissions for action
user = User.objects.create_user(
username="user",
email="user@email.org",
password="pass",
is_active=True,
is_staff=True,
is_superuser=False,
)
# Give user permissions to ShopAdmin change, add, view and delete
for permission in Permission.objects.filter(
codename__in=["change_shop", "view_shop", "add_shop", "delete_shop"]
):
user.user_permissions.add(permission)
self.client.force_login(user)
# ShopAdmin has defined:
# def has_delete_permission(self, request, obj=None):
# return request.user.is_superuser
post_params = {
"action": ["show_message"],
"select_across": ["0"],
"index": ["0"],
"_selected_action": ["3", "2", "1"],
}
response = self.client.post(
reverse("admin:market_shop_changelist"),
data=post_params,
follow=True, # Follow the redirect to get content
)
self.assertIsNotNone(response)
self.assertEqual(response.status_code, 200)
# Should not use confirmaiton page
self.assertEqual(
response.template_name,
[
"admin/market/shop/change_list.html",
"admin/market/change_list.html",
"admin/change_list.html",
],
)
# The action was to show user a message, and should not happen yet
self.assertNotIn("You selected", response.rendered_content)
# Django won't show the action as an option to you
self.assertIn("No action selected", response.rendered_content)
def test_no_permissions_in_code_superuser_for_action_with_confirmation(self):
"""
Django would not show the action in changelist action selector
If the user doesn't have permissions, but this doesn't prevent
user from calling post with the params to perform the action.
When permissions are denied from a change in code
(ie has_xxx_permission in ModelAdmin), Django should still
redirect to changelist. This should be true even if the user is
a superuser.
"""
# ShopAdmin has defined:
# def has_delete_permission(self, request, obj=None):
# return request.user.is_superuser
ShopAdmin.has_delete_permission = lambda self, request, obj=None: False
post_params = {
"action": ["show_message"],
"select_across": ["0"],
"index": ["0"],
"_selected_action": ["3", "2", "1"],
}
response = self.client.post(
reverse("admin:market_shop_changelist"),
data=post_params,
follow=True, # Follow the redirect to get content
)
self.assertIsNotNone(response)
self.assertEqual(response.status_code, 200)
# Should not use confirmaiton page
self.assertEqual(
response.template_name,
[
"admin/market/shop/change_list.html",
"admin/market/change_list.html",
"admin/change_list.html",
],
)
# The action was to show user a message, and should not happen yet
self.assertNotIn("You selected", response.rendered_content)
# Django won't show the action as an option to you
self.assertIn("No action selected", response.rendered_content)
# Remove our modification for ShopAdmin
ShopAdmin.has_delete_permission = (
lambda self, request, obj=None: request.user.is_superuser
)
def test_confirm_action_submit_button_should_perform_action(self):
"""
The submit button should have param "_confirm_action"
Simulate calling the post request that the button would
"""
post_params = {
"_confirm_action": ["Yes, I'm sure"],
"action": ["show_message"],
"_selected_action": ["3", "2", "1"],
}
response = self.client.post(
reverse("admin:market_shop_changelist"),
data=post_params,
follow=True, # Follow the redirect to get content
)
self.assertIsNotNone(response)
self.assertEqual(response.status_code, 200)
# Should not use confirmaiton page, since we clicked Yes, I'm sure
self.assertEqual(
response.template_name,
[
"admin/market/shop/change_list.html",
"admin/market/change_list.html",
"admin/change_list.html",
],
)
# The action was to show user a message, and should happen
self.assertIn("You selected", response.rendered_content)
def test_should_use_action_confirmation_template_if_set(self):
expected_template = "market/admin/my_custom_template.html"
ShopAdmin.action_confirmation_template = expected_template
admin = ShopAdmin(Shop, AdminSite())
actual_template = admin.render_action_confirmation(
self.factory.request(), context={}
).template_name
self.assertEqual(expected_template, actual_template)
# Clear our setting to not affect other tests
ShopAdmin.action_confirmation_template = None

View File

@ -11,7 +11,7 @@ from tests.market.models import Item, Inventory
from tests.factories import ItemFactory, ShopFactory, InventoryFactory from tests.factories import ItemFactory, ShopFactory, InventoryFactory
class TestAdminConfirmMixin(TestCase): class TestConfirmChangeAndAdd(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.superuser = User.objects.create_superuser( cls.superuser = User.objects.create_superuser(
@ -138,13 +138,14 @@ class TestAdminConfirmMixin(TestCase):
def test_custom_template(self): def test_custom_template(self):
expected_template = "market/admin/my_custom_template.html" expected_template = "market/admin/my_custom_template.html"
ItemAdmin.confirmation_template = expected_template ItemAdmin.change_confirmation_template = expected_template
admin = ItemAdmin(Item, AdminSite()) admin = ItemAdmin(Item, AdminSite())
actual_template = admin.render_change_confirmation( actual_template = admin.render_change_confirmation(
self.factory.request(), context={} self.factory.request(), context={}
).template_name ).template_name
self.assertEqual(expected_template, actual_template) self.assertEqual(expected_template, actual_template)
ItemAdmin.confirmation_template = None # Clear our setting to not affect other tests
ItemAdmin.change_confirmation_template = None
def test_form_invalid(self): def test_form_invalid(self):
self.assertEqual(InventoryAdmin.confirmation_fields, ["quantity"]) self.assertEqual(InventoryAdmin.confirmation_fields, ["quantity"])
@ -162,7 +163,7 @@ class TestAdminConfirmMixin(TestCase):
f"/admin/market/inventory/{inventory.id}/change/", data f"/admin/market/inventory/{inventory.id}/change/", data
) )
# Form invalid should show erros on form # Form invalid should show errors on form
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
print(response.rendered_content) print(response.rendered_content)
self.assertIsNotNone(response.context_data.get("errors")) self.assertIsNotNone(response.context_data.get("errors"))

View File

@ -0,0 +1,2 @@
def snake_to_title_case(string: str) -> str:
return " ".join(string.split("_")).title()

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

@ -6,7 +6,7 @@ README = open(os.path.join(here, "README.md")).read()
setup( setup(
name="django-admin-confirm", name="django-admin-confirm",
version="0.1", version="0.2.dev1",
packages=["admin_confirm"], packages=["admin_confirm"],
description="Adds confirmation to Django Admin changes and additions", description="Adds confirmation to Django Admin changes and additions",
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
@ -18,5 +18,5 @@ setup(
install_requires=[ install_requires=[
"Django>=1.7", "Django>=1.7",
], ],
python_requires='>=3', python_requires=">=3",
) )

View File

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from admin_confirm.admin import AdminConfirmMixin from admin_confirm.admin import AdminConfirmMixin, confirm_action
from .models import Item, Inventory, Shop from .models import Item, Inventory, Shop
@ -20,6 +20,21 @@ class InventoryAdmin(AdminConfirmMixin, admin.ModelAdmin):
class ShopAdmin(AdminConfirmMixin, admin.ModelAdmin): class ShopAdmin(AdminConfirmMixin, admin.ModelAdmin):
confirmation_fields = ["name"] confirmation_fields = ["name"]
actions = ["show_message", "show_message_no_confirmation"]
@confirm_action
def show_message(modeladmin, request, queryset):
shops = ", ".join(shop.name for shop in queryset)
modeladmin.message_user(request, f"You selected with confirmation: {shops}")
show_message.allowed_permissions = ('delete',)
def show_message_no_confirmation(modeladmin, request, queryset):
shops = ", ".join(shop.name for shop in queryset)
modeladmin.message_user(request, f"You selected without confirmation: {shops}")
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
admin.site.register(Item, ItemAdmin) admin.site.register(Item, ItemAdmin)
admin.site.register(Inventory, InventoryAdmin) admin.site.register(Inventory, InventoryAdmin)

View File

@ -24,7 +24,7 @@ SECRET_KEY = "=yddl-40388w3e2hl$e8)revce=n67_idi8pfejtn3!+2%!_qt"
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = ["127.0.0.1"] ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
# Application definition # Application definition