diff --git a/backend/api/serializers.py b/backend/api/serializers.py index b0c4ab3..b87530c 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -2,8 +2,9 @@ from django.core.files.base import ContentFile from django.db.transaction import atomic -from rest_framework import serializers +from rest_framework import serializers, status from rest_framework.validators import UniqueTogetherValidator +from rest_framework.exceptions import ValidationError from recipes.models import Ingredient, IngredientsInRecipe, Recipe, Tag from users.models import Follow, User @@ -47,43 +48,68 @@ class Meta: 'is_subscribed') -class FollowSerializer(serializers.ModelSerializer): - email = serializers.ReadOnlyField(source='author.email') - id = serializers.ReadOnlyField(source='author.id') - username = serializers.ReadOnlyField(source='author.username') - first_name = serializers.ReadOnlyField(source='author.first_name') - last_name = serializers.ReadOnlyField(source='author.last_name') +class FollowRepresentationSerializer(serializers.ModelSerializer): + email = serializers.ReadOnlyField() + id = serializers.ReadOnlyField() + username = serializers.ReadOnlyField() + first_name = serializers.ReadOnlyField() + last_name = serializers.ReadOnlyField() is_subscribed = serializers.SerializerMethodField() recipes = serializers.SerializerMethodField() recipes_count = serializers.SerializerMethodField() class Meta: - model = Follow + model = User fields = ( 'email', 'id', 'username', 'first_name', 'last_name', 'is_subscribed', 'recipes', 'recipes_count' ) - validators = ( - UniqueTogetherValidator( - queryset=Follow.objects.all(), - fields=('user', 'author') - ), - ) - def get_is_subscribed(self, obj): request = self.context.get('request') if request is None or request.user.is_anonymous: return False - return obj.user.follower.exists() + return obj.follower.exists() def get_recipes(self, obj): - queryset = obj.author.recipe.all() + queryset = obj.recipe.all() serializer = RecipeInfoSerializer(queryset, many=True) return serializer.data def get_recipes_count(self, obj): - return obj.author.recipe.count() + return obj.recipe.count() + + +class FollowSerializer(serializers.ModelSerializer): + + class Meta: + model = Follow + fields = ('user', 'author') + + def to_representation(self, instance): + request = self.context.get('request') + return FollowRepresentationSerializer( + instance.author, context={'request': request} + ).data + + def create(self, validated_data): + user = self.context['request'].user + author = validated_data['author'] + + if user.follower.filter(author=author).exists(): + raise ValidationError( + 'Already subscribed', + code=status.HTTP_400_BAD_REQUEST + ) + + if user == author: + raise ValidationError( + 'Unable to subscribe to yourself', + code=status.HTTP_400_BAD_REQUEST + ) + + follow = Follow.objects.create(user=user, author=author) + return follow class IngredientSerializer(serializers.ModelSerializer): diff --git a/backend/api/views.py b/backend/api/views.py index 80c5a95..abb266e 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -70,31 +70,21 @@ def subscriptions(self, request): @action( ('POST', 'DELETE'), detail=True, permission_classes=(IsAuthenticated,) ) - def subscribe(self, request, id=None): + def subscribe(self, request, id): author = get_object_or_404(User, id=id) user = request.user - follower = user.follower.filter(author=author) - if ((request.method == 'POST') and (user.id != author.id) - and not follower.exists()): + if request.method == 'POST': serializer = FollowSerializer( - Follow.objects.create(user=user, author=author), - context={'request': request}, - ) - return Response( - serializer.data, - status=status.HTTP_201_CREATED, + data={'user': user.id, 'author': author.id}, + context={'request': request} ) + serializer.is_valid() + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) if request.method == 'DELETE': - get_object_or_404( - Follow, user=user, author=author - ).delete() + get_object_or_404(Follow, user=user, author=author).delete() return Response(status=status.HTTP_204_NO_CONTENT) - return Response( - {'errors': 'Invalid operation'}, - status=status.HTTP_400_BAD_REQUEST, - ) - class IngredientViewSet(viewsets.ModelViewSet): queryset = Ingredient.objects.all() diff --git a/backend/foodgram/settings.py b/backend/foodgram/settings.py index d05f5fd..029f77a 100644 --- a/backend/foodgram/settings.py +++ b/backend/foodgram/settings.py @@ -10,7 +10,11 @@ SECRET_KEY = os.getenv('SECRET_KEY', default='secret_key') -DEBUG = os.getenv('DEBUG', default='False') +if os.getenv('DEBUG', default='False').lower() == 'true': + DEBUG = True +else: + DEBUG = False + ALLOWED_HOSTS = ['*'] @@ -73,12 +77,14 @@ MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + # DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), # } # } + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', @@ -86,7 +92,7 @@ 'USER': os.getenv('POSTGRES_USER', 'django'), 'PASSWORD': os.getenv('POSTGRES_PASSWORD', ''), 'HOST': os.getenv('DB_HOST', ''), - 'PORT': os.getenv('DB_PORT', 5432) + 'PORT': os.getenv('DB_PORT', 5432), } } diff --git a/backend/requirements.txt b/backend/requirements.txt index f908de0..ad6aaee 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -2,41 +2,35 @@ asgiref==3.7.2 certifi==2023.7.22 cffi==1.15.1 charset-normalizer==3.2.0 -cryptography==41.0.3 +cryptography==41.0.4 defusedxml==0.7.1 Django==3.2 django-cors-headers==4.2.0 -django-filter==23.2 -django-rest-framework==0.1.0 +django-filter==23.3 django-templated-mail==1.1.1 djangorestframework==3.14.0 djangorestframework-simplejwt==5.3.0 djoser==2.2.0 drf-yasg==1.21.7 -flake8==6.1.0 gunicorn==21.2.0 idna==3.4 inflection==0.5.1 isort==5.12.0 -mccabe==0.7.0 oauthlib==3.2.2 packaging==23.1 -Pillow==10.0.0 +Pillow==10.0.1 psycopg2-binary==2.9.7 -pycodestyle==2.11.0 pycparser==2.21 -pyflakes==3.1.0 PyJWT==2.8.0 python-dotenv==1.0.0 python3-openid==3.2.0 -pytz==2023.3 +pytz==2023.3.post1 PyYAML==6.0.1 -reportlab==4.0.4 requests==2.31.0 requests-oauthlib==1.3.1 social-auth-app-django==5.3.0 social-auth-core==4.4.2 sqlparse==0.4.4 -typing_extensions==4.7.1 +typing_extensions==4.8.0 uritemplate==4.1.1 -urllib3==2.0.4 +urllib3==2.0.5 diff --git a/backend/users/migrations/0009_remove_user_role.py b/backend/users/migrations/0009_remove_user_role.py new file mode 100644 index 0000000..0d96401 --- /dev/null +++ b/backend/users/migrations/0009_remove_user_role.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2 on 2023-09-23 03:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0008_user_role'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='role', + ), + ] diff --git a/backend/users/migrations/0010_alter_user_table.py b/backend/users/migrations/0010_alter_user_table.py new file mode 100644 index 0000000..4cbdb33 --- /dev/null +++ b/backend/users/migrations/0010_alter_user_table.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2 on 2023-09-23 05:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0009_remove_user_role'), + ] + + operations = [ + migrations.AlterModelTable( + name='user', + table=None, + ), + ] diff --git a/backend/users/migrations/0011_auto_20230923_0641.py b/backend/users/migrations/0011_auto_20230923_0641.py new file mode 100644 index 0000000..35c7590 --- /dev/null +++ b/backend/users/migrations/0011_auto_20230923_0641.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2 on 2023-09-23 06:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0010_alter_user_table'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='follow', + name='unique_pair', + ), + migrations.RemoveConstraint( + model_name='follow', + name='author_not_user', + ), + ] diff --git a/backend/users/migrations/0012_auto_20230923_0703.py b/backend/users/migrations/0012_auto_20230923_0703.py new file mode 100644 index 0000000..8e1f031 --- /dev/null +++ b/backend/users/migrations/0012_auto_20230923_0703.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2 on 2023-09-23 07:03 + +from django.db import migrations, models +import django.db.models.expressions + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0011_auto_20230923_0641'), + ] + + operations = [ + migrations.AddConstraint( + model_name='follow', + constraint=models.UniqueConstraint(fields=('user', 'author'), name='unique_pair'), + ), + migrations.AddConstraint( + model_name='follow', + constraint=models.CheckConstraint(check=models.Q(_negated=True, author=django.db.models.expressions.F('user')), name='author_not_user'), + ), + ] diff --git a/backend/users/models.py b/backend/users/models.py index a1f949c..f2a32a5 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -6,14 +6,6 @@ class User(AbstractUser): """Кастомная модель пользователя.""" - ADMIN = 'admin' - MODERATOR = 'moderator' - USER = 'user' - - ROLE_CHOICES = ( - (ADMIN, 'ADMIN'), (USER, 'USER') - ) - email = models.EmailField('email адрес', unique=True) username = models.CharField( 'имя пользователя', @@ -24,9 +16,6 @@ class User(AbstractUser): first_name = models.CharField('имя', max_length=150) last_name = models.CharField('фамилия', max_length=150) password = models.CharField('пароль', max_length=150) - role = models.CharField( - max_length=15, choices=ROLE_CHOICES, default='user' - ) USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username', 'password', 'first_name', 'last_name'] @@ -37,12 +26,7 @@ def get_full_name(self): return f'{self.first_name} {self.last_name}' - @property - def is_admin(self): - return self.role == 'admin' - class Meta: - db_table = 'auth_user' ordering = ['id'] verbose_name = 'Пользователь' verbose_name_plural = 'Пользователи' @@ -79,7 +63,7 @@ class Meta: ), models.CheckConstraint( check=~models.Q(author=models.F('user')), - name='author_not_user' + name='author_not_user', ), )