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

69 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-08-04 12:59 +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 

11 

12from rest_framework import serializers 

13 

14from src.products.serializers.inventory import InventorySerializer 

15from src.products.serializers.review import ReviewSerializer 

16from src.products.models.product import ( 

17 Earwear, 

18 Neckwear, 

19 Fingerwear, 

20 Wristwear, 

21) 

22 

23 

24class BaseProductListSerializer(serializers.ModelSerializer): 

25 average_rating = serializers.DecimalField( 

26 max_digits=7, 

27 decimal_places=2, 

28 ) 

29 is_sold_out = serializers.BooleanField() 

30 collection__name = serializers.CharField() 

31 color__name = serializers.CharField() 

32 stone__name = serializers.CharField() 

33 metal__name = serializers.CharField() 

34 min_price = serializers.DecimalField( 

35 max_digits=7, 

36 decimal_places=2, 

37 ) 

38 max_price = serializers.DecimalField( 

39 max_digits=7, 

40 decimal_places=2, 

41 ) 

42 

43 class Meta: 

44 fields = [ 

45 'id', 

46 'first_image', 

47 'second_image', 

48 'collection__name', 

49 'color__name', 

50 'stone__name', 

51 'metal__name', 

52 'average_rating', 

53 'min_price', 

54 'max_price', 

55 ] 

56 depth = 2 

57 

58 

59class AverageRatingField(serializers.Field): 

60 def to_representation(self, value): 

61 # Always calculate average from approved reviews only 

62 # This ensures consistency across all users 

63 avg = ( 

64 value.review.filter( 

65 approved=True, 

66 ).aggregate( 

67 avg=Avg('rating'), 

68 )['avg'] 

69 or 0 

70 ) 

71 

72 return round(avg, 2) 

73 

74 

75class RelatedProductSerializer(serializers.ModelSerializer): 

76 class Meta: 

77 fields = [ 

78 'id', 

79 'first_image', 

80 ] 

81 model = None 

82 

83 

84class BaseProductItemSerializer(serializers.ModelSerializer): 

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

86 review = serializers.SerializerMethodField() 

87 average_rating = AverageRatingField(source='*') 

88 related_collection_products = serializers.SerializerMethodField() 

89 related_products = serializers.SerializerMethodField() 

90 

91 class Meta: 

92 fields = '__all__' 

93 depth = 2 

94 

95 def get_review(self, obj): 

96 # Get the request from context to check user permissions 

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

98 

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

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

101 latest_reviews = obj.review.all()[:6] 

102 else: 

103 # Regular users only see approved reviews 

104 latest_reviews = obj.review.filter(approved=True)[:6] 

105 

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

107 

108 def get_related_collection_products(self, obj): 

109 model_class = obj.__class__ 

110 related_products = model_class.objects.filter( 

111 collection=obj.collection 

112 ) 

113 

114 class DynamicRelatedProductSerializer(RelatedProductSerializer): 

115 class Meta(RelatedProductSerializer.Meta): 

116 model = model_class 

117 

118 serializer = DynamicRelatedProductSerializer( 

119 related_products, many=True 

120 ) 

121 

122 return serializer.data 

123 

124 def get_related_products(self, obj): 

125 color_id = obj.color_id 

126 current_product_type = type(obj) 

127 related_products = [] 

128 

129 def serialize_products_of_type(model_class): 

130 # Only include products from other types 

131 if model_class == current_product_type: 

132 return [] 

133 products = model_class.objects.filter( 

134 color_id=color_id, 

135 ) 

136 result = [] 

137 for product in products: 

138 result.append( 

139 { 

140 'id': product.id, 

141 'first_image': product.first_image, 

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

143 } 

144 ) 

145 return result 

146 

147 related_products.extend(serialize_products_of_type(Wristwear)) 

148 related_products.extend(serialize_products_of_type(Earwear)) 

149 related_products.extend(serialize_products_of_type(Neckwear)) 

150 related_products.extend(serialize_products_of_type(Fingerwear)) 

151 

152 return related_products[:6] 

153 

154 

155class BaseAttributesSerializer(serializers.ModelSerializer): 

156 count = serializers.IntegerField() 

157 

158 class Meta: 

159 fields = [ 

160 'id', 

161 'name', 

162 'count', 

163 ]