Populating selects other than querysets

I have two problems that I am working on, both of which involve manipulating the set of objects listed in a form select.

The basics of this are that I have edited the __init__ method in the form, to give custom querysets, in this example matching the sessions available to choose to the day of the week (I’ve hardcoded the day of the week just for simplicity). The query also finds sessions that are available for “any” day (in this scheme the day int for “any” is “8” – cf Lennon and Macartney). This scheme uses ISO day ints:

form.fields['session'].queryset = Session.objects.filter(is_inactive=False).filter(Q(dayint=1)|Q(dayint=8))

This works pretty well, it finds all the sessions for “Monday” and “Any”, with the “Any” at the bottom due to the default ordering.

However, there are two bits of UI I’d like to finesse around this solution: inserting a break between “Monday” and “Any” sessions, and giving an indicator where no “day” sessions are found (I suppose that it is possible that there are no “Any” sessions, but that problem is solved with the “day” fix).

So, the select currently looks like:

<select name="session" class="form-control" title="" required id="id_session">
    <option value="" selected>---------</option>
    <option value="1">Monday 10:00</option>
    <option value="2">Monday 13:00</option>
    <option value="3">Monday 16:00</option>
    <option value="4">Monday 19:00</option>
    <option value="17">Anyday drop-in 12:30</option>
</select>

Or, where there are no “day” sessions:

<select name="session" class="form-control" title="" required id="id_session">
    <option value="" selected>---------</option>
    <option value="17">Anyday drop-in 12:30</option>
</select>

But what I’d prefer to do is something like:

<select name="session" class="form-control" title="" required id="id_session">
    <option value="" selected>---------</option>
    <option value="1">Monday 10:00</option>
    <option value="2">Monday 13:00</option>
    <option value="3">Monday 16:00</option>
    <option value="4">Monday 19:00</option>
    <option value="">---------</option>
    <option value="17">Anyday drop-in 12:30</option>
</select>

or:

<select name="session" class="form-control" title="" required id="id_session">
    <option value="" selected>---------</option>
    <option value="">No scheduled sessions for Monday</option>
    <option value="">---------</option>
    <option value="17">Anyday drop-in 12:30</option>
</select>

A solution I am using for some of these lists is to insert the breaks into the database, and let the sorting put them in the right places:

Table "classes":
id        name               sort

5        -----                   -1
8       French               0
22     German              1
396   Italian                3
1       -----                   20
66      Maths                21
60      Physics             22
600   Chemistry         23
etc

This method has its own issues: it only really works for relatively “fixed” lists (so doesn’t work for the “Sessions” table) and the user can select the “-----” breaks - so they could potentially get past the form checking entering an invalid selection (or at least one we don’t want them to choose).

With the Session queryset I know that I can join two querysets, but I can’t work out how to put an “empty(ish)” instance between them:

bigqueryset = monday_set | empty_instance | any_set

And how to get the “blanks/spacers” not to have id in the select -> option so that they are not valid in the form?

I thought that probably the easiest way to solve the Session issue was to concatenate two or three lists together, but form.fields['fieldname'].queryset actually wants a queryset, rather than something iterable.

Sorry if I am asking too many questions at once! Thanks in advance.

What I would recommend for a situation like this is that you don’t try to create it using a queryset, but as a list of choices. This may mean changing from a ModelChoiceField to a ChoiceField, but I think overall it’s going to make things easier for you.
You can still query the database to get the values you need, but you can completely build the options list as you see fit.

So, as a ChoiceField you could supply a list (of dicts)?

Or could I just populate choices with the list? I thought I saw choices as an element of ModelChoiceField.

Not of dicts. The choices are supplied to the form as a list of 2-tuples. (In this case, you would define the field as a reference to a callable, and that callable would be responsible for building the list.)

Thanks. As it turns out seting the choices:

self.fields['group'].choices = [('hello', 'hello'), ('world', 'world')]

appears to override, or have priority over :

self.fields['group'].queryset = …

And on a ModelField, so easy enough to fabricate a list to complete the task required.

However, the problem will occur with the validation of the data in the form.

The field validation in a ModelChoiceField is performed on the queryset attribute and not the choices attribute. If those two aren’t the same, then you will either have options that can’t be selected, or invalid options that can be injected.

So, you need to set the field’s queryset attribute, appropriately, even though, in this case, you are not using it to populate the select?

If you are using a ModelChoiceField, yes. If you change it to a ChoiceField, no.

Replacing the queryset attribute with the choices attribute effectively converts the ModelChoiceField to a ChoiceField anyway. I don’t see a value in using a ModelChoiceField if you’re not using the queryset attribute. (ModelChoiceField is a subclass of ChoiceField that adds the queryset attribute and the functions necessary to support it.)

Well, having fiddled around with it for a while, I think that if you let the field “default” to a ModelChoiceField and you do not set a value for field.queryset, then you do not get any validation on input values - so invalid (in the related table, but not in the set you want) values can be injected. (Completely invalid choices are avoided because they aren’t ids for the relevant model objects.)

Setting the field manually to ChoiceField necessitates injecting the submitted data into the .save, which tends to start overly complicating the process…

Although I was concerned about triggering multiple identical/similar queries (though these were very small/quick), I worked out ways to avoid these, and formatted the select as required.

Thanks for the pointer on the validation.

Correct. The default queryset is Model.objects.all().

Please clarify what you mean by this. I’m not sure I understand where this would be necessary - especially not in save. At the very least, you should be able to set the values in the clean_<field> method.