перепеши сериалайзер и аction subscribe для модели...

Creato il: 14 febbraio 2025

Risposto usando O1 da Chat01

Domanda

перепеши сериалайзер и аction subscribe для модели class Follow(models.Model):
"""Модель для отображения подписок."""
user = models.ForeignKey( # from_user/текущий пользователь
User,
on_delete=models.CASCADE,
related_name='follower',
verbose_name='Подписчик'
)
author = models.ForeignKey( # to_user/на кого подписан
User,
on_delete=models.CASCADE,
related_name='author', # following
verbose_name='Автор'
)
is_subscribed = models.BooleanField(
default=False,
blank=True,
verbose_name='Наличие подписки')

text
class Meta: verbose_name = 'Подписка' verbose_name_plural = 'Подписки' constraints = [ models.UniqueConstraint( fields=['user', 'author'], name='%(app_label)s_%(class)s_unique_follow' ), models.CheckConstraint( name="%(app_label)s_%(class)s_prevent_self_follow", check=~models.Q(user=models.F('author')) ) ] def __str__(self): return (f'{self.user} подписан на {self.author}')

from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import TagViewSet, IngredientViewSet, RecipeViewSet
from foodgram_users.views import UserViewSet

router = DefaultRouter()

router.register(r'tags', TagViewSet, basename='tags') # работает
router.register(r'ingredients', IngredientViewSet, basename='ingredients') # работает
router.register(r'recipes', RecipeViewSet, basename='recipes')
router.register(r'users', UserViewSet, basename='users') #

router.register(r'users/me/avatar', UserViewSet, basename='avatar')

urlpatterns = [
path('', include(router.urls)),
path('', include('djoser.urls')), # стандартная обработка страниц входа
path('auth/', include('djoser.urls.authtoken'))
]
class FollowListSerializer(serializers.ModelSerializer):
"""
Сериализатор отрисовывает сраницу -- СПИСОК "Мои подписки"
Используемые адреса:
список всех подписок -- api/users/subscriptions/
"""
is_subscribed = serializers.SerializerMethodField()
recipes = SerializerMethodField() # возможно через slug related fid правильнее было бы
recipes_count = SerializerMethodField() # найти количество рецептов автора

text
class Meta: # поля наследуемого сериализатора будут выведены без дополнительного указания на них model = Follow fields = ( 'id', 'username', 'first_name', 'last_name', 'email', 'is_subscribed', 'avatar', 'recipes_count', 'recipes' ) def validate_author(self, value): # по аналогии точно также МОЖНО ПОДПРАВИТЬ ПРОВЕРКА ЧТО ПОДПИСКА УЖЕ ЕСТЬ """ На страницы рецепты, собственные рецепты тоже есть. Запрет на подписку. """ current_user = self.context['request'].user if current_user == value: raise serializers.ValidationError( 'Подписаться на себя нельзя.') return value def get_is_subscribed(self, obj): """Возвращает True если пользователь подписан на автора.""" current_user = self.context['request'].user if current_user.is_anonymous: return False return current_user.follower.filter(author=obj).exists() def get_recipes(self, obj): """Получаем все рецептовы автора""" # Нужен сереалайзер для отображения полученных рецептов # добавить ограничение на вывод кличества рецептов через limit (посмотреть что есть во фронтенде) from foodgram_api.serializers import ListRecipeUsersSerializer recipes_author = obj.recipe.all() request = self.context.get('request') limit = request.query_params['recipes_limit'] # request.query_params.get('recipes_limit') if limit: try: recipes_author = recipes_author[:int(limit)] except ValueError: pass serializer = ListRecipeUsersSerializer(recipes_author, many=True) # name, image, coocking_time -- микро сериалайзер return serializer.data def get_recipes_count(self, obj): """Определяет количество рецептов автора на которого подписан""" return obj.recipe.count() # related_name='recipe' .filter()

