Jordan Reiter
Jordan Reiter

Reputation: 20992

How to I prevent Django Admin for overriding the custom widget for a custom field?

I have a model field, a form field, and a widget that I am using in an app. The details of the code don't matter. The key is that the field correctly renders in regular forms but is overridden in admin.

Here is some pseudo-code of what the field basically looks like:

class SandwichWidget(forms.Widget):
    template_name = 'sandwichfield/widgets/sandwichfield.html'

    def __init__(self, attrs=None, date_format=None, time_format=None):
        widgets = (
            NumberInput(),
            Select(choices=FILLING_CHOICES),
            NumberInput(),
        )
        super(SandwichWidget, self).__init__(widgets, attrs)

    def decompress(self, value):
        if value:
            value = Sandwich(value)
            return [
                value.top,
                value.middle,
                value.bottom
            ]
        return [None, None, None]


class SandwichFormField(forms.MultiValueField):
    widget = SandwichWidget

    def __init__(self, input_date_formats=None, input_time_formats=None, *args, **kwargs):
        fields = (
            forms.IntegerField(),
            forms.CharField(),
            forms.IntegerField(),
        )
        super(SandwichFormField, self).__init__(fields, *args, **kwargs)


class SandwichField(models.PositiveIntegerField):

    def get_internal_type(self):
        return 'IntegerField'

    def formfield(self, *args, **kwargs):
        defaults={'form_class': SandwichFormField}
        defaults.update(kwargs)
        return super(SandwichField, self).formfield(*args, **defaults)

It apparently happens because of this line in django/contrib/admin/options.py which specifies the override of the models.IntegerField should be widgets.AdminIntegerFieldWidget. Because models.PositiveIntegerField inherits from models.IntegerField, and because line 181 loops over all subclasses of the field, it seems like there is no way to prevent the widget from being overridden in admin.

This is a real problem, because I use this custom field, with its custom widget, all over my site and throughout admin, and I do not want to have put in a custom value for override_fields every time I want to use the field. Ideally, developers should be able to use the custom field without having to provide a custom admin each time.

Currently I'm inheriting from forms.PositiveIntegerField because when stored and retrieved from the database, it is a positive integer, and I want to take advantage of all of the coding already in place for handling positive integer values.

Currently it looks like the only solution is changing my field to inherit from models.Field and then copying and pasting all of the PositiveIntegerField and IntegerField functionality from the django code. Is there an alternative to this?

Of course, I can always have my formfield ignore whatever widget is sent to it and always use the custom widget, but this raises a problem when I actively want to override the widget, which absolutely could happen. I just don't want admin to override my widget.

Upvotes: 2

Views: 580

Answers (2)

Kound
Kound

Reputation: 2521

I ran into the same problem with django-pint.

Looking into AdminIntegerFieldWidget I figured that it only overwrites the class, so that the width differ between BigInt and Int.

So I took care that during the creation of the FormField only these Widgets are ignored, but only those.

class SandwichFormField(forms.MultiValueField):
    def __init__(self, input_date_formats=None, input_time_formats=None, *args, **kwargs):
        fields = (
            forms.IntegerField(),
            forms.CharField(),
            forms.IntegerField(),
        )


        def is_special_admin_widget(widget) -> bool:
            """
            There are some special django admin widgets, defined
            in django/contrib/admin/options.py in the variable
            FORMFIELD_FOR_DBFIELD_DEFAULTS
            The intention for Integer and BigIntegerField is only to
            define the width.

            They are set through a complicated process of the
            modelform_factory setting formfield_callback to
            ModelForm.formfield_fo_dbfield

            As they will overwrite our Widget we check for them and
            will ignore them, if they are set as attribute.

            We still will allow subclasses, so the end user has still
            the possibility to use this widget.
            """
            WIDGETS_TO_IGNORE = [
                FORMFIELD_FOR_DBFIELD_DEFAULTS[models.IntegerField],
                FORMFIELD_FOR_DBFIELD_DEFAULTS[models.BigIntegerField],
            ]
            classes_to_ignore = [
                ignored_widget["widget"].__name__
                for ignored_widget in WIDGETS_TO_IGNORE
            ]
            return getattr(widget, "__name__") in classes_to_ignore

        widget = kwargs.get("widget", None)
        if widget is None or is_special_admin_widget(widget):
            widget = SandwichWidget()
        kwargs["widget"] = widget
        super(SandwichFormField, self).__init__(fields, *args, **kwargs)

This way you can make sure, that you get correct widget in the admin but are still able to overwrite them (with everything but the special admin fields).

Upvotes: 1

Dave Alan
Dave Alan

Reputation: 21

Try this (I added the second line from the bottom to your code):

class SandwichField(models.PositiveIntegerField):

    def formfield(self, *args, **kwargs):
        defaults={'form_class': SandwichFormField}
        defaults.update(kwargs)
        defaults['widget'] = SandwichFormField.widget
        return super(SandwichField, self).formfield(*args, **defaults)

The admin code is passing you a kwargs that has their own widget, so you have to override it.

Upvotes: 2

Related Questions