moddayjob
moddayjob

Reputation: 708

Django - How to add multiple instances of the same model in a field

I have a class that looks like this. In the pack items I want to have the number of that instance of Item and how many of that as a number.

class Item(models.Model):
    title = models.CharField(max_length=100)
    price = models.DecimalField(decimal_places=2, max_digits=10)
    is_pack = models.BooleanField(default=False)
    pack_items = models.ManyToManyField(PackItem, blank=True)
class PackItem(models.Model):
    packitem = models.ForeignKey('Item', on_delete=models.CASCADE)
    quantity = models.IntegerField(default=1)

This seems to work but every time I create a new pack in the admin, I have to create first, one by one, each PackItem and then put them in the packitems field of Item. The goal is: I have bottles of drinks as Items and I want to have pre-made Packs of these drinks. Would my way of doing it cause problems later on? Is there a better practice? Or is this already wrong?

Upvotes: 2

Views: 2754

Answers (2)

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 476557

If I understand this correctly, what you need is a many-to-many field with a through=… model [Django-doc]. So we can implement a

class Item(models.Model):
    title = models.CharField(max_length=100)
    price = models.DecimalField(decimal_places=2, max_digits=10)

class Pack(models.Model):
    items = models.ManyToManyField(Item, through='PackItem')

class PackItem(models.Model):
    item = models.ForeignKey(Item, on_delete=models.CASCADE)
    pack = models.ForeignKey(Pack, on_delete=models.CASCADE)
    quantity = models.IntegerField(default=1)

Here we thus specify that a Pack consists out of a collection of items, but the junction table [wiki] will contain an extra parameter named quantity that specifies how many times we add the Item to the Pack object.

Upvotes: 4

Mr Layman
Mr Layman

Reputation: 15

This example is an excellent one that helps answer what a through model is.

To further clear things as I needed an extra hour or so of tutorials to understand this:

1- The Item model should be easily understood

class Item(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(decimal_places=2, max_digits=10)
    # Display below in admin 
    def __str__(self): 
       return f"{self.name}" 

2- The Pack model is the model you create instances in.

class Pack(models.Model):
    pack = models.ForeignKey(Item, on_delete=models.CASCADE, related_name="packname")
    items = models.ManyToManyField(Item, through='PackItem')
    # Display below in admin 
    def __str__(self): 
       return f"{self.id}" 

3- The "PackInfo" model is the intermediary model. You don't manually create any instances here as it does so automatically. What's important is that you add 2 foreignkey fields; one for each model you wish to connect and then whatever additional fields you need (like 'quantity'). The purpose of this example is to add the 'quantity' field.

class PackInfo(models.Model):
    item = models.ForeignKey(Item, on_delete=models.CASCADE)
    pack = models.ForeignKey(Pack, on_delete=models.CASCADE)
    quantity = models.IntegerField(default=1)
    # Display below in admin 
    def __str__(self):
        return f"{self.id}"

The Pack model in the admin should now look like the below (I just added an extra foreignkey field in the Pack model that allows you to select an item): enter image description here

Notice how although the Pack model only has 2 fields ('pack' and 'items'), there is an additional field called 'quantity'. This in my humble opinion is the main advantage of using a through model.

Finally, I was able to display the admin as shown in the image using "admin.TabularInline". You set the intermediary model (PackInfo) as a TabularInline admin view. You can then display it as such in the model with the M2M field (Pack). The extra = 0 is so that no extra empty instances are added in the admin until you press "Add another Pack Info".

@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
    list_display = ('id', 'name', 'price')

class PackInfoInline(admin.TabularInline):
    model = PackInfo
    extra = 0       # Default extra instances to add is 3

@admin.register(Pack)
class PackAdmin(admin.ModelAdmin):
    inlines = [PackInfoInline]
    class Meta:
        model = Pack

Upvotes: 0

Related Questions