Django: How can I change my code to access ForeignKey fields through 3 tables?

Question:

I tried to get values with using three tables.

cart_c = Cart.objects.select_related('item').filter(user=2)
context = {'cart_c': cart_c}

I got QuerySet, ‘cart_c’, but I could not refer ItemPhoto.photo from ‘cart_c’.

So, I made three tables joined with prefetch_related().

items = Item.objects.prefetch_related('item_photo', 'cart_item').filter(cart__user=2)
context = {'items': items}

But, I do not have any good idea to access ItemPhoto.photo and Cart.quantity except this.

items[0].cart_item.values()[0].get('quantity')
# Or [i.quantity for item in items for i in item.cart_item.all()]

I think this way is not proper and sophisticated to use in Django Template System.

It’s complicated I think.

I found a relative question in StackOverFlow, and one of the answers is same like code I wrote.

items[0].cart_item.all()[0]

Could anyone teach me how I should change and write code to send QuerySet or dictionary to Django Template to access Item.name, Item.price, ItemPhoto.photo and Cart.quantity with using three tables, Item, ItemPhoto and Cart?

Thanks.

models.py(omitted)

class Item(models.Model): # Product
    name = models.CharField("item_name", max_length=255, blank=False, default="")
    price = models.DecimalField("price",
        max_digits=12,
        decimal_places=2,
        default=0.00, 
        blank=False,
    )


class ItemPhoto(models.Model): # Photo
    item = models.ForeignKey(Item, on_delete=models.CASCADE,
        related_name="item_photo", verbose_name="item_id",)
    photo = models.ImageField("item_photo", blank=True, 
        upload_to=get_itemimage_path)


class Cart(models.Model): # Order
    user = models.ForeignKey(settings.AUTH_USER_MODEL, 
        verbose_name="user",
        on_delete=models.CASCADE, null=False,
        related_name="cart_user",)
    item = models.ForeignKey(Item, verbose_name="item_id", 
        on_delete=models.CASCADE, 
        related_name="cart_item", null=False,)
    quantity = models.PositiveSmallIntegerField("quantity", default=0)

Python: 3.9 / Django 4.1

Asked By: K-Yuuma

||

Answers:

What you have as Cart, should actually be CartItem. A model that represents the quantity of each item that a user can put in their actual Cart. In my answer, I’m referring it as CartItem.

You can access Item’s attributes, its photo, and cart quantity in the following manner:

For Postgres, we’ve used ArrayAgg to combine the list of photos per item and annotate it on the CartItem object:

from django.contrib.postgres.aggregates import ArrayAgg

cart_item_qs = CartItem.objects.filter(user=2).select_related('item').annotate(
    item_photos=ArrayAgg('item__item_photo__photo')
)

# You can then pass this in the context
context = {"cart_items": cart_item_qs}

Finally, in the template, you can iterate on your cart_items like:

{% for cart_item in cart_items %}
    {{ cart_item.item.name }} - {{ cart_item.item.price }}
    {% for photo in cart_item.item_photos %}
        <img src="{{ photo.url }}" />
    {% endfor %}
{% endfor %}

UPDATE

For MySQL, we can use Func expression to apply GROUP_CONCAT SQL function to concatenate all photo from CartItem

cart_item_qs = CartItem.objects.filter(user=2).select_related('item').annotate(
    item_photos=models.Func(models.Value('GROUP_CONCAT'), models.Value('item_photo.photo'), function='GROUP_CONCAT')
)

# You can then pass this in the context
context = {"cart_items": cart_item_qs}

To parse this in the template, you’ll do something like this:

{% for cart_item in cart_items %}
    {{ cart_item.item.name }} - {{ cart_item.item.price }}
    {% for photo in cart_item.photos.split(",") %}
        <img src="{{ photo }}" />
    {% endfor %}
{% endfor %}

If the GROUP_CONCAT is not supported, then you can also write this query using Subquery in Django.

PS: You can use django-versatilimagefield for storing your images. It provides a lot of additional functionality for storing and retrieving images in different dimensions.

Answered By: Sanyam Khurana