field choices() as queryset?
Asked Answered
I

4

17

I need to make a form, which have 1 select and 1 text input. Select must be taken from database. model looks like this:

class Province(models.Model):
    name = models.CharField(max_length=30)
    slug = models.SlugField(max_length=30)

    def __unicode__(self):
        return self.name

It's rows to this are added only by admin, but all users can see it in forms. I want to make a ModelForm from that. I made something like this:

class ProvinceForm(ModelForm):
    class Meta:
        CHOICES = Province.objects.all()

        model = Province
        fields = ('name',)
        widgets = {
            'name': Select(choices=CHOICES),
        }

but it doesn't work. The select tag is not displayed in html. What did I wrong?

UPDATE:

This solution works as I wanto it to work:

class ProvinceForm(ModelForm):
    def __init__(self, *args, **kwargs):
        super(ProvinceForm, self).__init__(*args, **kwargs)
        user_provinces = UserProvince.objects.select_related().filter(user__exact=self.instance.id).values_list('province')
        self.fields['name'].queryset = Province.objects.exclude(id__in=user_provinces).only('id', 'name')

    name = forms.ModelChoiceField(queryset=None, empty_label=None)

    class Meta:
        model = Province
        fields = ('name',)
Introjection answered 24/2, 2011 at 11:54 Comment(0)
S
20

Read Maersu's answer for the method that just "works".

If you want to customize, know that choices takes a list of tuples, ie (('val','display_val'), (...), ...)

Choices doc:

An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this field.

from django.forms.widgets import Select


class ProvinceForm(ModelForm):
    class Meta:
        CHOICES = Province.objects.all()

        model = Province
        fields = ('name',)
        widgets = {
            'name': Select(choices=( (x.id, x.name) for x in CHOICES )),
        }
Starnes answered 24/2, 2011 at 13:8 Comment(7)
Hi @yuij, I am not using ModelForm but I have a similar requirement where I need to have choices from queryset on charfield in Model itself. I tried your suggested way but In my case its giving me error such as "django.core.exceptions.AppRegistryNotReady: Models aren't loaded yet.", Where it says Model aren't loaded yet. So any idea how can we achieve it Models ?Vernice
@Vernice either way, wherever you're injecting this, if it's called at model class init time (i.e. before App ready) it's going to be a static value.. won't take shape until restart. You should ensure it's in a ModelForm, or relevant view code which is executed per request.Indigested
I am not using ModelForm because I am using django for rest api only.Vernice
would CHOICES = Province.objects.values_list('id', 'name') work without widget customization.Suckle
I doubt if this would work as expected, If i add a new province and does not restart the server, does the new province appear in the choices without having to restart the server?Triolein
@MohammedShareefC fair enough. That is what the OP posted though.Indigested
@Yuji'Tomita'Tomita Maybe OP is unaware of the issueTriolein
S
6

ModelForm covers all your needs (Also check the Conversion List)

Model:

class UserProvince(models.Model):
    user = models.ForeignKey(User)
    province = models.ForeignKey(Province)

Form:

class ProvinceForm(ModelForm):
    class Meta:
        model = UserProvince
        fields = ('province',)

View:

   if request.POST:
        form = ProvinceForm(request.POST)
        if form.is_valid():
            obj = form.save(commit=True)
            obj.user = request.user
            obj.save()
   else:
        form = ProvinceForm() 
Seem answered 24/2, 2011 at 12:50 Comment(1)
+1: but shouldn't it 'commit=False', otherwise you will hit the db twiceCloistered
L
4

If you need to use a query for your choices then you'll need to overwrite the __init__ method of your form.

Your first guess would probably be to save it as a variable before your list of fields but you shouldn't do that since you want your queries to be updated every time the form is accessed. You see, once you run the server the choices are generated and won't change until your next server restart. This means your query will be executed only once and forever hold your peace.

# Don't do this
class MyForm(forms.Form):
    # Making the query
    MYQUERY = User.objects.values_list('id', 'last_name')
    myfield = forms.ChoiceField(choices=(*MYQUERY,))

    class Meta:
        fields = ('myfield',)

The solution here is to make use of the __init__ method which is called on every form load. This way the result of your query will always be updated.

# Do this instead
class MyForm(forms.Form):
    class Meta:
        fields = ('myfield',)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Make the query here
        MYQUERY = User.objects.values_list('id', 'last_name')
        self.fields['myfield'] = forms.ChoiceField(choices=(*MYQUERY,))

Querying your database can be heavy if you have a lot of users so in the future I suggest some caching might be useful.

Legionnaire answered 13/4, 2020 at 7:57 Comment(0)
W
1

the two solutions given by maersu and Yuji 'Tomita' Tomita perfectly works, but there are cases when one cannot use ModelForm (django3 link), ie the form needs sources from several models / is a subclass of a ModelForm class and one want to add an extra field with choices from another model, etc.

ChoiceField is to my point of view a more generic way to answer the need.

The example below provides two choice fields from two models and a blank choice for each :

class MixedForm(forms.Form):
    speaker = forms.ChoiceField(choices=([['','-'*10]]+[[x.id, x.__str__()] for x in Speakers.objects.all()]))
    event = forms.ChoiceField(choices=( [['','-'*10]]+[[x.id, x.__str__()] for x in Events.objects.all()]))

If one does not need a blank field, or one does not need to use a function for the choice label but the model fields or a property it can be a bit more elegant, as eugene suggested :

class MixedForm(forms.Form):
    speaker = forms.ChoiceField(choices=((x.id, x.__str__()) for x in Speakers.objects.all()))
    event = forms.ChoiceField(choices=(Events.objects.values_list('id', 'name')))

using values_list() and a blank field :

    event = forms.ChoiceField(choices=([['','-------------']] + list(Events.objects.values_list('id', 'name'))))

as a subclass of a ModelForm, using the one of the robos85 question :

class MixedForm(ProvinceForm):
    speaker = ...
Waly answered 5/4, 2020 at 10:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.