Вложенные формы

Разрабатываю сайт для системы тестирования (тест с вопросами и вариантами ответов)

Модели следующие:

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>

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