Skip to content
26 changes: 26 additions & 0 deletions partner_programs/permissions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
from rest_framework.permissions import BasePermission

from partner_programs.models import PartnerProgram


class IsProjectLeader(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.project.leader == request.user


class IsExpertOrManagerOfProgram(BasePermission):
"""
Доступ разрешён только экспертам и менеджерам конкретной программы.
"""

def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False

program_id = view.kwargs.get("pk")
if not program_id:
return False

try:
program = PartnerProgram.objects.get(pk=program_id)
except PartnerProgram.DoesNotExist:
return False

if program.is_manager(request.user):
return True

return program.experts.filter(user=request.user).exists()
6 changes: 6 additions & 0 deletions partner_programs/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
PartnerProgramDataSchema,
PartnerProgramDetail,
PartnerProgramList,
PartnerProgramProjectsAPIView,
PartnerProgramProjectSubmitView,
PartnerProgramRegister,
PartnerProgramSetLiked,
Expand Down Expand Up @@ -44,4 +45,9 @@
ProgramProjectFilterAPIView.as_view(),
name="program-projects-filter",
),
path(
"<int:pk>/projects/",
PartnerProgramProjectsAPIView.as_view(),
name="partner-program-projects",
),
]
17 changes: 16 additions & 1 deletion partner_programs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
PartnerProgramUserProfile,
)
from partner_programs.pagination import PartnerProgramPagination
from partner_programs.permissions import IsProjectLeader
from partner_programs.permissions import IsExpertOrManagerOfProgram, IsProjectLeader
from partner_programs.serializers import (
PartnerProgramDataSchemaSerializer,
PartnerProgramFieldSerializer,
Expand Down Expand Up @@ -424,3 +424,18 @@ def post(self, request, pk):
projects, many=True, context={"request": request}
)
return paginator.get_paginated_response(serializer_out.data)


class PartnerProgramProjectsAPIView(generics.ListAPIView):
"""
Список всех проектов участников конкретной партнёрской программы.
Доступ разрешён только менеджерам и экспертам программы.
"""

serializer_class = ProjectListSerializer
permission_classes = [IsAuthenticated, IsExpertOrManagerOfProgram]
pagination_class = PartnerProgramPagination

def get_queryset(self):
program = get_object_or_404(PartnerProgram, pk=self.kwargs["pk"])
return Project.objects.filter(program_links__partner_program=program).distinct()
57 changes: 57 additions & 0 deletions users/migrations/0054_alter_customuser_first_name_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Generated by Django 4.2.11 on 2025-08-20 07:55

from django.db import migrations, models
import functools
import users.validators


class Migration(migrations.Migration):

dependencies = [
("users", "0053_customuser_is_mospolytech_student_and_more"),
]

operations = [
migrations.AlterField(
model_name="customuser",
name="first_name",
field=models.CharField(
max_length=255,
validators=[
functools.partial(
users.validators.user_name_validator, *(), **{"field_name": "Имя"}
)
],
),
),
migrations.AlterField(
model_name="customuser",
name="last_name",
field=models.CharField(
max_length=255,
validators=[
functools.partial(
users.validators.user_name_validator,
*(),
**{"field_name": "Фамилия"}
)
],
),
),
migrations.AlterField(
model_name="customuser",
name="patronymic",
field=models.CharField(
blank=True,
max_length=255,
null=True,
validators=[
functools.partial(
users.validators.user_name_validator,
*(),
**{"field_name": "Отчество"}
)
],
),
),
]
29 changes: 24 additions & 5 deletions users/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from functools import partial

from django.contrib.auth.models import AbstractUser
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from django.db import models
from django.db.models import QuerySet
from django.utils import timezone
Expand Down Expand Up @@ -60,9 +63,18 @@ class CustomUser(AbstractUser):
INVESTOR = constants.INVESTOR

