Validating delete on django-admin inline forms - django

I am trying to perform a validation such that you cannot delete a user if he's an admin. I'd therefore like to check and raise an error if there's a user who's an admin and has been marked for deletion.
This is my inline ModelForm
class UserGroupsForm(forms.ModelForm):
class Meta:
model = UserGroups
def clean(self):
delete_checked = self.fields['DELETE'].widget.value_from_datadict(
self.data, self.files, self.add_prefix('DELETE'))
if bool(delete_checked):
#if user is admin of group x
raise forms.ValidationError('You cannot delete a user that is the group administrator')
return self.cleaned_data
The if bool(delete_checked): condition returns true and stuff inside the if block gets executed but for some reason this validation error is never raised. Could someone please explain to me why?
Better yet if there's another better way to do this please let me know

The solution I found was to clean in the InlineFormSet instead of the ModelForm
class UserGroupsInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
delete_checked = False
for form in self.forms:
try:
if form.cleaned_data:
if form.cleaned_data['DELETE']:
delete_checked = True
except AttributeError:
pass
if delete_checked:
raise forms.ValidationError(u'You cannot delete a user that is the group administrator')

Although #domino's answer may work for now, the "kinda" recommended approach is to use formset's self._should_delete_form(form) function together with self.can_delete.
There's also the issue of calling super().clean() to perform standard builtin validation. So the final code may look like:
class UserGroupsInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
super().clean()
if any(self.errors):
return # Don't bother validating the formset unless each form is valid on its own
for form in self.forms:
if self.can_delete and self._should_delete_form(form):
if <...form.instance.is_admin...>:
raise ValidationError('...')

Adding to domino's Answer:
In some other scenarios, Sometimes user wants to delete and add object in the same time, so in this case delete should be fine!
Optimized version of code:
class RequiredImageInlineFormset(forms.models.BaseInlineFormSet):
""" Makes inline fields required """
def clean(self):
# get forms that actually have valid data
count = 0
delete_checked = 0
for form in self.forms:
try:
if form.cleaned_data:
count += 1
if form.cleaned_data['DELETE']:
delete_checked += 1
if not form.cleaned_data['DELETE']:
delete_checked -= 1
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
# Case no images uploaded
if count < 1:
raise forms.ValidationError(
'At least one image is required.')
# Case one image added and another deleted
if delete_checked > 0 and ProductImage.objects.filter(product=self.instance).count() == 1:
raise forms.ValidationError(
"At least one image is required.")

Related

Wagtail: How to validate m2m inline model?

I have a custom form inheriting from WagtailAdminPageForm and I want to validate an m2m model field (ClusterableModel). Right now I am using the clean() method on my form class.
def clean(self):
cleaned_data = super().clean()
my_field_total_sum = 0
for form in self.formsets['my_field'].forms:
if form.is_valid():
cleaned_form_data = form.clean()
my_field_total_sum += cleaned_form_data.get('my_value')
if total_sum > 100:
form.add_error('my_value', 'More then 100 is not allowed')
return cleaned_data
This works fine until I add and/or remove some inline panels in the admin interface and save my page as a draft and try to validate again. Because self.formsets['my_field'].forms still contains already removed forms and never gets reset to the actual amount of inline panels in the admin.
So, is it possible to (re-)set self.formsets['my_field'].forms to the actual amount inline panels visible in the admin interface? Or should I validate elsewhere anyways?
After more digging I found the answer. Checking for the "DELETE" key in a forms cleaned_data dict is the key. So my clean method from above needs to look like this:
def clean(self):
cleaned_data = super().clean()
my_field_total_sum = 0
for form in self.formsets['my_field'].forms:
# simply check, if "DELETE" is not true
if form.is_valid() and not form.cleaned_data.get("DELETE"):
cleaned_form_data = form.clean()
my_field_total_sum += cleaned_form_data.get('my_value')
# this check needs to be outside the loop ;-)
if my_field_total_sum > 100:
form.add_error('my_value', 'More then 100 is not allowed')
return cleaned_data

Custom validation in the clean method triggers before checking the fields exist or not in Django

