Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/recipe additional fields #216

Merged
merged 10 commits into from
Dec 25, 2023
19 changes: 19 additions & 0 deletions backend/api/orders_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ class Meta:
)
model = Order

# TODO: commented code doesn't reduce DB hits
@classmethod
def setup_eager_loading(cls, queryset):
"""Perform necessary eager loading of orders data."""
return queryset.select_related("user")
# ).prefetch_related(
# Prefetch(
# "products",
# queryset=Product.objects.select_related(
# "category", "subcategory", "producer"
# ).prefetch_related(
# "components",
# "tags",
# "promotions",
# "reviews",
# ),
# )
# )


class OrderGetAnonSerializer(serializers.ModelSerializer):
"""Serializer for anonimous user order representation."""
Expand Down
27 changes: 9 additions & 18 deletions backend/api/orders_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import PermissionDenied
from django.db.models import Prefetch
from django.utils.decorators import method_decorator
from drf_standardized_errors.openapi_serializers import (
ErrorResponse401Serializer,
Expand Down Expand Up @@ -95,7 +94,7 @@ class ShoppingCartViewSet(

def list(self, request, **kwargs):
shopping_cart = ShopCart(request)
logger.info("Success.")
logger.info("The user's shopping cart list was successfully received.")
return Response(
{
"products": shopping_cart.__iter__(),
Expand All @@ -112,7 +111,7 @@ def create(self, request, **kwargs):
serializer.is_valid(raise_exception=True)
for product in products:
shopping_cart.add(product=product, quantity=product["quantity"])
logger.info("Success.")
logger.info("The shopping cart was successfully created.")
return Response(
{
"products": shopping_cart.__iter__(),
Expand Down Expand Up @@ -214,7 +213,7 @@ class OrderViewSet(
):
"""Viewset for Order."""

queryset = Order.objects.all()
queryset = OrderGetAuthSerializer.setup_eager_loading(Order.objects.all())
permission_classes = [AllowAny]

def get_serializer_class(self):
Expand Down Expand Up @@ -268,23 +267,15 @@ def retrieve(self, request, **kwargs):
if user.is_authenticated and order.user != user:
return Response({"errors": ORDER_USER_ERROR_MESSAGE})
serializer = self.get_serializer(order)
logger.info("Success.")
logger.info("The user's order was successfully received.")
return Response(serializer.data, status=status.HTTP_200_OK)

def list(self, request, **kwargs):
if self.request.user.is_authenticated:
queryset = (
Order.objects.select_related("user", "address", "delivery_point")
.prefetch_related(
Prefetch(
"products",
queryset=Product.objects.prefetch_related("promotions"),
)
)
.filter(user=self.request.user)
)
queryset = self.get_queryset().filter(user=self.request.user)
# TODO: need to solve n+1 problem
serializer = self.get_serializer(queryset, many=True)
logger.info("Success.")
logger.info("The user's order list was successfully received.")
return Response(serializer.data, status=status.HTTP_200_OK)
logger.error(METHOD_ERROR_MESSAGE)
return Response(
Expand Down Expand Up @@ -340,7 +331,7 @@ def create(self, request, *args, **kwargs):
else OrderGetAnonSerializer
)
response_serializer = response_serializer(order)
logger.info("Order was successful.")
logger.info("The order was successfully created.")
return Response(response_serializer.data, status=status.HTTP_201_CREATED)

def destroy(self, request, *args, **kwargs):
Expand All @@ -356,7 +347,7 @@ def destroy(self, request, *args, **kwargs):
else:
order = get_object_or_404(Order, id=self.kwargs.get("pk"))
if order.user != self.request.user:
logger.error("PermissionDenied.")
logger.error("PermissionDenied during order creation.")
raise PermissionDenied()

if order.status in order_restricted_deletion_statuses:
Expand Down
158 changes: 99 additions & 59 deletions backend/api/products_serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.db.models import Avg
from django.db.models import Avg, Exists, OuterRef
from drf_spectacular.utils import extend_schema_field
from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers

from .users_serializers import UserLightSerializer
Expand All @@ -16,14 +17,14 @@
Tag,
)

NO_RATING_MESSAGE = "У данного продукта еще нет оценок."
RATING_DECIMAL_PLACES = 1


class SubcategoryLightSerializer(serializers.ModelSerializer):
"""Serializer for subcategories representation in product serializer."""

subcategory_name = serializers.CharField(source="name")
subcategory_slug = serializers.CharField(source="slug")
subcategory_slug = serializers.SlugField(source="slug")

class Meta:
model = Subcategory
Expand All @@ -41,7 +42,7 @@ class CategoryLightSerializer(serializers.ModelSerializer):
"""Serializer for categories representation in product serializer."""

category_name = serializers.CharField(source="name")
category_slug = serializers.CharField(source="slug")
category_slug = serializers.SlugField(source="slug")

class Meta:
model = Category
Expand All @@ -56,63 +57,22 @@ class Meta:
fields = ("id", "category_name", "slug", "image")


class CategorySerializer(CategoryLightSerializer):
"""Serializer for displaying categories."""

subcategories = SubcategoryLightSerializer(many=True, required=False)
top_three_products = serializers.SerializerMethodField()

class Meta(CategoryLightSerializer.Meta):
fields = ("id", "name", "slug", "image", "subcategories", "top_three_products")

def get_top_three_products(self, obj):
"""Shows three most popular products of a particular category."""
top_three_products_queryset = (
obj.products.select_related("category", "subcategory", "producer")
.prefetch_related("components", "tags", "promotions", "reviews")
.order_by("-orders_number")[:3]
)
return ProductSerializer(
top_three_products_queryset, many=True, context=self.context
).data


class TagLightSerializer(serializers.ModelSerializer):
"""Serializer for tags representation in product serializer."""

tag_name = serializers.CharField(source="name")
tag_slug = serializers.CharField(source="slug")
tag_slug = serializers.SlugField(source="slug")

class Meta:
model = Tag
fields = ("tag_name", "tag_slug")


class TagSerializer(TagLightSerializer):
"""Serializer for tags representation."""

top_three_products = serializers.SerializerMethodField()

class Meta(TagLightSerializer.Meta):
fields = ("id", "name", "slug", "image", "top_three_products")

def get_top_three_products(self, obj):
"""Shows three most popular products of a particular tag."""
top_three_products_queryset = (
obj.products.select_related("category", "subcategory", "producer")
.prefetch_related("components", "tags", "promotions", "reviews")
.order_by("-orders_number")[:3]
)
return ProductSerializer(
top_three_products_queryset, context=self.context, many=True
).data


class ComponentLightSerializer(serializers.ModelSerializer):
"""Serializer for components representation in product serializer."""

component_name = serializers.CharField(source="name")
component_slug = serializers.CharField(source="slug")
component_slug = serializers.SlugField(source="slug")

class Meta:
model = Component
Expand All @@ -130,7 +90,7 @@ class ProducerLightSerializer(serializers.ModelSerializer):
"""Serializer for produsers representation in product serializer."""

producer_name = serializers.CharField(source="name")
producer_slug = serializers.CharField(source="slug")
producer_slug = serializers.SlugField(source="slug")

class Meta:
model = Producer
Expand All @@ -156,7 +116,7 @@ class PromotionLightSerializer(serializers.ModelSerializer):
"""Serializer for promotions representation in product serializer."""

promotion_name = serializers.CharField(source="name")
promotion_slug = serializers.CharField(source="slug")
promotion_slug = serializers.SlugField(source="slug")

class Meta:
model = Promotion
Expand Down Expand Up @@ -233,26 +193,62 @@ class Meta:
"is_favorited",
)

@classmethod
def setup_eager_loading(cls, queryset, user):
"""Perform necessary eager loading of products data."""
if user.is_anonymous:
queryset = (
queryset.select_related("category", "subcategory", "producer")
.prefetch_related(
"components",
"tags",
"promotions",
"reviews",
)
.annotate(rating=Avg("reviews__score"))
)
else:
queryset = (
queryset.select_related("category", "subcategory", "producer")
.prefetch_related(
"components",
"tags",
"promotions",
"reviews",
)
.annotate(
rating=Avg("reviews__score"),
favorited=Exists(
FavoriteProduct.objects.filter(
user=user, product=OuterRef("id")
)
),
)
)
return queryset

@extend_schema_field(bool)
def get_is_favorited(self, obj):
def get_is_favorited(self, obj) -> bool:
request = self.context.get("request")
if not request or request.user.is_anonymous:
return False
return obj.is_favorited(request.user)
return obj.favorited

@extend_schema_field(int)
def get_promotion_quantity(self, obj):
def get_promotion_quantity(self, obj) -> int:
return obj.promotions.count()

@extend_schema_field(float)
def get_final_price(self, obj):
def get_final_price(self, obj) -> float:
return obj.final_price

def get_rating(self, obj):
product_reviews = obj.reviews.all()
if product_reviews:
return round(product_reviews.aggregate(Avg("score"))["score__avg"], 1)
return NO_RATING_MESSAGE
def get_rating(self, obj) -> float:
"""Rounds and returns the annotated rating value."""
return (
round(obj.rating, RATING_DECIMAL_PLACES)
if obj.rating is not None
else obj.rating
)


class ProductCreateSerializer(ProductSerializer):
Expand Down Expand Up @@ -361,7 +357,7 @@ class Meta:
)

@extend_schema_field(float)
def get_final_price(self, obj):
def get_final_price(self, obj) -> float:
return obj.final_price


Expand Down Expand Up @@ -389,3 +385,47 @@ class FavoriteProductCreateSerializer(serializers.ModelSerializer):
class Meta:
model = FavoriteProduct
fields = ("id",)


class CategorySerializer(CategoryLightSerializer):
"""Serializer for displaying categories."""

subcategories = SubcategoryLightSerializer(many=True, required=False)
top_three_products = serializers.SerializerMethodField()

class Meta(CategoryLightSerializer.Meta):
fields = ("id", "name", "slug", "image", "subcategories", "top_three_products")

# TODO: need to solve n+1 problem
# TODO: possibly need to create an endpoint without this field
@swagger_serializer_method(serializer_or_field=ProductSerializer(many=True))
def get_top_three_products(self, obj):
"""Shows three most popular products of a particular category."""
top_three_products_queryset = ProductSerializer.setup_eager_loading(
obj.products.order_by("-orders_number")[:3],
self.context.get("request").user,
)
return ProductSerializer(
top_three_products_queryset, many=True, context=self.context
).data


class TagSerializer(TagLightSerializer):
"""Serializer for tags representation."""

top_three_products = serializers.SerializerMethodField()

class Meta(TagLightSerializer.Meta):
fields = ("id", "name", "slug", "image", "top_three_products")

# TODO: need to solve n+1 problem
@swagger_serializer_method(serializer_or_field=ProductSerializer(many=True))
def get_top_three_products(self, obj):
"""Shows three most popular products of a particular tag."""
top_three_products_queryset = ProductSerializer.setup_eager_loading(
obj.products.order_by("-orders_number")[:3],
self.context.get("request").user,
)
return ProductSerializer(
top_three_products_queryset, context=self.context, many=True
).data
15 changes: 7 additions & 8 deletions backend/api/products_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
ValidationErrorResponseSerializer,
)
from drf_yasg.utils import swagger_auto_schema
from rest_framework import decorators, permissions, response, status, viewsets
from rest_framework import decorators, filters, permissions, response, status, viewsets

from .filters import ProductFilter
from .mixins import MESSAGE_ON_DELETE, DestroyWithPayloadMixin
Expand Down Expand Up @@ -504,13 +504,12 @@ class ProductViewSet(DestroyWithPayloadMixin, viewsets.ModelViewSet):
"""Viewset for products."""

http_method_names = ["get", "post", "patch", "delete"]
queryset = Product.objects.select_related(
"category", "subcategory", "producer"
).prefetch_related("components", "tags", "promotions", "reviews")
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [IsAdminOrReadOnly]
filter_backends = [rf_filters.DjangoFilterBackend]
filter_backends = [rf_filters.DjangoFilterBackend, filters.OrderingFilter]
filterset_class = ProductFilter
ordering = ["pk"]
pagination_class = CustomPageNumberPagination

def get_serializer_class(self):
Expand All @@ -523,9 +522,9 @@ def get_serializer_class(self):
return ProductSerializer

def get_queryset(self):
return Product.objects.select_related(
"category", "subcategory", "producer"
).prefetch_related("components", "tags", "promotions", "reviews")
return ProductSerializer.setup_eager_loading(
Product.objects.all(), self.request.user
)

@transaction.atomic
def create_delete_or_scold(self, model, product, request):
Expand Down
Loading