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
« 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.
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"""
10from django.db.models import Avg, Q, F
12from rest_framework import serializers
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
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 )
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
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 )
69 return round(avg, 2)
72class RelatedProductSerializer(serializers.ModelSerializer):
73 class Meta:
74 fields = [
75 'id',
76 'first_image',
77 ]
78 model = None
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()
88 class Meta:
89 fields = '__all__'
90 depth = 2
92 def get_review(self, obj):
93 # Get the request from context to check user permissions
94 request = self.context.get('request')
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]
103 return ReviewSerializer(latest_reviews, many=True).data
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 )
111 class DynamicRelatedProductSerializer(RelatedProductSerializer):
112 class Meta(RelatedProductSerializer.Meta):
113 model = model_class
115 serializer = DynamicRelatedProductSerializer(
116 related_products, many=True
117 )
119 return serializer.data
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 = []
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 []
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 )
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
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))
159 return related_products[:5]
162class BaseAttributesSerializer(serializers.ModelSerializer):
163 count = serializers.IntegerField()
165 class Meta:
166 fields = [
167 'id',
168 'name',
169 'count',
170 ]