I have the following code in my models file in Django:
class MyModel(models.Model):
...
foo = models.IntegerField()
bar = models.IntegerField()
def validate_foo_bar(self):
self._validation_errors = {}
if self.foo > self.bar:
self._validation_errors['foo'] = ['Must be greater than bar.']
self._validation_errors['bar'] = ['Must be less than foo.']
def clean(self):
self.validate_foo_bar()
if bool(self._validation_errors):
raise ValidationError(self._validation_errors)
super(MyModel, self).clean()
Hopefully the idea is clear. I check for errors in the clean method and raise them if they occur. When I use an admin form to create an object, if I leave the foo and bar fields empty, I get the following error:
if self.foo > self.bar:
TypeError: '>' not supported between instances of 'NoneType' and 'NoneType'
Why is this happening? Shouldn't the requirement check trigger before the method I wrote? Thanks for any help.
EDIT
Due to the nature of the answers and comments, I feel compelled to add this. I know the problem can be solved simply by doing the following:
def validate_foo_bar(self):
self._validation_errors = {}
if self.foo and self.bar:
if self.foo > self.bar:
self._validation_errors['foo'] = ['Must be greater than bar.']
self._validation_errors['bar'] = ['Must be less than foo.']
However, that is missing the point because shouldn't this check be done by the built-in form methods themselves before the validate_foo_bar() method is triggered?
According to the docs, the clean_fields method is called before the clean method. The clean_fields method in fact skips validation for fields with a None type:
def clean_fields(self, exclude=None):
"""
Clean all fields and raise a ValidationError containing a dict
of all validation errors if any occur.
"""
if exclude is None:
exclude = []
errors = {}
for f in self._meta.fields:
if f.name in exclude:
continue
# Skip validation for empty fields with blank=True. The developer
# is responsible for making sure they have a valid value.
raw_value = getattr(self, f.attname)
if f.blank and raw_value in f.empty_values:
continue
try:
setattr(self, f.attname, f.clean(raw_value, self))
except ValidationError as e:
errors[f.name] = e.error_list
if errors:
raise ValidationError(errors)
You can read up more about the reasons why here, where it says:
It is valid based on blank=True though. A use case for blank=True, null=False would be a field that gets populated in save(), for example. If we changed the behavior, it would be backwards incompatible and wouldn't allow this use case anymore. If you want the field required for model validation but not for form validation, then you should drop blank=True on the model and customize the form.

Django forms raise general validation error if all fields empty

I have a form where none of the fields are required indivdually, but I want to raise a validation error if all fields are left empty. What is the best way to do this? I tried the following but it it didn't work:
def clean(self):
cleaned_data = super(SearchForm, self).clean()
if len(cleaned_data) == 0:
raise forms.ValidationError(ugettext_lazy("You must fill at least one field!"))
Rather than checking the length of the cleaned_data (it should always contain one entry for each form field), you should check each entry and confirm that the values are all empty.
Here's an example of how you could do it.
def clean(self):
cleaned_data = super(SearchForm, self).clean()
form_empty = True
for field_value in cleaned_data.itervalues():
# Check for None or '', so IntegerFields with 0 or similar things don't seem empty.
if field_value is not None and field_value != '':
form_empty = False
break
if form_empty:
raise forms.ValidationError(ugettext_lazy("You must fill at least one field!"))
return cleaned_data # Important that clean should return cleaned_data!
Ignore fields filled with just whitespace as well: and not field_value.isspace()

Django modelform remove "required" attribute based on other field choice

I have ModelForm with several fields. Some of fields are required, some not. Also I have Select field with different choices, and I want to make some of fields "required" or not based on this Select field choice.
I tried in clean() method of Form
def clean(self):
cleaned_data = self.cleaned_data
some_field = cleaned_data.get("some_field")
if some_field == 'some_value':
self.fields['other_field'].required = False
return cleaned_data
but it doesn't work
See the Django documentation on Cleaning and validating fields that depend on each other. The standard practice would be to perform the following handling instead:
def clean(self):
cleaned_data = self.cleaned_data
some_field = cleaned_data.get("some_field")
if some_field == 'some_value':
# 'other_field' is conditionally required.
if not cleaned_data['other_field']:
raise forms.ValidationError("'Other_field' is required.")
return cleaned_data
You have the right idea but the problem is that the individual field validations have already run before the form clean. You have a couple options. You could make the field not required and handle the logic of when it is required in your form.clean. Or you could leave the field as required and remove the validation errors it might raise in the clean.
def clean(self):
cleaned_data = self.cleaned_data
some_field = cleaned_data.get("some_field")
if some_field == 'some_value':
if 'other_field' in self.errors:
del self.errors['other_field']
cleaned_data['other_field'] = None
return cleaned_data
This has some problems in that it removes all errors, not just missing/required errors. There is also a problem with the cleaned_data. You now have a required field which isn't in the cleaned_data which is why I've added it as None. The rest of your application will have to handle this case. It might seem odd to have a required field which doesn't have a value.
If you like to print error message for required field in common way, you can do this:
def clean(self):
cleaned_data = super(PasswordChangeForm, self).clean()
token = cleaned_data.get('token')
old_password = cleaned_data.get('old_password')
if not token and not old_password:
self._errors['old_password'] = self.error_class([self.fields['old_password'].error_messages['required']])
I found a possible solution.
You can use your condition before form.is_valid() and include your change in your field.
if form.data['tid_id'] is None: # my condition
form.fields['prs_razonsocial'].required = True
form.fields['prs_nombres'].required = False
form.fields['prs_apellidos'].required = False
if form.is_valid():
...

