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
« 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.
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
12from rest_framework import serializers
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)
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 )
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
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 )
72 return round(avg, 2)
75class RelatedProductSerializer(serializers.ModelSerializer):
76 class Meta:
77 fields = [
78 'id',
79 'first_image',
80 ]
81 model = None
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()
91 class Meta:
92 fields = '__all__'
93 depth = 2
95 def get_review(self, obj):
96 # Get the request from context to check user permissions
97 request = self.context.get('request')
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]
106 return ReviewSerializer(latest_reviews, many=True).data
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 )
114 class DynamicRelatedProductSerializer(RelatedProductSerializer):
115 class Meta(RelatedProductSerializer.Meta):
116 model = model_class
118 serializer = DynamicRelatedProductSerializer(
119 related_products, many=True
120 )
122 return serializer.data
124 def get_related_products(self, obj):
125 color_id = obj.color_id
126 current_product_type = type(obj)
127 related_products = []
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
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))
152 return related_products[:6]
155class BaseAttributesSerializer(serializers.ModelSerializer):
156 count = serializers.IntegerField()
158 class Meta:
159 fields = [
160 'id',
161 'name',
162 'count',
163 ]