简体   繁体   English

Django ModelForm - ManyToMany 嵌套选择

[英]Django ModelForm - ManyToMany nested selection

I'm building a Django application and I'm facing an issue I don't know how to solve... I'll try to explain it as clear as I can.我正在构建一个 Django 应用程序,但我面临一个我不知道如何解决的问题......我会尽量解释清楚。

I've got an app called "Impostazioni" which has a model called "AnniScolastici":我有一个名为“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}"

I also have another app called "Attivita" which has a model called "Laboratori":我还有另一个名为“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}"

I've coded up another model called "RichiesteLaboratori" which is related to different models in my Django app (and on the two above, of course):我编写了另一个名为“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}"

I'm populating entries of this model through a ModelForm and a view.我正在通过 ModelForm 和视图填充此模型的条目。 Here is the form:这是表格:

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')

and here is the view:这是视图:

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

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

The view refers to a template called "richiestalaboratorio_crea.html" which render the form.该视图引用了一个名为“richiestalaboratorio_crea.html”的模板,该模板呈现表单。 Here is the code:这是代码:

{% 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 %}

The actual result looks like this:实际结果如下所示:

里奇斯塔实验室

Now, what I'm supposed to do is to nest the ManyToMany selection in the model form.现在,我应该做的是在模型表单中嵌套多对多选择。 In fact, every "Laboratori" is associated with a specific "AnniScolastici".事实上,每一个“Laboratori”都与一个特定的“AnniScolastici”相关联。 I would like to show the user a checkbox for an "AnniScolastici" entry and after that all the "Laboratori" entries associated with it.我想向用户显示“AnniScolastici”条目的复选框,然后显示与之关联的所有“Laboratori”条目。

The final result is supposed to be something like:最终结果应该是这样的:

最后结果

I'm trying to solve this but I can't find a solution... Can you help me, please?我正在尝试解决这个问题,但我找不到解决方案......你能帮我吗?

Thanks for your time!谢谢你的时间!

You are relying too much on Django abstraction here and will need to peel through the layers.您在这里过分依赖 Django 抽象,需要逐层剥离。 A Model ManyToManyField in Django creates a separate table with two foreign key fields, in your case the laboratori field is a foreign key to that table. Django 中的模型 ManyToManyField 创建一个带有两个外键字段的单独表,在您的情况下,laboratori 字段是该表的外键。 The M2M table will have a foreign key to RichiesteLaboratori and one to Laboratori. M2M 表将有一个指向 RichiesteLaboratori 的外键和一个指向 Laboratori 的外键。

M2M fields default form widget is a MultiChoiceSelectField, basically all you can do is choose zero or more of the records in the above mentioned M2M table. M2M 字段默认表单小部件是一个 MultiChoiceSelectField,基本上您所能做的就是选择上述 M2M 表中的零个或多个记录。 This is why you are only seeing a list of Laboratori, the other foreign key.这就是为什么您只能看到另一个外键 Laboratori 的列表。 Django doesnt have a widget for what you want to do. Django 没有您想要做什么的小部件。

You need to do this "manually" using nested formsets.您需要使用嵌套表单集“手动”执行此操作。 See a tutorial here .请参阅此处的教程。 You will also have to render the form fields for that yourself, I usually use widget_tweaks .您还必须自己渲染表单字段,我通常使用widget_tweaks

You can write a custom form field and widget.您可以编写自定义表单字段和小部件。 Is a not very portable solution, but can work for you.不是很便携的解决方案,但可以为您工作。

In the field add customized choices, like anni_{anni.pk} and lab_{lab.pk} to differentiate the options when rendering the widget and when saving the form.在该字段中添加自定义选项,如anni_{anni.pk}lab_{lab.pk}以区分渲染小部件和保存表单时的选项。 In the form's save method check what options are available and save these related objects.在表单的保存方法中检查哪些选项可用并保存这些相关对象。



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

Anyway, I think thats not the bes option.无论如何,我认为那不是最好的选择。 I would rewrote the models, creating onte model that represents the relation betwen AnniScolastici and Laboratori (you can use it with the through argument of the ManyToManyField ).我会重写模型,创建代表AnniScolasticiLaboratori之间关系的模型(您可以将它与ManyToManyFieldthrough参数一起使用)。 Then I would replace the fields classi_attivita and laboratori with a ManyToManyField to this new model.然后我会将字段classi_attivitalaboratori替换为一个ManyToManyField到这个新模型。 Through this model you can access easy to the related AnniScolastici and Laboratori , without saving duplicated information in the database.通过该模型您可以轻松访问相关的AnniScolasticiLaboratori ,而无需在数据库中保存重复信息。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM