Skip to content

Commit

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

Feature/category list optimization
  • Loading branch information
juliana-str authored Dec 28, 2023
2 parents e7c9d61 + a98dcca commit 74e641a
Show file tree
Hide file tree
Showing 15 changed files with 150 additions and 17,113 deletions.
3 changes: 0 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@ loaddb:
collectstatic:
cd backend; python3 manage.py collectstatic --no-input

schema:
cd backend; python3 manage.py spectacular --color --file schema.yml

up-compose:
cd infra; sudo docker compose -f docker-compose.local.yml up -d

Expand Down
4 changes: 0 additions & 4 deletions backend/api/orders_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,6 @@ def validate(self, attrs):
raise serializers.ValidationError(NO_MATCH_ERROR_MESSAGE)
if attrs["delivery_method"] == Order.COURIER and Order.address is None:
raise serializers.ValidationError(COURIER_DELIVERY_ERROR_MESSAGE)
# if attrs["delivery_method"] == Order.COURIER and "delivery_point" in attrs:
# raise serializers.ValidationError(DELIVERY_ERROR_MESSAGE)
return super().validate(attrs)


Expand Down Expand Up @@ -257,8 +255,6 @@ def validate(self, attrs):
and attrs["delivery_method"] == Order.DELIVERY_POINT
):
raise serializers.ValidationError(NO_MATCH_ERROR_MESSAGE)
# if attrs["delivery_method"] == Order.COURIER and "delivery_point" in attrs:
# raise serializers.ValidationError(DELIVERY_ERROR_MESSAGE)
return super().validate(attrs)


Expand Down
153 changes: 104 additions & 49 deletions backend/api/products_serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
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 django.db.models import Avg, Exists, OuterRef, Prefetch
from rest_framework import serializers

from .users_serializers import UserLightSerializer
Expand Down Expand Up @@ -197,38 +195,31 @@ class Meta:
def setup_eager_loading(cls, queryset, user):
"""Perform necessary eager loading of products data."""
if user.is_anonymous:
queryset = (
return (
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.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) -> bool:
request = self.context.get("request")
if not request or request.user.is_anonymous:
return False
return obj.favorited

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

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

Expand Down Expand Up @@ -328,11 +319,36 @@ def validate_promotions(self, value):


class ProductPresentSerializer(serializers.ModelSerializer):
"""Serializer for short presentation products."""
"""Serializer for short presentation products in order list."""

photo = serializers.ImageField(required=False)
final_price = serializers.SerializerMethodField()
category = CategoryLightSerializer(read_only=True)

class Meta:
model = Product
fields = (
"id",
"name",
"measure_unit",
"amount",
"final_price",
"photo",
"category",
)

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


class ProductTopSerializer(serializers.ModelSerializer):
"""
Serializer for products in top_products field in lists of categories or tags."""

photo = serializers.ImageField(required=False)
final_price = serializers.SerializerMethodField()
category = CategoryLightSerializer(read_only=True)
is_favorited = serializers.SerializerMethodField()

class Meta:
model = Product
Expand All @@ -344,12 +360,19 @@ class Meta:
"final_price",
"photo",
"category",
"is_favorited",
"orders_number",
)

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

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


class ProductLightSerializer(ProductSerializer):
"""Serializer for products representation in favorite product serializer."""
Expand Down Expand Up @@ -381,23 +404,39 @@ class CategorySerializer(CategoryLightSerializer):
"""Serializer for displaying categories and their top three products."""

subcategories = SubcategoryLightSerializer(many=True, required=False)
top_three_products = serializers.SerializerMethodField()
top_products = ProductTopSerializer(many=True, source="products")

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,
fields = ("id", "name", "slug", "image", "subcategories", "top_products")

@classmethod
def setup_eager_loading(cls, queryset, user):
"""Perform necessary eager loading of categories data."""
if user.is_anonymous:
return queryset.prefetch_related(
"subcategories",
Prefetch(
"products",
queryset=Product.objects.prefetch_related("promotions").order_by(
"-orders_number"
),
),
)
return queryset.prefetch_related(
"subcategories",
Prefetch(
"products",
queryset=Product.objects.prefetch_related("promotions")
.annotate(
favorited=Exists(
FavoriteProduct.objects.filter(
user=user, product=OuterRef("id")
)
)
)
.order_by("-orders_number"),
),
)
return ProductSerializer(
top_three_products_queryset, many=True, context=self.context
).data


class CategoryBriefSerializer(CategorySerializer):
Expand All @@ -410,19 +449,35 @@ class Meta(CategorySerializer.Meta):
class TagSerializer(TagLightSerializer):
"""Serializer for tags representation."""

top_three_products = serializers.SerializerMethodField()
top_products = ProductTopSerializer(many=True, source="products", required=False)

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,
fields = ("id", "name", "slug", "image", "top_products")

@classmethod
def setup_eager_loading(cls, queryset, user):
"""Perform necessary eager loading of tags data."""
if user.is_anonymous:
return queryset.prefetch_related(
Prefetch(
"products",
queryset=Product.objects.select_related("category")
.prefetch_related("promotions")
.order_by("-orders_number"),
),
)
return queryset.prefetch_related(
Prefetch(
"products",
queryset=Product.objects.select_related("category")
.prefetch_related("promotions")
.annotate(
favorited=Exists(
FavoriteProduct.objects.filter(
user=user, product=OuterRef("id")
)
)
)
.order_by("-orders_number"),
),
)
return ProductSerializer(
top_three_products_queryset, context=self.context, many=True
).data
10 changes: 9 additions & 1 deletion backend/api/products_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class CategoryViewSet(DestroyWithPayloadMixin, viewsets.ModelViewSet):
"""Viewset for categories."""

