I've just implemented this and I thought It could well be incorporated
into Django itself; I guess it's a fairly common feature that one may need
on any project.
What I propose is to add a `opt_group` argument to ModelChoiceField and
ModelMultipleChoiceField; which indicates the item's field whose value is
used to group the choices. It should be used in conjunction with a
queryset which is (primarily) sorted by the same field.
Let me show with an example:
{{{
#!python
class Category(models.Model):
name = models.CharField(max_length=20)
class Item(models.Model):
name = models.CharField(max_length=20)
category = models.ForeignKey(Category)
}}}
And in some form's initialization process
{{{
#!python
field = ModelChoiceField(queryset=Item.objects.order_by('category__name',
'name'), opt_group='category')
}}}
field.choices will dynamically collect choices into named groups as a
2-tuple, which the underlying widget should present using an optgroup HTML
element.
--
Ticket URL: <https://code.djangoproject.com/ticket/27331>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
* needs_docs: => 0
* needs_tests: => 0
* needs_better_patch: => 0
Old description:
> Hello,
>
> I've just implemented this and I thought It could well be incorporated
> into Django itself; I guess it's a fairly common feature that one may
> need on any project.
>
> What I propose is to add a `opt_group` argument to ModelChoiceField and
> ModelMultipleChoiceField; which indicates the item's field whose value is
> used to group the choices. It should be used in conjunction with a
> queryset which is (primarily) sorted by the same field.
>
> Let me show with an example:
> {{{
> #!python
> class Category(models.Model):
> name = models.CharField(max_length=20)
>
> class Item(models.Model):
> name = models.CharField(max_length=20)
> category = models.ForeignKey(Category)
> }}}
> And in some form's initialization process
> {{{
> #!python
> field = ModelChoiceField(queryset=Item.objects.order_by('category__name',
> 'name'), opt_group='category')
> }}}
> field.choices will dynamically collect choices into named groups as a
> 2-tuple, which the underlying widget should present using an optgroup
> HTML element.
New description:
Hello,
I've just implemented this and I thought It could well be incorporated
into Django itself; I guess it's a fairly common feature that one may need
on any project.
What I propose is to add a `opt_group` argument to ModelChoiceField and
ModelMultipleChoiceField; which indicates the item's field whose value is
used to group the choices. It should be used in conjunction with a
queryset which is (primarily) sorted by the same field.
Let me show with an example:
{{{
#!python
class Category(models.Model):
name = models.CharField(max_length=20)
class Item(models.Model):
name = models.CharField(max_length=20)
category = models.ForeignKey(Category)
}}}
And in some form's initialization process
{{{
#!python
field = ModelChoiceField(queryset=Item.objects.order_by('category__name',
'name'), opt_group='category')
}}}
`field.choices` will dynamically collect choices into named groups as a
2-tuple, which the underlying widget should present using `optgroup` HTML
elements.
--
--
Ticket URL: <https://code.djangoproject.com/ticket/27331#comment:1>
* has_patch: 1 => 0
Comment:
At first glance, that looks more appropriate as a widget option since
`ModelChoiceField` isn't required to use a `Select` widget.
--
Ticket URL: <https://code.djangoproject.com/ticket/27331#comment:2>
Comment (by Héctor Urbina):
Replying to [comment:2 Tim Graham]:
> At first glance, that looks more appropriate as a widget option since
`ModelChoiceField` isn't required to use a `Select` widget.
The default `ModelChoiceField`'s widget (`Select`) is prepared to
understand 2-tuples coming on the field's choices attribute and translates
them to optgroups.
`ModelChoiceField` uses `ModelChoiceIterator` to generate the choices;
that is the actual class that is doing the job on
[https://github.com/ajendrex/django/commit/1ae142489f36d65b5cd0a51c243040c571fe01b8
my current implementation]. My idea is to include support for optgroup,
not require it; I'm updating my proposal to clarify that.
--
Ticket URL: <https://code.djangoproject.com/ticket/27331#comment:3>
Old description:
> Hello,
>
> I've just implemented this and I thought It could well be incorporated
> into Django itself; I guess it's a fairly common feature that one may
> need on any project.
>
> What I propose is to add a `opt_group` argument to ModelChoiceField and
> ModelMultipleChoiceField; which indicates the item's field whose value is
> used to group the choices. It should be used in conjunction with a
> queryset which is (primarily) sorted by the same field.
>
> Let me show with an example:
> {{{
> #!python
> class Category(models.Model):
> name = models.CharField(max_length=20)
>
> class Item(models.Model):
> name = models.CharField(max_length=20)
> category = models.ForeignKey(Category)
> }}}
> And in some form's initialization process
> {{{
> #!python
> field = ModelChoiceField(queryset=Item.objects.order_by('category__name',
> 'name'), opt_group='category')
> }}}
> `field.choices` will dynamically collect choices into named groups as a
> 2-tuple, which the underlying widget should present using `optgroup` HTML
> elements.
New description:
Hello,
I've just implemented this and I thought It could well be incorporated
into Django itself; I guess it's a fairly common feature that one may need
on any project.
What I propose is to add an optional `opt_group` argument to
ModelChoiceField and ModelMultipleChoiceField; which indicates the item's
field whose value is used to group the choices. It should be used in
conjunction with a queryset which is (primarily) sorted by the same field.
Let me show with an example:
{{{
#!python
class Category(models.Model):
name = models.CharField(max_length=20)
class Item(models.Model):
name = models.CharField(max_length=20)
category = models.ForeignKey(Category)
}}}
And in some form's initialization process
{{{
#!python
field = ModelChoiceField(queryset=Item.objects.order_by('category__name',
'name'), opt_group='category')
}}}
`field.choices` will dynamically collect choices into named groups as a
2-tuple, which the underlying widget should present using `optgroup` HTML
elements.
--
--
Ticket URL: <https://code.djangoproject.com/ticket/27331#comment:4>
Comment (by Tim Graham):
I'm still unconvinced that adding an argument with an HTML looking name to
a model field that only affects selected widgets is a good design. I see
that it's convenient compared to the alternative of providing a custom
`ModelChoiceIterator` but I don't know that the coupling is a good design.
--
Ticket URL: <https://code.djangoproject.com/ticket/27331#comment:5>
* status: new => closed
* resolution: => wontfix
Comment:
As the discussion has stalled here, you can write to the
DevelopersMailingList for other opinions. Thanks.
--
Ticket URL: <https://code.djangoproject.com/ticket/27331#comment:6>
Comment (by Simon Charette):
By the way, this is currently possible with the following code
{{{#!python
from functools import partial
from itertools import groupby
from operator import attrgetter
class GroupedModelChoiceIterator(ModelChoiceIterator):
def __init__(self, field, groupby):
self.groupby = groupby
super(field)
def __iter__(self):
if self.field.empty_label is not None:
yield ("", self.field.empty_label)
queryset = self.queryset
# Can't use iterator() when queryset uses prefetch_related()
if not queryset._prefetch_related_lookups:
queryset = queryset.iterator()
for group, objs in groupby(queryset, self.groupby):
yield (group, [self.choice(obj) for obj in objs])
class GroupedModelChoiceField(ModelChoiceField):
def __init__(self, *args, choices_groupby, **kwargs):
if isinstance(choices_groupby, str):
choices_groupby = attrgetter(choices_groupby)
self.iterator = partial(GroupedModelChoiceIterator,
groupby=choices_groupby)
super(*args, **kwargs)
}}}
While I won't push to get this feature included I think that a request for
feedback on django-developers has a good chance of being accepted. It
looks like the main argument against closing this ticket as _wontfix_ was
the naming of the `ModeChoice(opt_group)` argument.
--
Ticket URL: <https://code.djangoproject.com/ticket/27331#comment:7>
Comment (by angelafoli):
Replying to [comment:7 Simon Charette]:
> By the way, this is currently possible with the following code
>
> {{{#!python
> from functools import partial
> from itertools import groupby
> from operator import attrgetter
>
> class GroupedModelChoiceIterator(ModelChoiceIterator):
> def __init__(self, field, groupby):
> self.groupby = groupby
> super().__init__(field)
>
> def __iter__(self):
> if self.field.empty_label is not None:
> yield ("", self.field.empty_label)
> queryset = self.queryset
> # Can't use iterator() when queryset uses prefetch_related()
> if not queryset._prefetch_related_lookups:
> queryset = queryset.iterator()
> for group, objs in groupby(queryset, self.groupby):
> yield (group, [self.choice(obj) for obj in objs])
>
> class GroupedModelChoiceField(ModelChoiceField):
> def __init__(self, *args, choices_groupby, **kwargs):
> if isinstance(choices_groupby, str):
> choices_groupby = attrgetter(choices_groupby)
> elif not callable(choices_groupby):
> raise TypeError('choices_groupby must either be a str or a
callable accepting a single argument')
> self.iterator = partial(GroupedModelChoiceIterator,
groupby=choices_groupby)
> super().__init__(*args, **kwargs)
> }}}
>
> While I won't push to get this feature included I think that a request
for feedback on django-developers has a good chance of being accepted. It
looks like the main argument against closing this ticket as _wontfix_ was
the naming of the `ModelChoice(opt_group)` argument which was effectively
leaking HTML implementation details at the form layer.
This solution no longer works as of this change:
https://github.com/django/django/commit/5ec64f96b2d83ec3c0ef574f52e4767a440017b8
--
Ticket URL: <https://code.djangoproject.com/ticket/27331#comment:8>