Skip to content

Commit fe14e0a

Browse files
authored
feat: Add support to skip confirmation (#10)
* feat: Add support to skip confirmation * feat: Add support to skip confirmation
1 parent f922725 commit fe14e0a

File tree

5 files changed

+143
-5
lines changed

5 files changed

+143
-5
lines changed

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Django Modal Actions is a reusable Django app that provides a convenient way to
1313
- Support for both list-view and object-view actions
1414
- Customizable modal forms
1515
- AJAX-based form submission
16+
- Skip confirmation dialog for immediate action execution
1617

1718
## Requirements
1819

@@ -211,6 +212,47 @@ def approve(self, request, obj, form_data=None):
211212

212213
In this case, the user must both have the `can_approve_items` permission and be a staff member to see and use the approve action.
213214

215+
## Skip Confirmation Dialog
216+
217+
Sometimes you may want to execute an action immediately without showing a confirmation dialog. You can use the `skip_confirmation` parameter to achieve this:
218+
219+
```python
220+
from django.contrib import admin
221+
from django_modal_actions import ModalActionMixin, modal_action
222+
223+
@admin.register(YourModel)
224+
class YourModelAdmin(ModalActionMixin, admin.ModelAdmin):
225+
list_display = ['name', 'status']
226+
modal_actions = ['toggle_status']
227+
list_modal_actions = ['bulk_toggle_status']
228+
229+
@modal_action(
230+
modal_header="Toggle Status",
231+
skip_confirmation=True
232+
)
233+
def toggle_status(self, request, obj, form_data=None):
234+
if obj.status == 'active':
235+
obj.status = 'inactive'
236+
else:
237+
obj.status = 'active'
238+
obj.save()
239+
return f"{obj} status toggled to {obj.status}"
240+
241+
@modal_action(
242+
modal_header="Bulk Toggle Status",
243+
skip_confirmation=True
244+
)
245+
def bulk_toggle_status(self, request, queryset, form_data=None):
246+
for obj in queryset:
247+
obj.status = 'inactive' if obj.status == 'active' else 'active'
248+
obj.save()
249+
return f"Toggled status for {queryset.count()} items"
250+
```
251+
252+
In this example, both actions will execute immediately when clicked, without showing a confirmation modal. The page will reload automatically after the action completes.
253+
254+
**Important Note**: You cannot use `skip_confirmation=True` together with `form_class`. The skip confirmation feature is designed for actions that don't require any user input. If you need to collect form data, the modal must be shown to display the form.
255+
214256
## Custom Admin Templates
215257

216258
If you need to customize the admin templates while still using the modal actions, you can override the `change_form_template` and `change_list_template` in your ModelAdmin class. Here's how to do it:

django_modal_actions/mixins.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ def get_modal_content(
4646
) -> JsonResponse:
4747
obj = self.get_object(request, object_id) if object_id else None
4848
action_func: Callable = getattr(self, action)
49+
skip_confirmation: bool = getattr(action_func, "skip_confirmation", False)
50+
51+
if skip_confirmation:
52+
return self.execute_modal_action(request, action, object_id)
53+
4954
form_class: Optional[Type] = getattr(action_func, "form_class", None)
5055
form = form_class(request.POST or None) if form_class else None
5156

@@ -201,7 +206,13 @@ def modal_action(
201206
modal_description: Optional[str] = None,
202207
permissions: Optional[Union[Callable, List[Callable]]] = None,
203208
form_class: Optional[Type] = None,
209+
skip_confirmation: bool = False,
204210
):
211+
if form_class and skip_confirmation:
212+
raise ValueError(
213+
"Cannot use form_class with skip_confirmation. Skip confirmation means no modal and no form."
214+
)
215+
205216
def decorator(func):
206217
@wraps(func)
207218
def wrapper(self, request, queryset_or_obj, form_data=None):
@@ -211,6 +222,7 @@ def wrapper(self, request, queryset_or_obj, form_data=None):
211222
wrapper.modal_description = modal_description
212223
wrapper.permissions = permissions
213224
wrapper.form_class = form_class
225+
wrapper.skip_confirmation = skip_confirmation
214226
return wrapper
215227

216228
return decorator

django_modal_actions/static/django_modal_actions/js/modal_actions.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,20 @@
8383
url += "?selected_ids=" + JSON.stringify(selectedIds);
8484
}
8585
$.get(url, function (data) {
86-
$modalContent.html(data.content);
87-
$modal.show();
88-
// Initialize conditional fields after modal content is loaded
89-
handleConditionalFields();
86+
if (data.success !== undefined) {
87+
// Skip confirmation case - action was executed directly
88+
if (data.success) {
89+
location.reload();
90+
} else if (data.errors) {
91+
displayErrors(data.errors);
92+
}
93+
} else if (data.content) {
94+
// Normal case - show modal with confirmation
95+
$modalContent.html(data.content);
96+
$modal.show();
97+
// Initialize conditional fields after modal content is loaded
98+
handleConditionalFields();
99+
}
90100
});
91101
});
92102

