class FavoriteAndShoppingCart(models.Model): "...
생성일: 2025년 2월 12일
생성일: 2025년 2월 12일
class FavoriteAndShoppingCart(models.Model):
"""Модель связывает рецепт и пользователя."""
recipe = models.ForeignKey(
Recipe, on_delete=models.CASCADE,
related_name='favorite_recipes', # для доступа к объектам исходной модели
verbose_name='Рецепт')
user = models.ForeignKey(
User, on_delete=models.CASCADE,
related_name='favorite_recipes',
verbose_name='Пользователь'
)
is_in_shopping_cart = models.BooleanField(
default=False,
blank=True,
verbose_name='Рецепт в корзине')
is_favorited = models.BooleanField(
default=False,
blank=True,
verbose_name='Избранные рецепты')
textclass Meta: verbose_name = 'Выбранный рецепт' verbose_name_plural = 'Выбранные рецепты' ordering = ['recipe', 'user'] constraints = [ models.UniqueConstraint( fields=['recipe', 'user'], name='%(app_label)s_%(class)s_unique_relationships' ), models.CheckConstraint( name='%(app_label)s_%(class)s_prevent_self_favorite', check=~models.Q(recipe=models.F('user')) ) ] def __str__(self): return (f'{self.user} выбрал рецепт {self.recipe} как: \ любимый рецепт {self.is_favorited}, \ рецепт для покупок ингридиентов {self.is_in_shopping_cart}')
нужно написать ViewSet на добавление рецепта к орзину
Ниже приведён один из возможных вариантов реализации ViewSet для добавления рецепта в корзину покупок. Предположим, у вас уже есть сериализатор FavoriteAndShoppingCartSerializer (или вы используете RecipeSerializer, если нужно вернуть данные о самом рецепте).
Чаще всего в подобных задачах удобно использовать метод create или отдельный экшен (через декоратор @action
) в ViewSet’е, где вы:
is_in_shopping_cart = True
и сохраняете объект.Пример (используем GenericViewSet + @action
):
pythonfrom rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from .models import Recipe, FavoriteAndShoppingCart from .serializers import RecipeSerializer # Или ваш кастомный сериализатор class ShoppingCartViewSet(viewsets.GenericViewSet): """ ViewSet для управления добавлением рецептов в корзину покупок. """ queryset = FavoriteAndShoppingCart.objects.all() permission_classes = (IsAuthenticated,) @action(detail=True, methods=['post'], url_path='add-to-cart') def add_to_cart(self, request, pk=None): """ POST /shopping-cart/{pk}/add-to-cart/ Добавляет рецепт в корзину (is_in_shopping_cart = True). """ # Проверяем, что рецепт с таким pk существует try: recipe = Recipe.objects.get(pk=pk) except Recipe.DoesNotExist: return Response( {'detail': 'Рецепт не найден.'}, status=status.HTTP_404_NOT_FOUND ) # Получаем или создаём связь между пользователем и рецептом fav_cart_obj, created = FavoriteAndShoppingCart.objects.get_or_create( user=request.user, recipe=recipe ) # Если уже в корзине, возвращаем 400 if fav_cart_obj.is_in_shopping_cart: return Response( {'detail': 'Этот рецепт уже добавлен в корзину.'}, status=status.HTTP_400_BAD_REQUEST ) # Устанавливаем флаг "в корзине" и сохраняем fav_cart_obj.is_in_shopping_cart = True fav_cart_obj.save() # Возвращаем информацию о рецепте (или можете вернуть сериализатор самой связи) serializer = RecipeSerializer(recipe, context={'request': request}) return Response(serializer.data, status=status.HTTP_201_CREATED) @action(detail=True, methods=['delete'], url_path='remove-from-cart') def remove_from_cart(self, request, pk=None): """ DELETE /shopping-cart/{pk}/remove-from-cart/ Убирает рецепт из корзины (is_in_shopping_cart = False). """ try: recipe = Recipe.objects.get(pk=pk) except Recipe.DoesNotExist: return Response( {'detail': 'Рецепт не найден.'}, status=status.HTTP_404_NOT_FOUND ) try: fav_cart_obj = FavoriteAndShoppingCart.objects.get( user=request.user, recipe=recipe ) except FavoriteAndShoppingCart.DoesNotExist: return Response( {'detail': 'У вас не было этого рецепта в корзине.'}, status=status.HTTP_400_BAD_REQUEST ) if not fav_cart_obj.is_in_shopping_cart: return Response( {'detail': 'У вас не было этого рецепта в корзине.'}, status=status.HTTP_400_BAD_REQUEST ) fav_cart_obj.is_in_shopping_cart = False fav_cart_obj.save() return Response(status=status.HTTP_204_NO_CONTENT)
@action(detail=True, methods=['post'])
– создаёт маршрут вида /shopping-cart/<pk>/add-to-cart/
, где <pk>
– первичный ключ рецепта, который нужно добавить.add_to_cart
мы пытаемся найти рецепт, а затем получаем (или создаём) запись в таблице FavoriteAndShoppingCart
.is_in_shopping_cart=True
и сохраняем.remove_from_cart
) выставляем is_in_shopping_cart=False
.HTTP_204_NO_CONTENT
).Разумеется, вы можете адаптировать логику под свои нужды:
create
, destroy
или отдельные экшены для корзины.Но представленный пример показывает один из распространённых подходов к тому, как можно “добавлять рецепт в корзину” в рамках Django REST Framework.
class ShoppingCartSerializer(serializers.ModelSerializer):
'''Сериалайзер отображает страницу "Список покупок"
рецептов в списке покупок может: много или один.
исопользуется для добавления/удаления рецептов из списка покупок
после скачивания списка покупок, обнулить ведь его надо? - добавить на вью
'''
user = serializers.SlugRelatedField(
queryset=User.objects.all(),
source='favorite_recipes', many=True,
slug_field='usersfavorites',
default=serializers.CurrentUserDefault() # Текущий пользователь
)
recipe = serializers.SlugRelatedField(
slug_field='favoriterecipes', many=True,
source='favorite_recipes',
queryset=Recipe.objects.all(), # список рецептов текущего пользователя
)
is_in_shopping_cart = serializers.SerializerMethodField()
textclass Meta: model = FavoriteAndShoppingCart fields = ['image', 'name', 'cooking_time', 'is_in_shopping_cart'] def get_is_in_shopping_cart(self, obj): # ---- возможно можно сделать полезнее """Функция отображает, находится ли рецепт в корзине покупок текущего пользователя. Возвращает False если текущий пользователь не авторизован. """ current_user = self.context.get('request').user if current_user.is_anonymous: return False return current_user.favorite_recipes.filter( # проверить выход через рeлатед нейм recipe=obj ).exists()
Перепиши ответ с использованием этого сериализатора
Ниже приведён пример, как можно переписать логику добавления/удаления рецепта в корзину, используя ваш ShoppingCartSerializer
. Предположим, что основная идея в том, чтобы при добавлении рецепта в корзину мы создаём (или получаем) объект FavoriteAndShoppingCart
и выставляем ему is_in_shopping_cart = True
, а при удалении — наоборот.
Обратите внимание: в самом сериализаторе у вас указаны поля ['image', 'name', 'cooking_time', 'is_in_shopping_cart']
, которые фактически принадлежат модели Recipe
, а не FavoriteAndShoppingCart
. Поэтому есть несколько вариантов, как вы можете настроить работу:
FavoriteAndShoppingCart
возвращать поля самой модели Recipe
(через recipe = RecipeSerializer()
или аналог).Recipe
, а is_in_shopping_cart
получать через дополнительное поле (например, SerializerMethodField
).FavoriteAndShoppingCart
физически хранятся ссылки на поля image
, name
, и т. п. — но обычно такого не делают, так как это дублирование данных).Ниже условный пример ViewSet’a, который показывает общую идею — как использовать ShoppingCartSerializer
при добавлении и удалении рецептов из корзины:
pythonfrom rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from .models import Recipe, FavoriteAndShoppingCart from .serializers import ShoppingCartSerializer class ShoppingCartViewSet(viewsets.GenericViewSet): """ ViewSet для управления рецептами в корзине покупок. """ queryset = FavoriteAndShoppingCart.objects.all() permission_classes = [IsAuthenticated] serializer_class = ShoppingCartSerializer @action(detail=True, methods=['post'], url_path='add-to-cart') def add_to_cart(self, request, pk=None): """ POST /shopping-cart/{pk}/add-to-cart/ Добавляет рецепт в корзину (is_in_shopping_cart = True). """ try: recipe = Recipe.objects.get(pk=pk) except Recipe.DoesNotExist: return Response( {'detail': 'Рецепт не найден.'}, status=status.HTTP_404_NOT_FOUND ) # Получаем (или создаём) связь между пользователем и рецептом. fav_cart_obj, created = FavoriteAndShoppingCart.objects.get_or_create( user=request.user, recipe=recipe ) if fav_cart_obj.is_in_shopping_cart: return Response( {'detail': 'Этот рецепт уже в корзине покупок.'}, status=status.HTTP_400_BAD_REQUEST ) fav_cart_obj.is_in_shopping_cart = True fav_cart_obj.save() # Возвращаем данные сериализатора serializer = self.get_serializer( fav_cart_obj, context={'request': request} ) return Response(serializer.data, status=status.HTTP_201_CREATED) @action(detail=True, methods=['delete'], url_path='remove-from-cart') def remove_from_cart(self, request, pk=None): """ DELETE /shopping-cart/{pk}/remove-from-cart/ Удаляет рецепт из корзины (is_in_shopping_cart = False). """ try: recipe = Recipe.objects.get(pk=pk) except Recipe.DoesNotExist: return Response( {'detail': 'Рецепт не найден.'}, status=status.HTTP_404_NOT_FOUND ) # Пытаемся найти связь именно у текущего пользователя try: fav_cart_obj = FavoriteAndShoppingCart.objects.get( user=request.user, recipe=recipe ) except FavoriteAndShoppingCart.DoesNotExist: return Response( {'detail': 'У вас не было этого рецепта в корзине.'}, status=status.HTTP_400_BAD_REQUEST ) if not fav_cart_obj.is_in_shopping_cart: return Response( {'detail': 'У вас не было этого рецепта в корзине.'}, status=status.HTTP_400_BAD_REQUEST ) fav_cart_obj.is_in_shopping_cart = False fav_cart_obj.save() return Response(status=status.HTTP_204_NO_CONTENT)
pk
существует.get_or_create
получаем экземпляр FavoriteAndShoppingCart
, если записи ещё нет, она будет создана.is_in_shopping_cart
: включаем или выключаем флаг, в зависимости от экшена (add_to_cart
или remove_from_cart
).ShoppingCartSerializer
. При удалении достаточно статуса 204 NO CONTENT
.Обратите внимание, что ваш текущий ShoppingCartSerializer
выглядит примерно так, как будто он прочитывает данные напрямую из полей image
, name
, cooking_time
(которые чаще относятся к модели Recipe
). Поэтому внутри сериализатора, скорее всего, понадобится либо:
get_image(...)
, get_name(...)
, get_cooking_time(...)
, которые берут их из obj.recipe
,recipe = RecipeSerializer(source='recipe', ...)
.Вариант с методами будет выглядеть примерно так:
pythonclass ShoppingCartSerializer(serializers.ModelSerializer): image = serializers.SerializerMethodField() name = serializers.SerializerMethodField() cooking_time = serializers.SerializerMethodField() is_in_shopping_cart = serializers.SerializerMethodField() class Meta: model = FavoriteAndShoppingCart fields = ['image', 'name', 'cooking_time', 'is_in_shopping_cart'] def get_image(self, obj): return obj.recipe.image.url if obj.recipe.image else None def get_name(self, obj): return obj.recipe.name def get_cooking_time(self, obj): return obj.recipe.cooking_time def get_is_in_shopping_cart(self, obj): """Проверяет, находится ли рецепт в корзине у текущего пользователя.""" current_user = self.context.get('request').user if current_user.is_anonymous: return False return FavoriteAndShoppingCart.objects.filter( user=current_user, recipe=obj.recipe, is_in_shopping_cart=True ).exists()
Тогда в ViewSet
мы можем возвращать fav_cart_obj
, а в сериализаторе уже подтянутся нужные данные из obj.recipe
.
Если вы хотите использовать именно SlugRelatedField
, как в вашем исходном коде, убедитесь, что:
slug_field
соответствует реальному полю в модели User
или Recipe
.source='...'
указывает на корректные связи (а не создает конфликт).Однако в большинстве случаев для пользовательских списков SlugRelatedField
применяют так:
pythonuser = serializers.SlugRelatedField( slug_field='username', read_only=True ) recipe = serializers.SlugRelatedField( slug_field='title', # например, если есть поле title read_only=True )
Или же совсем убирают эти поля из сериализатора, если достаточно возвращать вложенный объект Recipe
через отдельный сериализатор.
Таким образом, главный смысл:
add_to_cart
/ remove_from_cart
.FavoriteAndShoppingCart
, либо (чаще) показывает информацию о Recipe
, используя объект связи для определения флага is_in_shopping_cart
.Надеюсь, это поможет переписать и адаптировать логику под ваш проект.
с учётом написанного, исправь код ниже
class RecipeViewSet(viewsets.ModelViewSet):
queryset = Recipe.objects.all()
pagination_class = CustomPagination
permission_classes = (IsAuthorOrReadOnly, )
filter_backends = (DjangoFilterBackend, )
filterset_class = TagAuthorFilter
@action(detail=False, methods=['post', 'delete'], # сработает для всех проверку надо делать подругому, весь код другой
permission_classes=[IsAuthenticated])
def favorite(self, request, **kwargs):
recipe = get_object_or_404(Recipe, id=kwargs['pk'])
if request.method == 'POST':
serializer = RecipeSerializer(recipe, data=request.data,
context={"request": request})
serializer.is_valid(raise_exception=True)
if not FavoriteAndShoppingCart.objects.filter(user=request.user,
recipe=recipe,
is_favorited=True
).exists():
FavoriteAndShoppingCart.objects.create(
user=request.user, recipe=recipe
)
return Response(serializer.data,
status=status.HTTP_201_CREATED)
return Response({'errors': 'Рецепт уже в избранном.'},
status=status.HTTP_400_BAD_REQUEST)
if request.method == 'DELET':
get_object_or_404(FavoriteAndShoppingCart, user=request.user,
recipe=recipe).delete()
return Response({'detail': 'Рецепт успешно удален из избранного.'},
status=status.HTTP_204_NO_CONTENT)
Ниже приведён пример, как можно переработать метод favorite
в рамках вашего RecipeViewSet
.
Основные проблемы в исходном коде:
detail=False
и использование kwargs['pk']
– если вы хотите работать с конкретным рецептом по его id
(pk), нужно использовать detail=True
(чтобы в URL был параметр /{pk}/favorite/
).'DELET'
нужно 'DELETE'
.request.data
, ведь при добавлении в избранное чаще всего дополнительных данных не отправляем.pythonfrom django.shortcuts import get_object_or_404 from rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response class RecipeViewSet(viewsets.ModelViewSet): queryset = Recipe.objects.all() pagination_class = CustomPagination permission_classes = (IsAuthorOrReadOnly, ) filter_backends = (DjangoFilterBackend, ) filterset_class = TagAuthorFilter @action( detail=True, methods=['post', 'delete'], permission_classes=[IsAuthenticated], url_path='favorite' # будет маршрутом /recipes/{pk}/favorite/ ) def favorite(self, request, pk=None): """ POST /recipes/{pk}/favorite/ -> добавить рецепт в избранное DELETE /recipes/{pk}/favorite/ -> удалить рецепт из избранного """ recipe = get_object_or_404(Recipe, pk=pk) # --- Добавление в избранное --- if request.method == 'POST': # Сериализатор только для возврата данных рецепта, # поэтому передаем сам рецепт + контекст: serializer = RecipeSerializer(recipe, context={'request': request}) # Проверяем, не добавлен ли уже в избранное if not FavoriteAndShoppingCart.objects.filter( user=request.user, recipe=recipe, is_favorited=True ).exists(): FavoriteAndShoppingCart.objects.create( user=request.user, recipe=recipe, is_favorited=True ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response( {'errors': 'Рецепт уже в избранном.'}, status=status.HTTP_400_BAD_REQUEST ) # --- Удаление из избранного --- if request.method == 'DELETE': favorite_obj = FavoriteAndShoppingCart.objects.filter( user=request.user, recipe=recipe, is_favorited=True ).first() if not favorite_obj: return Response( {'errors': 'Рецепт не найден в избранном.'}, status=status.HTTP_400_BAD_REQUEST ) favorite_obj.delete() return Response( {'detail': 'Рецепт успешно удалён из избранного.'}, status=status.HTTP_204_NO_CONTENT )
@action(detail=True)
– теперь эндпоинт будет иметь вид /recipes/<pk>/favorite/
. Использование detail=False
имеет смысл, если вы не опираетесь на pk
рецепта, но в вашем случае kwargs['pk']
говорит о том, что вы всё-таки передаёте идентификатор рецепта.POST
:
FavoriteAndShoppingCart
с is_favorited=True
.RecipeSerializer
, чтобы показать информацию о добавленном рецепте.DELETE
:
FavoriteAndShoppingCart
с is_favorited=True
.Опционально, вы можете вместо удаления самой записи просто выставлять флаг is_favorited=False
. Но если по бизнес-логике запись не нужна, её проще удалить (корзина и избранное всё равно отслеживаются через тот же объект).
Также обратите внимание, что у вас, возможно, есть аналогичная логика для корзины покупок. Вы можете вынести общие части (поиск рецепта, создание связи и т. п.) в отдельные функции, если хотите избежать дублирования кода.
Хорошее предложение, вынеси общие все части: поиск рецепта, создание связи и т.д. в отдельные функции
Ниже приведён пример переработанного RecipeViewSet
с выделением общей логики в отдельные (внутренние) методы. Это упрощает чтение кода и его поддержку.
pythonfrom django.shortcuts import get_object_or_404 from rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response class RecipeViewSet(viewsets.ModelViewSet): queryset = Recipe.objects.all() pagination_class = CustomPagination permission_classes = (IsAuthorOrReadOnly,) filter_backends = (DjangoFilterBackend,) filterset_class = TagAuthorFilter # --------------------------- # Вспомогательные методы: # --------------------------- def _get_recipe_or_404(self, pk): """ Находит рецепт по pk или возвращает 404. """ return get_object_or_404(Recipe, pk=pk) def _get_favorite_relation(self, user, recipe): """ Возвращает объект FavoriteAndShoppingCart для (user, recipe, is_favorited=True), или None, если такого объекта нет. """ return FavoriteAndShoppingCart.objects.filter( user=user, recipe=recipe, is_favorited=True ).first() def _add_recipe_to_favorites(self, user, recipe): """ Добавляет рецепт в избранное, если он ещё не там. """ # Проверяем, не добавлен ли уже if self._get_favorite_relation(user, recipe) is not None: return None # Сигнализирует, что уже было в избранном # Создаем связь return FavoriteAndShoppingCart.objects.create( user=user, recipe=recipe, is_favorited=True ) def _remove_recipe_from_favorites(self, user, recipe): """ Удаляет рецепт из избранного, если он там есть. """ favorite_obj = self._get_favorite_relation(user, recipe) if not favorite_obj: return False # Не было в избранном favorite_obj.delete() return True # --------------------------- # Основные экшены: # --------------------------- @action( detail=True, methods=['post', 'delete'], permission_classes=[IsAuthenticated], url_path='favorite' ) def favorite(self, request, pk=None): """ POST /recipes/{pk}/favorite/ -> добавить рецепт в избранное DELETE /recipes/{pk}/favorite/ -> удалить рецепт из избранного """ recipe = self._get_recipe_or_404(pk) if request.method == 'POST': # Сериализатор для возврата информации о рецепте serializer = RecipeSerializer(recipe, context={'request': request}) # Добавляем в избранное created_relation = self._add_recipe_to_favorites(request.user, recipe) if created_relation is None: # Уже был в избранном return Response( {'errors': 'Рецепт уже в избранном.'}, status=status.HTTP_400_BAD_REQUEST ) # Возвращаем данные рецепта, если успешно добавлен return Response(serializer.data, status=status.HTTP_201_CREATED) if request.method == 'DELETE': removed = self._remove_recipe_from_favorites(request.user, recipe) if not removed: # Рецепта не было в избранном return Response( {'errors': 'Рецепт не найден в избранном.'}, status=status.HTTP_400_BAD_REQUEST ) return Response( {'detail': 'Рецепт успешно удалён из избранного.'}, status=status.HTTP_204_NO_CONTENT )
Вспомогательные методы:
_get_recipe_or_404(pk)
— универсальный метод для поиска рецепта по pk
._get_favorite_relation(user, recipe)
— ищет в базе запись FavoriteAndShoppingCart
для данного пользователя и рецепта, где is_favorited=True
. Возвращает либо объект, либо None
._add_recipe_to_favorites(user, recipe)
— проверяет, есть ли связь в избранном. Если нет — создаёт. Если есть — возвращает None
(чтобы в вызывающем коде знать, что запись уже существовала)._remove_recipe_from_favorites(user, recipe)
— пытается найти запись в избранном, если существует — удаляет её и возвращает True
, иначе False
.Экшен favorite
в итоге остаётся компактным:
POST
используем _add_recipe_to_favorites
. Если _add_recipe_to_favorites
вернёт None
, значит связь уже существует, возвращаем 400. Если всё ок — сериализуем рецепт и отправляем назад.DELETE
вызываем _remove_recipe_from_favorites
. Если тот вернул False
, значит рецепт не был в избранном.Таким образом, мы добиваемся разделения ответственности: вся логика по поиску/созданию/удалению вынесена во вспомогательные методы, а в самом экшене лишь вызываем их и обрабатываем результат.
from django.shortcuts import get_object_or_404
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
class RecipeViewSet(viewsets.ModelViewSet):
queryset = Recipe.objects.all()
pagination_class = CustomPagination
permission_classes = (IsAuthorOrReadOnly, )
filter_backends = (DjangoFilterBackend, )
filterset_class = TagAuthorFilter
text@action( detail=True, methods=['post', 'delete'], permission_classes=[IsAuthenticated], url_path='favorite' # будет маршрутом /recipes/{pk}/favorite/ ) def favorite(self, request, pk=None): """ POST /recipes/{pk}/favorite/ -> добавить рецепт в избранное DELETE /recipes/{pk}/favorite/ -> удалить рецепт из избранного """ recipe = get_object_or_404(Recipe, pk=pk) # --- Добавление в избранное --- if request.method == 'POST': # Сериализатор только для возврата данных рецепта, # поэтому передаем сам рецепт + контекст: serializer = RecipeSerializer(recipe, context={'request': request}) # Проверяем, не добавлен ли уже в избранное if not FavoriteAndShoppingCart.objects.filter( user=request.user, recipe=recipe, is_favorited=True ).exists(): FavoriteAndShoppingCart.objects.create( user=request.user, recipe=recipe, is_favorited=True ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response( {'errors': 'Рецепт уже в избранном.'}, status=status.HTTP_400_BAD_REQUEST ) # --- Удаление из избранного --- if request.method == 'DELETE': favorite_obj = FavoriteAndShoppingCart.objects.filter( user=request.user, recipe=recipe, is_favorited=True ).first() if not favorite_obj: return Response( {'errors': 'Рецепт не найден в избранном.'}, status=status.HTTP_400_BAD_REQUEST ) favorite_obj.delete() return Response( {'detail': 'Рецепт успешно удалён из избранного.'}, status=status.HTTP_204_NO_CONTENT )
Напиши такой же action для Cart, и вынеси общие части в отдельные методы
Ниже приведён пример, как можно реализовать два экшена — favorite
и cart
— в одном RecipeViewSet
и при этом вынести общую логику (поиск рецепта, добавление/удаление связи) во вспомогательные методы.
Обратите внимание, что мы разделяем действия по флагам: is_favorited
и is_in_shopping_cart
. Методы _add_relation
и _remove_relation
принимают параметр field
, который указывает, какой флаг устанавливать или искать в модели FavoriteAndShoppingCart
.
pythonfrom django.shortcuts import get_object_or_404 from rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response class RecipeViewSet(viewsets.ModelViewSet): queryset = Recipe.objects.all() pagination_class = CustomPagination permission_classes = (IsAuthorOrReadOnly,) filter_backends = (DjangoFilterBackend,) filterset_class = TagAuthorFilter # --------------------------- # Вспомогательные методы: # --------------------------- def _get_recipe_or_404(self, pk): """Возвращает объект Recipe или вызывает 404, если не найден.""" return get_object_or_404(Recipe, pk=pk) def _add_relation(self, user, recipe, field): """ Добавляет связь в FavoriteAndShoppingCart для указанного поля (field), если такой связи ещё нет. Возвращает True, если добавлена новая связь, или False, если связь уже существовала. field: 'is_favorited' или 'is_in_shopping_cart' """ exists = FavoriteAndShoppingCart.objects.filter( user=user, recipe=recipe, **{field: True} ).exists() if exists: return False # Указывает, что связь уже была # Создаём новую связь FavoriteAndShoppingCart.objects.create( user=user, recipe=recipe, **{field: True} ) return True def _remove_relation(self, user, recipe, field): """ Удаляет связь из FavoriteAndShoppingCart для указанного поля (field), если такая связь есть. Возвращает True, если связь была и удалена, или False, если связи не было. field: 'is_favorited' или 'is_in_shopping_cart' """ relation_obj = FavoriteAndShoppingCart.objects.filter( user=user, recipe=recipe, **{field: True} ).first() if not relation_obj: return False relation_obj.delete() return True # --------------------------- # Экшен для избранного: # --------------------------- @action( detail=True, methods=['post', 'delete'], permission_classes=[IsAuthenticated], url_path='favorite' ) def favorite(self, request, pk=None): """ POST /recipes/{pk}/favorite/ -> добавить рецепт в избранное DELETE /recipes/{pk}/favorite/ -> удалить рецепт из избранного """ recipe = self._get_recipe_or_404(pk) # --- Добавление в избранное --- if request.method == 'POST': serializer = RecipeSerializer(recipe, context={'request': request}) added = self._add_relation(request.user, recipe, field='is_favorited') if not added: return Response( {'errors': 'Рецепт уже в избранном.'}, status=status.HTTP_400_BAD_REQUEST ) return Response(serializer.data, status=status.HTTP_201_CREATED) # --- Удаление из избранного --- if request.method == 'DELETE': removed = self._remove_relation(request.user, recipe, field='is_favorited') if not removed: return Response( {'errors': 'Рецепт не найден в избранном.'}, status=status.HTTP_400_BAD_REQUEST ) return Response( {'detail': 'Рецепт успешно удалён из избранного.'}, status=status.HTTP_204_NO_CONTENT ) # --------------------------- # Экшен для корзины покупок: # --------------------------- @action( detail=True, methods=['post', 'delete'], permission_classes=[IsAuthenticated], url_path='cart' ) def cart(self, request, pk=None): """ POST /recipes/{pk}/cart/ -> добавить рецепт в корзину покупок DELETE /recipes/{pk}/cart/ -> удалить рецепт из корзины покупок """ recipe = self._get_recipe_or_404(pk) # --- Добавление в корзину --- if request.method == 'POST': serializer = RecipeSerializer(recipe, context={'request': request}) added = self._add_relation(request.user, recipe, field='is_in_shopping_cart') if not added: return Response( {'errors': 'Рецепт уже в корзине.'}, status=status.HTTP_400_BAD_REQUEST ) return Response(serializer.data, status=status.HTTP_201_CREATED) # --- Удаление из корзины --- if request.method == 'DELETE': removed = self._remove_relation(request.user, recipe, field='is_in_shopping_cart') if not removed: return Response( {'errors': 'Рецепт не найден в корзине.'}, status=status.HTTP_400_BAD_REQUEST ) return Response( {'detail': 'Рецепт успешно удалён из корзины.'}, status=status.HTTP_204_NO_CONTENT )
Вспомогательные методы:
_get_recipe_or_404(pk)
— единый метод для поиска рецепта по первичному ключу, чтобы не дублировать код get_object_or_404(...)
в каждом экшене._add_relation(...)
/ _remove_relation(...)
— универсальные методы, куда мы передаём field
('is_favorited'
или 'is_in_shopping_cart'
). Эти методы решают задачу проверки “есть ли уже связь” и создания/удаления записи в модели FavoriteAndShoppingCart
.Экшен favorite
и экшен cart
устроены одинаково:
field=True
), возвращаем данные о рецепте через RecipeSerializer
.field=True
), возвращаем пустой ответ со статусом 204
, если удаление прошло успешно, или 400
, если рецепт там не найден.Таким образом, весь код для избранного и корзины покупок разделяет общий механизм, а различия сводятся к тому, какой именно флаг устанавливать в модели (is_favorited
или is_in_shopping_cart
).