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, статьях, учебниках, книгах и т.д.), но пока безуспешно.