Coverage for src/products/managers/base.py: 42%

19 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-08-04 12:59 +0300

1from django.db import models 

2from django.db.models import Min, Max, Avg, Count 

3 

4 

5class BaseProductManager(models.Manager): 

6 """ 

7 Base manager for product models with optimized query methods. 

8 

9 This manager provides methods for retrieving products using select_related and prefetch_related 

10 to minimize database hits and includes annotations for calculated fields like average ratings 

11 and stock status. 

12 

13 The manager is designed to work with any product type (Earwear, Neckwear, etc.) 

14 and provides consistent query patterns across all product categories. 

15 """ 

16 

17 def get_product_item(self, item_id): 

18 """ 

19 Retrieve a single product by its ID. 

20 

21 This method provides a simple way to get a specific product 

22 by its primary key. It's used when displaying individual 

23 product details. 

24 """ 

25 return self.get(pk=item_id) 

26 

27 def get_product_list(self, filters, ordering): 

28 """ 

29 Retrieve a filtered and ordered list of products. 

30 

31 This method applies filters to the product queryset and then 

32 calls the optimized _get_raw_products method to add annotations 

33 and related data. It's used for product listing pages with 

34 filtering and sorting capabilities. 

35 """ 

36 # Apply filters to the base queryset 

37 qs = self.filter(filters) 

38 

39 # Get optimized product data with annotations 

40 raw_products = self._get_raw_products(qs, ordering) 

41 

42 return raw_products 

43 

44 def _get_raw_products(self, qs, ordering): 

45 """ 

46 This method takes a filtered queryset and adds optimized database 

47 queries with select_related, prefetch_related, and annotations. 

48 It calculates important product data like total quantity, price 

49 ranges, stock status, and average ratings in a single query. 

50 """ 

51 # Map user-friendly ordering parameters to database fields 

52 # This allows the frontend to use simple names while the database 

53 # uses optimized field names for sorting 

54 ordering_map = { 

55 'price_asc': 'min_price', # Sort by lowest price first 

56 'price_desc': '-max_price', # Sort by highest price first 

57 'rating': '-average_rating', # Sort by highest rating first 

58 } 

59 

60 # Get the database field name for ordering 

61 ordering_criteria = ordering_map[ordering] 

62 

63 # Return optimized queryset with all related data 

64 return ( 

65 qs 

66 # prefetch_related fetches many-to-many and reverse foreign key relationships 

67 # This prevents N+1 queries when accessing inventory and review data 

68 .prefetch_related('inventory', 'review') 

69 # values() specifies which fields to include in the result 

70 .values( 

71 'id', 

72 'collection__name', 

73 'first_image', 

74 'second_image', 

75 'color__name', 

76 'stone__name', 

77 'metal__name', 

78 ) 

79 # Annotations add calculated fields to each product 

80 .annotate( 

81 # Find the lowest price for this product 

82 min_price=Min('inventory__price'), 

83 # Find the highest price for this product 

84 max_price=Max('inventory__price'), 

85 # Calculate average rating from approved reviews 

86 average_rating=Avg('review__rating', distinct=True), 

87 ) 

88 # Order by the specified criteria, with ID as secondary sort 

89 .order_by( 

90 f'{ordering_criteria}', 

91 'id', 

92 ) 

93 ) 

94 

95 

96class BaseAttributesManager(models.Manager): 

97 """ 

98 Base manager for attribute models with counting functionality. 

99 

100 This manager provides methods for retrieving attributes (colors, metals, 

101 stones, collections) with counts of how many products use each attribute. 

102 It's used for building filter options in the frontend, showing users 

103 how many products are available for each filter choice. 

104 

105 The manager supports filtering by category, allowing it to show counts 

106 for specific product types (e.g., only earwear, only neckwear). 

107 """ 

108 

109 def get_attributes_count(self, filters, category): 

110 qs = self.get_queryset() 

111 

112 # Only use category if it is a valid, non-empty string 

113 if category and isinstance(category, str) and category.strip(): 

114 return ( 

115 qs.prefetch_related(category) 

116 .filter(filters) 

117 .values('id', 'name') 

118 .annotate(count=Count(category)) 

119 .filter(count__gt=0) 

120 ) 

121 

122 else: 

123 # If no valid category, just return all attributes with count=0 

124 return qs.values('id', 'name').annotate( 

125 count=models.Value(0, output_field=models.IntegerField()) 

126 )