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
« 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
5class BaseProductManager(models.Manager):
6 """
7 Base manager for product models with optimized query methods.
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.
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 """
17 def get_product_item(self, item_id):
18 """
19 Retrieve a single product by its ID.
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)
27 def get_product_list(self, filters, ordering):
28 """
29 Retrieve a filtered and ordered list of products.
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)
39 # Get optimized product data with annotations
40 raw_products = self._get_raw_products(qs, ordering)
42 return raw_products
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 }
60 # Get the database field name for ordering
61 ordering_criteria = ordering_map[ordering]
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 )
96class BaseAttributesManager(models.Manager):
97 """
98 Base manager for attribute models with counting functionality.
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.
105 The manager supports filtering by category, allowing it to show counts
106 for specific product types (e.g., only earwear, only neckwear).
107 """
109 def get_attributes_count(self, filters, category):
110 qs = self.get_queryset()
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 )
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 )