Django MultipleChoiceField does not preserve order of selected values
Asked Answered
C

4

6

I have a Django ModelForm which exposes a multiple choice field corresponding to a many-to-many relation through a model which holds order of selection (a list of documents) as an extra attribute. At the front-end, the field is displayed as two multiple select fields similar to that in admin, one to list available choices and the other holds the selected elements.

The form can be saved with the correct selection of elements but they are always in the order of the original order of choices, not the selection. The browser sends the selection in correct order, but order in form.cleaned_data['documents'] is always the order in original order of choices.

How can I make the MultipleChoiceField respect the order of elements selected?

Thanks.

Cahoot answered 24/4, 2012 at 10:43 Comment(2)
If the ManyToMany relationship is being returned the server with the correct sorting (via the through model) then why not resort it using an approach similar to this questionBoxthorn
Thanks for the comment. I want to order the elements in the order of the selection while creating the relation on server side. But cleaned_data['documents'] does not reflect the selection order in the select multiple field. I can re-order using the ids in request.GET.getlist('documents') but feels hacky.Cahoot
R
6

There is no simple way. You either need to override the clean method of the MultipleChoiceField or, as you mentioned in your comment, use the getlist to re-order them manually. It probably depends how often in your code do you need to do it.

The clean method of MultipleChoiceField creates a QuerySet that you are receiving, by filtering an object list through the IN operator like this, so the order is given by the database:

qs = self.queryset.filter(**{'%s__in' % key: value})

You can inherit from ModelMultipleChoiceField:

class OrderedModelMultipleChoiceField(ModelMultipleChoiceField):
    def clean(self, value):
        qs = super(OrderedModelMultipleChoiceField, self).clean(value)
        return sorted(qs, lambda a,b: sorted(qs, key=lambda x:value.index(x.pk)))

The drawback is that the returned value is no longer a QuerySet but an ordinary list.

Ruperto answered 24/4, 2012 at 13:18 Comment(1)
+1 plus sorted(qs, key=lambda x:value.index(x.pk)) is shorterEdouard
C
0

To return an ordered QuerySet when overriding the clean method you could also do this:

class OrderedModelMultipleChoiceField(ModelMultipleChoiceField):
    def clean(self, value):
        qs = super(OrderedModelMultipleChoiceField, self).clean(value)
        clauses = ' '.join(['WHEN id=%s THEN %s' % (pk, i) for i, pk in enumerate(value)])
        return qs.filter(pk__in=value).extra(
            select={'ordering': 'CASE %s END' % clauses},
            order_by=('ordering',)
        )
Coaler answered 3/3, 2016 at 16:43 Comment(0)
T
0

I did it via a Widget. The benefit of it is, it will sort properly in different languages:

class SortedSelectMultiple(SelectMultiple):

def render_options(self, selected_choices):
    self.choices = sorted(self.choices)
    self.choices.sort(key=lambda x: x[1])
    return super(SortedSelectMultiple, self).render_options(selected_choices)
Tipi answered 7/1, 2017 at 22:52 Comment(0)
G
0

I am able to maintain the order of the selection using following way:

class OrderedModelMultipleChoiceField(models.ModelMultipleChoiceField):

    def clean(self, value):
        qs = super(OrderedModelMultipleChoiceField, self).clean(value)
        preserved = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(value)])
        return qs.filter(pk__in=value).order_by(preserved)

Note: I am using Django 2.2

Giralda answered 22/5, 2021 at 8:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.