Coverage for src/products/views/review.py: 34%
80 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-08-04 12:59 +0300
« prev ^ index » next coverage.py v7.9.2, created at 2025-08-04 12:59 +0300
1from django.contrib.contenttypes.models import ContentType
2from django.db import IntegrityError
4from rest_framework import viewsets, status
5from rest_framework.decorators import action
6from rest_framework.response import Response
7from rest_framework.exceptions import ValidationError
8from rest_framework.permissions import IsAuthenticated
10from src.common.permissions import IsOrderManager
11from src.products.models.review import Review
12from src.products.serializers.review import ReviewSerializer
13from src.products.serializers.review_management import (
14 ReviewManagementSerializer,
15)
16from src.products.constants import ReviewErrorMessages
19class ReviewViewSet(viewsets.ModelViewSet):
20 """
21 This ViewSet provides CRUD operations for reviews with role-based access control.
22 Regular users can only see and manage their own approved reviews, while
23 reviewers can see all reviews and manage approval status.
24 """
26 permission_classes = [IsAuthenticated]
28 def get_serializer_class(self):
29 """
30 Return appropriate serializer based on user permissions.
32 This method determines which serializer to use based on the user's
33 permissions. Reviewers get the management serializer with approval
34 fields, while regular users get the standard serializer.
35 """
37 # Check if user is a reviewer and this is a reviewer-specific action
38 if self.request.user.has_perm(
39 'products.approve_review'
40 ) and self.action in ['approve', 'unapprove', 'pending']:
41 return ReviewManagementSerializer
43 return ReviewSerializer
45 def get_queryset(self):
46 """
47 Get filtered queryset based on user permissions.
49 This method returns different querysets based on user permissions:
50 - Regular users: Only their own reviews
51 - Reviewers: All reviews (approved and unapproved)
52 """
54 # Check if user is a reviewer
55 if self.request.user.has_perm('products.approve_review'):
56 # Reviewers can see all reviews
57 return Review.objects.all().select_related(
58 'user',
59 'content_type',
60 'user__userprofile',
61 'user__userphoto',
62 )
63 else:
64 # Regular users can only see their own reviews
65 return Review.objects.filter(
66 user=self.request.user
67 ).select_related(
68 'user',
69 'content_type',
70 'user__userprofile',
71 'user__userphoto',
72 )
74 def create(self, request, *args, **kwargs):
75 """
76 Create a new review.
78 Regular users can create reviews, but they start as unapproved.
79 Reviewers can create reviews and set approval status.
81 Args:
82 request: The HTTP request object
83 *args: Additional positional arguments
84 **kwargs: Additional keyword arguments
85 """
86 try:
87 serializer = self.get_serializer(data=request.data)
88 serializer.is_valid(raise_exception=True)
90 # Create the review with user
91 review = serializer.save(user=request.user)
93 response_serializer = self.get_serializer(review)
95 return Response(
96 response_serializer.data, status=status.HTTP_201_CREATED
97 )
99 except ValidationError as e:
100 return Response(
101 e.detail,
102 status=status.HTTP_400_BAD_REQUEST,
103 )
105 except IntegrityError:
106 return Response(
107 {'detail': 'You have already reviewed this item.'},
108 status=status.HTTP_400_BAD_REQUEST,
109 )
111 def update(self, request, *args, **kwargs):
112 """
113 Update an existing review.
115 Regular users can only update their own reviews.
116 Reviewers can update any review and change approval status.
117 When a regular user updates a review, it becomes unapproved again.
118 """
119 partial = kwargs.pop('partial', False)
120 instance = self.get_object()
121 serializer = self.get_serializer(
122 instance, data=request.data, partial=partial
123 )
124 serializer.is_valid(raise_exception=True)
126 # If the user is not a reviewer, set approved to False
127 if not request.user.has_perm('products.approve_review'):
128 serializer.save(approved=False)
129 else:
130 serializer.save()
132 return Response(serializer.data)
134 def destroy(self, request, *args, **kwargs):
135 """
136 Delete a review.
138 Regular users can only delete their own reviews.
139 Reviewers can delete any review.
140 """
141 return super().destroy(request, *args, **kwargs)
143 # The @action decorator is used here to add a custom endpoint to the ViewSet.
144 # Standard CRUD actions (list, retrieve, create, update, delete) do not cover the use case of fetching
145 # the current user's review for a specific product (by content type and object ID).
146 # @action allows us to define a flexible, RESTful, and organized custom route for this special workflow.
147 # This keeps the API clean and groups related logic together in the ViewSet.
148 @action(
149 detail=False,
150 methods=['get'],
151 url_path='user-review/(?P<content_type_name>[^/.]+)/(?P<object_id>[^/.]+)',
152 )
153 def get_user_review(self, request, content_type_name=None, object_id=None):
154 """
155 Custom action to retrieve the current user's review for a specific product.
157 - Exposed as a GET endpoint at /user-review/<content_type_name>/<object_id>/
158 - content_type_name: The model name of the product type (e.g., 'earwear', 'neckwear')
159 - object_id: The primary key of the product instance
161 Workflow:
162 1. Validates the content type and object ID from the URL.
163 2. Looks up the review for the current user, product type, and product ID.
164 3. If found, returns the serialized review data.
165 4. If not found, returns a 404 error with a helpful message.
166 """
167 try:
168 # Get the ContentType object for the given model name (e.g., 'earwear')
169 content_type = ContentType.objects.get(model=content_type_name)
170 object_id_int = int(object_id) if object_id is not None else None
171 except (ContentType.DoesNotExist, ValueError):
172 # Invalid content type or object ID
173 return Response(
174 {
175 'error': ReviewErrorMessages.ERROR_INVALID_CONTENT_TYPE_OR_ID
176 },
177 status=status.HTTP_400_BAD_REQUEST,
178 )
180 try:
181 # Look up the review for the current user and product
182 review = Review.objects.get(
183 user=request.user,
184 content_type=content_type,
185 object_id=object_id_int,
186 )
187 serializer = self.get_serializer(review)
189 return Response(
190 serializer.data,
191 status=status.HTTP_200_OK,
192 )
194 except Review.DoesNotExist:
195 # No review found for this user and product
196 return Response(
197 {
198 'error': ReviewErrorMessages.ERROR_REVIEW_NOT_FOUND,
199 },
200 status=status.HTTP_204_NO_CONTENT,
201 )
203 @action(detail=True, methods=['post'], permission_classes=[IsOrderManager])
204 def approve(self, request, pk=None):
205 """
206 Approve a review (reviewers only).
208 This action allows reviewers to approve reviews, making them
209 visible to regular users. Only users with reviewer permissions
210 can access this endpoint.
212 """
213 try:
214 review = self.get_object()
215 review.approved = True
216 review.save()
218 return Response(
219 {
220 'message': 'Review approved successfully',
221 },
222 status=status.HTTP_200_OK,
223 )
224 except Exception as e:
225 return Response(
226 {
227 'error': 'Failed to approve review',
228 },
229 status=status.HTTP_400_BAD_REQUEST,
230 )
232 @action(detail=True, methods=['post'], permission_classes=[IsOrderManager])
233 def unapprove(self, request, pk=None):
234 """
235 Unapprove a review (reviewers only).
237 This action allows reviewers to unapprove reviews, making them
238 invisible to regular users. Only users with reviewer permissions
239 can access this endpoint.
240 """
241 try:
242 review = self.get_object()
243 review.approved = False
244 review.save()
246 return Response(
247 {
248 'message': 'Review unapproved successfully',
249 },
250 status=status.HTTP_200_OK,
251 )
252 except Exception as e:
253 return Response(
254 {
255 'error': 'Failed to unapprove review',
256 },
257 status=status.HTTP_400_BAD_REQUEST,
258 )
260 @action(detail=False, methods=['get'], permission_classes=[IsOrderManager])
261 def pending(self, request):
262 """
263 Get all pending (unapproved) reviews (reviewers only).
265 This action returns all reviews that haven't been approved yet.
266 Only users with reviewer permissions can access this endpoint.
267 """
268 pending_reviews = Review.objects.filter(approved=False).select_related(
269 'user',
270 'content_type',
271 'user__userprofile',
272 'user__userphoto',
273 )
275 serializer = ReviewManagementSerializer(pending_reviews, many=True)
277 return Response(
278 serializer.data,
279 status=status.HTTP_200_OK,
280 )