From 0b5055a9f1b43e065bce0527404d3fbaa98e6009 Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Fri, 5 Sep 2025 15:54:50 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=BB=D0=B2=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=83=D1=80=D0=BE=D0=B2=D0=BD=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=BE=D0=B2=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D0=B3=D0=BB=D0=B0=D1=88=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B2=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=20=D0=B8?= =?UTF-8?q?=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B5=20=D0=BD=D0=B0=20=D1=83?= =?UTF-8?q?=D1=87=D0=B0=D1=81=D1=82=D0=B8=D0=B5=20=D0=B2=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B3=D1=80=D0=B0=D0=BC=D0=BC=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- invites/serializers.py | 44 +++++++++++++++++++++++++++++++++ projects/models.py | 31 +++++++++++++++++++++++ projects/permissions.py | 54 ++++++++++++++++++++++++++++++++--------- projects/serializers.py | 2 +- projects/views.py | 3 ++- 5 files changed, 121 insertions(+), 13 deletions(-) diff --git a/invites/serializers.py b/invites/serializers.py index 1ea64c59..1152ed73 100644 --- a/invites/serializers.py +++ b/invites/serializers.py @@ -1,6 +1,8 @@ +from django.apps import apps from rest_framework import serializers from invites.models import Invite +from projects.models import Collaborator from projects.serializers import ProjectListSerializer from users.serializers import UserDetailSerializer @@ -18,6 +20,48 @@ class Meta: "is_accepted", ] + def validate(self, attrs): + project = attrs["project"] + user = attrs["user"] + + if project.leader_id == user.id: + raise serializers.ValidationError( + {"user": "Пользователь уже является лидером проекта."} + ) + + if Collaborator.objects.filter(project=project, user=user).exists(): + raise serializers.ValidationError( + {"user": "Пользователь уже состоит в проекте."} + ) + + if Invite.objects.filter( + project=project, user=user, is_accepted__isnull=True + ).exists(): + raise serializers.ValidationError( + {"user": "У пользователя уже есть активное приглашение в этот проект."} + ) + + link = project.program_links.select_related("partner_program").first() + if link: + PartnerProgramUserProfile = apps.get_model( + "partner_programs", "PartnerProgramUserProfile" + ) + is_participant = PartnerProgramUserProfile.objects.filter( + user_id=user.id, + partner_program_id=link.partner_program_id, + ).exists() + if not is_participant: + raise serializers.ValidationError( + { + "user": ( + "Нельзя пригласить пользователя: проект относится к программе, " + "а пользователь не является её участником." + ) + } + ) + + return attrs + class InviteDetailSerializer(serializers.ModelSerializer[Invite]): user = UserDetailSerializer(many=False, read_only=True) diff --git a/projects/models.py b/projects/models.py index 9bc99bfc..92b4c8d2 100644 --- a/projects/models.py +++ b/projects/models.py @@ -1,7 +1,9 @@ from typing import Optional +from django.apps import apps from django.contrib.auth import get_user_model from django.contrib.contenttypes.fields import GenericRelation +from django.core.exceptions import ValidationError from django.core.validators import ( MaxLengthValidator, MaxValueValidator, @@ -310,6 +312,35 @@ class Meta: ) ] + def clean(self): + """ + Если проект привязан к программе, добавлять коллаборатора можно + только если пользователь — участник этой программы. + (Проект привязан максимум к одной программе.) + """ + link = self.project.program_links.select_related("partner_program").first() + if not link: + return + + PartnerProgramUserProfile = apps.get_model( + "partner_programs", + "PartnerProgramUserProfile", + ) + + is_participant = PartnerProgramUserProfile.objects.filter( + user_id=self.user_id, + partner_program_id=link.partner_program_id, + ).exists() + + if not is_participant: + raise ValidationError( + "Пользователь не является участником программы, к которой относится проект." + ) + + def save(self, *args, **kwargs): + self.full_clean() + return super().save(*args, **kwargs) + class ProjectNews(models.Model): """ diff --git a/projects/permissions.py b/projects/permissions.py index 4a18a68a..18b28556 100644 --- a/projects/permissions.py +++ b/projects/permissions.py @@ -1,12 +1,11 @@ -from datetime import timedelta, datetime +from datetime import datetime, timedelta from django.utils import timezone +from rest_framework.exceptions import PermissionDenied, ValidationError +from rest_framework.permissions import SAFE_METHODS, BasePermission -from rest_framework.permissions import BasePermission, SAFE_METHODS -from rest_framework.exceptions import PermissionDenied - +from partner_programs.models import PartnerProgram, PartnerProgramUserProfile from projects.models import Project -from partner_programs.models import PartnerProgramUserProfile class IsProjectLeaderOrReadOnlyForNonDrafts(BasePermission): @@ -79,6 +78,7 @@ class TimingAfterEndsProgramPermission(BasePermission): for `_SECONDS_AFTER_CANT_EDIT` seconds -> days from the end of the program. If the project is not in program or the request in `SAFE_METHODS` -> allowed. """ + _SECONDS_AFTER_CANT_EDIT: int = 60 * 60 * 24 * 30 # Now 30 days. def has_object_permission(self, request, view, obj) -> bool: @@ -86,22 +86,29 @@ def has_object_permission(self, request, view, obj) -> bool: return True program_profile = ( - PartnerProgramUserProfile.objects - .filter(user=request.user, project=obj) + PartnerProgramUserProfile.objects.filter(user=request.user, project=obj) .select_related("partner_program") .first() ) moscow_time: datetime = timezone.localtime(timezone.now()) if program_profile: - date_from_end_program: timedelta = (moscow_time - program_profile.partner_program.datetime_finished) + date_from_end_program: timedelta = ( + moscow_time - program_profile.partner_program.datetime_finished + ) days_from_end_program: int = date_from_end_program.days seconds_from_end_program: int = date_from_end_program.total_seconds() if 0 <= seconds_from_end_program <= self._SECONDS_AFTER_CANT_EDIT: - raise PermissionDenied(detail=self._prepare_exception_detail(days_from_end_program, program_profile)) + raise PermissionDenied( + detail=self._prepare_exception_detail( + days_from_end_program, program_profile + ) + ) return True - def _prepare_exception_detail(self, days_from_end_program: int, program_profile: PartnerProgramUserProfile): + def _prepare_exception_detail( + self, days_from_end_program: int, program_profile: PartnerProgramUserProfile + ): """ Prepare response body when `PermissionDenied` exception raised: program_name: str -> Program title @@ -112,7 +119,11 @@ def _prepare_exception_detail(self, days_from_end_program: int, program_profile: when_can_edit: datetime = timezone.localtime( datetime_finished + timedelta(seconds=self._SECONDS_AFTER_CANT_EDIT) ) - days_until_resolution: int = int(self._SECONDS_AFTER_CANT_EDIT / 60 / 60 / 24) - days_from_end_program - 1 + days_until_resolution: int = ( + int(self._SECONDS_AFTER_CANT_EDIT / 60 / 60 / 24) + - days_from_end_program + - 1 + ) return { "program_name": program_profile.partner_program.name, "when_can_edit": when_can_edit, @@ -140,3 +151,24 @@ def has_object_permission(self, request, view, obj): ) or obj.project.leader == request.user: return True return False + + +class CanBindProjectToProgram(BasePermission): + message = "Привязать проект к программе может только её участник (или менеджер)." + + def has_permission(self, request, view): + program_id = (request.data or {}).get("partner_program_id") + if not program_id: + return True + + try: + program = PartnerProgram.objects.get(pk=program_id) + except PartnerProgram.DoesNotExist: + raise ValidationError({"partner_program_id": "Программа не найдена."}) + + if program.is_manager(request.user): + return True + + return PartnerProgramUserProfile.objects.filter( + user=request.user, partner_program=program + ).exists() diff --git a/projects/serializers.py b/projects/serializers.py index b6e409bf..abb9bcae 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -387,7 +387,7 @@ def validate(self, data): if project.leader != request.user: raise serializers.ValidationError( - "Только лидер проекта может дублировать его в программу." + {"error": "Только лидер проекта может дублировать его в программу."} ) try: diff --git a/projects/views.py b/projects/views.py index 7fbdce6b..c843b61f 100644 --- a/projects/views.py +++ b/projects/views.py @@ -33,6 +33,7 @@ from projects.models import Achievement, Collaborator, Project, ProjectNews from projects.pagination import ProjectNewsPagination, ProjectsPagination from projects.permissions import ( + CanBindProjectToProgram, HasInvolvementInProjectOrReadOnly, IsNewsAuthorIsProjectLeaderOrReadOnly, IsProjectLeader, @@ -659,7 +660,7 @@ def patch(self, request, project_pk: int, user_to_leader_pk: int) -> Response: class DuplicateProjectView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated, CanBindProjectToProgram] @swagger_auto_schema( request_body=ProjectDuplicateRequestSerializer,