Formset forms rendering all fields in all forms with the same ID

Hi everyone, I’m experiencing some pretty strange behaviour when trying to render formsets.

I’ve got a bit of Django experience but I always feel like I’m hacking at Formsets. In this case though the code looks alright to me. I’ve included it below and a screenshot that shows the incorrect output.

What is happening, as in the title, each field in the different forms all have the same IDs. If anyone has any insight here I could use an assist. I got someone on a Django Discord to eyeball the code and they agreed it looks okay so I’m kind of stumped.

As an aside, rendering the form as a table or a paragraph doesn’t work either.

I’m using Django 4.1. I thought I’d check here in case I’m doing something exceedingly obvious before I start digging through the form renderer code to see if anything jumps out at me.

Here’s the rendered HTML:

Template:

{% extends "base.html" %}
{% load static %}

{% block body %}
<form method="post">
    {% csrf_token %}
    {{ form.management_form }}
    <p>Please select each player that {{ rule }}</p>
    {% for subform in form %}
        {{ subform.as_table }}
    {% endfor %}
    <input type="submit" value="Save">
</form>
{% endblock %}

Relevant View Code:

class PlayerPointsView(FormView):
    template_name = 'add_points.html'
    success_url = '/success/'
    form_class = PlayerPointsFormSet

    def get_context_data(self, **kwargs):
        context = super(PlayerPointsView, self).get_context_data(**kwargs)
        league = League.objects.get(name__icontains=self.kwargs['league_name'])
        context['rule'] = LeagueRule.objects.get(
            number=self.kwargs['rule_number'],
            league=league
        )
        context['players'] = Player.objects.filter(league=league)
        print(context)
        return context

    def get_form(self):
        league = League.objects.get(name__icontains=self.kwargs['league_name'])
        rule = LeagueRule.objects.get(league=league,
                                      number=self.kwargs['rule_number'])
        round = Round.objects.get(league=league,
                                  number=self.kwargs['round_number'])
        kwargs = self.get_form_kwargs()
        if self.request.POST:
            formset = self.form_class(data=self.request.POST,
                                      rule=rule, form_kwargs=kwargs)
        else:
            formset =  self.form_class(rule=rule, form_kwargs=kwargs)
            for form in formset:
                if not form.instance.id:
                    form.fields['round'].initial = round
                    form.fields['rule'].initial = rule
                    form.fields['points'].initial = 0
        return formset

    def get_form_kwargs(self):
        league = League.objects.get(name__icontains=self.kwargs['league_name'])
        round = Round.objects.filter(number=self.kwargs['round_number'],
                                  league=league)
        rule = LeagueRule.objects.get(league=league,
                                      number=self.kwargs['rule_number'])
        kwargs = super(PlayerPointsView, self).get_form_kwargs()
        kwargs.update({'rule': rule,
                       'league': league,
                       'round': round})
        return kwargs

Forms

class PlayerPointsForm(ModelForm):
    class Meta:
        model = PlayerPoints
        fields = ['id', 'player', 'round', 'rule', 'points']
        widgets = {
            'round': HiddenInput(),
            'rule': HiddenInput(),
            'points': HiddenInput(),
            'id': HiddenInput()
        }

    def __init__(self, *args, **kwargs):
        league = kwargs.pop('league')
        round = kwargs.pop('round')
        rule = kwargs.pop('rule')
        players = Player.objects.filter(league=league)
        super(PlayerPointsForm, self).__init__(*args, **kwargs)
        self.fields['rule'].label = rule.title
        self.fields['round'].queryset = round
        self.fields['player'].queryset = players

    def clean(self):
        cleaned_data = super(PlayerPointsForm, self).clean()
        round = cleaned_data.get('round')
        player = cleaned_data.get('player')
        rule = cleaned_data.get('rule')
        if round and player and rule:
            position = PlayerRoundPosition.objects.get(
                player=player, round=round).position
            if position == "Keeper":
                cleaned_data['points'] = rule.keeper_points
            elif position == "Defender":
                cleaned_data['points'] = rule.defender_points
            elif position == "Midfield":
                cleaned_data['points'] = rule.midfield_points
            elif position == "Forward":
                cleaned_data['points'] = rule.forward_points
        return cleaned_data


class BasePlayerPointsFormSet(BaseModelFormSet):
    def __init__(self, *args, **kwargs):
        rule = kwargs.pop('rule')
        super(BasePlayerPointsFormSet, self).__init__(*args, **kwargs)
        self.queryset = PlayerPoints.objects.filter(rule=rule)
        for form in self.forms:
            form.fields['player'].widget.attrs.update(
                {'class': 'form-control'})
            form.empty_permitted = True

PlayerPointsFormSet = modelformset_factory(
    PlayerPoints,
    fields=['player', 'round', 'rule', 'points'],
    formset=BasePlayerPointsFormSet,
    form=PlayerPointsForm,
    extra=1
)

Cheers

A formset is not a form, it is a container for a list of forms. It does not work exactly like a form, although many function names are the same or similar.

It’s not a form that you can use as the form class of a CBV based on FormView. In fact, I generally recommend that when you’re using formsets, if you want to use a generic CBV as the base class, you use nothing more detailed than View - and perform all the handling of the formset yourself. (Or, if you’re going to have multiple pages working with multiple different formsets, you create your own CBV base classes for working with formsets, which is what we did.)

Thanks, I appreciate the insight. I’ll give that a go.