diff --git a/partner_programs/permissions.py b/partner_programs/permissions.py index b96037df..ec2bc4c7 100644 --- a/partner_programs/permissions.py +++ b/partner_programs/permissions.py @@ -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() diff --git a/partner_programs/urls.py b/partner_programs/urls.py index 3a361024..423d24d5 100644 --- a/partner_programs/urls.py +++ b/partner_programs/urls.py @@ -6,6 +6,7 @@ PartnerProgramDataSchema, PartnerProgramDetail, PartnerProgramList, + PartnerProgramProjectsAPIView, PartnerProgramProjectSubmitView, PartnerProgramRegister, PartnerProgramSetLiked, @@ -44,4 +45,9 @@ ProgramProjectFilterAPIView.as_view(), name="program-projects-filter", ), + path( + "/projects/", + PartnerProgramProjectsAPIView.as_view(), + name="partner-program-projects", + ), ] diff --git a/partner_programs/views.py b/partner_programs/views.py index 993a1c6d..9f981509 100644 --- a/partner_programs/views.py +++ b/partner_programs/views.py @@ -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, @@ -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() diff --git a/users/migrations/0054_alter_customuser_first_name_and_more.py b/users/migrations/0054_alter_customuser_first_name_and_more.py new file mode 100644 index 00000000..12b193ec --- /dev/null +++ b/users/migrations/0054_alter_customuser_first_name_and_more.py @@ -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": "Отчество"} + ) + ], + ), + ), + ] diff --git a/users/models.py b/users/models.py index 3f231842..d3e3a91f 100644 --- a/users/models.py +++ b/users/models.py @@ -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 @@ -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( @@ -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( @@ -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], ) diff --git a/users/serializers.py b/users/serializers.py index 0e074f9e..c43bc1d9 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -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 @@ -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() diff --git a/users/validators.py b/users/validators.py index 41929397..61e9e527 100644 --- a/users/validators.py +++ b/users/validators.py @@ -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 @@ -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): @@ -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):