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:
@@ -616,4 +616,22 @@ 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}"
|
||||||
@@ -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__"
|
||||||
@@ -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
|
||||||
@@ -414,4 +414,16 @@ class RefundPublicView(APIView):
|
|||||||
"base64": pdf_b64,
|
"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"]
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user