Skip to content

Commit

Permalink
12780 Provide a hook for compound form/formset validation in ModelAdmin
Browse files Browse the repository at this point in the history
  • Loading branch information
RamezIssac committed Apr 18, 2023
1 parent 594fcc2 commit 8be66d4
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 1 deletion.
11 changes: 10 additions & 1 deletion django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -1815,7 +1815,9 @@ def _changeform_view(self, request, object_id, form_url, extra_context):
new_object = self.save_form(request, form, change=not add)
else:
new_object = form.instance
if all_valid(formsets) and form_validated:
all_formsets_valid = all_valid(formsets)
change_form_valid = self.is_change_form_valid(request, form, formsets, add)
if all_formsets_valid and form_validated and change_form_valid:
self.save_model(request, new_object, form, not add)
self.save_related(request, form, formsets, not add)
change_message = self.construct_change_message(
Expand Down Expand Up @@ -1903,6 +1905,13 @@ def _changeform_view(self, request, object_id, form_url, extra_context):
request, context, add=add, change=not add, obj=obj, form_url=form_url
)

def is_change_form_valid(self, request, form, formsets, add):
"""
Hook for doing extra form validation. Return True if valid, False
otherwise. Called in response to a POST request.
"""
return True

def add_view(self, request, form_url="", extra_context=None):
return self.changeform_view(request, None, form_url, extra_context)

Expand Down
23 changes: 23 additions & 0 deletions docs/ref/contrib/admin/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2012,6 +2012,29 @@ templates used by the :class:`ModelAdmin` views:
def get_changeform_initial_data(self, request):
return {"name": "custom_initial_value"}

.. method:: ModelAdmin.is_change_form_valid(request, form, formsets, add)

.. versionadded:: 5.0

A hook for customizing the validation of the change form as a whole.
By default, it return True.
You have access to the request, the form, the formsets and a boolean add.

This method should return a boolean value::

def is_change_form_valid(self, request, form, formsets, add):

has_special_name_in_form = form.cleaned_data["name"] == "Special name"
has_special_name_in_formset = any(
formset.cleaned_data["related_name"] == "Special name" for formset in formsets
)
if has_special_name_in_form and has_special_name_in_formset:
# add an error to the form to inform the user
form.add_error(None, 'Can not have "Special name" as a name and a related name')
return False
return True


.. method:: ModelAdmin.get_deleted_objects(objs, request)

A hook for customizing the deletion process of the :meth:`delete_view` and
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/5.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ Minor features
``RelatedOnlyFieldListFilter`` admin filters now handle multi-valued query
parameters.

* The new :meth:`.ModelAdmin.is_change_form_valid` method provides
a hook for compound form/formset validation.

:mod:`django.contrib.admindocs`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
9 changes: 9 additions & 0 deletions tests/admin_views/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,15 @@ class DependentChildInline(admin.TabularInline):
class ParentWithDependentChildrenAdmin(admin.ModelAdmin):
inlines = [DependentChildInline]

def is_change_form_valid(self, request, form, formsets, add):
if form.cleaned_data.get("family_name") == "Foo" and len(formsets) > 0:
form.add_error(
"family_name",
"The name 'Foo' with depends is not allowed is this test case.",
)
return False
return True


# Tests for ticket 11277 ----------------------------------

Expand Down
34 changes: 34 additions & 0 deletions tests/admin_views/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -8234,6 +8234,40 @@ def test_change_view_form_and_formsets_run_validation(self):
],
)

def test_change_view_whole_form_validation(self):
"""
Issue #12780
Verifying that is_change_form_valid hook is called.
"""
pwdc = ParentWithDependentChildren.objects.create(
some_required_info=6, family_name="Foo"
)
# The validation should fail because we're not allowing the name 'Foo' to have
# depends, asserting that is_change_form_valid is called and have access to
# the form and to formsets.

post_data = {
"family_name": "Foo",
"some_required_info": "4",
"dependentchild_set-TOTAL_FORMS": "1",
"dependentchild_set-INITIAL_FORMS": "0",
"dependentchild_set-MAX_NUM_FORMS": "1",
"dependentchild_set-0-id": "",
"dependentchild_set-0-parent": str(pwdc.id),
"dependentchild_set-0-family_name": "Foo",
}
response = self.client.post(
reverse(
"admin:admin_views_parentwithdependentchildren_change", args=(pwdc.id,)
),
post_data,
)
self.assertFormError(
response.context["adminform"],
"family_name",
["The name 'Foo' with depends is not allowed is this test case."],
)

def test_check(self):
"The view_on_site value is either a boolean or a callable"
try:
Expand Down

0 comments on commit 8be66d4

Please sign in to comment.