2
votes

I have code snippets below, but the summary of my issue is this:

When displaying a formset with a known number of extra forms, each needing initialization with data from another object, the form's __init__() function is called for each form in the formset, and then one extra time. This causes an error, because the in the last call to __init__(), kwargs does not contain the expected item used for initialization.

My friends and I play a spreadsheet-based sports picking game which is very tedious to make changes to. I've wanted to learn Django for a while so I've been working on creating it as a webapp. Here's the relevant model for my issue:

class Pick(models.Model):
  sheet = models.ForeignKey(Sheet)
  game = models.ForeignKey(Game)
  HOME = 'H'
  AWAY = 'A'
  PICK_TEAM_CHOICES = (
    (HOME, 'Home'),
    (AWAY, 'Away'),
  )
  pick_team = models.CharField(max_length=4,
                                  choices=PICK_TEAM_CHOICES,
                                  default=HOME)
... other stuff

And I've defined a form related to this model. The custom __init__() is so the form is initialized with information from a related Game object, passed with the 'initial' parameter in form creation:

class PickForm(ModelForm):
  class Meta:
    model = Pick
    widgets = {'game': forms.HiddenInput()}
    fields = ['sheet','game','amount','pick_type','pick_team']

  def __init__(self, *args, **kwargs):
    game = kwargs['initial']['game']
    super(PickForm, self).__init__(*args, **kwargs)
    self.fields['pick_team'].choices = ( ('H', game.home_team), ('A', game.away_team), )

I recently created the 'atomic' case where a user can pick a game via a PickForm in the related template, and that form is processed in the post() method of an adapted class based view. I'm trying to extend this case to handle multiple forms by creating a formset of PickForms:

class GameList(ListView):
  template_name = 'app/games.html'
  context_object_name = 'game_list'

  def get_queryset(self):
    games = get_list_or_404(Game, week = self.kwargs['week'])
    return games

  def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    #create a formset of PickForms
    PickFormSet = formset_factory(PickForm, extra = len(context['game_list'])-1)
    pick_list = [] 
    sheet = Sheet.objects.get(user=self.request.user,league_week=self.kwargs['week'])
    picks = Pick.objects.filter(sheet = sheet)
    for index, game in enumerate(context['game_list'],start=0):
       #logic to create a list of objects for initial data

    #create the formset with the pick dictionary
    context['pickforms'] = PickFormSet(initial=[{'game':pick.game,
                                      'sheet':pick.sheet,
                                      'amount':pick.amount,
                                      'pick_type':pick.pick_type,
                                      'pick_team':pick.pick_team,} for pick in pick_list])

      return context

The get_context_data() in the view contructs the pick_list properly and initializes the PickFormSet- my issue occurs in the template. I'm letting Django handle the rendering so it's very simple right now:

<form action="{% url 'game_list' week %}" method="post">
    {{ pickforms }}
    <input type="submit" name="pickformset" value="Submit" />
</form>

It seems Django actually initializes the PickForms while rendering the template, because the problem occurs in the __init__() of my PickForm. When debugging, i can step through as it initializes a PickForm for each form in the formset- there are a total of 6 right now. So for 'form-0' (autogenerated prefix, I think) through 'form-5', the initialization works properly, because the kwargs dictionary contains an 'initial', as expected.

However, after initializing those 6 forms, it loops through the __init__() again, for a form with prefix 'form-6' (so a 7th form). This form has no initial data associated with it, and therefore errors out in the __init__() due to a KeyError.

Why is Django attempting to create another form? I have extra = 5 specified in the formset_factory call, so there should only be 6 forms total, each having a related initial data dictionary.

I thought it may be related to the included management_form of the formset, however explicitly rendering that, then using a for loop to iterate over the PickForms didn't work either- I ran into the same issue where the template engine is trying to initialize an extra form without any initial data.

Also: I tried using modelformset_factory and specifying PickForm, however in that case the PickForms seem to be initialized differently. There's no 'initial' data in kwargs, but an 'instance' instead, which behaves differently. I'm still new to Django so I'm confused as to why the two methods would pass different kwargs to the PickForm __init__()

1

1 Answers

0
votes

Alright, after mulling this over all day, I decided to add a try-except block in my __init__() to just catch the KeyError thrown when kwargs doesn't have initial data.

def __init__(self, *args, **kwargs):
    try:
        game = kwargs['initial']['game']
        super(PickForm, self).__init__(*args, **kwargs)
        self.fields['pick_team'].choices = ( ('H', game.home_team),('A', game.away_team), )
    except KeyError:
        super(PickForm, self).__init__(*args, **kwargs)

Adding that enabled rendering of the formset, and I realized something that (to me) isn't obvious from the docs:

The number of extra forms specified in the formset are created in addition to any forms defined by initial data. My interpretation of the docs was that I'd need enough extra forms to cover however many forms I wanted to initialize with desired data. In hindsight, that sort of bookeeping could be annoying- it's nice that your initial list of dictionaries can vary in length without having to worry about specifying the correct extra.

So, the formset initializes a form for every dictionary in initial list of dictionaries, then creates extra number of blank forms.

I feel rather dumb, but at the same time I don't think this specific case is all that clear in the documentation.

EDIT: On closer reading, there is some text that clearly says extra forms are created in addition to the number of forms generated by initial data. Conclusion: I need to learn to read