Django ModelForm - вложенный выбор ManyToMany

Я создаю приложение 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", который выводит форму. Вот код:

Фактический результат выглядит следующим образом:

RichiestaLaboratorio

Теперь, что я должен сделать, это вложить выбор ManyToMany в форму модели. Фактически, каждый "Laboratori" связан с определенным "AnniScolastici". Я хочу показать пользователю флажок для записи "AnniScolastici", а затем все записи "Laboratori", связанные с ней.

Конечный результат должен быть примерно таким:

Final result

Я пытаюсь решить эту проблему, но не могу найти решение... Помогите мне, пожалуйста?

Спасибо за ваше время!

Вы слишком полагаетесь на абстракцию Django, и вам нужно просмотреть все слои. Модель ManyToManyField в Django создает отдельную таблицу с двумя полями внешнего ключа, в вашем случае поле laboratori является внешним ключом к этой таблице. Таблица M2M будет иметь внешний ключ к RichiesteLaboratori и один к Laboratori.

Виджет формы по умолчанию для полей M2M - это MultiChoiceSelectField, в основном все, что вы можете сделать, это выбрать ноль или более записей в вышеупомянутой таблице M2M. Вот почему вы видите только список Laboratori, другой внешний ключ. В Django нет виджета для того, что вы хотите сделать.

Вам нужно сделать это "вручную", используя вложенные наборы форм. Смотрите учебник здесь. Вам также придется самостоятельно отрисовывать поля формы, я обычно использую widget_tweaks.

Вы можете написать пользовательское поле формы и виджет. Это не очень портативное решение, но может сработать для вас.

В поле добавьте настраиваемые варианты, такие как 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

В любом случае, я думаю, что это не лучший вариант. Я бы переписал модели, создав одну модель, представляющую связь между AnniScolastici и Laboratori (ее можно использовать с аргументом through в ManyToManyField). Затем я бы заменил поля classi_attivita и laboratori на ManyToManyField на эту новую модель. Через эту модель можно легко получить доступ к связанным AnniScolastici и Laboratori, не сохраняя дублирующуюся информацию в базе данных.

Вернуться на верх