From 333073d24974e0bda0381c640bb1a68e835199e9 Mon Sep 17 00:00:00 2001 From: Kozin Date: Wed, 27 Mar 2024 22:05:11 +0300 Subject: [PATCH 1/2] [*] Refactoring of project creation. R: Errors in the code and flaws in the project creation process. FB: Fixed bugs in the code and improved the project creation process. --- src/backend/api/v1/projects/serializers.py | 57 ++++++++++++++----- src/backend/apps/general/constants.py | 2 +- .../apps/general/migrations/0001_initial.py | 2 +- src/backend/apps/projects/apps.py | 5 ++ src/backend/apps/projects/constants.py | 5 +- .../0003_alter_direction_name_and_more.py | 40 +++++++++++++ src/backend/apps/projects/mixins.py | 4 +- src/backend/apps/projects/models.py | 6 ++ src/backend/apps/projects/signals.py | 13 +++++ 9 files changed, 113 insertions(+), 21 deletions(-) create mode 100644 src/backend/apps/projects/migrations/0003_alter_direction_name_and_more.py create mode 100644 src/backend/apps/projects/signals.py diff --git a/src/backend/api/v1/projects/serializers.py b/src/backend/api/v1/projects/serializers.py index d2191f8..eca0625 100644 --- a/src/backend/api/v1/projects/serializers.py +++ b/src/backend/api/v1/projects/serializers.py @@ -1,17 +1,19 @@ from datetime import date +from queue import Queue from typing import Any, Dict, List, Optional from django.db import transaction from rest_framework import serializers from api.v1.general.serializers import SkillSerializer, SpecialistSerializer +from apps.general.models import Skill from apps.projects.constants import BUSYNESS_CHOICES, STATUS_CHOICES from apps.projects.mixins import RecruitmentStatusMixin from apps.projects.models import Direction, Project, ProjectSpecialist class DirectionSerializer(serializers.ModelSerializer): - """Сериализатор специалиста.""" + """Сериализатор направления разработки.""" class Meta: model = Direction @@ -104,9 +106,7 @@ class WriteProjectSerializer( creator = serializers.SerializerMethodField(read_only=True) owner = serializers.SerializerMethodField(read_only=True) - project_specialists = WriteProjectSpecialistSerializer( - many=True, - ) + project_specialists = WriteProjectSpecialistSerializer(many=True) busyness = serializers.ChoiceField( choices=BUSYNESS_CHOICES, write_only=True ) @@ -186,7 +186,7 @@ def validate_ended(self, value) -> date: return self._validate_date(value, "завершения проекта") def validate_status(self, value) -> int: - """Метод валидации даты завершения проекта.""" + """Метод валидации статуса проекта.""" if value == Project.DRAFT: raise serializers.ValidationError( @@ -202,38 +202,65 @@ def validate(self, attrs) -> Dict[str, Any]: queryset = Project.objects.filter( name=attrs.get("name"), creator=self.context.get("request").user, - owner=self.context.get("request").user, ) if queryset.exists(): errors.setdefault("unique", []).append( - "У вас уже есть проект с таким названием." + "У вас уже есть проект или его черновик с таким названием." ) if attrs.get("started") > attrs.get("ended"): errors.setdefault("invalid_dates", []).append( "Дата завершения проекта не может быть раньше даты начала." ) + project_specialists_data = attrs.get("project_specialists") + project_specialists_fields = [ + (data["specialist"], data["level"]) + for data in project_specialists_data + ] + if len(project_specialists_data) != len( + set(project_specialists_fields) + ): + errors.setdefault("unique_project_specialists", []).append( + "Дублирование специалистов c их грейдом для проекта не " + "`допустимо." + ) if errors: raise serializers.ValidationError(errors) - return attrs + return super().validate(attrs) def create(self, validated_data) -> Project: """Метод создания проекта.""" directions = validated_data.pop("directions") project_specialists = validated_data.pop("project_specialists") + + project_specialists_to_create = [] + skills_data_to_create: Queue[List[Skill]] = Queue() + with transaction.atomic(): - project_instance, _ = Project.objects.get_or_create( - **validated_data - ) + project_instance = super().create(validated_data) project_instance.directions.set(directions) + for project_specialist_data in project_specialists: - skills_data = project_specialist_data.pop("skills") - project_specialist_instance = ProjectSpecialist.objects.create( - project=project_instance, **project_specialist_data + skills_data_to_create.put( + project_specialist_data.pop("skills") + ) + project_specialist_data["project_id"] = project_instance.id + project_specialists_to_create.append( + ProjectSpecialist(**project_specialist_data) + ) + + created_project_specialists = ( + ProjectSpecialist.objects.bulk_create( + project_specialists_to_create ) - project_specialist_instance.skills.set(skills_data) + ) + + for project_specialist in created_project_specialists: + skills_data = skills_data_to_create.get() + project_specialist.skills.set(skills_data) + return project_instance diff --git a/src/backend/apps/general/constants.py b/src/backend/apps/general/constants.py index dd37333..3d26f8d 100644 --- a/src/backend/apps/general/constants.py +++ b/src/backend/apps/general/constants.py @@ -4,7 +4,7 @@ MIN_LENGTH_SPECIALIZATION_NAME = 2 LENGTH_SPECIALIZATION_NAME_ERROR_TEXT = ( f"Длина поля от {MIN_LENGTH_SPECIALIZATION_NAME} до " - f"{MIN_LENGTH_SPECIALIZATION_NAME} символов." + f"{MAX_LENGTH_SPECIALIZATION_NAME} символов." ) REGEX_SPECIALIZATION_NAME = r"(^[A-Za-zА-Яа-яЁё\s\/]+)\Z" REGEX_SPECIALIZATION_NAME_ERROR_TEXT = ( diff --git a/src/backend/apps/general/migrations/0001_initial.py b/src/backend/apps/general/migrations/0001_initial.py index e7b2fb3..e673f9b 100644 --- a/src/backend/apps/general/migrations/0001_initial.py +++ b/src/backend/apps/general/migrations/0001_initial.py @@ -102,7 +102,7 @@ class Migration(migrations.Migration): validators=[ django.core.validators.MinLengthValidator( limit_value=2, - message="Длина поля от 2 до 2 символов.", + message="Длина поля от 2 до 100 символов.", ), django.core.validators.RegexValidator( message="Специализация может содержать: кириллические и латинские символы,пробелы и символ /", diff --git a/src/backend/apps/projects/apps.py b/src/backend/apps/projects/apps.py index cbd9abe..2414b56 100644 --- a/src/backend/apps/projects/apps.py +++ b/src/backend/apps/projects/apps.py @@ -4,3 +4,8 @@ class ProjectConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "apps.projects" + + def ready(self): + from . import signals # noqa: F401, + + return super().ready() diff --git a/src/backend/apps/projects/constants.py b/src/backend/apps/projects/constants.py index a1d3e15..de04e88 100644 --- a/src/backend/apps/projects/constants.py +++ b/src/backend/apps/projects/constants.py @@ -20,8 +20,8 @@ MAX_LENGTH_DIRECTION_NAME = 20 MIN_LENGTH_DIRECTION_NAME = 2 LENGTH_DIRECTION_NAME_ERROR_TEXT = ( - f"Длина поля от {MIN_LENGTH_PROJECT_NAME} до {MAX_LENGTH_PROJECT_NAME} " - "символов." + f"Длина поля от {MIN_LENGTH_DIRECTION_NAME} до " + f" {MAX_LENGTH_DIRECTION_NAME} символов." ) REGEX_DIRECTION_NAME = r"(^[A-Za-zА-Яа-яЁё]+)\Z" REGEX_DIRECTION_NAME_ERROR_TEXT = ( @@ -47,5 +47,4 @@ (3, "Черновик"), ) - PROJECTS_PER_PAGE = 10 diff --git a/src/backend/apps/projects/migrations/0003_alter_direction_name_and_more.py b/src/backend/apps/projects/migrations/0003_alter_direction_name_and_more.py new file mode 100644 index 0000000..839a598 --- /dev/null +++ b/src/backend/apps/projects/migrations/0003_alter_direction_name_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 5.0.1 on 2024-03-27 11:07 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("general", "0002_alter_specialist_specialization_and_more"), + ("projects", "0002_alter_project_busyness"), + ] + + operations = [ + migrations.AlterField( + model_name="direction", + name="name", + field=models.CharField( + max_length=20, + unique=True, + validators=[ + django.core.validators.MinLengthValidator( + limit_value=2, + message="Длина поля от 2 до 20 символов.", + ), + django.core.validators.RegexValidator( + message="Направление разработки может содержать: кириллические и латинские символы.", + regex="(^[A-Za-zА-Яа-яЁё]+)\\Z", + ), + ], + verbose_name="Название", + ), + ), + migrations.AddConstraint( + model_name="projectspecialist", + constraint=models.UniqueConstraint( + fields=("project", "specialist", "level"), + name="projects_projectspecialist_unique_specialist_per_project", + ), + ), + ] diff --git a/src/backend/apps/projects/mixins.py b/src/backend/apps/projects/mixins.py index 384b512..61aa736 100644 --- a/src/backend/apps/projects/mixins.py +++ b/src/backend/apps/projects/mixins.py @@ -1,5 +1,7 @@ class RecruitmentStatusMixin: - def calculate_recruitment_status(self, obj): + """Миксин с методом определения статуса набора специалистов в проект.""" + + def calculate_recruitment_status(self, obj) -> str: """Метод определения статуса набора в проект.""" if any( diff --git a/src/backend/apps/projects/models.py b/src/backend/apps/projects/models.py index f59c1d7..a26f29e 100644 --- a/src/backend/apps/projects/models.py +++ b/src/backend/apps/projects/models.py @@ -199,3 +199,9 @@ class Meta: verbose_name = "Специалист проекта" verbose_name_plural = "Специалисты проекта" default_related_name = "project_specialists" + constraints = ( + models.UniqueConstraint( + fields=("project", "specialist", "level"), + name="%(app_label)s_%(class)s_unique_specialist_per_project", + ), + ) diff --git a/src/backend/apps/projects/signals.py b/src/backend/apps/projects/signals.py new file mode 100644 index 0000000..70085ab --- /dev/null +++ b/src/backend/apps/projects/signals.py @@ -0,0 +1,13 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver + +from apps.projects.models import Project + + +@receiver(post_save, sender=Project) +def create_user_profile(sender, instance, created, **kwargs): + """Метод присвоения статуса is_organizer пользователю.""" + + if created and not instance.creator.is_organizer: + instance.creator.is_organizer = True + instance.creator.save() From db57eec90ac26ebab8b29c0eef8d2354f0541e2f Mon Sep 17 00:00:00 2001 From: Kozin Date: Wed, 27 Mar 2024 22:35:44 +0300 Subject: [PATCH 2/2] [~] Fix migrations. R: Typos in migrations. FB: Fixed errors in migrations. --- src/backend/apps/general/migrations/0001_initial.py | 2 +- .../migrations/0002_alter_specialist_specialization_and_more.py | 2 +- src/backend/apps/projects/constants.py | 2 +- .../projects/migrations/0003_alter_direction_name_and_more.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/apps/general/migrations/0001_initial.py b/src/backend/apps/general/migrations/0001_initial.py index e673f9b..e7b2fb3 100644 --- a/src/backend/apps/general/migrations/0001_initial.py +++ b/src/backend/apps/general/migrations/0001_initial.py @@ -102,7 +102,7 @@ class Migration(migrations.Migration): validators=[ django.core.validators.MinLengthValidator( limit_value=2, - message="Длина поля от 2 до 100 символов.", + message="Длина поля от 2 до 2 символов.", ), django.core.validators.RegexValidator( message="Специализация может содержать: кириллические и латинские символы,пробелы и символ /", diff --git a/src/backend/apps/general/migrations/0002_alter_specialist_specialization_and_more.py b/src/backend/apps/general/migrations/0002_alter_specialist_specialization_and_more.py index 10d4255..fbe52f9 100644 --- a/src/backend/apps/general/migrations/0002_alter_specialist_specialization_and_more.py +++ b/src/backend/apps/general/migrations/0002_alter_specialist_specialization_and_more.py @@ -17,7 +17,7 @@ class Migration(migrations.Migration): max_length=100, validators=[ django.core.validators.MinLengthValidator( - limit_value=2, message="Длина поля от 2 до 2 символов." + limit_value=2, message="Длина поля от 2 до 100 символов." ), django.core.validators.RegexValidator( message="Специализация может содержать: кириллические и латинские символы,пробелы и символ /", diff --git a/src/backend/apps/projects/constants.py b/src/backend/apps/projects/constants.py index de04e88..ba1d412 100644 --- a/src/backend/apps/projects/constants.py +++ b/src/backend/apps/projects/constants.py @@ -21,7 +21,7 @@ MIN_LENGTH_DIRECTION_NAME = 2 LENGTH_DIRECTION_NAME_ERROR_TEXT = ( f"Длина поля от {MIN_LENGTH_DIRECTION_NAME} до " - f" {MAX_LENGTH_DIRECTION_NAME} символов." + f"{MAX_LENGTH_DIRECTION_NAME} символов." ) REGEX_DIRECTION_NAME = r"(^[A-Za-zА-Яа-яЁё]+)\Z" REGEX_DIRECTION_NAME_ERROR_TEXT = ( diff --git a/src/backend/apps/projects/migrations/0003_alter_direction_name_and_more.py b/src/backend/apps/projects/migrations/0003_alter_direction_name_and_more.py index 839a598..b3c914f 100644 --- a/src/backend/apps/projects/migrations/0003_alter_direction_name_and_more.py +++ b/src/backend/apps/projects/migrations/0003_alter_direction_name_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.1 on 2024-03-27 11:07 +# Generated by Django 5.0.1 on 2024-03-27 19:19 import django.core.validators from django.db import migrations, models