1

I'm looking for some advice on how to correctly use my DRF API I have built, specifically the PATCH method at present. I am trying to formulate a script to patch the quantity of a Product / Cart Item that has been successfully added to the cart but I cannot get it to work.

For context of my project: My goal of the scripts are to be able to create a Cart, use a button to either add and Item or remove an Item (-1 quantity), Once the customer clicks a proceed to payment button the cart items are then transfered into the Order model So I can then process the order. Once order status is complete, cart is deleted and recreated.

Here is my current script:

const updateBtns = document.querySelectorAll(".update-cart");
const user = "{{request.user}}";
for (let i = 0; i < updateBtns.length; i++) {
  updateBtns[i].addEventListener("click", function () {
    event.preventDefault(); // prevent page from refreshing
    const productId = this.dataset.product;
    const action = this.dataset.action;
    console.log("productId:", productId, "action:", action);
    console.log("USER:", user);
    createCart(productId, action);
  });
}
function createCart(productId, action, cartId) {
  var csrftoken = getCookie("csrftoken");
  console.log("User is logged in, sending data...");
  fetch("/get_cart_id/")
    .then((response) => response.json())
    .then((data) => {
      const cartId = data.cart_id;
      console.log("Cart ID:", cartId); // log cartId in console

      let method, url;
      if (action === "add") {
        method = "POST";
        url = `/api/carts/${cartId}/items/`;
      } else if (action === "remove") {
        method = "PATCH";

        url = `/api/carts/${cartId}/items/${THIS_IS_WHERE_I_NEED_ITEM_ID}/`;
      } else {
        console.log(`Invalid action: ${action}`);
        return;
      }

      fetch(url, {
        method: method,
        headers: {
          "Content-Type": "application/json",
          "X-CSRFToken": csrftoken,
        },
        body: JSON.stringify({
          product_id: productId,
          quantity: 1,
        }),
      })
        .then((response) => response.json())
        .then((data) => {
          console.log(`Item ${action}ed in cart:`, data);
        });
    });
}

Here are my Serializera relating to my cart(please ask for me if you require more info to assist me):

class CartItemSerializer(serializers.ModelSerializer):
    product = SimpleProductSerializer()
    total_price = serializers.SerializerMethodField()

    def get_total_price(self, cart_item:CartItem):
        return cart_item.quantity * cart_item.product.price
    
    class Meta:
        model = CartItem
        fields = ['id', 'product', 'quantity', 'total_price']

 CartSerializer(serializers.ModelSerializer):
    id = serializers.UUIDField(read_only=True)
    items = CartItemSerializer(many=True, read_only=True)
    total_price = serializers.SerializerMethodField()

    def get_total_price(self, cart):
        return sum([item.quantity * item.product.price for item in cart.items.all()])

    class Meta:
        model = Cart
        fields = ['id', 'items', 'total_price']
    
class AddCartItemSerializer(serializers.ModelSerializer):
    product_id = serializers.IntegerField()

    def validate_product_id(self, value):
        if not Product.objects.filter(pk=value).exists():
            raise serializers.ValidationError('No product with the given ID was found.')
        return value

    def save(self, **kwargs):
        cart_id = self.context['cart_id']
        product_id = self.validated_data['product_id']
        quantity = self.validated_data['quantity']

        try: 
            cart_item = CartItem.objects.get(cart_id=cart_id, product_id=product_id)
            cart_item.quantity += quantity
            cart_item.save()
            self.instance = cart_item
        except CartItem.DoesNotExist:
            self.instance = CartItem.objects.create(cart_id=cart_id, **self.validated_data)
        
        return self.instance

    class Meta:
        model = CartItem
        fields = ['id', 'product_id', 'quantity']

class UpdateCartItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = CartItem
        fields = ['quantity']

Finally here are my viewsets:

class CartViewSet(CreateModelMixin,
                  RetrieveModelMixin,
                  DestroyModelMixin,
                  GenericViewSet):
    queryset = Cart.objects.prefetch_related('items__product').all()
    serializer_class = CartSerializer
 
class CartItemViewSet(ModelViewSet):
    http_method_names = ['get', 'post', 'patch', 'delete']
   
    def get_serializer_class(self):
        if self.request.method == 'POST':
            return AddCartItemSerializer 
        elif self.request.method == 'PATCH':
            return UpdateCartItemSerializer
        return CartItemSerializer

    def get_serializer_context(self):
        return {'cart_id': self.kwargs['cart_pk']}

    def get_queryset(self):
        return CartItem.objects \
                .filter(cart_id=self.kwargs['cart_pk']) \
                .select_related('product')

