diff --git a/backend/api/mixins.py b/backend/api/mixins.py index 7e6d11f..93a62df 100644 --- a/backend/api/mixins.py +++ b/backend/api/mixins.py @@ -53,4 +53,4 @@ def is_valid(self, *, raise_exception=False): errors_db } ) - return bool(self._errors) + return not bool(self._errors) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index ac422f4..39aac1f 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -3,6 +3,7 @@ from djoser.serializers import UserCreateSerializer, UserSerializer from drf_extra_fields.fields import Base64ImageField from rest_framework import serializers +from rest_framework.validators import UniqueTogetherValidator from taggit.models import Tag from api.utils import NonEmptyBase64ImageField, create_user @@ -19,6 +20,7 @@ Category, Organization, Project, + ProjectFavorite, ProjectIncomes, ProjectParticipants, Volunteer, @@ -165,10 +167,19 @@ class ProjectGetSerializer(serializers.ModelSerializer): event_address = AddressSerializer(read_only=True) skills = SkillsSerializer(many=True, read_only=True) - is_favorited = serializers.BooleanField(default=False) + is_favorited = serializers.SerializerMethodField() status = serializers.SerializerMethodField() city = serializers.SlugRelatedField(slug_field='name', read_only=True) + def get_is_favorited(self, obj): + request = self.context.get('request') + if request and request.user.is_authenticated: + return ProjectFavorite.objects.filter( + user=request.user, + project=obj, + ).exists() + return False + def get_status(self, data): OPEN = 'open' READY = 'ready_for_feedback' @@ -752,19 +763,40 @@ class Meta: ) -class VolunteerFavoriteGetSerializer(serializers.ModelSerializer): +# class ProjectFavoriteGetSerializer(serializers.ModelSerializer): +# """ +# Сериализатор для отображения избранных проектов. +# """ +# +# class Meta: +# model = Project +# fields = ( +# 'id', +# 'name', +# 'picture', +# 'organization', +# ) + + +class ProjectFavoriteSerializer(IsValidModifyErrorForFrontendMixin, + serializers.ModelSerializer): """ - Сериализатор для отображения избранных проектов волонтера. + Сериализатор для отображения избранных проектов. """ class Meta: - model = Project + model = ProjectFavorite fields = ( - 'id', - 'name', - 'picture', - 'organization', + 'user', + 'project', ) + validators = [ + UniqueTogetherValidator( + ProjectFavorite.objects.all(), + fields=('user', 'project'), + message='Этот проект уже присутствует в избранном!', + ) + ] class CurrentUserSerializer(UserSerializer): diff --git a/backend/api/views.py b/backend/api/views.py index 199ad81..cc708a1 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -23,9 +23,9 @@ Category, Organization, Project, + ProjectFavorite, ProjectIncomes, Volunteer, - VolunteerFavorite, ) from .filters import ( @@ -58,6 +58,7 @@ PlatformAboutSerializer, PreviewNewsSerializer, ProjectCategorySerializer, + ProjectFavoriteSerializer, ProjectGetSerializer, ProjectIncomesGetSerializer, ProjectIncomesSerializer, @@ -65,7 +66,6 @@ SkillsSerializer, TagSerializer, VolunteerCreateSerializer, - VolunteerFavoriteGetSerializer, VolunteerGetSerializer, VolunteerUpdateSerializer, ) @@ -229,53 +229,49 @@ def destroy(self, request, *args, **kwargs): instance.delete() return Response(status=status.HTTP_204_NO_CONTENT) - def add_to(self, volunteer, project, errors): + def create_favorite(self, serializer_class, user, project): """ Добавить проект в избранное. """ - _, created = VolunteerFavorite.objects.get_or_create( - volunteer=volunteer, project=project + serializer = serializer_class( + data={'user': user, 'project': project, } + ) + if serializer.is_valid(): + serializer.save() + return Response(status=status.HTTP_201_CREATED) + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST, ) - if not created: - return Response( - {'errors': errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - serializer = VolunteerFavoriteGetSerializer(instance=project) - return Response(serializer.data, status=status.HTTP_201_CREATED) - def delete_from(self, volunteer, project, errors): + def delete_favorite(self, model, user, project): """ Удалить проект из избранного. """ - cnt_deleted, _ = VolunteerFavorite.objects.filter( - volunteer=volunteer, project=project - ).delete() - - if cnt_deleted == 0: - return Response( - {'errors': errors}, - status=status.HTTP_400_BAD_REQUEST, - ) + model.objects.filter(user=user, project=project).delete() return Response(status=status.HTTP_204_NO_CONTENT) @action( ['POST', 'DELETE'], detail=True, - permission_classes=(IsVolunteer,), + permission_classes=[IsVolunteer | IsOrganizer], + serializer_class=ProjectFavoriteSerializer, ) def favorite(self, request, **kwargs): """ Избранные проекты волонтера. """ project = get_object_or_404(Project, pk=kwargs.get('pk')) - volunteer = get_object_or_404(Volunteer, user=request.user) if request.method == 'POST': - return self.add_to( - volunteer, project, 'Данный проект уже есть в избранном!' + return self.create_favorite( + serializer_class=self.serializer_class, + user=request.user.pk, + project=project.pk ) - return self.delete_from( - volunteer, project, 'Данного проекта нет в избранном!' + return self.delete_favorite( + model=ProjectFavorite, + user=request.user.pk, + project=kwargs.get('pk'), ) @action( @@ -543,7 +539,7 @@ class ProjectMeViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): def get_queryset(self): if self.request.user.is_volunteer: volunteer = get_object_or_404(Volunteer, user=self.request.user.id) - from_volunteer_favorite = VolunteerFavorite.objects.filter( + from_volunteer_favorite = ProjectFavorite.objects.filter( project=OuterRef('pk'), volunteer=volunteer ) return ( diff --git a/backend/projects/migrations/0005_projectfavorite_alter_volunteer_date_of_birth_and_more.py b/backend/projects/migrations/0005_projectfavorite_alter_volunteer_date_of_birth_and_more.py new file mode 100644 index 0000000..5338add --- /dev/null +++ b/backend/projects/migrations/0005_projectfavorite_alter_volunteer_date_of_birth_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.6 on 2023-11-19 21:04 + +import datetime +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('projects', '0004_alter_projectincomes_cover_letter'), + ] + + operations = [ + migrations.CreateModel( + name='ProjectFavorite', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project', verbose_name='Проект')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), + ], + options={ + 'verbose_name': 'Избранный проект', + 'verbose_name_plural': 'Избранные проекты', + 'default_related_name': 'project_favorite', + }, + ), + migrations.AlterField( + model_name='volunteer', + name='date_of_birth', + field=models.DateField(help_text='Введите дату в формате "ГГГГ-ММ-ДД", пример: "2000-01-01".', validators=[django.core.validators.MinValueValidator(limit_value=datetime.date(1900, 1, 1)), django.core.validators.MaxValueValidator(limit_value=datetime.date(2023, 11, 19))], verbose_name='Дата рождения'), + ), + migrations.DeleteModel( + name='VolunteerFavorite', + ), + migrations.AddConstraint( + model_name='projectfavorite', + constraint=models.UniqueConstraint(fields=('user', 'project'), name='projects_projectfavorite_unique_project_in_favorite'), + ), + ] diff --git a/backend/projects/models.py b/backend/projects/models.py index 9b548cc..959466b 100644 --- a/backend/projects/models.py +++ b/backend/projects/models.py @@ -532,21 +532,42 @@ def __str__(self): ) -class VolunteerFavorite(models.Model): +class ProjectFavorite(models.Model): """ - Модель избранных проектов волонтеров. + Модель избранных проектов пользователей. + + При добавлении проекта в избранное все поля обязательны для заполнения. + + Attributes: + user(int): + Поле ForeignKey на пользователя, у которого проект в избранном. + project(int): + Поле ForeignKey на проект, добавленный в избранное. """ - volunteer = models.ForeignKey(Volunteer, on_delete=models.CASCADE) - project = models.ForeignKey(Project, on_delete=models.CASCADE) + user = models.ForeignKey( + User, + verbose_name='Пользователь', + on_delete=models.CASCADE, + ) + project = models.ForeignKey( + Project, + verbose_name='Проект', + on_delete=models.CASCADE, + ) class Meta: + verbose_name = 'Избранный проект' + verbose_name_plural = 'Избранные проекты' + default_related_name = 'project_favorite' constraints = ( models.UniqueConstraint( - fields=('volunteer', 'project'), - name='unique_volunteer_favorites', + fields=('user', 'project'), + name='%(app_label)s_%(class)s_unique_project_in_favorite', ), ) def __str__(self): - return f'{self.volunteer} {self.project}' + return ( + f'Проект {self.project.name} в избранном у {self.user}' + )