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

1from django.contrib.contenttypes.models import ContentType 

2from django.db import IntegrityError 

3 

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 

9 

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 

17 

18 

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

25 

26 permission_classes = [IsAuthenticated] 

27 

28 def get_serializer_class(self): 

29 """ 

30 Return appropriate serializer based on user permissions. 

31 

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

36 

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 

42 

43 return ReviewSerializer 

44 

45 def get_queryset(self): 

46 """ 

47 Get filtered queryset based on user permissions. 

48 

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

53 

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 ) 

73 

74 def create(self, request, *args, **kwargs): 

75 """ 

76 Create a new review. 

77 

78 Regular users can create reviews, but they start as unapproved. 

79 Reviewers can create reviews and set approval status. 

80 

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) 

89 

90 # Create the review with user 

91 review = serializer.save(user=request.user) 

92 

93 response_serializer = self.get_serializer(review) 

94 

95 return Response( 

96 response_serializer.data, status=status.HTTP_201_CREATED 

97 ) 

98 

99 except ValidationError as e: 

100 return Response( 

101 e.detail, 

102 status=status.HTTP_400_BAD_REQUEST, 

103 ) 

104 

105 except IntegrityError: 

106 return Response( 

107 {'detail': 'You have already reviewed this item.'}, 

108 status=status.HTTP_400_BAD_REQUEST, 

109 ) 

110 

111 def update(self, request, *args, **kwargs): 

112 """ 

113 Update an existing review. 

114 

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) 

125 

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() 

131 

132 return Response(serializer.data) 

133 

134 def destroy(self, request, *args, **kwargs): 

135 """ 

136 Delete a review. 

137 

138 Regular users can only delete their own reviews. 

139 Reviewers can delete any review. 

140 """ 

141 return super().destroy(request, *args, **kwargs) 

142 

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. 

156 

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 

160 

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 ) 

179 

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) 

188 

189 return Response( 

190 serializer.data, 

191 status=status.HTTP_200_OK, 

192 ) 

193 

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 ) 

202 

203 @action(detail=True, methods=['post'], permission_classes=[IsOrderManager]) 

204 def approve(self, request, pk=None): 

205 """ 

206 Approve a review (reviewers only). 

207 

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. 

211 

212 """ 

213 try: 

214 review = self.get_object() 

215 review.approved = True 

216 review.save() 

217 

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 ) 

231 

232 @action(detail=True, methods=['post'], permission_classes=[IsOrderManager]) 

233 def unapprove(self, request, pk=None): 

234 """ 

235 Unapprove a review (reviewers only). 

236 

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() 

245 

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 ) 

259 

260 @action(detail=False, methods=['get'], permission_classes=[IsOrderManager]) 

261 def pending(self, request): 

262 """ 

263 Get all pending (unapproved) reviews (reviewers only). 

264 

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 ) 

274 

275 serializer = ReviewManagementSerializer(pending_reviews, many=True) 

276 

277 return Response( 

278 serializer.data, 

279 status=status.HTTP_200_OK, 

280 )