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

@@ -617,3 +617,21 @@ class Invoice(models.Model):
# Save directly to FileField # Save directly to FileField
self.pdf_file.save(f"{self.invoice_number}.pdf", ContentFile(pdf_bytes)) 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 thirdparty.stripe.client import StripeClient
from .models import Refund, Order, Invoice from .models import Refund, Order, Invoice, Review
class RefundCreatePublicSerializer(serializers.Serializer): class RefundCreatePublicSerializer(serializers.Serializer):
@@ -465,3 +465,9 @@ class OrderReadSerializer(serializers.ModelSerializer):
return list(obj.discount.values_list("code", flat=True)) 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 from rest_framework import status
import base64 import base64
from .models import Refund from .models import Refund, Review
from .serializers import RefundCreatePublicSerializer from .serializers import RefundCreatePublicSerializer, ReviewSerializerPublic
from django.db import transaction from django.db import transaction
@@ -415,3 +415,15 @@ class RefundPublicView(APIView):
}, },
} }
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; original._retry = true;
try { try {
await privateApi.post("/auth/refresh/"); await privateApi.post("/api/account/token/refresh/");
return privateApi(original); return privateApi(original);
} catch { } catch {
// optional: logout // optional: logout
@@ -37,6 +37,11 @@ privateApi.interceptors.response.use(
export const privateMutator = async <T>( export const privateMutator = async <T>(
config: AxiosRequestConfig config: AxiosRequestConfig
): Promise<T> => { ): 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); const response = await privateApi.request<T>(config);
return response.data; return response.data;
}; };