This is my view that I created to retrieve the cart that is related to the current user which is created when they enter the home/gallery/product page:

def get_cart_id(request):
    if request.user.is_authenticated:
        cart = Cart.objects.get(user=request.user)
        cart_id = cart.id
        return JsonResponse({'cart_id': cart_id})
    else:
        return JsonResponse({'error': 'User is not authenticated'})
Igonato
  • 10,175
  • 3
  • 35
  • 64
David Henson
  • 355
  • 1
  • 10
  • I would [use `get_queryset` to define your user-based filtering](https://www.django-rest-framework.org/api-guide/filtering/#filtering-against-the-current-user) . You can then [set the default permission policy](https://www.django-rest-framework.org/api-guide/permissions/#setting-the-permission-policy) to disallow unauthenticated users. So far, what I'm seeing in your code doesn't need a custom view. I'd take a look at instantiating a cart ModelViewSet as in the [url examples](https://www.django-rest-framework.org/tutorial/quickstart/#urls) – Ross Rogers Apr 18 '23 at 16:00
  • See also [ModelViewSet](https://www.django-rest-framework.org/api-guide/viewsets/#modelviewset) – Ross Rogers Apr 18 '23 at 16:01
  • Oh really, thats very interesting thank you, would it be possible for you to post a snippet for me to investigate, I am very intrigued to learn and appreciate your time massively. For example; I'm not quite sure how I would use a query set to automatically create a cart for authenticated users – David Henson Apr 18 '23 at 17:07

1 Answers1

1

I would recommend moving if (action === "...") checks a bit higher in the nesting. It should simplify your code greatly, even if you need to make it longer by repeating fetch("/get_cart_id/"), it will be easier to follow. Even better would be to separate the actions into different functions.

After you create an item using POST, if successful, the response will contain an id of your freshly created item. You can use that id to remove the item. The method should be DELETE and not PATCH.

function createCart(productId, action, cartId) {
  // ...
  
  if (action === "add") {
    fetch("/get_cart_id/")
    .then((response) => response.json())
    .then((data) => {
      // ...

      fetch(`/api/carts/${cartId}/items/`, {
        method: 'POST',
        // ...
      })
      .then((response) => response.json())
      .then((data) => {
        // Save data.id somewhere
        console.log(`Created Item with id: ${data.id}`);
      });
    });
  }
  else if (action === "remove") {
    // Use saved id there
    fetch(`/api/carts/${cartId}/items/${SAVED_ID_OF_THE_ITEM_TO_REMOVE}/`, {
      method: 'DELETE',
      // ...
    })
    .then(...)
    // ...
  }
  // ...
}

One last thing, you should be storing your cart_id, since, I imagine, it's not something that's going to change, and you shouldn't fetch it every time you want to add a product.

Igonato
  • 10,175
  • 3
  • 35
  • 64
  • Brilliant advice, would you have any idea of how to Make a post to create a cart, I know the route but the difficulty I'm facing is making the Cart the only one a User can have until that cart has been turning into an Order. Currently a new cart is been made regardless of a user already having one – David Henson Apr 19 '23 at 16:48
  • 1
    I see. You can add [`@action(detail=False)`](https://www.django-rest-framework.org/api-guide/viewsets/#marking-extra-actions-for-routing) to your viewset. You can call it `current`, which will result in `/api/carts/current` or you might use a separate view - [for inspiration](https://stackoverflow.com/a/46159125/723891) – Igonato Apr 19 '23 at 18:36
  • I am using a separate view currently, I was just hoping to make full use of my API with http methods but I am struggling with the cart creation – David Henson Apr 19 '23 at 19:09
  • So, what's wrong with using `POST` for it? – Igonato Apr 19 '23 at 19:43
  • It isn't working, I cant get it to work with a get_or_create method as I only want one user to have one cart at a time – David Henson Apr 19 '23 at 20:09
  • Have you seen the example I linked that allows managing currently logged-in user? You can change the model to Cart. add a `get_or_create` to `get_object` and that should do it for you. Basically, don't think about creating as a separate action, just get the user's cart using GET and, if doesn't exist it will be created by `get_or_create` – Igonato Apr 19 '23 at 20:30
  • 1
    I didn't even notice the link, apologies. I will look now. – David Henson Apr 19 '23 at 20:44