From 98426f8b05d4a863887d004526240b91a10c5d86 Mon Sep 17 00:00:00 2001 From: Brunobrno Date: Wed, 14 Jan 2026 00:10:46 +0100 Subject: [PATCH] Add product review model, serializer, and API endpoint Introduces a Review model for product reviews, including rating and comment fields. Adds a public serializer and a ModelViewSet for reviews with search and ordering capabilities. Also updates the frontend API client to use the correct token refresh endpoint and improves FormData handling. --- backend/commerce/models.py | 20 +++++++++++++++++++- backend/commerce/serializers.py | 8 +++++++- backend/commerce/views.py | 18 +++++++++++++++--- frontend/src/api/privateClient.ts | 7 ++++++- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/backend/commerce/models.py b/backend/commerce/models.py index d6be566..bd4e7fc 100644 --- a/backend/commerce/models.py +++ b/backend/commerce/models.py @@ -616,4 +616,22 @@ class Invoice(models.Model): # Save directly to FileField self.pdf_file.save(f"{self.invoice_number}.pdf", ContentFile(pdf_bytes)) - self.save() \ No newline at end of file + self.save() + + +class Review(models.Model): + product = models.ForeignKey(Product, related_name="reviews", on_delete=models.CASCADE) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="reviews" + ) + + rating = models.PositiveIntegerField( + validators=[MinValueValidator(1), MaxValueValidator(5)] + ) + comment = models.TextField(blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Review for {self.product.name} by {self.user.username}" \ No newline at end of file diff --git a/backend/commerce/serializers.py b/backend/commerce/serializers.py index aa5a488..68fa3f8 100644 --- a/backend/commerce/serializers.py +++ b/backend/commerce/serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers from thirdparty.stripe.client import StripeClient -from .models import Refund, Order, Invoice +from .models import Refund, Order, Invoice, Review class RefundCreatePublicSerializer(serializers.Serializer): @@ -465,3 +465,9 @@ class OrderReadSerializer(serializers.ModelSerializer): return list(obj.discount.values_list("code", flat=True)) + +class ReviewSerializerPublic(serializers.Serializer): + + class Meta: + model = Review + fields = "__all__" \ No newline at end of file diff --git a/backend/commerce/views.py b/backend/commerce/views.py index 3e310d4..59d9be2 100644 --- a/backend/commerce/views.py +++ b/backend/commerce/views.py @@ -4,8 +4,8 @@ from rest_framework.response import Response from rest_framework import status import base64 -from .models import Refund -from .serializers import RefundCreatePublicSerializer +from .models import Refund, Review +from .serializers import RefundCreatePublicSerializer, ReviewSerializerPublic from django.db import transaction @@ -414,4 +414,16 @@ class RefundPublicView(APIView): "base64": pdf_b64, }, } - return Response(resp, status=status.HTTP_201_CREATED) \ No newline at end of file + return Response(resp, status=status.HTTP_201_CREATED) + + +class ReviewPublicViewSet(viewsets.ModelViewSet): + queryset = Review.objects.all() + serializer_class = ReviewSerializerPublic + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ["product__name", "user__username", "comment"] + ordering_fields = ["rating", "created_at"] + ordering = ["-created_at"] + + \ No newline at end of file diff --git a/frontend/src/api/privateClient.ts b/frontend/src/api/privateClient.ts index a300a26..6798278 100644 --- a/frontend/src/api/privateClient.ts +++ b/frontend/src/api/privateClient.ts @@ -22,7 +22,7 @@ privateApi.interceptors.response.use( original._retry = true; try { - await privateApi.post("/auth/refresh/"); + await privateApi.post("/api/account/token/refresh/"); return privateApi(original); } catch { // optional: logout @@ -37,6 +37,11 @@ privateApi.interceptors.response.use( export const privateMutator = async ( config: AxiosRequestConfig ): Promise => { + // If sending FormData, remove Content-Type header to let axios set it with boundary + if (config.data instanceof FormData) { + delete config.headers?.['Content-Type']; + } + const response = await privateApi.request(config); return response.data; }; \ No newline at end of file