Вложенные формы
Разрабатываю сайт для системы тестирования (тест с вопросами и вариантами ответов)
Модели следующие:
class FullTest(models.Model):
"""Модель содержащая названия тестов"""
title = models.CharField(max_length=150, default='', verbose_name='Название теста')
class TestQuestion(models.Model):
"""Модель для вопроса относящегося к тесту"""
question_title = models.CharField(max_length=200, verbose_name='Вопрос', blank=False)
test: FullTest = models.ForeignKey(FullTest, on_delete=models.CASCADE, verbose_name='Тест')
class QuestionVariant(models.Model):
"""Модель для варианта ответа относящегося к вопросу"""
variant_title = models.CharField(max_length=50, default='', verbose_name='Вариант ответа')
is_correct = models.BooleanField(default=False, verbose_name='Верно')
question: TestQuestion = models.ForeignKey(TestQuestion, on_delete=models.CASCADE,
verbose_name='Вопрос')
Возникло желание сделать функционал для администратора - страницу "Добавить тест" (чтобы можно было указать название теста и добавить любое количество вопросов и вариантов к ним)
Для этого, чтобы заполнять данные сразу в три БД (создавая общий тест) пришлось воспользоваться inline_formset_factory
. Реализовать добавление одновременно теста и вопросов к нему - у меня получилось. Следующим шагом нужно к каждому вопросу добавить ещё возможность указывать варианты ответов (модель QuestionVariant
), а это ещё один уровень вложенности.
По данному вопросу нашёл следующее руководство Как использовать вложенные формы в Djanjo, но реализовав его обнаружил, что ничего не получилось и вложенная форма (через атрибут form.nested
) всегда выдаёт значение form.nested.is_valid() -> False
несмотря на то, что на странице данные для этой модели были заполнены.
Потратил уже много времени и так и не могу понять, что я сделал не так, что вложенная форма для вариантов ответа не проходит валидность. При этом если убрать проверку вложенной формы, то данные из неё всё равно не сохраняются, а сохраняется только название теста и его вопросы.
Код прилагаю ниже:
forms.py
class BaseQuestionFormset(BaseInlineFormSet):
def add_fields(self, form, index):
super().add_fields(form, index)
form.nested = VariantFormset(
instance=form.instance,
data=form.data if form.is_bound else None,
files=form.files if form.is_bound else None,
prefix='variants')
def is_valid(self):
result = super().is_valid()
if self.is_bound:
for form in self.forms:
if hasattr(form, 'nested'):
result = result and form.nested.is_valid()
return result
def save(self, commit=True):
result = super().save(commit=commit)
for form in self.forms:
if hasattr(form, 'nested'):
if not self._should_delete_form(form):
form.nested.save(commit=commit)
return result
QuestionFormset_inline = inlineformset_factory(FullTest, TestQuestion, formset=BaseQuestionFormset, form=QuestionForm, can_delete=False, can_delete_extra=False)
VariantFormset = inlineformset_factory(TestQuestion, QuestionVariant, form=VariantForm,
can_delete=False, can_delete_extra=False)
views.py
class TestInline:
form_class = TestForm
model = FullTest
template_name = "testing_module/add_test_or_update.html"
def form_valid(self, form):
named_formsets = self.get_named_formsets()
if not all((x.is_valid() for x in named_formsets.values())):
return self.render_to_response(self.get_context_data(form=form))
self.object = form.save()
for name, formset in named_formsets.items():
formset_save_func = getattr(self, 'formset_{0}_valid'.format(name), None)
if formset_save_func is not None:
formset_save_func(formset)
else:
formset.save()
return redirect('test_view')
def formset_questions_valid(self, formset):
questions = formset.save(commit=False)
for obj in formset.deleted_objects:
obj.delete()
for question in questions:
question.test = self.object
question.save()
class TestUpdate(TestInline, UpdateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['question_formset'] = self.get_named_formsets()
return context
def get_named_formsets(self):
return {
'questions': QuestionFormset_update(self.request.POST or None, self.request.FILES or None,
instance=self.object, prefix='questions')
}
class TestCreate(TestInline, CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['question_formset'] = self.get_named_formsets()
return context
def get_named_formsets(self):
if self.request.method == "GET":
return {
'questions': QuestionFormset_inline(prefix='questions')
}
else:
return {
'questions': QuestionFormset_inline(self.request.POST or None, self.request.FILES or None, prefix='questions')
}
html-template
<form enctype="multipart/form-data" class="container" method="post" id="test_form">
{% csrf_token %}
<div class="card">
<div class="card-header card-header-secondary">
<h4 class="card-title">Добавление теста</h4>
</div>
{% for field in form %}
<div class="form-group card-body">
<label>{{field.label}}</label>
{% if field.field.required %}
<span style="color: red;" class="required">*</span>
{% endif %}
{{field}}
{% for error in field.errors %}
<p style="color: red">{{ error }}</p>
{% endfor %}
</div>
{% endfor %}
</div>
{% with question_formset.questions as formset %}
{{ formset.management_form }}
<script type="text/html" id="questions-template">
<tr id="questions-__prefix__" class= hide_all>
{% for fields in formset.empty_form.hidden_fields %}
{{ fields }}
{% endfor %}
{% for fields in formset.empty_form.visible_fields %}
<td>{{fields}}</td>
{% endfor %}
</tr>
</script>
<div class="table-responsive card mt-4">
<div class="card-header card-header-secondary">
<h4 class="card-title">Вопросы</h4>
</div>
<table class="table card-header">
<thead class="text-secondary">
<th>Содержание вопроса <span style="color: red;" class="required">*</span></th>
<th>Удалить</th>
</thead>
<tbody id="item-questions">
{% for error in formset.non_form_errors %}
<span style="color: red">{{ error }}</span>
{% endfor %}
{% for formss in formset %}
{{ formss.management_form }}
<tr id="questions-{{ forloop.counter0 }}" class= hide_all>
{{ formss.id }}
{% for field in formss.visible_fields %}
<td>
{{field}}
{% for error in field.errors %}
<span style="color: red">{{ error }}</span>
{% endfor %}
</td>
{% endfor %}
{% for nested_form in formss.nested.forms %}
{{nested_form}}
{% endfor %}
{% endwith %}
<div class="form-group">
<button type="submit" class="btn btn-secondary btn-block">Добавить тест</button>
</div>
</form>