Виджет Django Form 'readonly' делает недействительным 'initial'

Вкратце: у меня есть приложение, в котором пользователь вводит тренировки. Прежде чем добавить упражнения в тренировку, пользователь должен заполнить анкету "готовности", состоящую из 5-10 вопросов, оценивая каждый из них как низкий-высокий.

Модели: Модель с набором вопросов (ReadinessQuestion), и модель, хранящая readiness_question, рейтинг и тренировку, к которой он принадлежит (WorkoutReadiness).

Форма: Я создал ModelForm в formset_factory. Когда я устанавливаю виджету поля формы 'readiness_question' значение 'readonly', это делает мою форму недействительной и игнорирует мои 'начальные' данные

Представление: Я хочу создать представление, в котором 5-10 вопросов из ReadinessQuestion находятся в списке, с выпадающими списками для ответов.

  1. I have set initial data for my form (each different question)
  2. the user selects the 'rating' they want for each readiness_question
  3. a new Workout is instantiated
  4. the WorkoutReadiness is saved, with ForeignKey to the new Workout

Как выглядит моя страница сейчас (почти так, как я хочу, чтобы она выглядела): https://imgur.com/a/HD1l3oe

Ошибка при отправке формы:

Cannot assign "'Sleep'": "WorkoutReadiness.readiness_question" must be a "ReadinessQuestion" instance.

Документация Django здесь очень плохая, widget=forms.TextInput(attrs={'readonly':'readonly'}) нет даже на официальном сайте Django. Если я не сделаю вопрос readiness_question 'readonly', у пользователя будут выпадающие поля, которые он может изменить.

#models.py
class Workout(models.Model):
    user         = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
    date         = models.DateField(auto_now_add=True, null=True)
    date_created = models.DateTimeField(auto_now_add=True, null=True)
    def __str__(self):
        return f'{self.date}'


class ReadinessQuestion(models.Model):
    name = models.CharField(max_length=40, unique=True)
    def __str__(self):
        return f'{self.name}'


class WorkoutReadiness(models.Model):
    class Rating(models.IntegerChoices):
        LOWEST  = 1
        LOWER   = 2
        MEDIUM  = 3
        HIGHER  = 4
        HIGHEST = 5

    workout            = models.ForeignKey(Workout, on_delete=models.CASCADE, null=True, blank=True)
    readiness_question = models.ForeignKey(ReadinessQuestion, on_delete=models.CASCADE, null=True)
    rating             = models.IntegerField(choices=Rating.choices)
#forms.py
class WorkoutReadinessForm(forms.ModelForm):
    class Meta:
        model  = WorkoutReadiness
        fields = ['readiness_question', 'rating',]
    
    readiness_question = forms.CharField(
        widget=forms.TextInput(attrs={'readonly':'readonly'})
    )

WRFormSet = forms.formset_factory(WorkoutReadinessForm, extra=0)
#views.py
def create_workoutreadiness(request):
    questions = ReadinessQuestion.objects.all()
    
    initial_data = [{'readiness_question':q} for q in questions]
    
    if request.method == 'POST':
        formset = WRFormSet(request.POST)
        workout = Workout.objects.create(user=request.user)
        if formset.is_valid():
            for form in formset:
                cd = form.cleaned_data
                readiness_question = cd.get('readiness_question')
                rating = cd.get('rating')
                w_readiness = WorkoutReadiness(
                    workout=workout,
                    readiness_question=readiness_question,
                    rating=rating
                )
                w_readiness.save()

        return HttpResponseRedirect(reverse_lazy('routines:workout_exercise_list', kwargs={'pk':workout.id}))
    else:
        formset = WRFormSet(initial=initial_data)
    
                
    context = {
        'questions':questions,
        'formset':formset,
    }
    return render(request, 'routines/workout_readiness/_form.html', context)

Мне удалось решить эту проблему (используя альтернативу №1). Я не уверен, что это лучший способ, но пока он работает:

  1. #models.py - Добавили blank=True к readiness_question в WorkoutReadiness модели
  2. .
  3. #forms.py - Удален виджет с attrs={'readonly':'readonly'} - я не думаю, что Django поддерживает это больше, он определенно не поощряет это
  4. .
class WorkoutReadinessForm(forms.ModelForm):
    class Meta:
        model  = WorkoutReadiness
        fields = ['readiness_question','rating']

WRFormSet = forms.formset_factory(WorkoutReadinessForm, extra=0)
  1. #views.py - Помимо того, что набор форм был заранее заполнен initial_data, я убедился, что нужно добавить его обратно в каждую форму по отдельности после валидации. Поле form.cleaned_data.readiness_question является None, пока я не заменю его на каждое поле в initial_data
  2. .
#CBV
class WorkoutReadinessCreateView(CreateView):
    model         = WorkoutReadiness
    fields        = ['readiness_question','rating']
    template_name = 'routines/workout_readiness/_form.html'
    initial_data = [{'readiness_question':q} for q in ReadinessQuestion.objects.all()]

    def post(self, request, *args, **kwargs):
        formset = WRFormSet(request.POST)
        if formset.is_valid():            
            return self.form_valid(formset)
        return super().post(request, *args, **kwargs)
    
    def form_valid(self, formset):
        workout = Workout.objects.create(user=self.request.user)
        for idx, form in enumerate(formset):
            cd = form.cleaned_data
            readiness_question = self.initial_data[idx]['readiness_question']
            rating = cd.get('rating')
            w_readiness = WorkoutReadiness(
                workout=workout,
                readiness_question=readiness_question,
                rating=rating
            )
            w_readiness.save()
        return HttpResponseRedirect(
            reverse_lazy(
                'routines:workout_exercise_list', 
                kwargs={'pk':workout.id})
            )

    def get_context_data(self, **kwargs):
        """Render initial form"""
        context = super().get_context_data(**kwargs)
        context['formset'] = WRFormSet(
            initial =self.initial_data
        )
        return context

#FBV
def create_workoutreadiness(request):
    questions = ReadinessQuestion.objects.all()
    initial_data = [{'readiness_question':q} for q in questions]
    
    if request.method == 'POST':
        formset = WRFormSet(request.POST, initial=initial_data)
        workout = Workout.objects.create(user=request.user)
        if formset.is_valid():
            for idx, form in enumerate(formset):
                cd = form.cleaned_data
                readiness_question = initial_data[idx]['readiness_question']
                rating = cd.get('rating')
                w_readiness = WorkoutReadiness(
                    workout=workout,
                    readiness_question=readiness_question,
                    rating=rating
                )
                w_readiness.save()

        return HttpResponseRedirect(reverse_lazy('routines:workout_exercise_list', kwargs={'pk':workout.id}))
    else:
        formset = WRFormSet(initial=initial_data)
          
    context = {
        'questions':questions,
        'formset':formset,
    }
    return render(request, 'routines/workout_readiness/_form.html', context)
  1. #html/template - я отображаю form.initial.readiness_question, что означает, что пользователь не может редактировать его в выпадающем списке
  2. .
<div class="form-group my-5">
  <form action="" method="post">
    <table class="mb-3">
      {% csrf_token %}
      {{ formset.management_form }}
      {% for form in formset %}
        <tr>
          <td>{{ form.initial.readiness_question }}</td> 
          <td>{{ form.rating }}</td> 
        </tr>
      {% endfor %}
    </table>
    <button type="submit" class="btn btn-outline-primary">Submit</button>
  </form>
</div>
  1. Как это выглядит - требует приведения в порядок, но функциональность работает
Вернуться на верх