Creating a grouped formset, based on existing data

I want to create a grouped formset, based on data within an existing model.

I have the below models, which are pre-populated with data. Where there are several technique ‘categories’ I want to group my formsets by in TechniqueCategory, with associated ‘subcategories’ part of each group in TechniqueSubCategory.

Note that I also want to use the title field entries in TechniqueSubCategory as labels for each form.


class TechniqueCategory(models.Model):
	title = models.CharField(max_length=120)

    class Meta:
        verbose_name = "Technique Category"
        verbose_name_plural = "Technique Categories"
        ordering = ('title',)


class TechniqueSubCategory(models.Model):
	title = models.CharField(max_length=120)

    class Meta:
        verbose_name = "Technique Sub Category"
        verbose_name_plural = "Technique Sub Categories"
        ordering = ('title',)


class Technique(models.Model):
    technique_category = models.ForeignKey(
        TechniqueCategory,
        related_name='technique',
        verbose_name=_('Technique Category'),
        on_delete=models.CASCADE
    )

    technique_sub_category = models.ForeignKey(
        TechniqueSubCategory,
        related_name='technique',
        verbose_name=_('Technique Sub Category'),
        on_delete=models.CASCADE
    )

    class Meta:
        verbose_name = "Technique"
        verbose_name_plural = "Techniques"

I then have the following model, which is what I want to iteratively create my formsets from. I want to allow the user to set a ‘rating’ for each technique sub category. Note that the profile field is associated to each user.

class TechniqueRating(models.Model):
    profile = models.ForeignKey(
        Profile,
        related_name='technique_rating',
        verbose_name=_('Profile'),
        on_delete=models.CASCADE
    )

    technique_sub_category = models.ForeignKey(
        TechniqueSubCategory,
        related_name='technique_rating',
        verbose_name=_('Technique Sub Category'),
        on_delete=models.CASCADE
    )

    rating = models.PositiveSmallIntegerField(_('Rating'), default=0)

    class Meta:
        verbose_name = "Technique Rating"
        verbose_name_plural = "Technique Rating"

How would I go about doing this? At the moment, I have the following in get_context_data of my formview:

technique_sub_category_qs = TechniqueSubCategory.objects.all()
technique_rating_qs = TechniqueRating.objects.filter(profile=user_profile_instance).order_by('technique_sub_category__title').values()

technique_rating_formset_class = modelformset_factory(
	TechniqueRating,
	fields=('technique_sub_category', 'rating'),
	extra=len(technique_sub_category_qs),
	max_num=len(technique_sub_category_qs),
	formset=TechniqueRatingFormSet
)

ctx['technique_formset'] = technique_rating_formset_class(
	self.request.POST or None,
	prefix='technique',
	initial=technique_rating_qs
)

Where these are my form classes:

class TechniqueRatingFormSet(BaseFormSet):

    def __init__(self, *args, **kwargs):
        super(TechniqueRatingFormSet, self).__init__(*args, **kwargs)

        technique_sub_category_qs = TechniqueSubCategory.objects.all().order_by('title').values_list('title', flat=True)
        for i in range(0, len(self)):
            self[i].fields['technique_sub_category'].label = technique_sub_category_qs[i]


class TechniqueRatingModelForm(forms.ModelForm):
    class Meta:
        model = TechniqueRating
        fields = ('technique_sub_category', 'rating',)

And in my view post, I’m doing this:

technique_formset = technique_formset_class(request.POST, prefix='technique')

if technique_formset.is_valid():
	ls_technique_cleaned_data = []

	for count, technique_form in enumerate(technique_formset):
		ls_technique_cleaned_data.append(
			TechniqueRating(
				profile=user_profile_instance,
				**technique_form.cleaned_data
			)
		)

	TechniqueRating.objects.filter(profile=user_profile_instance).delete()
	TechniqueRating.objects.bulk_create(ls_technique_cleaned_data)

I’m not quite sure I’m following what you mean by this.

