Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions invites/serializers.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions projects/models.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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):
"""
Expand Down
54 changes: 43 additions & 11 deletions projects/permissions.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -79,29 +78,37 @@ 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:
if request.method in SAFE_METHODS:
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
Expand All @@ -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,
Expand Down Expand Up @@ -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()
2 changes: 1 addition & 1 deletion projects/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ def validate(self, data):

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

try:
Expand Down
3 changes: 2 additions & 1 deletion projects/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down