Django ModelForm - 多对多嵌套选择

Posted

技术标签:

【中文标题】Django ModelForm - 多对多嵌套选择【英文标题】:Django ModelForm - ManyToMany nested selection 【发布时间】:2021-12-19 03:09:44 【问题描述】:

我正在构建一个 Django 应用程序,但遇到了一个我不知道如何解决的问题...我会尽量解释清楚。

我有一个名为“Impostazioni”的应用程序,它有一个名为“AnniScolastici”的模型:

class AnniScolastici(models.Model):
    nome = models.CharField(max_length=50)

    class Meta:
        verbose_name = "Anno scolastico"
        verbose_name_plural = "Anni scolastici"

    def __str__(self):
        return f"self.nome"

我还有另一个名为“Attivita”的应用程序,它有一个名为“Laboratori”的模型:

class Laboratori(models.Model):
    nome = models.CharField(max_length=25)
    durata = models.IntegerField(default=0)
    anniscolastici = models.ManyToManyField(AnniScolastici)
    note = models.TextField(null=True, blank=True)

    class Meta:
        verbose_name = "Laboratorio"
        verbose_name_plural = "Laboratori"

    def __str__(self):
        return f"self.nome"

我编写了另一个名为“RichiesteLaboratori”的模型,它与我的 Django 应用程序中的不同模型相关(当然还有上面的两个模型):

class RichiesteLaboratori(models.Model):
    date_added = models.DateTimeField(auto_now_add=True)
    date_valid = models.DateTimeField(null=True, blank=True)
    provincia = models.ForeignKey("impostazioni.Province", related_name="richiesta_provincia", null=True, on_delete=models.CASCADE)
    istituto = models.ForeignKey("contatti.Istituto", related_name="richiesta_istituto", null=True, on_delete=models.CASCADE)
    plesso = models.ForeignKey("contatti.Plesso", related_name="richiesta_plesso", null=True, on_delete=models.CASCADE)
    classe = models.CharField(max_length=25)
    numero_studenti = models.PositiveIntegerField()
    nome_referente = models.CharField(max_length=50)
    cognome_referente = models.CharField(max_length=50)
    email = models.EmailField()
    telefono = models.CharField(max_length=20)
    termini_servizio = models.BooleanField()
    classi_attivita = models.ManyToManyField(AnniScolastici, related_name="richiesta_anniScolastici")
    laboratori = models.ManyToManyField(Laboratori)
    note = models.TextField(null=True, blank=True)
    approvato = models.BooleanField(default=False)

    class Meta:
        verbose_name = "Richiesta - Laboratorio"
        verbose_name_plural = "Richieste - Laboratorio"

    def __str__(self):
        return f"self.pk"

我正在通过 ModelForm 和视图填充此模型的条目。这是表格:

class RichiesteLaboratoriModelForm(forms.ModelForm):
    classi_attivita = forms.ModelMultipleChoiceField(
        queryset=AnniScolastici.objects.all(),
        widget=forms.CheckboxSelectMultiple,
        required=True
    )
    
    laboratori = forms.ModelMultipleChoiceField(
        queryset=Laboratori.objects.all(),
        widget=forms.CheckboxSelectMultiple,
        required=True
    )
    
    termini_servizio = forms.BooleanField(
        label = "Conferma di voler aderire al progetto con la classe indicata e di impegnarsi a rispettare le regole previste",
        required=True
    )

    class Meta:
        model = RichiesteLaboratori
        fields = (
            'provincia',
            'istituto',
            'plesso',
            'classe',
            'numero_studenti',
            'nome_referente',
            'cognome_referente',
            'email',
            'telefono',
            'classi_attivita',
            'laboratori',
            'note',
            'termini_servizio'
        )
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['istituto'].queryset = Istituto.objects.none()
        self.fields['plesso'].queryset = Plesso.objects.none()

        if 'provincia' in self.data:
            try:
                id_provincia = int(self.data.get('provincia'))
                self.fields['istituto'].queryset = Istituto.objects.filter(provincia=id_provincia).order_by('nome')
            except (ValueError, TypeError):
                pass
        elif self.instance.pk:
            self.fields['istituto'].queryset = self.instance.provincia.istituto_set.order_by('nome')

        if 'istituto' in self.data:
            try:
                id_istituto = int(self.data.get('istituto'))
                self.fields['plesso'].queryset = Plesso.objects.filter(istituto=id_istituto).order_by('nome')
            except (ValueError, TypeError):
                pass
        elif self.instance.pk:
            self.fields['plesso'].queryset = self.instance.istituto.plesso_set.order_by('nome')

