M2MFields - How to Render Form Categories and Subcategories

Hi!

I’m in the process of creating a plant database that members should be able to comment on, update, and supplement with information.

I’m using M2M fields to classify and store unique attributes of each plant.

The structure of the data is built as follows:
SubCategory → Category
examples of content in subcategory/category
Green → Color
Red → Color
Yellow → Color

1,0 m → Height
1,5 m → Height
2,0 m → Height
2,5 m → Height

January-February → Fruit
February-March → Fruit
March-April → Fruit

My question concerns how to render the form. Each “Category” should be displayed as its own “CheckboxSelectMultiple,” where the associated “SubCategory” linked to the current “Category” is listed.

For example:
Category: Color - SubCategory: Red, Green, Yellow, …

I hope the question is clear and informative.
Any Help Appreciated!

# models.py
class Category(models.Model):
    name = models.CharField(max_length=50)

    @staticmethod
    def get_all_categories():
        return Category.objects.all()

    def __str__(self):
        return self.name

  
class SubCategory(models.Model):
    name = models.CharField(max_length=50)
    categories = models.ManyToManyField(Category, related_name='parent_categories')

    def __str__(self):
        #return self.name + str(self.get_category())
        return self.name

    def get_category(self):
        return self.categories.first()

  
class Plant(models.Model):
    name = models.CharField(max_length=60)
    # ...
    categorys= models.ManyToManyField(SubCategory, related_name='child_categories')
    # ...

    def __str__(self):
        return self.name

    def get_category_name(self):
        parent = self.category.all()
        category = parent.categories.first()
        return category

``

# forms.py
class PlantForm (forms.ModelForm):

# How to generate form for example Color, Height, Fruit.
# Is it possible på dynamically generate form?
color = forms.ModelMultipleChoiceField(
			SubCategory.objects.filter(categories__name="Color"), label="Color",
			widget=forms.CheckboxSelectMultiple)

height = forms.ModelMultipleChoiceField(
			SubCategory.objects.filter(categories__name="Height"), label="Height",
			widget=forms.CheckboxSelectMultiple)
# ...
# Fruit
# ...

    class Meta:
        model = Plant
        fields = "__all__"
        #exclude = ["category"]

    def save(self, commit=True):
        # do something with self.cleaned_data['color']
        
        # How to handle extra form from M2M field.
        # This code doesn't work
        subform_color = SubCategory(self.cleaned_data['color'])
        subform_color().save
        
        subform_height = SubCategory(self.cleaned_data['height'])
        subform_height().save
        
        return super(PlantForm, self).save(commit=commit)
# views.py
class PlantDetailView(DetailView):
    model= Plant
    context_object_name = 'plant'
    form = PlantForm()

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'] = self.form
        
        # How to handle the this?
        context['categorys'] = Category.objects.all()
        context['sub_categorys'] = SubCategory.objects.all()
        return context

    def post(self, request, *args, **kwargs):
        form = PlantForm(request.POST)
        if form.is_valid():

            post = self.get_object()
            # How to handle the this?
            # form.instance.name = request.POST.get('name', '')
            # form.instance.user = self.request.user
            # form.save()
            return redirect(reverse("ProductsDetailView", kwargs={'pk': post.pk}))
1 Like

Hi.

  1. I think a part of your problem lies in the design of the database.
    You use Category to store plant attributes and SubCategory to store all kinds of values for those attributes (height, color, fruit etc.). And there’s no way to distinguish which values belong to plant “A” and which to some other plants.

I suggest you take a look at some ER diagrams to get hints or ideas on how to properly design your database.

Here are some examples of ERD that I found on the web (hope it may help):
https://www.researchgate.net/figure/Entity-Relation-diagram-of-plant-database_fig2_333758467
https://www.researchgate.net/figure/Simplified-conceptual-Phytotracker-database-structure-Grey-boxes-show-a-selection-of_fig4_232245306

  1. As for the form:
    I see that you’re trying to create a form from the Plant model.
    But some pieces of code that you have inside of the PlantForm are not really needed.
    Here’s an example.
    Suppose you have Plant and Color models defined as follows:
class Color(models.Model):
    name = models.CharField(max_length=100)


class Plant(models.Model):
    name = models.CharField(
        max_length=60
    )
    color = models.ManyToManyField(
        Color,
        related_name='colors'
    )

Then you can create model based form like this:

from django.forms import ModelForm, CheckboxSelectMultiple

class PlantForm(ModelForm):
    class Meta:
        model = Plant
        fields = ['name', 'color']
        widgets = {
            'color': CheckboxSelectMultiple()
        }

And inside “class Meta” you simply define “widgets” attribute with field name (as a key) and widget type (as a value). That’s probably all you need to display multiple checkboxes under the “Color” field.

  1. views.py
    DetailView is designed to display a single object from the database. To create a new entry in the database (i.e. add a new plant) you should use CreateView.

From Django docs:
CreateView: A view that displays a form for creating an object, redisplaying the form with validation errors (if there are any) and saving the object.

This is how you can use CreateView to display the PlantForm on a page and save data to the Plant table:

from django.views.generic.edit import CreateView
from django.urls import reverse_lazy

class PlantCreateView(CreateView):
    model = Plant
    form_class = PlantForm
    template_name = 'your_app/your_form_template.html'
    success_url = reverse_lazy('some_url')
    
    def form_valid(self, form):
        return super().form_valid(form)