Features: 1) Add with_related method to managers for optimized prefetching; 2) Add indexes to models for enhanced query performance; 3) Introduce cached_property for feedback count and product quantity; 4) Implement Exists and OuterRef filters for active stock validation in viewsets.

Fixes: 1) Simplify rating and feedback count logic by removing caching layers; 2) Refactor price and quantity calculations for improved accuracy and simplicity; 3) Optimize total price and quantity aggregations in orders by leveraging Django ORM tools.

Extra: Adjusted import statements, removed redundant cache logic, and cleaned up methods for better readability and maintainability.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-10-16 14:04:18 +03:00
parent 00e94a2b29
commit e894affad7
3 changed files with 52 additions and 47 deletions

View file

@ -84,3 +84,8 @@ class ProductManager(MultilingualManager):
stocks__vendor__is_active=True,
stocks__quantity__gt=0,
)
def with_related(self):
return self.select_related("category", "brand").prefetch_related(
"tags", "stocks", "images", "attributes__attribute__group"
)

View file

@ -20,6 +20,7 @@ from django.db.models import (
CharField,
DateTimeField,
DecimalField,
F,
FileField,
FloatField,
ForeignKey,
@ -31,6 +32,7 @@ from django.db.models import (
OneToOneField,
PositiveIntegerField,
QuerySet,
Sum,
TextField,
URLField,
)
@ -38,6 +40,7 @@ from django.db.models.indexes import Index
from django.http import Http404
from django.utils import timezone
from django.utils.encoding import force_bytes
from django.utils.functional import cached_property
from django.utils.http import urlsafe_base64_encode
from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields import AutoSlugField
@ -177,6 +180,7 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel): # type: ignore [
verbose_name_plural = _("vendors")
indexes = [
GinIndex(fields=["authentication"]),
Index(fields=["name"]),
]
@ -512,6 +516,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): # type: ignore
max_length=255,
help_text=_("provide a clear identifying name for the product"),
verbose_name=_("product name"),
db_index=True,
)
description = TextField(
blank=True,
@ -556,49 +561,37 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): # type: ignore
class Meta:
verbose_name = _("product")
verbose_name_plural = _("products")
indexes = [
Index(fields=["is_active", "brand", "category"]),
Index(fields=["slug"]),
Index(fields=["sku"]),
]
def __str__(self):
return self.name
@property
def rating(self) -> float:
cache_key = f"product_rating_{self.pk}"
rating = cache.get(cache_key)
if rating is None:
feedbacks = Feedback.objects.filter(order_product__product_id=self.pk)
rating = feedbacks.aggregate(Avg("rating"))["rating__avg"] or 0
cache.set(cache_key, rating, 86400)
feedbacks = Feedback.objects.filter(order_product__product_id=self.pk)
rating = feedbacks.aggregate(Avg("rating"))["rating__avg"] or 0
return float(round(rating, 2))
@rating.setter
def rating(self, value: float):
self.__dict__["rating"] = value
@property
@cached_property
def feedbacks_count(self) -> int:
cache_key = f"product_feedbacks_count_{self.pk}"
feedbacks_count = cache.get(cache_key)
if feedbacks_count is None:
feedbacks_count = Feedback.objects.filter(order_product__product_id=self.pk).count()
cache.set(cache_key, feedbacks_count, 604800)
return feedbacks_count
return Feedback.objects.filter(order_product__product_id=self.pk).count()
@property
def price(self: Self) -> float:
stock = self.stocks.all().order_by("-price").only("price").first()
price = stock.price if stock else 0.0
return round(price, 2)
stock = self.stocks.only("price").order_by("-price").first()
return round(stock.price, 2) if stock else 0.0
@property
@cached_property
def quantity(self) -> int:
cache_key = f"product_quantity_{self.pk}"
quantity = cache.get(cache_key, 0)
if not quantity:
stocks = self.stocks.only("quantity")
for stock in stocks:
quantity += stock.quantity
cache.set(cache_key, quantity, 3600)
return quantity
return self.stocks.aggregate(total=Sum("quantity"))["total"] or 0
@property
def total_orders(self):
@ -1178,6 +1171,10 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
class Meta:
verbose_name = _("order")
verbose_name_plural = _("orders")
indexes = [
Index(fields=["user", "status"]),
Index(fields=["status", "buy_time"]),
]
def __str__(self) -> str:
return f"#{self.human_readable_id} for {self.user.email if self.user else 'unregistered user'}"
@ -1225,24 +1222,16 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
@property
def total_price(self) -> float:
return (
round(
sum(
(
order_product.buy_price * order_product.quantity
if order_product.status not in FAILED_STATUSES and order_product.buy_price is not None
else 0.0
)
for order_product in self.order_products.all()
),
2,
)
or 0.0
)
total = self.order_products.exclude(status__in=FAILED_STATUSES).aggregate(
total=Sum(F("buy_price") * F("quantity"), output_field=FloatField())
)["total"]
return round(total or 0.0, 2)
@property
def total_quantity(self) -> int:
return sum([op.quantity for op in self.order_products.all()])
total = self.order_products.aggregate(total=Sum("quantity"))["total"]
return total or 0
def add_product(
self,
@ -1676,6 +1665,8 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # t
verbose_name_plural = _("order products")
indexes = [
GinIndex(fields=["notifications", "attributes"]),
Index(fields=["order", "status"]),
Index(fields=["product", "status"]),
]
def return_balance_back(self):

View file

@ -5,7 +5,7 @@ from uuid import UUID
from constance import config
from django.conf import settings
from django.db.models import Prefetch, Q
from django.db.models import Prefetch, Q, OuterRef, Exists
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
@ -452,14 +452,23 @@ class ProductViewSet(EvibesViewSet):
def get_queryset(self):
qs = super().get_queryset()
qs = qs.select_related("brand", "category")
if self.request.user.has_perm("core.view_product"):
return qs
return qs.filter(
is_active=True,
brand__is_active=True,
category__is_active=True,
stocks__isnull=False,
stocks__vendor__is_active=True,
active_stocks = Stock.objects.filter(product_id=OuterRef("pk"), vendor__is_active=True)
return (
qs.filter(
is_active=True,
brand__is_active=True,
category__is_active=True,
)
.annotate(has_active_stocks=Exists(active_stocks))
.filter(has_active_stocks=True)
.distinct()
)
def get_object(self):