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", который выводит форму. Вот код:
Фактический результат выглядит следующим образом:
Теперь, что я должен сделать, это вложить выбор ManyToMany в форму модели. Фактически, каждый "Laboratori" связан с определенным "AnniScolastici". Я хочу показать пользователю флажок для записи "AnniScolastici", а затем все записи "Laboratori", связанные с ней.
Конечный результат должен быть примерно таким:
Я пытаюсь решить эту проблему, но не могу найти решение... Помогите мне, пожалуйста?
Спасибо за ваше время!
Вы слишком полагаетесь на абстракцию 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
, не сохраняя дублирующуюся информацию в базе данных.