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
parent
9edc66f31a
commit
1b617170bf
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"python.formatting.provider": "black",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
8
Makefile
8
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)*
|
||||
|
|
|
|||
80
README.md
80
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||
› {% 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, I’m 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 %}
|
||||
|
|
@ -30,35 +30,19 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if add %}
|
||||
|
||||
{% if add %}
|
||||
<p>{% blocktrans with escaped_object=object %}Are you sure you want to add the {{ model_name }}?{% endblocktrans %}</p>
|
||||
{% if changed_data %}
|
||||
<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>
|
||||
{% include "admin/change_data.html" %}
|
||||
<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>
|
||||
{% 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 %}
|
||||
{% include "admin/change_data.html" %}
|
||||
<form method="post" action="{% url opts|admin_urlname:'change' object_id|admin_urlquote %}">{% csrf_token %}
|
||||
{% endif %}
|
||||
<div>
|
||||
{% endif %}
|
||||
|
||||
{% for key, value in form_data.items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
||||
{% endfor %}
|
||||
|
|
@ -70,6 +54,5 @@
|
|||
<a href="#" class="button cancel-link">{% trans "No, continue to edit" %}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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
|
||||
|
|
@ -11,7 +11,7 @@ from tests.market.models import Item, Inventory
|
|||
from tests.factories import ItemFactory, ShopFactory, InventoryFactory
|
||||
|
||||
|
||||
class TestAdminConfirmMixin(TestCase):
|
||||
class TestConfirmChangeAndAdd(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.superuser = User.objects.create_superuser(
|
||||
|
|
@ -138,13 +138,14 @@ class TestAdminConfirmMixin(TestCase):
|
|||
|
||||
def test_custom_template(self):
|
||||
expected_template = "market/admin/my_custom_template.html"
|
||||
ItemAdmin.confirmation_template = expected_template
|
||||
ItemAdmin.change_confirmation_template = expected_template
|
||||
admin = ItemAdmin(Item, AdminSite())
|
||||
actual_template = admin.render_change_confirmation(
|
||||
self.factory.request(), context={}
|
||||
).template_name
|
||||
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):
|
||||
self.assertEqual(InventoryAdmin.confirmation_fields, ["quantity"])
|
||||
|
|
@ -162,7 +163,7 @@ class TestAdminConfirmMixin(TestCase):
|
|||
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)
|
||||
print(response.rendered_content)
|
||||
self.assertIsNotNone(response.context_data.get("errors"))
|
||||
|
|
@ -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 |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
4
setup.py
4
setup.py
|
|
@ -6,7 +6,7 @@ README = open(os.path.join(here, "README.md")).read()
|
|||
|
||||
setup(
|
||||
name="django-admin-confirm",
|
||||
version="0.1",
|
||||
version="0.2.dev1",
|
||||
packages=["admin_confirm"],
|
||||
description="Adds confirmation to Django Admin changes and additions",
|
||||
long_description_content_type="text/markdown",
|
||||
|
|
@ -18,5 +18,5 @@ setup(
|
|||
install_requires=[
|
||||
"Django>=1.7",
|
||||
],
|
||||
python_requires='>=3',
|
||||
python_requires=">=3",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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
|
||||
|
||||
|
|
@ -20,6 +20,21 @@ class InventoryAdmin(AdminConfirmMixin, admin.ModelAdmin):
|
|||
class ShopAdmin(AdminConfirmMixin, admin.ModelAdmin):
|
||||
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(Inventory, InventoryAdmin)
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["127.0.0.1"]
|
||||
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
|
|
|||
Loading…
Reference in New Issue