username = None
email = models.EmailField(unique=True)
first_name = models.CharField(max_length=255, validators=[user_name_validator])
last_name = models.CharField(max_length=255, validators=[user_name_validator])
email = models.EmailField(
unique=True,
error_messages={
"unique": "Пользователь с таким email уже существует",
},
)
first_name = models.CharField(
max_length=255, validators=[partial(user_name_validator, field_name="Имя")]
)
last_name = models.CharField(
max_length=255, validators=[partial(user_name_validator, field_name="Фамилия")]
)
password = models.CharField(max_length=255)
is_active = models.BooleanField(default=False, editable=False)
user_type = models.PositiveSmallIntegerField(
Expand All @@ -74,7 +86,10 @@ class CustomUser(AbstractUser):
editable=False,
)
patronymic = models.CharField(
max_length=255, validators=[user_name_validator], null=True, blank=True
max_length=255,
validators=[partial(user_name_validator, field_name="Отчество")],
null=True,
blank=True,
)
# TODO need to be removed in future `key_skills` -> `skills`.
key_skills = models.CharField(
Expand All @@ -87,7 +102,11 @@ class CustomUser(AbstractUser):
"core.SkillToObject",
related_query_name="users",
)
avatar = models.URLField(null=True, blank=True)
avatar = models.URLField(
null=True,
blank=True,
validators=[URLValidator(message="Введите корректный URL")],
)
birthday = models.DateField(
validators=[user_birthday_validator],
)
Expand Down
16 changes: 16 additions & 0 deletions users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.forms.models import model_to_dict
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.validators import UniqueValidator
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

from core.models import Skill, SkillToObject, Specialization, SpecializationCategory
Expand Down Expand Up @@ -692,6 +693,21 @@ class Meta:
class UserListSerializer(
serializers.ModelSerializer[CustomUser], SkillsWriteSerializerMixin
):
email = serializers.EmailField(
validators=[
UniqueValidator(
queryset=CustomUser.objects.all(),
message="Пользователь с таким email уже существует",
)
],
error_messages={"invalid": "Введите корректный email адрес"},
)
avatar = serializers.URLField(
required=False,
allow_null=True,
error_messages={"invalid": "Введите корректный url адрес"},
)

member = MemberSerializer(required=False)
is_online = serializers.SerializerMethodField()

Expand Down
27 changes: 14 additions & 13 deletions users/validators.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import phonenumbers
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils import timezone
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from django.core.exceptions import ValidationError as DjangoValidationError

import phonenumbers

from users.constants import NOT_VALID_NUMBER_MESSAGE

Expand All @@ -18,19 +17,19 @@ def user_birthday_validator(birthday):
raise ValidationError("Человек младше 12 лет")


def user_name_validator(name):
"""returns true if name is valid"""
# TODO: add check for vulgar words
def user_name_validator(value, field_name="Поле"):
"""Валидатор для имени, фамилии и отчества"""
if not value:
return

valid_name_chars = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ"
for letter in name:
for letter in value:
if letter.upper() not in valid_name_chars:
raise ValidationError(
"Имя содержит недопустимые символы. Могут быть только символы кириллического алфавита."
raise DjangoValidationError(
f"{field_name} содержит недопустимые символы. Могут быть только символы кириллического алфавита."
)
if len(name) < 2:
raise ValidationError("Имя слишком короткое")
return True
if len(value) < 2:
raise DjangoValidationError(f"Поле '{field_name}' слишком короткое")


def specialization_exists_validator(pk: int):
Expand All @@ -49,7 +48,9 @@ def user_experience_years_range_validator(value: int):
(2000 - `now.year`)
"""
if value not in range(2000, timezone.now().year + 1):
raise DjangoValidationError(f"Год должен быть в диапазоне 2000 - {timezone.now().year}")
raise DjangoValidationError(
f"Год должен быть в диапазоне 2000 - {timezone.now().year}"
)


def user_phone_number_validation(value: str):
Expand Down