From 3a7d8031081b4092adea63cb23f203b0a297b9b8 Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Wed, 27 Aug 2025 14:01:06 +0500 Subject: [PATCH 1/3] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B2=D0=B5=D0=B4?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D0=B2?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...54_alter_customuser_first_name_and_more.py | 57 +++++++++++++++++++ users/models.py | 29 ++++++++-- users/serializers.py | 14 +++++ users/validators.py | 27 ++++----- 4 files changed, 109 insertions(+), 18 deletions(-) create mode 100644 users/migrations/0054_alter_customuser_first_name_and_more.py 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..879ce134 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,19 @@ 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( + 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): From 8c1799ce7d719a2de376c6293670aacedf48d95d Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Wed, 27 Aug 2025 14:25:16 +0500 Subject: [PATCH 2/3] =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D0=B5=20=D0=B0=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D0=B0=D1=80=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5?= =?UTF-8?q?=20=D0=BD=D0=B5=20=D0=BE=D0=B1=D1=8F=D0=B7=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE=D0=B5=20=D0=BF=D1=80=D0=B8=20=D1=81?= =?UTF-8?q?=D0=B5=D1=80=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=B0=D0=BD=D1=8B=D1=85,=20=D0=BA=D0=B0=D0=BA?= =?UTF-8?q?=20=D0=B1=D1=8B=D0=BB=D0=BE=20=D0=B7=D0=B0=D0=BF=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BE=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/users/serializers.py b/users/serializers.py index 879ce134..c43bc1d9 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -703,7 +703,9 @@ class UserListSerializer( error_messages={"invalid": "Введите корректный email адрес"}, ) avatar = serializers.URLField( - error_messages={"invalid": "Введите корректный url адрес"} + required=False, + allow_null=True, + error_messages={"invalid": "Введите корректный url адрес"}, ) member = MemberSerializer(required=False) From 628e2d1f76527f8af31a5630efbce42bda75d1ba Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Mon, 1 Sep 2025 14:21:54 +0500 Subject: [PATCH 3/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=BB=D0=B2=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D1=83=D1=87=D0=BA=D0=B0=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=D0=B0=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82=D0=BE=D0=B2=20=D1=83=D1=87=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B8=D1=85=20=D0=B2=20=D0=BF?= =?UTF-8?q?=D0=B0=D1=80=D1=82=D0=BD=D1=91=D1=80=D1=81=D0=BA=D0=B8=D1=85=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B0=D0=BC=D0=BC=D0=B0=D1=85=20?= =?UTF-8?q?=D1=81=20=D1=83=D1=80=D0=BE=D0=B2=D0=BD=D0=B5=D0=BC=20=D0=B4?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- partner_programs/permissions.py | 26 ++++++++++++++++++++++++++ partner_programs/urls.py | 6 ++++++ partner_programs/views.py | 17 ++++++++++++++++- 3 files changed, 48 insertions(+), 1 deletion(-) 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()