class FollowUserSerializer(EditUserSerializer):
"""
Сериализатор для отображения сраницы рецептов конкретного пользователя
Профиль пользователя api/users/{id}/ GET
Текущий пользователь api/users/me/ GET ->почти тоже
"""
user = serializers.SlugRelatedField(
queryset=User.objects.all(),
source='follower', many=True, # queryset=User.objects.all(),
slug_field='username',
default=serializers.CurrentUserDefault() # Текущий пользователь
)
author = serializers.SlugRelatedField(
slug_field='username', many=True,
source='following',
queryset=User.objects.all(), # список авторов
)

text
class Meta: # поля наследуемого сериализатора будут выведены без дополнительного указания на них model = Follow fields = ( 'id', 'author', 'user' ) validators = ( UniqueTogetherValidator( queryset=Follow.objects.all(), fields=('author', 'user'), message=('Подписка на этого автора уже добавлена.') ), )

User = get_user_model()

class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = EditUserSerializer
pagination_class = CustomPagination

text
def get_serializer_class(self): if self.action in ('list', 'retrieve'): return EditUserSerializer return RegistrationUserSerializer @action(detail=False, methods=['get'], #agination_class=None, permission_classes=(IsAuthenticated,)) def me(self, request): serializer = EditUserSerializer(request.user, context={'request': request}) return Response(serializer.data, status=status.HTTP_200_OK) @action(detail=False, methods=['post'], permission_classes=[IsAuthenticated]) def set_password(self, request, *args, **kwargs): serializer = SetPasswordSerializer( data=request.data, context={'request': request}) serializer.is_valid(raise_exception=True) self.request.user.set_password(serializer.data['new_password']) self.request.user.save() return Response('Пароль изменен', status=status.HTTP_204_NO_CONTENT) @action( detail=True, methods=['post', 'delete'], permission_classes=[IsAuthenticated] ) def subscribe(self, request, id=None): """ Подписка/отписка на автора с id=kwargs['id']. """ user = request.user author = get_object_or_404(User, id=id) if request.method == 'POST': if user == author: return Response( {'errors': 'Нельзя подписаться на самого себя.'}, status=status.HTTP_400_BAD_REQUEST ) # Проверяем, нет ли уже такой подписки. # Либо get_or_create, либо проверка существования: if Follow.objects.filter(user=user, author=author).exists(): return Response({ 'errors': 'Вы уже подписаны на данного пользователя' }, status=status.HTTP_400_BAD_REQUEST) follow = Follow.objects.create(user=user, author=author) serializer = FollowUserSerializer( follow, context={'request': request} ) # Если DELETE follow = get_object_or_404(Follow, user=user, author=author) follow.delete() return Response(serializer.data, status=status.HTTP_204_NO_CONTENT) '''def subscribe(self, request, **kwargs): # limit_param = request.query_params.get('recipes_limit') ----- лимит user = request.user author = get_object_or_404(User, id=self.kwargs.get('id')) if request.method == 'POST': serializer = FollowUserSerializer( data=request.data, context={'request': request, 'user': user, 'author': author} ) serializer.is_valid(raise_exception=True) Follow.objects.create(user=user, author=author) return Response(serializer.data, status=status.HTTP_201_CREATED) if request.method == 'DELETE': subscription = get_object_or_404( Follow, user=user, author=author ) subscription.delete() return Response(status=status.HTTP_204_NO_CONTENT)''' @action( detail=False, permission_classes=[IsAuthenticated] ) def subscriptions(self, request): queryset = User.objects.filter(follower__user=request.user) pages = self.paginate_queryset(queryset) serializer = FollowListSerializer(pages, many=True, context={'request': request}) return self.get_paginated_response(serializer.data) @action( detail=True, methods=['put', 'delete'], permission_classes=[IsAuthenticated]) def avatar(self, request, **kwargs): user = get_object_or_404(User, pk=request.user.id) if request.method == 'PUT': serializers = EditUserSerializer( user, data=request.data, partial=True, context={'request': request} ) serializers.is_valid(raise_exception=True) serializers.save() return Response({'avatar': serializers.data.get('avatar')}, status=HTTP_200_OK) user.avatar = None user.save() return Response(status=status.HTTP_204_NO_CONTENT)

сейчас получаю ошибку

FAILED
Статус-код ответа должен быть 201 | AssertionError: Запрос зарегистрированного пользователя на создание подписки должен вернуть ответ со статус-кодом 201: expected 'Internal Server Error' to deeply equal 'Created'
FAILED
Структура ответа должна соответствовать ожидаемой | JSONError: Unexpected token '<' at 1:1 <!DOCTYPE html> ^
|

Pensiero

I’m piecing together the requirements for the "subscribe" action in a Django REST framework API to create, handle errors for existing subscriptions, and manage self-subscribing issues.

I'm thinking through the user's request to return the "FollowListSerializer" or a specialized version, while addressing the incomplete or incorrect code. The 500 error might stem from testing or incorrect expectations with POST requests.

I’m diagnosing a missing return statement in the POST request block. This could lead to a 500 error. I’m adding the return statement to craft a response with status 201.

I’m mapping out the request methods to enhance separation between POST and DELETE actions. Ensuring the correct serializer, like FollowListSerializer, is crucial for returning the desired user data format.

I’m peering into the complexities of using serializers for the "subscribe" endpoint, considering FollowListSerializer for returning author data with subscription status. Ensuring clarity in the response is key.

I'm selectively narrowing down the integration of FollowListSerializer to ensure it captures user fields and addressing the "Internal Server Error" by revealing the POST and DELETE logic.

Clarifying endpoint logic

I’m detailing the "subscribe" endpoint, differentiating POST and DELETE actions, and aligning functional and URL paths to ensure smooth operation and accurate responses.

Examining serializer logic

I’m analyzing the FollowListSerializer fields. It uses the Follow model, but appears to require user model fields. Adjusting the model or adding subfields is necessary.

Revising serialization logic

Drafting a new FollowListSerializer to explicitly target the User model fields, including is_subscribed and recipes, to ensure accurate data representation.

Revisiting serializer structure

I’m rewriting the serializer to ensure it includes fields like id, username, first_name, last_name, email, is_subscribed, recipes, recipes_count, and that the Follow model references these fields correctly.

Combining efforts

I’m aligning the SubscriptionSerializer with the view logic, ensuring it incorporates subscription validation, user fields, and recipe details, leading to a comprehensive solution.

Reasoned for 38 seconds

Risposta

Ниже приведён один из рабочих вариантов, как можно переписать сериализатор для одной подписки (когда мы хотим получить данные автора, на которого только что подписались) и сам метод subscribe, чтобы при POST возвращался статус-код 201 с нужной структурой, а при DELETE204, без данных в теле ответа.

1. Сериализатор для отображения данных автора, на которого подписываемся

Чаще всего по требованиям тестов (и фронтенда) при создании подписки нужно вернуть данные пользователя (автора), включая поля id, username, first_name, last_name, email, а также признак is_subscribed и краткий список его рецептов (обычно recipes, recipes_count). При этом модель Follow здесь напрямую не нужна, потому что выводим мы поля из модели пользователя и лишь проверяем через Follow, подписан ли текущий пользователь.

Пример такого «короткого» сериализатора (условно назовём его SubscriptionSerializer). Он будет работать с моделью User, но внутри вычислять, подписан ли текущий пользователь:

python
from rest_framework import serializers from django.shortcuts import get_object_or_404 from django.contrib.auth import get_user_model from recipes.models import Recipe from .models import Follow # ваша модель подписки User = get_user_model() class RecipeShortSerializer(serializers.ModelSerializer): """ Короткий сериализатор для вывода рецептов автора в списке подписок. Обычно включает только нужные поля, например name, image, cooking_time. """ class Meta: model = Recipe fields = ('id', 'name', 'image', 'cooking_time') class SubscriptionSerializer(serializers.ModelSerializer): """ Сериализатор для вывода данных автора (User), на которого подписывается текущий пользователь. """ is_subscribed = serializers.SerializerMethodField() recipes = RecipeShortSerializer(many=True, source='recipe', read_only=True) recipes_count = serializers.SerializerMethodField() class Meta: model = User fields = ( 'id', 'username', 'first_name', 'last_name', 'email', 'is_subscribed', 'recipes', 'recipes_count' ) def get_is_subscribed(self, obj): """Проверяем, подписан ли request.user на данного автора (obj).""" user = self.context['request'].user if user.is_anonymous: return False return Follow.objects.filter(user=user, author=obj).exists() def get_recipes_count(self, obj): """Подсчитываем число рецептов у автора (obj).""" return obj.recipe.count()

Обратите внимание:

  1. В model = User мы указываем модель пользователя.
  2. В методе get_is_subscribed обращаемся к модели Follow, чтобы проверить наличие связи user=текущий_пользователь, author=obj.
  3. Похожие методы можно использовать для recipes_limit, если нужно выводить не все рецепты, а обрезать их до лимита.
  4. source='recipe' в поле recipes ставим тот related_name, который прописан у вас в модели Recipe (обычно related_name='recipe' или related_name='recipes'). Убедитесь, что совпадает с реальным related_name в модели.

2. Переписанный метод subscribe во viewset

В UserViewSet добавьте/исправьте метод-экшен так, чтобы он явно различал POST и DELETE и возвращал корректные статусы:

python
from rest_framework.decorators import action from rest_framework.response import Response from rest_framework import status from django.shortcuts import get_object_or_404 from rest_framework.permissions import IsAuthenticated # ... class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all() serializer_class = EditUserSerializer pagination_class = CustomPagination # ... @action( detail=True, methods=['post', 'delete'], permission_classes=[IsAuthenticated], url_path='subscribe' ) def subscribe(self, request, pk=None): """ POST /users/{id}/subscribe/ -> подписаться на автора (id) DELETE /users/{id}/subscribe/ -> отписаться от автора (id) """ user = request.user author = get_object_or_404(User, pk=pk) if request.method == 'POST': # Проверки if user == author: return Response( {'errors': 'Нельзя подписаться на самого себя.'}, status=status.HTTP_400_BAD_REQUEST ) if Follow.objects.filter(user=user, author=author).exists(): return Response( {'errors': 'Вы уже подписаны на этого пользователя.'}, status=status.HTTP_400_BAD_REQUEST ) # Создаём запись в модели Follow Follow.objects.create(user=user, author=author) # Возвращаем данные об авторе через наш SubscriptionSerializer serializer = SubscriptionSerializer(author, context={'request': request}) return Response(serializer.data, status=status.HTTP_201_CREATED) # Иначе DELETE subscription = get_object_or_404(Follow, user=user, author=author) subscription.delete() return Response(status=status.HTTP_204_NO_CONTENT)

Ключевые моменты:

  1. Разделили if request.method == 'POST': ... return ... и else: ... (DELETE) на два разных участка кода, чтобы не смешивать логику.
  2. При POST после создания подписки возвращаем SubscriptionSerializer(author), а не саму запись Follow. Тесты (и фронтенд) обычно хотят видеть данные профиля автора.
  3. Статус-код при успешном создании – 201 CREATED, а при удалении – 204 NO CONTENT.
  4. При ошибках подписки на себя или при дублировании подписки – 400 BAD_REQUEST.

3. Сериализатор для списка подписок (страница "Мои подписки")

Чтобы вывести список всех подписок, вы уже использовали что-то вроде FollowListSerializer. Но поскольку фронтенд чаще ожидает те же поля пользователя, что и при одиночном запросе, логика может быть очень похожей на SubscriptionSerializer. Основное отличие в том, что QuerySet у нас в таком случае обычно из User, отфильтрованных по подпискам:

python
# views.py @action( detail=False, permission_classes=[IsAuthenticated] ) def subscriptions(self, request): """ GET /users/subscriptions/ Список авторов, на которых подписан текущий пользователь. """ # Ищем всех пользователей, которые являются author # в связке Follow, где user = request.user queryset = User.objects.filter(author__user=request.user) # Применяем пагинацию pages = self.paginate_queryset(queryset) # Используем тот же сериализатор, что и при одиночном запросе serializer = SubscriptionSerializer(pages, many=True, context={'request': request}) return self.get_paginated_response(serializer.data)
  • Тут важно учесть, какой related_name вы используете в модели Follow:
    python
    author = models.ForeignKey( User, on_delete=models.CASCADE, related_name='author', # ... )
    Тогда у автора есть author_set, но вы, видимо, назвали его author. Часто делают что-то типа related_name='following', но у вас иначе. В любом случае нужно, чтобы User.objects.filter(author__user=request.user) корректно находил авторов.

Итог

  • Вместо хранения в сериализаторе FollowListSerializer полей, которых нет в модели Follow, правильнее взять модель User, а через методы get_is_subscribed и т.д. проверять связь.
  • В экшене subscribe нужно явно разделять POST и DELETE, возвращать нужные статус-коды и в случае успеха для POST сериализовать именно автора, а не объект подписки.

Таким образом, тест, который ожидает 201 Created с полями пользователя, перестанет ломаться на 500 Internal Server Error, а при повторном запросе на подписку (если уже есть подписка) или попытке подписаться на себя вы получите `400 Bad

Warning: The response is not complete, would not consume credits.

Domanda

FAILED
Статус-код ответа должен быть 400 | AssertionError: При попытке пользователя удалить несуществующую подписку должен вернуться ответ со статусом 400: expected 'Not Found' to deeply equal 'Bad Request'
FAILED
Статус-код ответа должен быть 400 | AssertionError: Запрос пользователя на удаление из избранного рецепта, который не был туда добавлен, должен вернуть ответ со статусом 400: expected 'Not Found' to deeply equal 'Bad Request'

Необходимо добавить проверку в action favorite и subscribe
@action(
detail=True,
methods=['post', 'delete'],
permission_classes=[IsAuthenticated],
url_path='favorite')
def favorite(self, request, pk):
recipe = get_object_or_404(Recipe, pk=pk)
if request.method == 'POST':
if Favorite.objects.filter(user=request.user, recipe=recipe).exists():
return Response(
{'errors': 'Рецепт уже в избранном.'},
status=status.HTTP_400_BAD_REQUEST
)
Favorite.objects.create(user=request.user, recipe=recipe)
serializer = ListRecipeUsersSerializer(
recipe,
context={'request': request}
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
if request.method == 'DELETE':
favorite_obj = get_object_or_404(
Favorite,
user=request.user,
recipe=recipe
)
deleted_count, _ = favorite_obj.delete()
if deleted_count:
return Response(
{'detail': 'Рецепт успешно удален из избранного.'},
status=status.HTTP_204_NO_CONTENT)
return Response(
{'detail': 'Рецепт не найден в избранном.'},
status=status.HTTP_400_BAD_REQUEST
)
def subscribe(self, request, pk=None):
"""
Подписка/отписка на автора с id=kwargs['id'].
"""
user = request.user
author = get_object_or_404(User, pk=pk)
limit_param = request.query_params.get('recipes_limit')
if request.method == 'POST':
if user == author:
return Response(
{'errors': 'Нельзя подписаться на самого себя.'},
status=status.HTTP_400_BAD_REQUEST
)
# Проверяем, нет ли уже подписки
follow, created = Follow.objects.get_or_create(user=user, author=author)
if not created:
return Response(
{'errors': 'Вы уже подписаны на этого автора.'},
status=status.HTTP_400_BAD_REQUEST
)
serializer = FollowListSerializer(
author, context={'request': request,
'limit_param': limit_param}
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
if request.method == 'DELETE':
follow = get_object_or_404(Follow, user=user, author=author)
follow.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

Pensiero

Addressing subscription errors

I’m noticing the error test suggests returning a 400 status for non-existing subscriptions or favorites instead of a 404. I’m thinking through adjusting the 'DELETE' method in Django to check if the item exists first.

Formulating a solution

I’m working through a method to handle non-existing favorites or subscriptions by checking their existence before attempting deletion, ensuring a 400 status is returned if they don’t exist.

Reasoned for 4 seconds

Risposta

Нужно понимать, что get_object_or_404(...) при отсутствии записи в БД автоматически выбрасывает 404 Not Found, а по вашим тестам при попытке удалить несуществующую подписку (или рецепт из избранного) должен возвращаться статус-код 400 Bad Request.

Поэтому, когда тесты хотят вместо «404 Not Found» ответить «400 Bad Request», нельзя использовать get_object_or_404() – нужно вручную проверять наличие записи в базе. Ниже приведён пример, как изменить методы favorite и subscribe, чтобы при удалении не существующей подписки/избранного возвращался код 400.


1. Пример изменения favorite

Чтобы при DELETE /favorite/ вернуть 400 для несуществующего избранного, вместо get_object_or_404 используем фильтрацию и проверку:

python
@action( detail=True, methods=['post', 'delete'], permission_classes=[IsAuthenticated], url_path='favorite' ) def favorite(self, request, pk): recipe = get_object_or_404(Recipe, pk=pk) if request.method == 'POST': if Favorite.objects.filter(user=request.user, recipe=recipe).exists(): return Response( {'errors': 'Рецепт уже в избранном.'}, status=status.HTTP_400_BAD_REQUEST ) Favorite.objects.create(user=request.user, recipe=recipe) serializer = ListRecipeUsersSerializer( recipe, context={'request': request} ) return Response(serializer.data, status=status.HTTP_201_CREATED) # Если DELETE favorite_obj = Favorite.objects.filter( user=request.user, recipe=recipe ).first() if not favorite_obj: # Здесь возвращаем 400, а не 404 return Response( {'errors': 'Рецепт не найден в избранном.'}, status=status.HTTP_400_BAD_REQUEST ) favorite_obj.delete() return Response( {'detail': 'Рецепт успешно удален из избранного.'}, status=status.HTTP_204_NO_CONTENT )

Что изменилось:

  1. Вместо get_object_or_404(Favorite, ...) мы используем Favorite.objects.filter(...).first().
  2. Если favorite_obj равен None, то возвращаем 400.
  3. Если нашли объект – удаляем и возвращаем 204.

2. Пример изменения subscribe

По аналогии, в методе подписки при DELETE нужно вернуть 400, если подписки нет:

python
def subscribe(self, request, pk=None): """ Подписка/отписка на автора с id=pk. """ user = request.user author = get_object_or_404(User, pk=pk) limit_param = request.query_params.get('recipes_limit') if request.method == 'POST': if user == author: return Response( {'errors': 'Нельзя подписаться на самого себя.'}, status=status.HTTP_400_BAD_REQUEST ) follow, created = Follow.objects.get_or_create(user=user, author=author) if not created: return Response( {'errors': 'Вы уже подписаны на этого автора.'}, status=status.HTTP_400_BAD_REQUEST ) serializer = FollowListSerializer( author, context={'request': request, 'limit_param': limit_param} ) return Response(serializer.data, status=status.HTTP_201_CREATED) # Если DELETE follow = Follow.objects.filter(user=user, author=author).first() if not follow: # Возвращаем 400, а не 404 return Response( {'errors': 'Подписка не найдена.'}, status=status.HTTP_400_BAD_REQUEST ) follow.delete() return Response(status=status.HTTP_204_NO_CONTENT)

Что изменилось:

  1. При DELETE вместо get_object_or_404 используем .filter(...).first().
  2. Если результат None, возвращаем 400 с сообщением "Подписка не найдена.".
  3. Если запись есть – удаляем и возвращаем 204 NO_CONTENT.

3. Резюме

  • Причина проблемы: тесты хотят 400 Bad Request, а get_object_or_404(...) даёт 404 Not Found.
  • Решение: перед удалением несуществующей записи вместо get_object_or_404 вручную проверять наличие и, если запись не найдена, возвращать 400.

Таким образом, тесты вроде:

При попытке пользователя удалить несуществующую подписку должен вернуться ответ со статусом 400
При удалении из избранного рецепта, который не был туда добавлен, должен вернуться ответ со статусом 400

начнут проходить без ошибок.

Condividi questa Q&A