Search code examples
djangodjango-modelscategoriesdjango-mptt

What is the correct way of dealing with a django model that is in many categories or sub categories


I'm unsure of the best way to design my models for this task. Searching around suggests that I need to use something like Django-MPTT.

I am creating a listings application where any 'item' can be listed under multiple categories. From any category I will need to be able to query for 'items' that match.

An example should illustrate what I want a little better.

I have one Item1 that is to be assigned to two sub categories.

TopLevel1
   --- Sublevel1
   --- Sublevel2
           -----> Item1

TopLevel1
   --- Sublevel1
          ------> Item1
   --- Sublevel2

Can anyone suggest how the models/relationships should be constructed? My current implementation

class Category(models.Model):
    name = models.CharField(max_length=128, blank=False)
    parent = models.ForeignKey('self', null=True, blank=True)


class Item(TimeStampedModel):
    name = models.TextField(null=False, blank=False)
    categories = models.ManyToManyField(Category)

Does not feel like the correct solution?


Solution

  • This is how I see it...

    Your models seem close, a m2m seems appropriate, but your diagram would look more like:

    Item1
        ---> TopLevel1 (accessed through `categories`)
            ---> SubLevel1 (accessed through `Category.children`, see below)
            ---> SubLevel2 (accessed through `Category.children`, see below)
    

    and your models would look more like

    class Category(models.Model):
        name = models.CharField(max_length=128) # `blank` is false by default
        parent = models.ForeignKey('self', null=True, blank=True, related_name='children') # by giving this field a nice `related_name`, we can easily access the linked categories
    
    class Item(TimeStampedModel):
        name = models.TextField() # `null` and `blank` are false by default
        categories = models.ManyToManyField(Category, null=True, blank=True, related_name='items') 
    

    Now, if you have an item and would like to get it's top-level categories, you can loop through item.categories.all()

    {% for category in item.categories.all %}
        Top Level: {{ category.name }}
    {% endfor %}
    

    and to access the sub-level categories, you loop through children.all() in each category

    {% for category in item.categories.all %}
        Top Level: {{ category.name }}
        {% for child in category.children.all %}
            Sub Level: {{ child.name }}
            Parent: {{ child.parent.name }}
        {% endfor %}
    {% endfor %}
    

    you can also get all of the items in a category

    {% for item in category.items.all %}
        Item: {{ item.name }}
    {% endfor %}
    

    or if you are in a top-level category

    {% for child in category.children.all %}
        {{ child.name }}
        {% for item in child.items.all %}
            Item: {{ item.name }}
        {% endfor %}
    {% endfor %}
    

    And with

    category_one = Category.objects.create('category one')
    category_two = Category.objects.create('category two')
    item_one = Item.objects.create('item one')
    

    You can add a Category through the related manager categories on Item

    item_one.categories.add(category_one, category_two)
    

    or you can add an Item through the related manager items on Category

    category_one.items.add(item_one)