0

I have a case that I am curious about for a long, long time. Say I have 3 models:

class Product(models.Model):
    manufacturer = models.CharField(max_length=100)
    description = models.TextField(blank=True)

class PurchasedProduct(models.Model):
    product = models.ForeignKey(Product)
    purchase = models.ForeignKey('Purchase')
    quantity = models.PositiveIntegerField()

class Purchase(models.Model):
    customer = models.ForeignKey('customers.Customer')
    products = models.ManyToManyField(Product, through=PurchasedProduct)
    comment = models.CharField(max_length=200)

I have an API and client application written in some JavaScript framework. So now I need to communicate between them! I am not sure how should I handle this situation in DRF, naturally I would expect to get something like this when accessing /purchase/1/

{
    "id": 1,
    "customer": 1,
    "comment": "Foobar",
    "products": [
        {
            "id": 1,
            "product": {
                "id": 1,
                 ....
            },
            ....
        },
        ....
    ]
}

So I created proper serializer specifying that products field should use PurchasedProductSerializer which in turn uses nested ProductSerializer. It is fine cause I get all necessary info to, say, display what specific products where purchased and in what quantity during shopping using appropriate components in say React.

The problem for me is however when I need to POST new PurchasedProduct. I would expect the most convenient form to be:

{
    "quantity": 10,
    "purchase": 1,
    "product": 1
}

As it carries all necessary info and has the smallest footprint. However I can't be accomplished using PurchasedProductSerializer as it requires product to be object instead of id.

So my question here, is this a good approach (it seems very natural to me), should I use two separate serializers for GET and POST? Should I perform this differently? Could you point me to some best practices/books how to write APIs and client apps?

bartlomieju
  • 418
  • 3
  • 4

1 Answers1

1

I had the exact same problem a few months back and would've been more than happy if someone would've told me. I ended up with the exact solution that you proposed to add products to a purchase. I do agree that your proposed POST request it is the most natural way with the minimal required footprint.

In order to correctly process the POST request's data correctly, though, I ended up using two separate Serializers, just as you described. If you're using DRF viewsets, one way to select the correct serializer on GET and POST is to override the get_serializer_class method as described here.

The deserializer for POST requests could look like this:

class PurchasedProductDeserializer(serializers.ModelSerializer):
  product = serializers.PrimaryKeyRelatedField(queryset=Product.objects.all())
  purchase = serializers.PrimaryKeyRelatedField(queryset=Purchase.objects.all())

  class Meta:
    model = PurchasedProduct
    fields = ('id', 'product', 'purchase', 'quantity')
    write_only_fields = ('product', 'purchase', 'quantity')

That deserializer can then be used for input validation and finally to add a product to a purchase (or increase its quantity).

E.g., inside your viewset:

def create(self, request, *args, **kwargs):
  # ...
  # init your serializer here
  serializer = self.get_serializer(data=request.data)
  if serializer.is_valid(raise_exception=True):
    # now check if the same item is already in the cart
    try:
      # try to find the product in the list of purchased products
      purchased_product = serializer.validated_data['purchase'].purchasedproduct_set.get(product=serializer.validated_data['product'])
      # if so, simply increase its quantity, else add the product as a new item to the cart (see except case)
      purchased_product.quantity += serializer.validated_data['quantity']
      purchased_product.save()
      # update the serializer so it knows the id of the existing instance
      serializer.instance = purchased_product
    except PurchasedProduct.DoesNotExist:
      # product is not yet part of the purchase cart, add it now
      self.perform_create(serializer)
  # ...
  # do other stuff here

As for best practices, there's a ton of documentation available on the internet, but if you're looking for books you might wanna look at some of the ones posted here. When you get bored of REST you might even wanna look into GraphQL.

Community
  • 1
  • 1
burnedikt
  • 962
  • 5
  • 17