Opened 16 years ago

Closed 12 years ago

#9245 closed Bug (fixed)

Using choices in a mode field forces use of TypedChoiceField in a form field

Reported by: Joey Wilhelm Owned by: Badri
Component: Forms Version: 1.0
Severity: Normal Keywords:
Cc: mmitar@…, s.kuzmenko@…, lukasz@…, David Gouldin Triage Stage: Accepted
Has patch: yes Needs documentation: yes
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

For most field types, I can override the class used to display the field with:

def formfield(self, **kwargs):
    defaults = {'form_class': CustomFormField}
    defaults.update(kwargs)
    return super(MyFieldClass, self).formfield(**defaults)

However, the Field class specifically overrides this when the field has 'choices'.
Lines 311-326 of django/db/models/fields/init.py contain the offending code. The specific override is on line 318:

form_class = forms.TypedChoiceField

In my case, I have a subclass of TypedChoiceField which I would like to use to display the field. Instead of the simple bit of code shown above, however, I had to duplicate the entire formfield function from the base Field class, and change that one offending line.

Attachments (5)

9245.diff (6.3 KB ) - added by Badri 16 years ago.
9245.2.diff (6.2 KB ) - added by Badri 16 years ago.
issue9245.diff (6.5 KB ) - added by Łukasz Langa 13 years ago.
Patch for 1.4RC
issue9245-alt.diff (10.1 KB ) - added by Łukasz Langa 13 years ago.
An alternative patch which puts the responsibility of handling overrides on model fields
9245.more-flexible-formfield.diff (4.2 KB ) - added by Julien Phalip 13 years ago.

Download all attachments as: .zip

Change History (21)

comment:1 by Malcolm Tredinnick, 16 years ago

Component: Database layer (models, ORM)Forms
Triage Stage: UnreviewedAccepted

comment:2 by Badri, 16 years ago

Owner: changed from nobody to Badri
Status: newassigned

comment:3 by Malcolm Tredinnick, 16 years ago

Summary: django.db.models.fields.Field forces use of TypedChoiceFieldUsing choices in a mode field forces use of TypedChoiceField in a form field

Changed description to match the problem.

comment:4 by Badri, 16 years ago

Has patch: set
Resolution: fixed
Status: assignedclosed

by Badri, 16 years ago

Attachment: 9245.diff added

by Badri, 16 years ago

Attachment: 9245.2.diff added

in reply to:  4 comment:5 by Badri, 16 years ago

Replying to badri:
I have uploaded the patch twice. please delete the first one.

comment:6 by Malcolm Tredinnick, 16 years ago

Resolution: fixed
Status: closedreopened

Seems to have been accidentally marked as "fixed". Reopening.

comment:7 by Skaffen, 16 years ago

Just a quick note: I've hit this issue for a different reason.

In my circumstance I'm passing in "choices" to a ManyToManyField declaration (as I don't want to use a query to declare the choices) which is leading to the model form setting the corresponding field up as a TypedChoiceField. This, of course, has a single selection widget as its default widget rather than multiple select.

comment:8 by Julien Phalip, 14 years ago

Patch needs improvement: set
Severity: Normal
Type: Bug

The tests would need to be rewritten using unittests since this is now Django's preferred way.

comment:9 by Mitar, 14 years ago

Cc: mmitar@… added
Easy pickings: unset

comment:10 by s.kuzmenko@…, 14 years ago

Cc: s.kuzmenko@… added

comment:11 by Aymeric Augustin, 13 years ago

UI/UX: unset

Change UI/UX from NULL to False.

comment:12 by Łukasz Langa, 13 years ago

I ended up creating a new patch since the old one used doctests and the solution was based on a hardcoded list of form fields that should be overriden which basically reintroduced the same problem in a different form.

by Łukasz Langa, 13 years ago

Attachment: issue9245.diff added

Patch for 1.4RC

by Łukasz Langa, 13 years ago

Attachment: issue9245-alt.diff added

An alternative patch which puts the responsibility of handling overrides on model fields

comment:13 by Łukasz Langa, 13 years ago

Cc: lukasz@… added

by Julien Phalip, 13 years ago

comment:14 by Julien Phalip, 13 years ago

Needs documentation: set
Patch needs improvement: unset

Thank you for your work and sorry for the late review.

In the revised patch attached, I've moved the logic to a new, separate method called choices_formfield(). This complicates the API slightly, but at least it removes the necessity for all subclasses of Field to modify their logic based on self.choices.

I'll seek some feedback. If this approach is approved then some documentation will be needed.

comment:15 by David Gouldin, 12 years ago

Cc: David Gouldin added

Julien, this patch will not work as-is. Your new Field.choices_formfield function is called from Field.formfield using **defaults, but defaults will never include form_class. This means that Field.choices_formfield will always be called with its default value for form_class (None unless overridden by a custom Field subclass) even when the user's intent was to provide a custom form_class for that model field. So for this model:

class Foo(models.Model):
    bar = models.IntegerField(choices=bar_choices, form_class=MyCustomFormClass)

MyCustomFormClass would never make it to bar's Field.choices_formfield, and so an instance of it would not be returned by Field.formfield.

However, it also not work to simply call self.choices_formfield(form_class=form_class, **defaults) from Field.formfield. Subclasses of Field (such as IntegerField) frequently override formfield() and call super's formfield() with a form_class kwarg. This means that calling Field.choices_formfield with Field.formfield's form_class would break models like this:

class Foo(models.Model):
    bar = models.IntegerField(choices=bar_choices)

In this case, IntegerField.formfield would call Field.formfield with form_class=forms.IntegerField, which would then be passed through to Field.choices_formfield, causing forms.TypedChoiceField not to be used as was intended.

As the implementation sits currently, Field has no way to determine whether the intent of the user was to specify their own form_class or whether the Field subclass injected its own form_class before calling Field.formfield. This is broken, plain and simple. In order for Field.formfield to make a good decision about which class to use when instantiating a form field, it needs both pieces of information: the model field's default form_class, and the user's override of that default (if provided). Here's the solution I propose:

class Field(object):
    default_form_class = forms.CharField

    # ...

    def formfield(self, form_class=None, **kwargs):
        if self.choices:
            form_class = form_class or forms.TypedChoiceField
            # ...
        else:
            form_class = form_class or self.default_form_class


class IntegerField(Field):
    default_form_class = forms.IntegerField

This allows Field.formfield to have both pieces of information, and it alleviates the need for IntegerField to override formfield. It is not backwards compatible, as any model field subclasses which use this formfield override approach would need to be changed to fit the new default_form_class property approach.

Last edited 12 years ago by David Gouldin (previous) (diff)

comment:16 by Claude Paroz, 12 years ago

Resolution: fixed
Status: reopenedclosed

Seems this issue has been fixed (#18162, [b6f4a92ff45d98a63dc29402d8ad86b88e6a6697]).

Note: See TracTickets for help on using tickets.
Back to Top