这是视图:

class RichiestaLaboratorioCreateView(LoginRequiredMixin, generic.CreateView):
    template_name = "richiestalaboratorio/richiestalaboratorio_crea.html"
    form_class = RichiesteLaboratoriModelForm

    def get_success_url(self):
        return reverse("operativita:richiestalaboratorio-lista")

视图引用了一个名为“richiestalaboratorio_crea.html”的模板,用于呈现表单。代码如下:

% extends "base.html" %
% load crispy_forms_tags %

% block title %
Nuova richiesta laboratorio
% endblock title %

% block js-head %
<!-- Select2 -->
<script>
    $(document).ready(function() 
        $('#id_provincia').select2(
            placeholder: "Seleziona una provincia...",
            allowClear: true,
            language: 
                noResults: function() 
                    return 'Nessuna provincia trovata';
                ,
            
        );

        $('#id_grado').select2(
            placeholder: "Seleziona un grado...",
            allowClear: true,
            language: 
                noResults: function() 
                    return 'Nessun grado trovato';
                ,
            
        );
        
        $('#id_istituto').select2(
            placeholder: "Seleziona un istituto...",
            allowClear: true,
            language: 
                noResults: function() 
                    return 'Nessun istituto trovato';
                ,
            
        );
        
        $('#id_plesso').select2(
            placeholder: "Seleziona un plesso...",
            allowClear: true,
            language: 
                noResults: function() 
                    return 'Nessun plesso trovato';
                ,
            
        );
    );
</script>
% endblock js-head %

% block content %
<div class="container">
    <div class="row border-bottom border-1">
        <div class="col-12 pb-2">
            <a href="% url 'operativita:richiestalaboratorio-lista' %">Torna alle richieste di laboratorio</a>
        </div>
    </div>
    <div class="row mt-3">
        <div class="col-12">
            <h1>Aggiungi una richiesta di laboratorio</h1>
            <p class="text-secondary">Compila il form per aggiungere una richiesta di laboratorio.</p>
        </div>
    </div>
    <div class="row mt-3 mb-5">
        <div class="col-12">
            <form method="post" id="richiestaLaboratorioForm" data-istituti-url="% url 'operativita:ajax_carica_istituti' %" data-plessi-url="% url 'operativita:ajax_carica_plessi' %">
                % csrf_token %
                 form|crispy 
                <button type='submit' class="btn btn-primary mt-3">Aggiungi</button>
            </form>
        </div>
    </div>
</div>
% endblock content %

% block js-footer %
<!-- Dynamic filtering -->
<script>
    $("#id_provincia").change(function () 
        var url = $("#richiestaLaboratorioForm").attr("data-istituti-url");
        var id_provincia = $(this).val();

        $.ajax(
            url: url,
            data: 
                'provincia': id_provincia
            ,
            success: function (data) 
                $("#id_istituto").html(data);
            
        );
    );

    $("#id_istituto").change(function () 
        var url = $("#richiestaLaboratorioForm").attr("data-plessi-url");
        var id_istituto = $(this).val();

        $.ajax(
            url: url,
            data: 
                'istituto': id_istituto
            ,
            success: function (data) 
                $("#id_plesso").html(data);
            
        );
    );
</script>
% endblock js-footer %

实际结果是这样的:

现在,我应该做的是在模型表单中嵌套 ManyToMany 选择。事实上,每个“Laboratori”都与特定的“AnniScolastici”相关联。我想向用户显示一个“AnniScolastici”条目的复选框,然后显示与之关联的所有“Laboratori”条目。

最终结果应该是这样的:

我正在尝试解决这个问题,但我找不到解决方案...请您帮帮我吗?

感谢您的宝贵时间!

【问题讨论】:

