Django ModelFormSet для ответов на упорядоченный список вопросов, которые не должны менять свой порядок в наборе форм после отправки, если ответы даются в случайном порядке

Мне нужно отобразить набор форм Django, содержащий список предопределенных вопросов (несколько десятков вопросов), расположенных в определенном порядке. Пользователь может отвечать на любые вопросы в произвольном порядке, сохранять свои ответы и продолжать отвечать позже. Поэтому здесь нет обязательных полей.

Например, пользователь отвечает на вопросы 2, 5, 6 и 7:

Question Answer
Question 1
Question 2 Answer to question 2
Question 3
Question 4
Question 5 Answer to question 5
Question 6 Answer to question 6
Question 7 Answer to question 7

Затем пользователь отправляет набор форм. Первая проблема, с которой я столкнулся, заключалась в том, что после отправки набора форм вновь добавленные ответы (и их вопросы) перемещались в верхнюю часть набора форм:

Question Answer
Question 2 Answer to question 2
Question 5 Answer to question 5
Question 6 Answer to question 6
Question 7 Answer to question 7
Question 5
Question 6
Question 7

Я использовал следующие (упрощенные) модели:

class Question(models.Model):
    position_nr = models.SmallIntegerField(db_index=True)
    content     = models.CharField(max_length=200)

class Answer(models.Model):
    question        = models.ForeignKey(Question, on_delete=models.PROTECT)
    content         = models.CharField(max_length=100,
                                       blank=True,
                                       null=True,  
                                       ) 

Я использовал ModelFormSet.

В упрощенных версиях моего представления и форм (см. ниже) вопросы оставлены как есть, в виде виджетов select, но в реальном коде они являются просто метками (поэтому пользователь не может изменить вопрос формы).

VIEW:

def manage_answers(request):
    questions_lst = list(Question.objects.all().order_by('category__position_nr', 'position_nr'))
    AnswerFormSet = modelformset_factory(Answer,
                                         form = AnswerForm,
                                         fields ='__all__',
                                         formset = BaseAnswerFormSet,  # extends BaseModelFormSet class
                                         max_num = 7,  # reduced to 7 just to make testing easier
                                         extra = 7,    # to display all 7 forms from the very beginning
                                         )

    if request.method == 'POST':
        formset = AnswerFormSet(request.POST,
                                form_kwargs={'questions': questions_lst, },
                                queryset = Answer.objects.order_by('question__category__position_nr', 'question__position_nr'),
                                )
        if formset.is_valid():
            formset.save()
            return HttpResponseRedirect(reverse("beta:manage_answers"))

    else:
        formset = AnswerFormSet(form_kwargs={'questions': questions_lst,},
                                queryset = Answer.objects.order_by('question__category__position_nr','question__position_nr'),
                                )

    context = {'formset': formset, }

    return render(request, 'beta/manage_answers.html', context)
 

ФОРМЫ:

class BaseAnswerFormSet(BaseModelFormSet):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def get_form_kwargs(self, index):
        kwargs = super().get_form_kwargs(index)
        crt_question = kwargs['questions'][index]
        return {'crt_index': index,
                'crt_question': crt_question,
                }


class AnswerForm(forms.ModelForm):
    # Class attributes:
    ca_instances_lst = []

    def __init__(self, *args,  **kwargs):
        instance = kwargs.get('instance', None)  # if the key 'instance" exists in the kwargs dict
        # instance is an obj of class Answer

        self.crt_question = kwargs.pop('crt_question', None)
        self.crt_index = kwargs.pop('crt_index', None)

        # -------------------------------------
        if instance:
            if self.crt_question.id != instance.question.id:
                AnswerForm.ca_instances_lst.append(kwargs.pop('instance', None))
            else:
                # instance (the answer) belongs to the current question
                pass

        if AnswerForm.ca_instances_lst:
            #  Check if I already have the right answer (for the current question) at the top of the ca_instances_lst list (class attribute)
            if self.crt_question.id == AnswerForm.ca_instances_lst[0].question.id:
                kwargs['instance'] = AnswerForm.ca_instances_lst.pop(0)
                instance = kwargs.get('instance', None)
            else:
                # so ca_instances_lst does not contain the answer for this form (for the current question)
                pass
        # -------------------------------------

        super().__init__(*args, **kwargs)

        self.fields['question'].initial = self.crt_question

    class Meta:
        model = Answer
        fields = '__all__'

Во многих ситуациях мое текущее решение работает просто отлично: все ответы сохраняются и отображаются правильно, например:

Question Answer
Question 1
Question 2 Answer to question 2
Question 3
Вопрос 4 Ответ на вопрос 4
Вопрос 5
Вопрос 6 Ответ на вопрос 6
Вопрос 7

Также в этой ситуации:

Question Answer
Question 1
Question 2 Answer to question 2
Question 3
Question 4
Question 5 Answer to question 5
Question 6 Answer to question 6
Question 7 Answer to question 7

Эти ответы также сохраняются и отображаются правильно:

Question Answer
Question 1
Вопрос 2
Вопрос 3
Вопрос 4 Ответ на вопрос 4
Вопрос 5 Ответ на вопрос 5
Question 6 Answer to question 6
Question 7 Answer to question 7

но после ответа на первый вопрос и попытки отправить набор форм снова, я получаю ошибку из-за дублирования идентификаторов:

[{}, {}, {}, {'all': ['Пожалуйста, исправьте дублирующиеся значения ниже.']}, {}, {}, {}, {}]

Пожалуйста, исправьте дубликаты данных для id.

Question Ответ Их соответствующие идентификаторы в request.POST:
Вопрос 1 Ответ на вопрос 1 id = 729 (должно быть пусто)
Вопрос 2 id = 730 (должно быть пусто)
Вопрос 3 id = 731 (должно быть пусто)
Вопрос 4 Пожалуйста, исправьте дублирующиеся значения ниже. Ответ на вопрос 4 id = 729
Вопрос 5 Ответ на вопрос 5 id = 730
Вопрос 6 Ответ на вопрос 6 id = 731
Question 7 Answer to question 7 id = 732

В request.POST вместо 'form-0-id': [''], (как ожидается для нового объекта ответа, еще не сохраненного в базе данных) у меня есть 'form-0-id': ['729'], где 729 - это id ответа "Ответ на вопрос 4", который Django только что пытался отобразить на первой форме набора форм. То же самое для следующих:

  • form-1-id - 730, где 730 на самом деле является id "Ответ на вопрос 5"
  • form-2-id - 731, где 731 на самом деле является id "Ответ на вопрос 6"

Они также должны были быть пустыми.

Поэтому я отредактировал копию request.POST, где изменил значения form-0-id, form-1-id и form-2-id на пустые. Это также привело к ошибке из-за отсутствующих идентификаторов.

OK, так что же может быть правильным решением, чтобы ModelFormSet вел себя так, как нам нужно? Я искал много и долго (в документации Django, статьях, учебниках, книгах и т.д.), но пока безуспешно.

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