Suggestions for returning relational data in Django REST Framework

Viewed 83

I'm new to Django/Django REST FW (and new to this community). I've spent a lot of time reading the documentation but I'm spinning my wheels at this point. I apologize in advance for being so long-winded here.

My back end DB is Postgres. I've got 3 Models, User, Item and ShoppingList. I need Item to contain a description (the item_name field) and its location. The user will select an item and add it to the ShoppingList for that day. The idea is that the user will then "check off" the item once acquired and it'll be "removed" from the view of the ShoppingList.

Here is where I'm having trouble: I don't want to duplicate the item_name and item_location fields in the shopping_list table, but I need to display those fields in the view of the shopping list (shopping_lists.py).

There is a one-to-many relationship between Item and ShoppingList respectively. The Item object is considered a "master items table" that stores descriptions and locations for each item. The ShoppingList object holds a temporary list of these "master items". I need a queryset that contains all fields from ShoppingList and 2 or more fields from Item.

I think this would be what Django REST FW considers a Reverse Relationship. I've tried a variety of changes to my Serialzer(s) and Models (including adding the Item Serializer to the ShoppingList Serializer) and gotten a variety of errors.

models/item.py:

from django.db import models
from django.contrib.auth import get_user_model

class Item(models.Model):
    item_name = models.CharField(max_length=50, db_index=True)
    item_location = models.CharField(max_length=10, blank=True, db_index=True)
    item_class = models.CharField(max_length=20, blank=True)
    # This is a relationship with User model
    shopper_id = models.ForeignKey(
        get_user_model(),
        on_delete=models.CASCADE
    )

    def __str__(self):
        return f"item_name: {self.item_name}, item_location: {self.item_location}, shopper_id: {self.shopper_id}"

models/shopping_list.py:

from django.db import models
from django.contrib.auth import get_user_model
from .item import Item

class ShoppingList(models.Model):
    item_num = models.ForeignKey(
        'Item',
        on_delete=models.DO_NOTHING # we don't want to delete the item from the "master" item list, just from this shopping list
    )
    # This is a relationship with user model. 
    shopper_id = models.ForeignKey(
        get_user_model(),
        on_delete=models.CASCADE # ...but we do want to delete the item if the user goes away as items are user-specific
    )
    item_qty = models.PositiveIntegerField()
    item_complete = models.BooleanField(default=False)
    added_on = models.DateField(auto_now=True)
    # setting list_num to blank=True for this version
    list_num = models.PositiveIntegerField(blank=True)

    def __str__(self):
        return f"item_num: {self.item_num}, shopper_id: {self.shopper_id}, item_qty: {self.item_qty}, item_complete: {self.item_complete}"    

serializers/item.py:

from rest_framework import serializers
from ..models.item import Item

class ItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = Item
        fields = ('id', 'item_name', 'item_location', 'item_class', 'shopper_id')

serializers/shopping_list.py:

from rest_framework import serializers
from ..models.shopping_list import ShoppingList

class ShoppingListSerializer(serializers.ModelSerializer):
    class Meta:
        model = ShoppingList
        fields = ('id', 'item_num', 'shopper_id', 'item_qty', 'item_complete', 'added_on', 'list_num')

Getting ERROR AttributeError: Manager isn't accessible via ShoppingList instances when I execute the GET method in class ShoppingListItemView in views/shopping_lists.py below:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.shortcuts import get_object_or_404
from rest_framework.exceptions import PermissionDenied
from ..models.shopping_list import ShoppingList
from ..serializers.shopping_list import ShoppingListSerializer
from ..models.item import Item
from ..serializers.item import ItemSerializer

class ShoppingListsView(APIView):
    def get(self, request, list_num):
        shopping_items = ShoppingList.objects.filter(shopper_id=request.user.id)
        shopping_list_items = shopping_items.filter(list_num=list_num)
        data = ShoppingListSerializer(shopping_list_items, many=True).data
        return Response(data)

    def post(self, request):
        request.data['shopper_id'] = request.user.id
        list_item = ShoppingListSerializer(data=request.data, partial=True)
        if list_item.is_valid():
            list_item.save()
            return Response(list_item.data, status=status.HTTP_201_CREATED)
        else:
            return Response(list_item.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, list_num):
        shopping_items = ShoppingList.objects.filter(shopper_id=request.user.id)
        shopping_list_items = shopping_items.filter(list_num=list_num)
        response_data = shopping_list_items.delete()
        return Response(response_data, status=status.HTTP_204_NO_CONTENT)

class ShoppingListsAllView(APIView):
    def get(self, request):
        shopping_items = ShoppingList.objects.filter(shopper_id=request.user.id)
        data = ShoppingListSerializer(shopping_items, many=True).data
        return Response(data)

class ShoppingListItemView(APIView):
    def get(self, request, pk):
        list_item = get_object_or_404(ShoppingList, pk=pk)
        if request.user != list_item.shopper_id:
            raise PermissionDenied('Unauthorized, this item belongs to another shopper')
        else:
            list_entry = list_item.objects.select_related('Item').get(id=pk)
            print(list_entry)
            data = ShoppingListSerializer(list_item).data
            return Response(data)

    def delete(self, request, pk):
        list_item = get_object_or_404(ShoppingList, pk=pk)
        if request.user != list_item.shopper_id:
            raise PermissionDenied('Unauthorized, this item belongs to another shopper')
        else:
            list_item.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)

    def patch(self, request, pk):
        list_item = get_object_or_404(ShoppingList, pk=pk)
        if request.user != list_item.shopper_id:
            raise PermissionDenied('Unauthorized, this item belongs to another shopper')
        else:
            request.data['shopper_id'] = request.user.id
            updated_list_item = ShoppingListSerializer(list_item, data=request.data, partial=True)
            if updated_list_item.is_valid():
                updated_list_item.save()
                return Response(updated_list_item.data)
            else:
                return Response(updated_item.errors, status=status.HTTP_400_BAD_REQUEST)
        
2 Answers

For reverse relationship you should use related_name when defining the model or using the suffix _set.

The related_name attribute specifies the name of the reverse relation from the User model back to your model. If you don't specify a related_name, Django automatically creates one using the name of your model with the suffix _set

if you want to display only a few properties of an item in your ShoppingList you can use the SerializerMethodField method in your serializer

this would work as -

class ShoppingListSerializer(serializers.ModelSerializer):
    itemProperty1 = serializers.SerializerMethodField()
    itemProperty2 = serializers.SerializerMethodField()

    class Meta:
        model = ShoppingList
        fields = ('id', "itemProperty1", "itemProperty2", 'more_fields')
   
    def get_itemProperty1(self, instance):
        return instance.item.anyPropertyOfItem if instance.item else ''

    def get_itemProperty2(self, instance):
        return instance.item.anyPropertyOfItem if instance.item.else ''

anyPropertyOfItem can be anything from item models. Setting your serializer this way, your ShoppingList view will automatically show 2 new fields.

or you can also define read only fields with the help of @property in models to get the required field.

If you want to display all the properties of the item in the ShoppingList view, you can write here, will edit my answer. There you need to use related_name and get the item serializer in Shoppinglist serializer as the extra field.

Related