4

I am trying to use a formset to create forms for a set of timeframes related to dates:

class Event(models.Model):
   date = models.DateField()

class TimeFrame(models.Model):
   start = models.DateTimeField()
   end = models.DateTimeField()
   event = models.ForeignKey('Event')

I have code that gets me a queryset of timeframes for each event and added a kwarg to pass this into my form:

class SelectDatesForm(forms.Form):
    timeframes = forms.ModelChoiceField(queryset=HostTimeFrame.objects.none())

    def __init__(self, *args, **kwargs):
       qs = kwargs.pop('timeframes')
       super(SelectDatesForm, self).__init__(*args, **kwargs)
       self.fields['timeframes'].queryset = qs

Now I'm trying to construct a formset that lets me show timeframes for multiple events on one page. I already found this question, explaining how to pass initial data, for serveral forms, but its not the same as passing it to a queryset.

Also there is this new function from django 1.9 but it doesnt allow me to get different querysets for each form.

UPDATE: I got the solution from the answer working, however, whenever im running formset.is_valid() i get the error:

Select a valid choice. That choice is not one of the available choices.

Here is what I do in my view:

timeframes = [HostTimeFrame.objects.all()]
SelectDatesFormset = formset_factory(form=SelectDatesForm, extra=len(timeframes), formset=BaseSelectDatesFormSet)
if request.method == 'POST':
    formset = SelectDatesFormset(request.POST, form_kwargs={'timeframes_list': timeframes})
    if formset.is_valid():
        # do something with the formset.cleaned_data
        print(formset)
        pass
else:
    formset = SelectDatesFormset(form_kwargs={'timeframes_list': timeframes})

Ive been trying for hours to find where this actual validation is done, but i cant find it for the live of me.

Edit: I tested this with the singular form, and i have the same issue, I created a new question for this here.

Community
  • 1
  • 1
Julian
  • 915
  • 8
  • 24

2 Answers2

6

UPDATE: Only partial solution, see question.

Solved it myself:

First I created a BaseFormSet:

class BaseSelectDatesFormSet(BaseFormSet):
    def get_form_kwargs(self, index):
        kwargs = super(BaseSelectDatesFormSet, self).get_form_kwargs(index)
        kwargs['timeframes'] = kwargs['timeframes_list'][index]
        return kwargs

Then I could pass the list of timeframes in the view:

 SelectDatesFormset = formset_factory(form=SelectDatesForm, extra=4, formset=BaseSelectDatesFormSet)
 formset = SelectDatesFormset(form_kwargs={'timeframes_list': timeframes})

Finally I had to update my form init to pop the list as well so the super constructor doesnt complain about unwanted kwargs:

def __init__(self, *args, **kwargs):
    qs = kwargs.pop('timeframes')
    qs_list = kwargs.pop('timeframes_list')
    super(SelectDatesForm, self).__init__(*args, **kwargs)
    self.fields['timeframes'].queryset = qs.order_by('start')
Julian
  • 915
  • 8
  • 24
  • in `get_form_kwargs` method, you can pop `timeframes_list` to self if it is in kwargs. This will avoid manipulating form's `init` method. – Maged Saeed Aug 24 '22 at 22:33
0

For peeps using Class Based View FormView along with form_class as formset or formset_factory, they can add an extra attribute as follows:

Pass form_kwargs in the get_form method by overriding it.

timeframes = [HostTimeFrame.objects.all()]

class SelectDatesView(FormView):
    form_class = formset_factory(form=SelectDatesForm, extra=len(timeframes)

    def get_form(self, form_class=None):
        """Override the method to add form kwargs. Returns an instance of the form to be used in this view."""
        if form_class is None:
            form_class = self.get_form_class()
        return form_class(**self.get_form_kwargs(), form_kwargs={"timeframes": timeframes})

One can access it directly in the __init__ method's kwargs.

def __init__(self, *args, **kwargs):
    super(SelectDatesForm, self).__init__(*args, **kwargs)
    qs = kwargs.get('timeframes')
    self.fields['timeframes'].queryset = qs.order_by('start')