Reputation: 20992
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
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
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