0

(This is all pseudocode and is not guaranteed to run.)

I am trying to make a "django admin form generator function" that outputs a django form. The current use case is to write reusable code that disallows admins from leaving a field empty, without also marking these fields as non-nullable.

So suppose there exists a model Foo, in which are some nullable fields:

class Foo(Model):
    field1 = FloatField(null=True, blank=True, default=0.0)
    field2 = FloatField(null=True, blank=True, default=0.0)
    field3 = FloatField(null=True, blank=True, default=0.0)

and corresponding FooAdmin and FooForm, such that these fields cannot be made None from the admin.

class FooAdmin(ModelAdmin):

    class FooForm(ModelForm):

        class Meta(object):
            model = Foo
            fields = '__all__'

        def _ensure_no_blanks(self, field):
            value = self.cleaned_data.get(field)
            if value is None:
                raise forms.ValidationError(_('This field is required.'))
            return value

        # repeat methods for every field to check
        def clean_field1(self):
            return self._ensure_no_blanks('field1')

        def clean_field2(self):
            return self._ensure_no_blanks('field2')

        def clean_field3(self):
            return self._ensure_no_blanks('field3')

    form = FooForm

As you can see, having to write clean_field1, clean_field2, and clean_field_n are repetitive and error-prone, so I write this helper function to generate model admins:

import functools
from django import forms

def form_with_fields(model_class, required_fields):

    class CustomForm(forms.ModelForm):
        class Meta(object):
            model = model_class
            fields = '__all__'

        def ensure_no_blanks(self, field):
            print field
            value = self.cleaned_data.get(field)
            if value is None:
                raise forms.ValidationError('This field is required.')
            return value

    # make a clean_bar method for every field that I need to check for None
    for field_name in required_fields:
        handler = functools.partial(CustomForm.ensure_no_blanks, field=field_name)
        setattr(CustomForm, 'clean_' + field_name, lambda self: handler(self))

    return CustomForm

class CustomAdmin(ModelAdmin):
    form = form_with_fields(Foo, ['field1', 'field2', 'field3'])

However, if you run such an admin, and if you do try to save the Foo model through the admin, you will see print field printing field3 three times in the terminal (i.e. all partials are retaining the last-run value).

Other attempts include overriding CustomForm's __getattr__(), and wrapping CustomForm in a type('Form', (CustomForm,), ..., which also exhibit the same behavior.

Is there a dry way to achieve this?

Brian
  • 7,394
  • 3
  • 25
  • 46
  • 1
    I think it would be much simpler to do that validation in the `clean` method and add the errors to the corresponding fields with [`add_error`](https://docs.djangoproject.com/en/2.0/ref/forms/api/#django.forms.Form.add_error). You could also make the fields `blank = False`, if it's just an on/off global operation. – Paulo Almeida Mar 07 '18 at 21:49

1 Answers1

2
setattr(CustomForm, 'clean_' + field_name, lambda self: handler(self))

The problem is the lambda function. The handler is defined outside of the lambda. It is accessed when the lambda is called, not when it is defined. Since this is after the for-loop has completed, you always get the function that uses the last field name.

See this FAQ entry from the Python docs for a fuller explanation.

In this case, you don't need a lambda at all. Just use handler itself.

setattr(CustomForm, 'clean_' + field_name, handler)

However, as Paulo suggests in the comment, it would be much

def clean(self):
    for field in required_fields(self):
        self.ensure_no_blanks(field)

You may need to change ensure_no_blanks to use add_error so that the validation errors are added to the correct field.

if value is None:
    self.add_error(field, 'This field is required.')

Another option would be to set the required=True for the fields in the __init__ method, then let the form take care of validation.

class CustomForm(forms.ModelForm):
    class Meta(object):
        model = model_class
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super(CustomForm, self).__init__(*args, **kwargs)
        for field in required_fields:
            self.fields[field].required = True      
Alasdair
  • 298,606
  • 55
  • 578
  • 516