diff --git a/src/backend/api/v1/projects/mixins.py b/src/backend/api/v1/projects/mixins.py index d4b75da..73f16d9 100644 --- a/src/backend/api/v1/projects/mixins.py +++ b/src/backend/api/v1/projects/mixins.py @@ -23,7 +23,7 @@ def get_recruitment_status(self, obj) -> str: return "Набор закрыт" -class ProjectOrDraftValidateMixin(serializers.ModelSerializer): +class ProjectOrDraftValidateMixin: """Миксин валидации данных проекта или его черновика.""" def _validate_date(self, value, field_name) -> date: @@ -35,6 +35,41 @@ def _validate_date(self, value, field_name) -> date: ) return value + def _check_not_unique_project_name(self, request=None, name=None) -> bool: + """ + Метод проверки уникальности создаваемого или редактируемого проекта или + его черновика по названию для создателя. + """ + + if request is not None and request.method in ("PUT", "PATCH", "POST"): + if name is not None: + queryset = Project.objects.filter( + name=name, creator=request.user + ) + instance = getattr(self, "instance", None) + if instance is not None: + queryset = queryset.exclude(id=instance.id) + return queryset.exists() + return False + + def _check_not_unique_project_specialists( + self, project_specialists_data=None + ) -> bool: + """ + Метод проверки дублирования специалистов необходимых проекту по их + специальности и грейду. + """ + + if project_specialists_data is not None: + project_specialists_fields = [ + (data["specialist"], data["level"]) + for data in project_specialists_data + ] + return len(project_specialists_data) != len( + set(project_specialists_fields) + ) + return False + def validate_started(self, value) -> date: """Метод валидации даты начала проекта.""" @@ -48,55 +83,46 @@ def validate_ended(self, value) -> date: def validate(self, attrs) -> Dict[str, Any]: """Метод валидации данных проекта или черновика.""" - request = self.context.get("request") + request = getattr(self, "context").get("request") errors: Dict = {} - if ( - request.method == "POST" - and Project.objects.filter( - name=attrs.get("name"), creator=request.user - ).exists() + if self._check_not_unique_project_name( + request, name=attrs.get("name", None) ): errors.setdefault("unique", []).append( "У вас уже есть проект или его черновик с таким названием." ) project_specialists_data = attrs.get("project_specialists", None) - if project_specialists_data is not None: - 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 self._check_not_unique_project_specialists( + project_specialists_data + ): + errors.setdefault("unique_project_specialists", []).append( + "Дублирование специалистов c их грейдом для проекта не " + "допустимо." + ) recruitment_status = request.data.get("recruitment_status", None) if ( recruitment_status is not None and project_specialists_data is not None ): - if recruitment_status: - if not any( - [ - specialist["is_required"] - for specialist in project_specialists_data - ] - ): - errors.setdefault("is_required", []).append( - "Отметьте хотя бы одного специалиста для поиска в " - "проект." - ) - else: + if not recruitment_status: for specialist in project_specialists_data: specialist["is_required"] = False + elif not any( + [ + specialist["is_required"] + for specialist in project_specialists_data + ] + ): + errors.setdefault("is_required", []).append( + "Отметьте хотя бы одного специалиста для поиска в " + "проект." + ) - started = attrs.get("started") - ended = attrs.get("ended") + started = attrs.get("started", None) + ended = attrs.get("ended", None) if (started and ended) is not None and started > ended: errors.setdefault("invalid_dates", []).append( "Дата завершения проекта не может быть раньше даты начала." @@ -104,52 +130,73 @@ def validate(self, attrs) -> Dict[str, Any]: if errors: raise serializers.ValidationError(errors) - return super().validate(attrs) + return attrs + + +class ToRepresentationOnlyIdMixin: + """Миксин с методом to_representation, возвращающим только id объекта.""" + def to_representation(self, instance): + """Метод представления объекта в виде словаря с полем 'id'.""" + + return {"id": instance.id} + + +class ProjectOrDraftCreateUpdateMixin: + """Миксин создания и редактирования проекта или его черновика.""" + + def process_project_specialists( + self, project_instance, project_specialists + ): + """Метод обработки специалистов необходимых проекту.""" + project_specialists_to_update = [] + skills_data_to_process: Queue[List[Skill]] = Queue() + + if project_specialists is not None: + project_instance.project_specialists.all().delete() + + for project_specialist_data in project_specialists: + skills_data_to_process.put( + project_specialist_data.pop("skills", []) + ) + project_specialist_data["project_id"] = project_instance.id + project_specialists_to_update.append( + ProjectSpecialist(**project_specialist_data) + ) + + created_project_specialists = ( + ProjectSpecialist.objects.bulk_create( + project_specialists_to_update + ) + ) -class ProjectOrDraftCreateMixin(serializers.ModelSerializer): - """Миксин создания проекта или его черновика.""" + for project_specialist in created_project_specialists: + skills_data = skills_data_to_process.get() + if skills_data: + project_specialist.skills.set(skills_data) - def create(self, validated_data) -> Project: + def create(self, validated_data): """Метод создания проекта или его черновика.""" - directions = validated_data.pop("directions", None) project_specialists = validated_data.pop("project_specialists", None) - project_specialists_to_create = [] - skills_data_to_create: Queue[List[Skill]] = Queue() - with transaction.atomic(): project_instance = super().create(validated_data) + self.process_project_specialists( + project_instance, project_specialists + ) - if directions is not None: - project_instance.directions.set(directions) - - if project_specialists is not None: - for project_specialist_data in project_specialists: - 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 - ) - ) - for project_specialist in created_project_specialists: - skills_data = skills_data_to_create.get() - project_specialist.skills.set(skills_data) return project_instance + def update(self, instance, validated_data): + """Метод обновления проекта или его черновика.""" -class ToRepresentationOnlyIdMixin: - """Миксин с методом to_representation, возвращающим только id объекта.""" + project_specialists = validated_data.pop("project_specialists", None) - def to_representation(self, instance): - """Метод представления объекта в виде словаря с полем 'id'.""" + with transaction.atomic(): + project_instance = super().update(instance, validated_data) + self.process_project_specialists( + project_instance, project_specialists + ) - return {"id": instance.id} + return project_instance diff --git a/src/backend/api/v1/projects/serializers.py b/src/backend/api/v1/projects/serializers.py index dbb62db..45f714b 100644 --- a/src/backend/api/v1/projects/serializers.py +++ b/src/backend/api/v1/projects/serializers.py @@ -4,7 +4,7 @@ from api.v1.general.serializers import SkillSerializer, SpecialistSerializer from api.v1.projects.mixins import ( - ProjectOrDraftCreateMixin, + ProjectOrDraftCreateUpdateMixin, ProjectOrDraftValidateMixin, RecruitmentStatusMixin, ToRepresentationOnlyIdMixin, @@ -65,6 +65,9 @@ class Meta: "creator", "owner", "link", + "phone_number", + "telegram_nick", + "email", "project_specialists", "status", ) @@ -107,14 +110,12 @@ class Meta(BaseProjectSerializer.Meta): class WriteProjectSerializer( ToRepresentationOnlyIdMixin, ProjectOrDraftValidateMixin, - ProjectOrDraftCreateMixin, + ProjectOrDraftCreateUpdateMixin, BaseProjectSerializer, ): """Сериализатор для записи проектов.""" - project_specialists = BaseProjectSpecialistSerializer( - many=True, - ) + project_specialists = BaseProjectSpecialistSerializer(many=True) status = serializers.ChoiceField(choices=STATUS_CHOICES, write_only=True) class Meta(BaseProjectSerializer.Meta): @@ -169,7 +170,7 @@ class ReadDraftSerializer(ReadProjectSerializer): class WriteDraftSerializer( ToRepresentationOnlyIdMixin, ProjectOrDraftValidateMixin, - ProjectOrDraftCreateMixin, + ProjectOrDraftCreateUpdateMixin, BaseProjectSerializer, ): """Сериализатор черновиков проекта.""" diff --git a/src/backend/apps/general/constants.py b/src/backend/apps/general/constants.py index 3d26f8d..e00dea0 100644 --- a/src/backend/apps/general/constants.py +++ b/src/backend/apps/general/constants.py @@ -30,7 +30,7 @@ MAX_LENGTH_EMAIL = 256 MAX_LENGTH_PHONE_NUMBER = 12 -PHONE_NUMBER_REGEX = r"^\+7\(\d{3}\)\d{3}-\d{2}-\d{2}$" +PHONE_NUMBER_REGEX = r"^\+7\d{10}$" PHONE_NUMBER_REGEX_ERROR_TEXT = ( "Телефон может содержать: цифры, спецсимволы, длина не должна превышать " "12 символов" diff --git a/src/backend/apps/profile/migrations/0001_initial.py b/src/backend/apps/profile/migrations/0001_initial.py index b1bab13..215376d 100644 --- a/src/backend/apps/profile/migrations/0001_initial.py +++ b/src/backend/apps/profile/migrations/0001_initial.py @@ -36,7 +36,7 @@ class Migration(migrations.Migration): validators=[ django.core.validators.RegexValidator( message="Телефон может содержать: цифры, спецсимволы, длина не должна превышать 12 символов", - regex="^\\+7\\(\\d{3}\\)\\d{3}-\\d{2}-\\d{2}$", + regex="^\\+7\\d{10}$", ) ], verbose_name="Номер телефона", diff --git a/src/backend/apps/projects/admin.py b/src/backend/apps/projects/admin.py index 8bb9de7..a37158a 100644 --- a/src/backend/apps/projects/admin.py +++ b/src/backend/apps/projects/admin.py @@ -31,6 +31,9 @@ def get_queryset(self, request): "started", "ended", "busyness", + "phone_number", + "telegram_nick", + "email", "link", "status", ) @@ -55,6 +58,9 @@ def recruitment_status(self, obj): "ended", "busyness", "link", + "phone_number", + "telegram_nick", + "email", "recruitment_status", "status", ) diff --git a/src/backend/apps/projects/migrations/0001_initial.py b/src/backend/apps/projects/migrations/0001_initial.py index 5607ccf..1c05dee 100644 --- a/src/backend/apps/projects/migrations/0001_initial.py +++ b/src/backend/apps/projects/migrations/0001_initial.py @@ -73,7 +73,7 @@ class Migration(migrations.Migration): validators=[ django.core.validators.RegexValidator( message="Телефон может содержать: цифры, спецсимволы, длина не должна превышать 12 символов", - regex="^\\+7\\(\\d{3}\\)\\d{3}-\\d{2}-\\d{2}$", + regex="^\\+7\\d{10}$", ) ], verbose_name="Номер телефона", diff --git a/src/backend/apps/projects/tasks.py b/src/backend/apps/projects/tasks.py index d0d5185..6f00ffe 100644 --- a/src/backend/apps/projects/tasks.py +++ b/src/backend/apps/projects/tasks.py @@ -1,4 +1,3 @@ -from django.db import transaction from django.utils import timezone from apps.projects.models import Project @@ -7,8 +6,7 @@ @app.task def auto_completion_projects_task(): - with transaction.atomic(): - Project.objects.filter( - ended__lt=timezone.localdate(), - status=Project.ACTIVE, - ).update(status=Project.ENDED) + Project.objects.filter( + ended__lt=timezone.localdate(), + status=Project.ACTIVE, + ).update(status=Project.ENDED) diff --git a/src/backend/config/settings/base.py b/src/backend/config/settings/base.py index a9a4807..b750b85 100644 --- a/src/backend/config/settings/base.py +++ b/src/backend/config/settings/base.py @@ -263,7 +263,7 @@ "handlers": [ "console", ], - "propagate": True, + "propagate": False, }, "django.security": { "level": "DEBUG", diff --git a/src/backend/config/settings/local.py b/src/backend/config/settings/local.py index f2e2d44..36d9518 100644 --- a/src/backend/config/settings/local.py +++ b/src/backend/config/settings/local.py @@ -25,41 +25,10 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" -# LOGGING = { -# "version": 1, -# "disable_existing_loggers": False, -# "formatters": { -# "verbose": { -# "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", -# }, -# }, -# "handlers": { -# "console": { -# "class": "logging.StreamHandler", -# "formatter": "verbose", -# }, -# }, -# "loggers": { -# "django": { -# "level": "INFO", -# "handlers": [ -# "console", -# ], -# }, -# "django.db.backends": { -# "level": "DEBUG", -# "handlers": [ -# "console", -# ], -# "propagate": False, -# }, -# }, -# } LOGGING["handlers"].pop("file", None) LOGGING["handlers"].pop("mail_admins", None) LOGGING["loggers"].pop("django.request", None) -LOGGING["loggers"].pop("django.request", None) LOGGING["loggers"].pop("django.security", None) LOGGING["loggers"].pop("django.security.csrf", None) LOGGING["loggers"].pop("customlogger", None)