Skip to content

Commit 0b5055a

Browse files
committed
Добалвены уровни доступов при приглашении в проект и заявке на участие в программе
1 parent 99ce799 commit 0b5055a

5 files changed

Lines changed: 121 additions & 13 deletions

File tree

invites/serializers.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from django.apps import apps
12
from rest_framework import serializers
23

34
from invites.models import Invite
5+
from projects.models import Collaborator
46
from projects.serializers import ProjectListSerializer
57
from users.serializers import UserDetailSerializer
68

@@ -18,6 +20,48 @@ class Meta:
1820
"is_accepted",
1921
]
2022

23+
def validate(self, attrs):
24+
project = attrs["project"]
25+
user = attrs["user"]
26+
27+
if project.leader_id == user.id:
28+
raise serializers.ValidationError(
29+
{"user": "Пользователь уже является лидером проекта."}
30+
)
31+
32+
if Collaborator.objects.filter(project=project, user=user).exists():
33+
raise serializers.ValidationError(
34+
{"user": "Пользователь уже состоит в проекте."}
35+
)
36+
37+
if Invite.objects.filter(
38+
project=project, user=user, is_accepted__isnull=True
39+
).exists():
40+
raise serializers.ValidationError(
41+
{"user": "У пользователя уже есть активное приглашение в этот проект."}
42+
)
43+
44+
link = project.program_links.select_related("partner_program").first()
45+
if link:
46+
PartnerProgramUserProfile = apps.get_model(
47+
"partner_programs", "PartnerProgramUserProfile"
48+
)
49+
is_participant = PartnerProgramUserProfile.objects.filter(
50+
user_id=user.id,
51+
partner_program_id=link.partner_program_id,
52+
).exists()
53+
if not is_participant:
54+
raise serializers.ValidationError(
55+
{
56+
"user": (
57+
"Нельзя пригласить пользователя: проект относится к программе, "
58+
"а пользователь не является её участником."
59+
)
60+
}
61+
)
62+
63+
return attrs
64+
2165

2266
class InviteDetailSerializer(serializers.ModelSerializer[Invite]):
2367
user = UserDetailSerializer(many=False, read_only=True)

projects/models.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from typing import Optional
22

3+
from django.apps import apps
34
from django.contrib.auth import get_user_model
45
from django.contrib.contenttypes.fields import GenericRelation
6+
from django.core.exceptions import ValidationError
57
from django.core.validators import (
68
MaxLengthValidator,
79
MaxValueValidator,
@@ -310,6 +312,35 @@ class Meta:
310312
)
311313
]
312314

315+
def clean(self):
316+
"""
317+
Если проект привязан к программе, добавлять коллаборатора можно
318+
только если пользователь — участник этой программы.
319+
(Проект привязан максимум к одной программе.)
320+
"""
321+
link = self.project.program_links.select_related("partner_program").first()
322+
if not link:
323+
return
324+
325+
PartnerProgramUserProfile = apps.get_model(
326+
"partner_programs",
327+
"PartnerProgramUserProfile",
328+
)
329+
330+
is_participant = PartnerProgramUserProfile.objects.filter(
331+
user_id=self.user_id,
332+
partner_program_id=link.partner_program_id,
333+
).exists()
334+
335+
if not is_participant:
336+
raise ValidationError(
337+
"Пользователь не является участником программы, к которой относится проект."
338+
)
339+
340+
def save(self, *args, **kwargs):
341+
self.full_clean()
342+
return super().save(*args, **kwargs)
343+
313344

314345
class ProjectNews(models.Model):
315346
"""

projects/permissions.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
from datetime import timedelta, datetime
1+
from datetime import datetime, timedelta
22

33
from django.utils import timezone
4+
from rest_framework.exceptions import PermissionDenied, ValidationError
5+
from rest_framework.permissions import SAFE_METHODS, BasePermission
46

5-
from rest_framework.permissions import BasePermission, SAFE_METHODS
6-
from rest_framework.exceptions import PermissionDenied
7-
7+
from partner_programs.models import PartnerProgram, PartnerProgramUserProfile
88
from projects.models import Project
9-
from partner_programs.models import PartnerProgramUserProfile
109

1110

1211
class IsProjectLeaderOrReadOnlyForNonDrafts(BasePermission):
@@ -79,29 +78,37 @@ class TimingAfterEndsProgramPermission(BasePermission):
7978
for `_SECONDS_AFTER_CANT_EDIT` seconds -> days from the end of the program.
8079
If the project is not in program or the request in `SAFE_METHODS` -> allowed.
8180
"""
81+
8282
_SECONDS_AFTER_CANT_EDIT: int = 60 * 60 * 24 * 30 # Now 30 days.
8383

