Coverage for src/products/serializers/base.py: 53%

74 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-09-21 16:24 +0300

1""" 

2This module contains base and shared serializers for product-related API endpoints. 

3 

4It provides: 

5- Base serializers for product lists and product detail views 

6- Logic for calculating average ratings and handling related products 

7- Shared fields and methods for reuse and extension in the product app 

8""" 

9 

10from django.db.models import Avg, Q, F 

11 

12from rest_framework import serializers 

13 

14from src.products.models.product import Bracelet, Earring, Necklace, Pendant, Ring, Watch 

15from src.products.serializers.inventory import InventorySerializer 

16from src.products.serializers.review import ReviewSerializer 

17 

18 

19class BaseProductListSerializer(serializers.ModelSerializer): 

20 average_rating = serializers.DecimalField( 

21 max_digits=7, 

22 decimal_places=2, 

23 ) 

24 is_sold_out = serializers.BooleanField() 

25 collection__name = serializers.CharField() 

26 color__name = serializers.CharField() 

27 stone__name = serializers.CharField() 

28 metal__name = serializers.CharField() 

29 min_price = serializers.DecimalField( 

30 max_digits=7, 

31 decimal_places=2, 

32 ) 

33 max_price = serializers.DecimalField( 

34 max_digits=7, 

35 decimal_places=2, 

36 ) 

37 

38 class Meta: 

39 fields = [ 

40 'id', 

41 'first_image', 

42 'second_image', 

43 'third_image', 

44 'fourth_image', 

45 'collection__name', 

46 'color__name', 

47 'stone__name', 

48 'metal__name', 

49 'average_rating', 

50 'min_price', 

51 'max_price', 

52 ] 

53 depth = 2 

54 

55 

56class AverageRatingField(serializers.Field): 

57 def to_representation(self, value): 

58 # Always calculate average from approved reviews only 

59 # This ensures consistency across all users 

60 avg = ( 

61 value.review.filter( 

62 approved=True, 

63 ).aggregate( 

64 avg=Avg('rating'), 

65 )['avg'] 

66 or 0 

67 ) 

68 

69 return round(avg, 2) 

70 

71 

72class RelatedProductSerializer(serializers.ModelSerializer): 

73 class Meta: 

74 fields = [ 

75 'id', 

76 'first_image', 

77 ] 

78 model = None 

79 

80 

81class BaseProductItemSerializer(serializers.ModelSerializer): 

82 inventory = InventorySerializer(many=True, read_only=True) 

83 review = serializers.SerializerMethodField() 

84 average_rating = AverageRatingField(source='*') 

85 related_collection_products = serializers.SerializerMethodField() 

86 related_products = serializers.SerializerMethodField() 

87 

88 class Meta: 

89 fields = '__all__' 

90 depth = 2 

91 

92 def get_review(self, obj): 

93 # Get the request from context to check user permissions 

94 request = self.context.get('request') 

95 

96 # If user is a reviewer, show all reviews (approved and unapproved) 

97 if request and request.user.has_perm('products.approve_review'): 

98 latest_reviews = obj.review.all()[:4] 

99 else: 

100 # Regular users only see approved reviews 

101 latest_reviews = obj.review.filter(approved=True)[:4] 

102 

103 return ReviewSerializer(latest_reviews, many=True).data 

104 

105 def get_related_collection_products(self, obj): 

106 model_class = obj.__class__ 

107 related_products = model_class.objects.filter( 

108 collection=obj.collection 

109 ) 

110 

111 class DynamicRelatedProductSerializer(RelatedProductSerializer): 

112 class Meta(RelatedProductSerializer.Meta): 

113 model = model_class 

114 

115 serializer = DynamicRelatedProductSerializer( 

116 related_products, many=True 

117 ) 

118 

119 return serializer.data 

120 

121 def get_related_products(self, obj): 

122 color_id = obj.color_id 

123 target_gender = obj.target_gender 

124 current_product_type = type(obj) 

125 related_products = [] 

126 

127 def serialize_products_of_type(model_class): 

128 # Only include products from other types 

129 if model_class == current_product_type and target_gender != 'M': 

130 return [] 

131 

132 if target_gender == 'M': 

133 products = model_class.objects.filter( 

134 Q(target_gender=target_gender) 

135 ) 

136 else: 

137 products = model_class.objects.filter( 

138 color_id=color_id, 

139 ) 

140 

141 result = [] 

142 for product in products: 

143 result.append( 

144 { 

145 'id': product.id, 

146 'first_image': product.first_image, 

147 'product_type': f'{model_class.__name__.lower()}s', 

148 } 

149 ) 

150 return result 

151 

152 related_products.extend(serialize_products_of_type(Earring)) 

153 related_products.extend(serialize_products_of_type(Necklace)) 

154 related_products.extend(serialize_products_of_type(Pendant)) 

155 related_products.extend(serialize_products_of_type(Ring)) 

156 related_products.extend(serialize_products_of_type(Bracelet)) 

157 related_products.extend(serialize_products_of_type(Watch)) 

158 

159 return related_products[:5] 

160 

161 

162class BaseAttributesSerializer(serializers.ModelSerializer): 

163 count = serializers.IntegerField() 

164 

165 class Meta: 

166 fields = [ 

167 'id', 

168 'name', 

169 'count', 

170 ]