如何实现分组模型选择字段(附例子)

54 阅读2分钟

Django表单API有两种字段类型来处理多个选项:ChoiceFieldModelChoiceField

两者都使用选择输入作为默认的部件,它们的工作方式相似,只是ModelChoiceField 是用来处理QuerySets和处理外键关系的。

一个使用ChoiceField 的基本实现将是:

class ExpenseForm(forms.Form):
    CHOICES = (
        (11, 'Credit Card'),
        (12, 'Student Loans'),
        (13, 'Taxes'),
        (21, 'Books'),
        (22, 'Games'),
        (31, 'Groceries'),
        (32, 'Restaurants'),
    )
    amount = forms.DecimalField()
    date = forms.DateField()
    category = forms.ChoiceField(choices=CHOICES)

Django ChoiceField


分组选择字段

你也可以将选择分组,以生成<optgroup> 标签,像这样:

class ExpenseForm(forms.Form):
    CHOICES = (
        ('Debt', (
            (11, 'Credit Card'),
            (12, 'Student Loans'),
            (13, 'Taxes'),
        )),
        ('Entertainment', (
            (21, 'Books'),
            (22, 'Games'),
        )),
        ('Everyday', (
            (31, 'Groceries'),
            (32, 'Restaurants'),
        )),
    )
    amount = forms.DecimalField()
    date = forms.DateField()
    category = forms.ChoiceField(choices=CHOICES)

Django Grouped ChoiceField


分组的模型选择字段

当你使用ModelChoiceField ,不幸的是,没有内置的解决方案。

最近我在Django的ticket tracker上发现了一个不错的解决方案,有人提议在ModelChoiceField 中加入一个opt_group 的参数。

当讨论还在进行时,Simon Charette提出了一个非常好的解决方案。

让我们看看如何在我们的项目中整合它。

首先考虑下面的模型:

models.py

from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=30)
    parent = models.ForeignKey('Category', on_delete=models.CASCADE, null=True)

    def __str__(self):
        return self.name

class Expense(models.Model):
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    date = models.DateField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)

    def __str__(self):
        return self.amount

所以现在我们的类别不再是一个普通的选择字段,而是一个模型,而且Expense 模型与它有一个使用外键的关系。

如果我们使用这个模型创建一个ModelForm ,其结果将与我们的第一个例子非常相似。

为了模拟一个分组的类别,你将需要下面的代码。首先创建一个名为fields.py的新模块。

fields.py

from functools import partial
from itertools import groupby
from operator import attrgetter

from django.forms.models import ModelChoiceIterator, ModelChoiceField


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)

这里是你如何在你的表单中使用它:

forms.py

from django import forms
from .fields import GroupedModelChoiceField
from .models import Category, Expense

class ExpenseForm(forms.ModelForm):
    category = GroupedModelChoiceField(
        queryset=Category.objects.exclude(parent=None), 
        choices_groupby='parent'
    )

    class Meta:
        model = Expense
        fields = ('amount', 'date', 'category')

Django Grouped ModelChoiceField

因为在上面的例子中,我使用了一个自引用关系,我必须添加exclude(parent=None) ,以隐藏 "分组类别 "在选择输入中显示为一个有效选项。