Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Form field for a foreign key in ModelForm with too many choices for ModelChoiceField?

I have a simple foreign key relationship I want to use in a ModelForm, but without a ModelChoiceField.

class Sample(models.Model):
    alt = IntegerField(db_index=True, unique=True)

class Assignment(models.Model):
    sample = models.ForeignKey(Sample, on_delete=models.CASCADE)

I want to have the AssignmentForm select the sample based on the contents of the sample's alt field. With a ModelChoiceField it would be like this:

class SampleSelect(ModelChoiceField):
    def label_from_instance(self, obj):
        return obj.alt

class AssignmentForm(ModelForm):
    sample = SampleSelect(queryset=Sample.objects.all())
    class Meta:
        model = Assignment
        fields = ['sample']

The ModelChoiceField documentation says to use something else if the number of choices is large.

Allows the selection of a single model object, suitable for representing a foreign key. Note that the default widget for ModelChoiceField becomes impractical when the number of entries increases. You should avoid using it for more than 100 items.

I think I need a custom form field, but I cannot figure out how to do this.

class SampleBAltField(IntegerField):
    def clean(self, value):
        try:
            return Sample.objects.get(alt=value)
        except Sample.DoesNotExist:
            raise ValidationError(f'Sample with alt {value} does not exist')

This existing code should take an integer from the form and map it back to a foreign key, but I cannot figure out what to override to populate the field for a bound form from the Sample instance.

Is there a relatively easy way to solve this issue with FormFields in the ModelForm, or do I need to write the Form from scratch?

like image 738
mattm Avatar asked Sep 01 '25 16:09

mattm


1 Answers

The ModelChoiceField documentation says to use something else if the number of choices is large.

The documentation suggests using a different widget (otherwise a user will have to select from a dropdown with too many items), but you don't necessarily need an entirely different field.

If you want the field of the bound form to be n instance of Sample, then ModelChoiceField is still appropriate.

To avoid the problem anticipated in the documentation, you could just change the widget for the field. You might need to decide exactly what that is. One simple choice would be to use a NumberInput widget where the user just enters an integer for the foreign key.

from django.forms.widgets import NumberInput

class AssignmentForm(ModelForm):
    sample = ModelChoiceField(queryset=Sample.objects.all(), widget=NumberInput)
    class Meta:
        model = Assignment
        fields = ['sample']

select the sample based on the contents of the sample's alt field

What you want here is a separate issue from what you quoted from the documentation. You can choose to implement this with or without changing the widget.

If you want the user to provide the alt value rather than the primary key of the Sample, you can use the to_field_name argument for ModelChoiceField (note this is only appropriate here because your alt field is unique)

class AssignmentForm(ModelForm):
    sample = ModelChoiceField(
        queryset=Sample.objects.all(),
        widget=NumberInput,
        help_text="Enter the alt of the sample",
        to_field_name='alt'
    )

    class Meta:
        model = Assignment
        fields = ["sample"]

In order for the initial value to render correctly when rendering a bound form, you can provide the initial keyword argument when instantiating the bound form:

form = AssignmentForm(instance=inst,
                      initial={'sample': inst.sample.alt})

Alternatively, you can override the __init__ method of the form to do this automatically when the form is instantiated:

class AssignmentForm(ModelForm):
    ...
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.instance.pk:
            self.initial.update({'sample': self.instance.sample.alt})

like image 145
sytech Avatar answered Sep 04 '25 05:09

sytech