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.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
之间关系的onte 模型(您可以将它与ManyToManyField
的through
参数一起使用)。然后我会用ManyToManyField
替换字段classi_attivita
和laboratori
到这个新模型。通过此模型,您可以轻松访问相关的AnniScolastici
和Laboratori
,而无需在数据库中保存重复信息。
【讨论】:
以上是关于Django ModelForm - 多对多嵌套选择的主要内容,如果未能解决你的问题,请参考以下文章
使 ModelForm 与 Django 中的中间模型的多对多关系工作的步骤是啥?
Django 'QuerySet' 对象没有使用 modelform 的属性'split'