Skip to content

Commit

Permalink
Merge pull request #216 from healthy-food-and-dietary-products/featur…
Browse files Browse the repository at this point in the history
…e/recipe_additional_fields

Feature/recipe additional fields
  • Loading branch information
earlinn authored Dec 25, 2023
2 parents 65b7397 + 886bf97 commit 8061693
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 127 deletions.
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

0 comments on commit 8061693

Please sign in to comment.