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.
This commit is contained in:
2026-01-14 00:10:46 +01:00
parent 2213e115c6
commit 98426f8b05
4 changed files with 47 additions and 6 deletions

View File

@@ -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()
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}"

View File

@@ -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__"

View File

@@ -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)
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"]

View File

@@ -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 <T>(
config: AxiosRequestConfig
): Promise<T> => {
// 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<T>(config);
return response.data;
};