8484
def has_object_permission(self, request, view, obj) -> bool:
8585
if request.method in SAFE_METHODS:
8686
return True
8787

8888
program_profile = (
89-
PartnerProgramUserProfile.objects
90-
.filter(user=request.user, project=obj)
89+
PartnerProgramUserProfile.objects.filter(user=request.user, project=obj)
9190
.select_related("partner_program")
9291
.first()
9392
)
9493
moscow_time: datetime = timezone.localtime(timezone.now())
9594

9695
if program_profile:
97-
date_from_end_program: timedelta = (moscow_time - program_profile.partner_program.datetime_finished)
96+
date_from_end_program: timedelta = (
97+
moscow_time - program_profile.partner_program.datetime_finished
98+
)
9899
days_from_end_program: int = date_from_end_program.days
99100
seconds_from_end_program: int = date_from_end_program.total_seconds()
100101
if 0 <= seconds_from_end_program <= self._SECONDS_AFTER_CANT_EDIT:
101-
raise PermissionDenied(detail=self._prepare_exception_detail(days_from_end_program, program_profile))
102+
raise PermissionDenied(
103+
detail=self._prepare_exception_detail(
104+
days_from_end_program, program_profile
105+
)
106+
)
102107
return True
103108

104-
def _prepare_exception_detail(self, days_from_end_program: int, program_profile: PartnerProgramUserProfile):
109+
def _prepare_exception_detail(
110+
self, days_from_end_program: int, program_profile: PartnerProgramUserProfile
111+
):
105112
"""
106113
Prepare response body when `PermissionDenied` exception raised:
107114
program_name: str -> Program title
@@ -112,7 +119,11 @@ def _prepare_exception_detail(self, days_from_end_program: int, program_profile:
112119
when_can_edit: datetime = timezone.localtime(
113120
datetime_finished + timedelta(seconds=self._SECONDS_AFTER_CANT_EDIT)
114121
)
115-
days_until_resolution: int = int(self._SECONDS_AFTER_CANT_EDIT / 60 / 60 / 24) - days_from_end_program - 1
122+
days_until_resolution: int = (
123+
int(self._SECONDS_AFTER_CANT_EDIT / 60 / 60 / 24)
124+
- days_from_end_program
125+
- 1
126+
)
116127
return {
117128
"program_name": program_profile.partner_program.name,
118129
"when_can_edit": when_can_edit,
@@ -140,3 +151,24 @@ def has_object_permission(self, request, view, obj):
140151
) or obj.project.leader == request.user:
141152
return True
142153
return False
154+
155+
156+
class CanBindProjectToProgram(BasePermission):
157+
message = "Привязать проект к программе может только её участник (или менеджер)."
158+
159+
def has_permission(self, request, view):
160+
program_id = (request.data or {}).get("partner_program_id")
161+
if not program_id:
162+
return True
163+
164+
try:
165+
program = PartnerProgram.objects.get(pk=program_id)
166+
except PartnerProgram.DoesNotExist:
167+
raise ValidationError({"partner_program_id": "Программа не найдена."})
168+
169+
if program.is_manager(request.user):
170+
return True
171+
172+
return PartnerProgramUserProfile.objects.filter(
173+
user=request.user, partner_program=program
174+
).exists()

projects/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ def validate(self, data):
387387

388388
if project.leader != request.user:
389389
raise serializers.ValidationError(
390-
"Только лидер проекта может дублировать его в программу."
390+
{"error": "Только лидер проекта может дублировать его в программу."}
391391
)
392392

393393
try:

projects/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from projects.models import Achievement, Collaborator, Project, ProjectNews
3434
from projects.pagination import ProjectNewsPagination, ProjectsPagination
3535
from projects.permissions import (
36+
CanBindProjectToProgram,
3637
HasInvolvementInProjectOrReadOnly,
3738
IsNewsAuthorIsProjectLeaderOrReadOnly,
3839
IsProjectLeader,
@@ -659,7 +660,7 @@ def patch(self, request, project_pk: int, user_to_leader_pk: int) -> Response:
659660

660661

661662
class DuplicateProjectView(APIView):
662-
permission_classes = [IsAuthenticated]
663+
permission_classes = [IsAuthenticated, CanBindProjectToProgram]
663664

664665
@swagger_auto_schema(
665666
request_body=ProjectDuplicateRequestSerializer,

0 commit comments

Comments
 (0)