How to clean and save multiple instances one after one in Django using clean and save methods?
I noticed that when using Django admin and whenever select/change multiple instances and click on save button (for example see the below image, it's not directly related to the code below), Django will clean/validate all instances and then save them one by one.
is this how things are working in Django or the process should be clean and then save the instance before repeat same process with the next instance? because when trying to set is_active
value to be true
for multiple instances, it passing the clean method condition without shown the error message that tells should only one instance be selected as true
and that's correct cause no one of the instances have is_active
as true
in the database yet But if I click the save button again will show the error message.
models.py:
class SupplierAddress(models.Model):
"""Model to create supplier's address instances"""
class Meta:
"""Customize django default way to plural the class name"""
verbose_name = 'Supplier Address'
verbose_name_plural = 'Supplier Addresses'
constraints = [
models.UniqueConstraint(
fields=['supplier', 'address'],
name='supplier_address_unique_appversion'
)
]
# Define model fields.
supplier = models.ForeignKey(
'Supplier',
on_delete=models.CASCADE,
related_name='supplier_addresses_supplier'
)
address = models.ForeignKey(
'Address',
on_delete=models.CASCADE,
related_name='supplier_addresses_address'
)
is_active = models.BooleanField(default=False)
def clean(self):
"""Restrict the add/change to model fields"""
if self.is_active is True:
if SupplierAddress.objects.filter(
supplier=self.supplier,
is_active=True
).exclude(id=self.id).count() >= 1:
raise forms.ValidationError(
{
"is_active": "You can't set more than one active address"
}
)
So, I was able to reproduce your issue. What happens is that Django admin page uses a formset to save data in this kind of editable list. What you need is to override this formset (thanks to this answer) and add validation to it, something like:
from django.forms import BaseModelFormSet
class MyAdminFormSet(BaseModelFormSet):
def clean(self):
active_count = 0
form_set = self.cleaned_data
for form_data in form_set:
if form_data['is_active']:
active_count += 1
if active_count > 1:
raise forms.ValidationError('Cannot have more than one active object')
return form_set
class MyModelAdmin(admin.ModelAdmin):
list_display = (..., 'is_active')
list_editable = ('is_active',)
def get_changelist_formset(self, request, **kwargs):
kwargs['formset'] = MyAdminFormSet
return super().get_changelist_formset(request, **kwargs)
Note that you need to adapt MyAdminFormSet
to your problem, I just did a shallow counting of active objects in mine.