AnniScolastici 之前的复选框应该做什么?检查所有相关实验室? @yagus 该复选框应该检查每个 Anni Scolastici 中的所有 Laboratori。我会放置一个函数,其中每个 Anni Scolastici 的关联实验室仅在选择相关 Anni Scolastici 时才会显示 【参考方案1】:

您在这里过于依赖 Django 抽象,需要逐层剥离。 Django 中的模型 ManyToManyField 创建一个带有两个外键字段的单独表,在您的情况下,laboratori 字段是该表的外键。 M2M 表将有一个外键到 RichiesteLaboratori 和一个到 Laboratori。

M2M 字段默认表单小部件是 MultiChoiceSelectField,基本上你所能做的就是在上述 M2M 表中选择零个或多个记录。这就是为什么您只看到另一个外键 Laboratori 的列表。 Django 没有你想做的小部件。

您需要使用嵌套表单集“手动”执行此操作。请参阅教程here。您还必须自己渲染表单字段,我通常使用widget_tweaks。

【讨论】:

【参考方案2】:

您可以编写自定义表单字段和小部件。不是一个非常便携的解决方案,但可以为您工作。

在字段中添加自定义选项,例如 anni_anni.pklab_lab.pk 以区分呈现小部件和保存表单时的选项。在表单的保存方法中检查可用的选项并保存这些相关对象。



class MyCheckboxSelectMultiple(forms.CheckboxSelectMultiple):

    def render(self, name, value, attrs=None, renderer=None):
        """Render the widget as an HTML string."""
        context = self.get_context(name, value, attrs)
        html = ['<ul>']
        last_lab = None
        for item in context['widget']['optgroups']:
            item = item[1][0]
            # print(item)
            value = item['value']
            if value.startswith('lab_'):
                if value != last_lab:
                    html.append('<ul>')
            html.append(
                f'<li><label>'
                f'<input type="checkbox" name=item["name"] value="value">'
                f' item["label"]'
                f'</label></li>'
            )
            if value.startswith('lab_'):
                if value != last_lab:
                    html.append('</ul>')
                    last_lab = value
        html.append('</ul>')
        return mark_safe(''.join(html))


class MyMultipleChoiceField(forms.MultipleChoiceField):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        choices = []
        for anni in AnniScolastici.objects.all():
            choices.append((f'anni_anni.pk', f'anni'))
            for lab in anni.laboratori_set.all():
                choices.append((f'lab_lab.pk', f'lab'))
        self.choices = choices



class RichiesteLaboratoriModelForm(forms.ModelForm):
    ani_laboratori = MyMultipleChoiceField(
        widget=MyCheckboxSelectMultiple
    )

    class Meta:
        model = RichiesteLaboratori
        fields = (
            'classe',
        )

    def save(self, commit=True):
        obj = forms.ModelForm.save(self, commit)

        ani_laboratori = self.cleaned_data.get('ani_laboratori')
        obj.classi_attivita.clear()
        for anni in AnniScolastici.objects.all():
            if f'anni_anni.pk' in ani_laboratori:
                obj.classi_attivita.add(anni)
        obj.laboratori.clear()
        for lab in Laboratori.objects.all():
            if f'lab_lab.pk' in ani_laboratori:
                obj.laboratori.add(lab)

        return obj

无论如何,我认为那不是最佳选择。我会重写模型,创建代表AnniScolasticiLaboratori 之间关系的onte 模型(您可以将它与ManyToManyFieldthrough 参数一起使用)。然后我会用ManyToManyField 替换字段classi_attivitalaboratori 到这个新模型。通过此模型,您可以轻松访问相关的AnniScolasticiLaboratori,而无需在数据库中保存重复信息。

【讨论】:

以上是关于Django ModelForm - 多对多嵌套选择的主要内容,如果未能解决你的问题,请参考以下文章

使 ModelForm 与 Django 中的中间模型的多对多关系工作的步骤是啥?

具有反向多对多字段的 ModelForm

Django 'QuerySet' 对象没有使用 modelform 的属性'split'

多对多查询的 Django 模型文件更新

Django:ModelForm 使用自定义查询预填充复选框

将 Django 中的多对多关系表示为两个多项选择