Inline Form Validation in Django

I would like to make an entire inline formset within an admin change form compulsory. So in my current scenario when I hit save on an Invoice form (in Admin) the inline Order form is blank. I'd like to stop people creating invoices with no orders associated.
Anyone know an easy way to do that?
Normal validation like (required=True) on the model field doesn't appear to work in this instance.
The best way to do this is to define a custom formset, with a clean method that validates that at least one invoice order exists.
class InvoiceOrderInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
# get forms that actually have valid data
count = 0
for form in self.forms:
try:
if form.cleaned_data:
count += 1
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
if count < 1:
raise forms.ValidationError('You must have at least one order')
class InvoiceOrderInline(admin.StackedInline):
formset = InvoiceOrderInlineFormset
class InvoiceAdmin(admin.ModelAdmin):
inlines = [InvoiceOrderInline]
Daniel's answer is excellent and it worked for me on one project, but then I realized due to the way Django forms work, if you are using can_delete and check the delete box while saving, it's possible to validate w/o any orders (in this case).
I spent a while trying to figure out how to prevent that from happening. The first situation was easy - don't include the forms that are going to get deleted in the count. The second situation was trickier...if all the delete boxes are checked, then clean wasn't being called.
The code isn't exactly straightforward, unfortunately. The clean method is called from full_clean which is called when the error property is accessed. This property is not accessed when a subform is being deleted, so full_clean is never called. I'm no Django expert, so this might be a terrible way of doing it, but it seems to work.
Here's the modified class:
class InvoiceOrderInlineFormset(forms.models.BaseInlineFormSet):
def is_valid(self):
return super(InvoiceOrderInlineFormset, self).is_valid() and \
not any([bool(e) for e in self.errors])
def clean(self):
# get forms that actually have valid data
count = 0
for form in self.forms:
try:
if form.cleaned_data and not form.cleaned_data.get('DELETE', False):
count += 1
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
if count < 1:
raise forms.ValidationError('You must have at least one order')
class MandatoryInlineFormSet(BaseInlineFormSet):
def is_valid(self):
return super(MandatoryInlineFormSet, self).is_valid() and \
not any([bool(e) for e in self.errors])
def clean(self):
# get forms that actually have valid data
count = 0
for form in self.forms:
try:
if form.cleaned_data and not form.cleaned_data.get('DELETE', False):
count += 1
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
if count < 1:
raise forms.ValidationError('You must have at least one of these.')
class MandatoryTabularInline(admin.TabularInline):
formset = MandatoryInlineFormSet
class MandatoryStackedInline(admin.StackedInline):
formset = MandatoryInlineFormSet
class CommentInlineFormSet( MandatoryInlineFormSet ):
def clean_rating(self,form):
"""
rating must be 0..5 by .5 increments
"""
rating = float( form.cleaned_data['rating'] )
if rating < 0 or rating > 5:
raise ValidationError("rating must be between 0-5")
if ( rating / 0.5 ) != int( rating / 0.5 ):
raise ValidationError("rating must have .0 or .5 decimal")
def clean( self ):
super(CommentInlineFormSet, self).clean()
for form in self.forms:
self.clean_rating(form)
class CommentInline( MandatoryTabularInline ):
formset = CommentInlineFormSet
model = Comment
extra = 1
#Daniel Roseman solution is fine but i have some modification with some less code to do this same.
class RequiredFormSet(forms.models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(RequiredFormSet, self).__init__(*args, **kwargs)
self.forms[0].empty_permitted = False
class InvoiceOrderInline(admin.StackedInline):
model = InvoiceOrder
formset = RequiredFormSet
class InvoiceAdmin(admin.ModelAdmin):
inlines = [InvoiceOrderInline]
try this it also works :)
The situation became a little bit better but still needs some work around. Django provides validate_min and min_num attributes nowadays, and if min_num is taken from Inline during formset instantiation, validate_min can be only passed as init formset argument. So my solution looks something like this:
class MinValidatedInlineMixIn:
validate_min = True
def get_formset(self, *args, **kwargs):
return super().get_formset(validate_min=self.validate_min, *args, **kwargs)
class InvoiceOrderInline(MinValidatedInlineMixIn, admin.StackedInline):
model = InvoiceOrder
min_num = 1
validate_min = True
class InvoiceAdmin(admin.ModelAdmin):
inlines = [InvoiceOrderInline]