Django Rest Framework: Get unique list of values from nested structure

Viewed 1671

I want to be able to return a list of strings from a deeply nested structure of data. In this scenario, I have a API that manages a chain of bookstores with many locations in different regions.

Currently, I have an API endpoint that takes a region's ID and returns a nested JSON structure of details about the region, the individual bookstores, and the books that can be found in each store.

{
  "region": [
    {
      "store": [
        {
          "book": {
            "name": "Foo"
          }
        },
        {
          "book": {
            "name": "Bar"
          }
        },
        {
          "book": {
            "name": "Baz"
          }
        }
      ],
    },
    {
      "store": [
        {
          "book": {
            "name": "Foo"
          }
        },
        {
          "book": {
            "name": "Bar"
          }
        }
      ],
    },
    {
      "store": [
        {
          "book": {
            "name": "Foo"
          }
        },
        {
          "book": {
            "name": "Baz"
          }
        },
        {
          "book": {
            "name": "Qux"
          }
        }
      ]
    }
  ]
}

My models look like the following. I am aware these models don't make the most sense for this contrived example, but it does reflect my real world code:

class Book(TimeStampedModel):
    name = models.CharField(default="", max_length=512)


class Bookstore(TimeStampedModel):
    value = models.CharField(default="", max_length=1024)
    book = models.ForeignKey(Book, on_delete=models.CASCADE)


class Region(TimeStampedModel):
    stores = models.ManyToManyField(Bookstore)


class BookstoreChain(TimeStampedModel):
    regions = models.ManyToManyField(Region)

The serializers I created for the above response look like:

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = "__all__"


class BookstoreSerializer(serializers.ModelSerializer):
    books = BookSerializer()

    class Meta:
        model = Bookstore
        fields = "__all__"


class RegionSerializer(serializers.ModelSerializer):
    stores = BookstoreSerializer(many=True)

    class Meta:
        model = Region
        fields = "__all__"


class BookstoreChainSerializer(serializers.ModelSerializer):
    regions = RegionSerializer(many=True)

    class Meta:
        model = BookstoreChain
        fields = "__all__"

I'm not sure what my view or serializer for this solution need to look like. I'm more familiar with writing raw SQL or using an ORM/Linq to get a set of results.

While the above response is certainty useful, what I really want is an API endpoint to return a unique list of book names that can be found in a given region (Foo, Bar, Baz, Qux). I would hope my response to look like:

{
    "books": [
        "Foo",
        "Bar",
        "Baz",
        "Qux"
    ]
}

My feeble attempt so far has a urls.py with the following path:

path("api/regions/<int:pk>/uniqueBooks/", views.UniqueBooksForRegionView.as_view(), name="uniqueBooksForRegion")

My views.py looks like:

class UniqueBooksForRegionView(generics.RetrieveAPIView):
    queryset = Regions.objects.all()
    serializer_class = ???
1 Answers

So you start from region you have to get the stores, so you can filter the books in the stores, here is a solution which will work.

Note:

Avoid using .get() in *APIView because it will trigger an error if the request does not have the ID, you can use get_object_or_404(), but then you cannot log your error in Sentry.

To get an element from an *APIView, use filter().

import logging as L
class UniqueBooksForRegionView(generics.RetrieveAPIView):
    lookup_field = 'pk'
    def get(self, *args, **kwargs)
       regions = Region.objects.filter(pk=self.kwargs[self.lookup_field])
       if regions.exists():
          region = regions.first()
          stores_qs = region.stores.all()
          books_qs = Book.objects.filter(store__in=stores_qs).distinct()
          # use your book serializer
          serializer = BookSerializer(books_qs, many=True)
          return Response(serializer.data, HTTP_200_OK)
       else:
          L.error(f'Region with id {self.kwargs[self.lookup_field]} not found.')
          return Response({'detail':f'Region with id {self.kwargs[self.lookup_field]} not found.'}, HTTP_404_NOT_FOUND)

Note

Here is the flow, the code may need some tweaks, but I hope it helps you understand the flow

Related