http_method_names = ["get", "post", "patch", "delete"]
queryset = Category.objects.prefetch_related("subcategories")
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [IsAdminOrReadOnly]

Expand All @@ -121,6 +121,11 @@ def get_serializer_class(self):
return CategoryBriefSerializer
return CategorySerializer

def get_queryset(self):
return CategorySerializer.setup_eager_loading(
Category.objects.all(), self.request.user
)

# TODO: test this endpoint
@method_decorator(
name="list",
Expand Down Expand Up @@ -365,6 +370,9 @@ class TagViewSet(DestroyWithPayloadMixin, viewsets.ModelViewSet):
serializer_class = TagSerializer
permission_classes = [IsAdminOrReadOnly]

def get_queryset(self):
return TagSerializer.setup_eager_loading(Tag.objects.all(), self.request.user)


@method_decorator(
name="list",
Expand Down
Empty file removed backend/api/tests/__init__.py
Empty file.
16 changes: 0 additions & 16 deletions backend/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
from django.urls import include, path, re_path
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularRedocView,
SpectacularSwaggerView,
)
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions
Expand Down Expand Up @@ -56,17 +51,6 @@
path("", include("djoser.urls")),
path("token/login/", CustomTokenCreateView.as_view(), name="login"),
path("token/logout/", CustomTokenDestroyView.as_view(), name="logout"),
path("schema/", SpectacularAPIView.as_view(), name="schema"),
path(
"schema/swagger/",
SpectacularSwaggerView.as_view(url_name="api:schema"),
name="swagger-ui",
),
path(
"schema/redoc/",
SpectacularRedocView.as_view(url_name="api:schema"),
name="redoc",
),
]

schema_view = get_schema_view(
Expand Down
7 changes: 2 additions & 5 deletions backend/api/users_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from djoser.serializers import UserCreateSerializer as DjoserUserCreateSerializer
from djoser.serializers import UserDeleteSerializer as DjoserUserDeleteSerializer
from djoser.serializers import UserSerializer as DjoserUserSerializer
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from rest_framework.validators import UniqueValidator

Expand Down Expand Up @@ -41,8 +40,7 @@ class Meta:
"password": {"required": True},
}

@extend_schema_field(str)
def get_city(self, obj):
def get_city(self, obj) -> str:
return city_choices[0][0]


Expand Down Expand Up @@ -71,8 +69,7 @@ class Meta:
"photo",
)

@extend_schema_field(int)
def get_address_quantity(self, obj):
def get_address_quantity(self, obj) -> int:
return obj.addresses.count()

@transaction.atomic
Expand Down
2 changes: 0 additions & 2 deletions backend/api/users_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from djoser.views import TokenCreateView as DjoserTokenCreateView
from djoser.views import TokenDestroyView as DjoserTokenDestroyView
from djoser.views import UserViewSet as DjoserUserViewSet
from drf_spectacular.utils import OpenApiParameter, extend_schema
from drf_standardized_errors.openapi_serializers import (
ErrorResponse401Serializer,
ErrorResponse403Serializer,
Expand Down Expand Up @@ -42,7 +41,6 @@
},
),
)
@extend_schema(parameters=[OpenApiParameter("id", int, OpenApiParameter.PATH)])
class AddressViewSet(viewsets.ReadOnlyModelViewSet):
"""Viewset for addresses."""

Expand Down
26 changes: 1 addition & 25 deletions backend/good_food/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@
"rest_framework.authtoken",
"django_cleanup.apps.CleanupSelectedConfig",
"corsheaders",
"drf_spectacular",
"drf_standardized_errors",
"django_filters",
"drf_yasg",
Expand Down Expand Up @@ -197,30 +196,7 @@
"EXCEPTION_HANDLER": "drf_standardized_errors.handler.exception_handler",
}

SPECTACULAR_SETTINGS = {
"TITLE": "Good Food API",
"DESCRIPTION": "API documentation for the GoodFood project",
"VERSION": "v1",
"SERVE_INCLUDE_SCHEMA": False,
"COMPONENT_SPLIT_REQUEST": True,
"ENUM_NAME_OVERRIDES": {
"ValidationErrorEnum": "drf_standardized_errors.openapi_serializers.ValidationErrorEnum.values",
"ClientErrorEnum": "drf_standardized_errors.openapi_serializers.ClientErrorEnum.values",
"ServerErrorEnum": "drf_standardized_errors.openapi_serializers.ServerErrorEnum.values",
"ErrorCode401Enum": "drf_standardized_errors.openapi_serializers.ErrorCode401Enum.values",
"ErrorCode403Enum": "drf_standardized_errors.openapi_serializers.ErrorCode403Enum.values",
"ErrorCode404Enum": "drf_standardized_errors.openapi_serializers.ErrorCode404Enum.values",
"ErrorCode405Enum": "drf_standardized_errors.openapi_serializers.ErrorCode405Enum.values",
"ErrorCode406Enum": "drf_standardized_errors.openapi_serializers.ErrorCode406Enum.values",
"ErrorCode415Enum": "drf_standardized_errors.openapi_serializers.ErrorCode415Enum.values",
"ErrorCode429Enum": "drf_standardized_errors.openapi_serializers.ErrorCode429Enum.values",
"ErrorCode500Enum": "drf_standardized_errors.openapi_serializers.ErrorCode500Enum.values",
},
"POSTPROCESSING_HOOKS": [
"drf_standardized_errors.openapi_hooks.postprocess_schema_enums"
],
}

# Email settings
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.yandex.ru"
EMAIL_PORT = 587
Expand Down
Loading

0 comments on commit 74e641a

Please sign in to comment.