django_modal_actions/tests/admin.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,13 @@ class UserAdmin(ModalActionMixin, BaseUserAdmin):
4141
"object_action",
4242
"object_action_with_form_class",
4343
"conditional_fields_action",
44+
"object_action_skip_confirmation",
45+
]
46+
list_modal_actions = [
47+
"list_action",
48+
"list_action_with_form_class",
49+
"list_action_skip_confirmation",
4450
]
45-
list_modal_actions = ["list_action", "list_action_with_form_class"]
4651

4752
@modal_action(modal_header="Object Action")
4853
def object_action(self, request, obj, form_data=None):
@@ -76,6 +81,16 @@ def conditional_fields_action(self, request, obj, form_data=None):
7681
return "No notification will be sent"
7782
return "Conditional fields action works"
7883

84+
@modal_action(
85+
modal_header="Object Action Skip Confirmation", skip_confirmation=True
86+
)
87+
def object_action_skip_confirmation(self, request, obj, form_data=None):
88+
return "Object action without confirmation works"
89+
90+
@modal_action(modal_header="List Action Skip Confirmation", skip_confirmation=True)
91+
def list_action_skip_confirmation(self, request, queryset, form_data=None):
92+
return f"List action without confirmation works on {queryset.count()} items"
93+
7994

8095
admin.site.unregister(User)
8196
admin.site.register(User, UserAdmin)

django_modal_actions/tests/test_modal_actions.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,65 @@ def test_conditional_fields(self):
264264
)
265265
self.assertIn("SMS will be sent to 123-456-7890", success_message.text)
266266

267+
def test_skip_confirmation_object_action(self):
268+
user = User.objects.first()
269+
self.selenium.get(
270+
self.live_server_url + reverse("admin:auth_user_change", args=[user.id])
271+
)
272+
273+
# Click the action button
274+
modal_button = WebDriverWait(self.selenium, 10).until(
275+
EC.element_to_be_clickable(
276+
(By.LINK_TEXT, "OBJECT ACTION SKIP CONFIRMATION")
277+
)
278+
)
279+
modal_button.click()
280+
281+
# Page should reload directly without showing modal
282+
# Check for success message
283+
success_message = WebDriverWait(self.selenium, 10).until(
284+
EC.presence_of_element_located((By.CLASS_NAME, "success"))
285+
)
286+
self.assertIn("Object action without confirmation works", success_message.text)
287+
288+
# Verify modal was never shown
289+
modal = self.selenium.find_element(By.ID, "dma-modal-action")
290+
self.assertFalse(modal.is_displayed())
291+
292+
def test_skip_confirmation_list_action(self):
293+
self.selenium.get(self.live_server_url + reverse("admin:auth_user_changelist"))
294+
295+
# Select some users
296+
checkboxes = self.selenium.find_elements(By.NAME, "_selected_action")
297+
if checkboxes:
298+
checkboxes[0].click()
299+
300+
# Click the action button
301+
modal_button = WebDriverWait(self.selenium, 10).until(
302+
EC.element_to_be_clickable((By.LINK_TEXT, "LIST ACTION SKIP CONFIRMATION"))
303+
)
304+
modal_button.click()
305+
306+
# Page should reload directly without showing modal
307+
# Check for success message
308+
success_message = WebDriverWait(self.selenium, 10).until(
309+
EC.presence_of_element_located((By.CLASS_NAME, "success"))
310+
)
311+
self.assertIn("List action without confirmation works", success_message.text)
312+
313+
def test_modal_action_validation_error(self):
314+
"""Test that using form_class with skip_confirmation raises a ValueError"""
315+
from django_modal_actions.mixins import modal_action
316+
from django_modal_actions.tests.admin import CustomForm
317+
318+
with self.assertRaises(ValueError) as cm:
319+
320+
@modal_action(form_class=CustomForm, skip_confirmation=True)
321+
def invalid_action(self, request, obj, form_data=None):
322+
pass
323+
324+
self.assertIn("Cannot use form_class with skip_confirmation", str(cm.exception))
325+
267326

268327
if __name__ == "__main__":
269328
import unittest

0 commit comments

Comments
 (0)