If you’re saying that you want multiple formsets for TechniqueSubCategory, where each formset is a set of TechniqueSubCategory that are related to an individual TechniqueCategory, and that you want multiple TechniqueCategory on a page, then what you’re looking for is known as a “nested formset”.

Or, more graphically, I’m understanding you to be looking for something like this:

Technique Category 1
        Technique field 1
        Technique field 2
    Sub Category 1
    Sub Category 2
    Sub Category 3

Technique Category 2
        Technique field 1
        Technique field 2
    Sub Category 4
    Sub Category 5
    Sub Category 6

If I’ve got the right impression of your situation, then I suggest you take a look at Multiples of one formset - #12 by KenWhitesell. You can also search here for other references to nested formsets, there have been a few discussions raise on this topic.

(On the other hand, if I’m completely missing what you’re trying to do here, some further details or examples of what you’re trying to achieve may help.)

That is correct.

But struggling to see why I need to create a formset for both category as well as sub_category (nested). I thought I’d only need to create formsets for sub_categories as that is all user only needs to submit ratings for.

So the user should see something like:

technique_category_1 label

technique_sub_category_1 label + rating input field
technique_sub_category_2 label + rating input field
technique_sub_category_3 label + rating input field

technique_category_2 label

technique_sub_category_4 label + rating input field
technique_sub_category_5 label + rating input field

I guess another way of saying it, is that I’m only trying to use the category - sub_category relationship to arrange the sub_category formset fields more logically.

I’ll do another search around nested formsets and see if I can uncover anything outside of your referenced post.

You may not need to - that’s why I was asking.

If you’re not editing any fields related to Technique, then no, no formset at that level would be necessary - you would just be creating multiple formsets for the sub category groups.

In that case, you’re not nesting formsets, you only need multiple independent formsets on a page. If that works for you, then all you really need to be concerned about is ensuring that each formset is created with a different prefix to ensure Django can tell them apart.

And even that may not be necessary in this case. You could create a single formset for all subcategories, and just break them up as you’re rendering it. (My gut tells me that that actually might be more work than the multiple formsets, but that’s only a guess.)

Yes, that sounds right.

I considered your former option of creating multiple independent formsets based on category. But that actually felt like a more difficult path than playing around with the rendering class. But I’m open to exploring both.

I’m also not sure if my code above is generally correct… Particularly w.r.t ordering and how I’m associating the sub_category labels to each field. Any advice there would be appreciated to.

Just to make sure I’m understanding the relationships - what you have effectively defined here is a Many-to-Many relationship between TechniqueCategory and TechniqueSubCategory with Technique as the “through” table. Is that correct?

If so, then you have a situation where you could have the same subcategory showing up in two different places - one for each of two different categories. Is that the intent?

I see what you’re saying. That’s possible. But the way I’m pushing data into the models wouldn’t allow that. And so, the fields for SubCategory should be unique even over all Categories.

I also have unique requirements on the title fields for Category and SubCategory, through a separate slug FYI. Which has been omitted from the above code.

If it makes this easier, I could just drop the Technique model, and bring TechniqueCategory as a foreign key rel into TechniqueSubCategory model.

That’s not a question that I’m in a position to answer. I don’t know what your data is, what the actual relationships are among your data, and what your requirements are.

I can say that if there is actually a One-to-many relationship between SubCategory and Category, then yes, I would recommend changing the relationship definition in your model to reflect that. It’s likely to make some things easier.

On another note, see this thread (and the referenced blog in the first reply) for information about customizing the individual forms within a formset: Pass different parameters to each form in formset

Alright, thanks for that. The last post in that thread seems helpful. I’ll take a shot at this later today.

This ended up being pretty straight forward. I just passed the list of categories into the formset via custom_kwarg (per thread noted above). Then popped and added the relevant category for each form at the field level, via a custom field widget attr.

Then, in the template, I used context of the same list of categories and used a simple if logical within the formset loop, to check if the currently iterated form fields widget attr value (the same custom category attr value I set), matches the category of the template context. If it matched, then I rendered the form in that particular category.

Thanks for the help.