【问题标题】:Django ModelForm - ManyToMany nested selectionDjango ModelForm - 多对多嵌套选择
【发布时间】: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 时才会显示

标签: python python-3.x django django-forms django-templates


【解决方案1】:

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

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

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

【讨论】:

    【解决方案2】:

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

    在字段中添加自定义选项,例如 anni_{anni.pk}lab_{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,而无需在数据库中保存重复信息。

    【讨论】:

      猜你喜欢
      • 2014-05-27
      • 2011-01-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-05-06
      • 1970-01-01
      • 2018-09-10
      相关资源
      最近更新 更多