diff --git a/README.md b/README.md index 9283da1..191bf1e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ RESTfull API приложение, разработанное для поиск реализовать что-то новое, для менеджеров проектов и для компаний которые хотят создать тестовое МВП нового продукта. -![workflow](https://github.com/Pet-projects-CodePET/Backend/actions/workflows/main.yml/badge.svg) +[![Code cheсks](https://github.com/Pet-projects-CodePET/Backend/actions/workflows/code_check.yml/badge.svg)](https://github.com/Pet-projects-CodePET/Backend/actions/workflows/code_check.yml) ## Стек технологий: diff --git a/src/backend/api/urls.py b/src/backend/api/urls.py new file mode 100644 index 0000000..e908b7c --- /dev/null +++ b/src/backend/api/urls.py @@ -0,0 +1,5 @@ +from django.urls import include, path + +urlpatterns = [ + path("v1/", include("api.v1.urls")), +] diff --git a/src/backend/api/v1/general/serializers.py b/src/backend/api/v1/general/serializers.py index 68d3b52..baf2772 100644 --- a/src/backend/api/v1/general/serializers.py +++ b/src/backend/api/v1/general/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from apps.general.models import Section, Skill, Specialization +from apps.general.models import Section, Skill, Specialist class SectionSerializer(serializers.ModelSerializer): @@ -9,12 +9,12 @@ class Meta: fields = "__all__" -class SpecializationSerializer(serializers.ModelSerializer): +class SpecialistSerializer(serializers.ModelSerializer): """Сериализатор специализации.""" class Meta: - model = Specialization - fields = ("id", "name") + model = Specialist + fields = "__all__" class SkillSerializer(serializers.ModelSerializer): @@ -22,4 +22,4 @@ class SkillSerializer(serializers.ModelSerializer): class Meta: model = Skill - fields = ("id", "name") + fields = "__all__" diff --git a/src/backend/api/v1/general/urls.py b/src/backend/api/v1/general/urls.py index 9fd4db2..05d7cef 100644 --- a/src/backend/api/v1/general/urls.py +++ b/src/backend/api/v1/general/urls.py @@ -1,8 +1,19 @@ -from django.urls import path +from django.urls import include, path +from rest_framework.routers import DefaultRouter -from api.v1.general.views import CounterApiView, SectionViewSet +from api.v1.general.views import ( + CounterApiView, + SectionViewSet, + SkillViewSet, + SpecialistViewSet, +) + +router = DefaultRouter() +router.register("specialists", SpecialistViewSet, basename="specialists") +router.register("skills", SkillViewSet, basename="skills") urlpatterns = [ - path("section", SectionViewSet.as_view({"get": "list"})), - path("counter", CounterApiView.as_view()), + path("section/", SectionViewSet.as_view({"get": "list"})), + path("counter/", CounterApiView.as_view()), + path("", include(router.urls)), ] diff --git a/src/backend/api/v1/general/views.py b/src/backend/api/v1/general/views.py index 6392593..9508fd5 100644 --- a/src/backend/api/v1/general/views.py +++ b/src/backend/api/v1/general/views.py @@ -3,14 +3,18 @@ from django.views.decorators.cache import cache_page from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics, viewsets +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from apps.general.models import Section +from api.v1.general.serializers import ( + SectionSerializer, + SkillSerializer, + SpecialistSerializer, +) +from apps.general.models import Section, Skill, Specialist -from .serializers import SectionSerializer - -class SectionViewSet(viewsets.ModelViewSet): +class SectionViewSet(viewsets.ReadOnlyModelViewSet): """Текстовая секция на странице""" queryset = Section.objects.all() @@ -26,7 +30,23 @@ class CounterApiView(generics.RetrieveAPIView): def get(self, request): with connection.cursor() as cursor: cursor.execute( - "SELECT count(*) from project_project union all SELECT count(*) from users_user " + "SELECT count(*) from projects_project union all SELECT count(*) from users_user " ) row = cursor.fetchall() return Response({"projects": row[0][0], "users": row[1][0]}) + + +class SpecialistViewSet(viewsets.ReadOnlyModelViewSet): + """Представление специальностей.""" + + queryset = Specialist.objects.all() + serializer_class = SpecialistSerializer + permission_classes = (IsAuthenticated,) + + +class SkillViewSet(viewsets.ReadOnlyModelViewSet): + """Представление специальностей.""" + + queryset = Skill.objects.all() + serializer_class = SkillSerializer + permission_classes = (IsAuthenticated,) diff --git a/src/backend/api/v1/projects/constants.py b/src/backend/api/v1/projects/constants.py index aee3a5f..729ae7d 100644 --- a/src/backend/api/v1/projects/constants.py +++ b/src/backend/api/v1/projects/constants.py @@ -1,10 +1,3 @@ PROJECT_PREVIEW_MAIN_PAGE_SIZE = 6 PROJECT_PAGE_SIZE = 7 MAX_PAGE_SIZE = 100 -PROJECT_PREVIEW_MAIN_FIELDS = ( - "id", - "name", - "started", - "ended", - "direction", -) diff --git a/src/backend/api/v1/projects/serializers.py b/src/backend/api/v1/projects/serializers.py index 840dac1..d2191f8 100644 --- a/src/backend/api/v1/projects/serializers.py +++ b/src/backend/api/v1/projects/serializers.py @@ -1,33 +1,25 @@ -from rest_framework import serializers +from datetime import date +from typing import Any, Dict, List, Optional -from api.v1.general.serializers import ( - SkillSerializer, - SpecializationSerializer, -) -from apps.project.models import ( - Project, - ProjectSpecialist, - Specialist, - Specialization, -) +from django.db import transaction +from rest_framework import serializers +from api.v1.general.serializers import SkillSerializer, SpecialistSerializer +from apps.projects.constants import BUSYNESS_CHOICES, STATUS_CHOICES +from apps.projects.mixins import RecruitmentStatusMixin +from apps.projects.models import Direction, Project, ProjectSpecialist -class SpecialistSerializer(serializers.ModelSerializer): - """Сериализатор специалистов.""" - specialization = SpecializationSerializer() +class DirectionSerializer(serializers.ModelSerializer): + """Сериализатор специалиста.""" class Meta: - model = Specialist - fields = ( - "id", - "name", - "specialization", - ) + model = Direction + fields = "__all__" -class ProjectSpecialistSerializer(SpecialistSerializer): - """Сериализатор специалистов необходимых проекту.""" +class ReadProjectSpecialistSerializer(SpecialistSerializer): + """Сериализатор для чтения специалиста необходимого проекту.""" specialist = SpecialistSerializer() skills = SkillSerializer(many=True) @@ -36,6 +28,7 @@ class ProjectSpecialistSerializer(SpecialistSerializer): class Meta: model = ProjectSpecialist fields = ( + "id", "specialist", "skills", "count", @@ -44,21 +37,91 @@ class Meta: ) def get_level(self, obj) -> str: + """Метод получения представления для грейда.""" + return obj.get_level_display() -class ProjectSerializer(serializers.ModelSerializer): - """Сериализатор проектов.""" +class WriteProjectSpecialistSerializer(SpecialistSerializer): + """Сериализатор для записи специалиста необходимого проекту.""" + + class Meta: + model = ProjectSpecialist + fields = ( + "id", + "specialist", + "skills", + "count", + "level", + "is_required", + ) + + +class ReadProjectSerializer( + RecruitmentStatusMixin, serializers.ModelSerializer +): + """Сериализатор для чтения проектов.""" + + directions = serializers.StringRelatedField(many=True) + status = serializers.ChoiceField( + choices=STATUS_CHOICES, source="get_status_display" + ) + recruitment_status = serializers.SerializerMethodField() + project_specialists = ReadProjectSpecialistSerializer(many=True) + creator = serializers.SlugRelatedField( + slug_field="username", read_only=True + ) + owner = serializers.SlugRelatedField(slug_field="username", read_only=True) + + class Meta: + model = Project + fields = ( + "id", + "name", + "description", + "started", + "ended", + "directions", + "creator", + "owner", + "link", + "recruitment_status", + "project_specialists", + "status", + ) + read_only_fields = fields + + def get_recruitment_status(self, obj) -> str: + """Метод определения статуса набора в проект.""" + + return self.calculate_recruitment_status(obj) - project_specialists = ProjectSpecialistSerializer(many=True) - direction = serializers.SerializerMethodField() - status = serializers.SerializerMethodField() - def get_direction(self, obj) -> str: - return obj.get_direction_display() +class WriteProjectSerializer( + RecruitmentStatusMixin, serializers.ModelSerializer +): + """Сериализатор для записи проектов.""" - def get_status(self, obj) -> str: - return obj.get_status_display() + creator = serializers.SerializerMethodField(read_only=True) + owner = serializers.SerializerMethodField(read_only=True) + project_specialists = WriteProjectSpecialistSerializer( + many=True, + ) + busyness = serializers.ChoiceField( + choices=BUSYNESS_CHOICES, write_only=True + ) + project_busyness = serializers.ChoiceField( + choices=BUSYNESS_CHOICES, + source="get_busyness_display", + read_only=True, + ) + status = serializers.ChoiceField(choices=STATUS_CHOICES, write_only=True) + project_status = serializers.ChoiceField( + choices=STATUS_CHOICES, + source="get_status_display", + read_only=True, + ) + recruitment_status = serializers.SerializerMethodField() class Meta: model = Project @@ -68,26 +131,123 @@ class Meta: "description", "started", "ended", - "direction", + "busyness", + "project_busyness", + "directions", "creator", "owner", + "link", + "recruitment_status", "project_specialists", "status", + "project_status", + ) + extra_kwargs = { + "description": {"required": True}, + "started": {"required": True}, + "ended": {"required": True}, + "busyness": {"required": True}, + "directions": {"required": True}, + "link": {"required": True}, + } + + def get_creator(self, obj) -> str: + """Метод получения username у создателя проекта.""" + + return obj.creator.username + + def get_owner(self, obj) -> str: + """Метод получения username у владельца проекта.""" + + return obj.owner.username + + def get_recruitment_status(self, obj) -> str: + """Метод определения статуса набора в проект.""" + + return self.calculate_recruitment_status(obj) + + def _validate_date(self, value, field_name) -> date: + """Метод валидации даты.""" + + if value < date.today(): + raise serializers.ValidationError( + f"Дата {field_name} не может быть в прошлом." + ) + return value + + def validate_started(self, value) -> date: + """Метод валидации даты начала проекта.""" + + return self._validate_date(value, "начала проекта") + + def validate_ended(self, value) -> date: + """Метод валидации даты завершения проекта.""" + + return self._validate_date(value, "завершения проекта") + + def validate_status(self, value) -> int: + """Метод валидации даты завершения проекта.""" + + if value == Project.DRAFT: + raise serializers.ValidationError( + "У проекта не может быть статуса 'Черновик'." + ) + return value + + def validate(self, attrs) -> Dict[str, Any]: + """Метод валидации данных о проекте.""" + + errors: Dict = {} + + 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( + "Дата завершения проекта не может быть раньше даты начала." + ) + + if errors: + raise serializers.ValidationError(errors) + return attrs + + def create(self, validated_data) -> Project: + """Метод создания проекта.""" + + directions = validated_data.pop("directions") + project_specialists = validated_data.pop("project_specialists") + with transaction.atomic(): + project_instance, _ = Project.objects.get_or_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 + ) + project_specialist_instance.skills.set(skills_data) + return project_instance + class ProjectPreviewMainSerializer(serializers.ModelSerializer): """Сериализатор превью проектов.""" - specializations = serializers.SerializerMethodField() - direction = serializers.SerializerMethodField() + specialists = serializers.SerializerMethodField() + directions = serializers.StringRelatedField(many=True) - def get_direction(self, obj) -> str: - return obj.get_direction_display() + def get_specialists(self, obj) -> Optional[List[Dict[str, Any]]]: + """Метод получения списка специалистов.""" - def get_specializations(self, obj) -> list[str]: return [ - specialist.specialist.specialization.name + SpecialistSerializer(specialist.specialist).data for specialist in obj.project_specialists.all() ] @@ -98,6 +258,6 @@ class Meta: "name", "started", "ended", - "direction", - "specializations", + "directions", + "specialists", ) diff --git a/src/backend/api/v1/projects/urls.py b/src/backend/api/v1/projects/urls.py index 182dce7..45093cc 100644 --- a/src/backend/api/v1/projects/urls.py +++ b/src/backend/api/v1/projects/urls.py @@ -1,14 +1,21 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter -from .views import ProjectPreviewMainViewSet, ProjectViewSet +from api.v1.projects.views import ( + DirectionViewSet, + ProjectPreviewMainViewSet, + ProjectViewSet, +) -router_v1 = DefaultRouter() +router = DefaultRouter() -router_v1.register( +router.register( "preview_main", ProjectPreviewMainViewSet, basename="projects-preview-main" ) -router_v1.register("", ProjectViewSet, basename="projects") +router.register("directions", DirectionViewSet, basename="projects-directions") +router.register("", ProjectViewSet, basename="projects") + + urlpatterns = [ - path("", include(router_v1.urls)), + path("", include(router.urls)), ] diff --git a/src/backend/api/v1/projects/views.py b/src/backend/api/v1/projects/views.py index c20ef8c..855728b 100644 --- a/src/backend/api/v1/projects/views.py +++ b/src/backend/api/v1/projects/views.py @@ -1,49 +1,89 @@ from django.db.models import Prefetch from rest_framework import mixins -from rest_framework.permissions import AllowAny, IsAuthenticatedOrReadOnly -from rest_framework.viewsets import GenericViewSet, ModelViewSet +from rest_framework.permissions import ( + SAFE_METHODS, + AllowAny, + IsAuthenticated, + IsAuthenticatedOrReadOnly, +) +from rest_framework.viewsets import ( + GenericViewSet, + ModelViewSet, + ReadOnlyModelViewSet, +) -from apps.general.models import Skill -from apps.project.models import Project, ProjectSpecialist +from api.v1.projects.paginations import ( + ProjectPagination, + ProjectPreviewMainPagination, +) +from api.v1.projects.serializers import ( + DirectionSerializer, + ProjectPreviewMainSerializer, + ReadProjectSerializer, + WriteProjectSerializer, +) +from apps.projects.models import Direction, Project, ProjectSpecialist -from .constants import PROJECT_PREVIEW_MAIN_FIELDS -from .paginations import ProjectPagination, ProjectPreviewMainPagination -from .serializers import ProjectPreviewMainSerializer, ProjectSerializer + +class DirectionViewSet(ReadOnlyModelViewSet): + """Представление направлений разработки.""" + + queryset = Direction.objects.all() + serializer_class = DirectionSerializer + permission_classes = (IsAuthenticated,) class ProjectViewSet(ModelViewSet): """Представление проектов.""" - queryset = ( - Project.objects.all() - .select_related("creator", "owner") - .prefetch_related( - Prefetch( - "project_specialists", - queryset=ProjectSpecialist.objects.select_related( - "specialist__specialization" - ).prefetch_related("skills"), - ), - ) + queryset = Project.objects.select_related( + "creator", "owner" + ).prefetch_related( + Prefetch( + "project_specialists", + queryset=ProjectSpecialist.objects.select_related( + "specialist" + ).prefetch_related("skills"), + ), + "directions", ) permission_classes = (IsAuthenticatedOrReadOnly,) - serializer_class = ProjectSerializer pagination_class = ProjectPagination + def get_serializer_class(self): + """Метод получения сериализатора проектов.""" + + if self.request.method in SAFE_METHODS: + return ReadProjectSerializer + return WriteProjectSerializer + + def perform_create(self, serializer): + """Метод предварительного создания объекта.""" + + user = self.request.user + serializer.save(creator=user, owner=user) + class ProjectPreviewMainViewSet(mixins.ListModelMixin, GenericViewSet): """Представление превью проектов на главной странице.""" queryset = ( - Project.objects.all() - .only(*PROJECT_PREVIEW_MAIN_FIELDS) + Project.objects.exclude(status=Project.DRAFT) + .only( + "id", + "name", + "started", + "ended", + "directions", + ) .prefetch_related( Prefetch( "project_specialists", queryset=ProjectSpecialist.objects.select_related( - "specialist__specialization" + "specialist" ), - ) + ), + "directions", ) ) permission_classes = (AllowAny,) diff --git a/src/backend/apps/general/constants.py b/src/backend/apps/general/constants.py index 30c050b..a7430a3 100644 --- a/src/backend/apps/general/constants.py +++ b/src/backend/apps/general/constants.py @@ -1,12 +1,49 @@ MAX_LENGTH_SKILL_NAME = 100 -MAX_LENGTH_SPECIALIZATION_NAME = 100 -LEVEL_CHOICES = [ - (1, "intern"), - (2, "junior"), - (3, "middle"), - (4, "senior"), - (5, "lead"), -] -TITLE_LENGTH = 100 -DESCRIPRION_LENGTH = 250 + +MAX_LENGTH_SPECIALIZATION_NAME = 50 +MIN_LENGTH_SPECIALIZATION_NAME = 2 +LENGTH_SPECIALIZATION_NAME_ERROR_TEXT = ( + f"Длина поля от {MIN_LENGTH_SPECIALIZATION_NAME} до " + f"{MIN_LENGTH_SPECIALIZATION_NAME} символов." +) +REGEX_SPECIALIZATION_NAME = r"(^[A-Za-zА-Яа-яЁё\s\/]+)\Z" +REGEX_SPECIALIZATION_NAME_ERROR_TEXT = ( + "Специализация может содержать: кириллические и латинские символы,пробелы " + "и символ /" +) + +MAX_LENGTH_SPECIALTY_NAME = 100 +MIN_LENGTH_SPECIALTY_NAME = 2 +LENGTH_SPECIALTY_NAME_ERROR_TEXT = ( + f"Длина поля от {MIN_LENGTH_SPECIALTY_NAME} до " + f"{MAX_LENGTH_SPECIALTY_NAME} символов." +) +REGEX_SPECIALTY_NAME = r"(^[A-Za-zА-Яа-яЁё]+)\Z" +REGEX_SPECIALTY_NAME_ERROR_TEXT = ( + "Специальность может содержать: кириллические и латинские символы." +) + +MAX_LENGTH_TITLE = 100 + +MAX_LENGTH_DESCRIPTION = 250 + 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_ERROR_TEXT = ( + "Телефон может содержать: цифры, спецсимволы, длина не должна превышать " + "12 символов" +) + +MAX_LENGTH_TELEGRAM_NICK = 32 +MIN_LENGTH_TELEGRAM_NICK = 5 +LENGTH_TELEGRAM_NICK_ERROR_TEXT = ( + f"Длина поля от {MIN_LENGTH_TELEGRAM_NICK} до " + f"{MAX_LENGTH_TELEGRAM_NICK} символов." +) +REGEX_TELEGRAM_NICK = r"^[a-zA-Z0-9_]+$" +REGEX_TELEGRAM_NICK_ERROR_TEXT = ( + "Введите корректное имя пользователя. Оно может состоять из латинских " + "букв, цифр и символа подчеркивания." +) diff --git a/src/backend/apps/general/migrations/0001_initial.py b/src/backend/apps/general/migrations/0001_initial.py index ccc51b3..29b24eb 100644 --- a/src/backend/apps/general/migrations/0001_initial.py +++ b/src/backend/apps/general/migrations/0001_initial.py @@ -1,15 +1,13 @@ -# Generated by Django 5.0.1 on 2024-02-17 10:06 +# Generated by Django 5.0.1 on 2024-03-13 13:24 +import django.core.validators from django.db import migrations, models -from django.conf import settings + class Migration(migrations.Migration): initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] + dependencies = [] operations = [ migrations.CreateModel( @@ -32,7 +30,17 @@ class Migration(migrations.Migration): "description", models.TextField(max_length=250, verbose_name="Текст"), ), + ( + "page_id", + models.PositiveSmallIntegerField( + verbose_name="Идентификатор страницы" + ), + ), ], + options={ + "verbose_name": "Секция", + "verbose_name_plural": "Секции", + }, ), migrations.CreateModel( name="Skill", @@ -48,7 +56,9 @@ class Migration(migrations.Migration): ), ( "name", - models.CharField(max_length=100, verbose_name="Название"), + models.CharField( + max_length=100, unique=True, verbose_name="Название" + ), ), ], options={ @@ -57,7 +67,7 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name="Specialization", + name="Specialist", fields=[ ( "id", @@ -69,13 +79,57 @@ class Migration(migrations.Migration): ), ), ( - "name", - models.CharField(max_length=100, verbose_name="Название"), + "specialty", + models.CharField( + max_length=100, + validators=[ + django.core.validators.MinLengthValidator( + limit_value=2, + message="Длина поля от 2 до 100 символов.", + ), + django.core.validators.RegexValidator( + message="Специальность может содержать: кириллические и латинские символы.", + regex="(^[A-Za-zА-Яа-яЁё\\s\\/]+)\\Z", + ), + ], + verbose_name="Специализация", + ), + ), + ( + "specialization", + models.CharField( + max_length=50, + validators=[ + django.core.validators.MinLengthValidator( + limit_value=2, + message="Длина поля от 2 до 2 символов.", + ), + django.core.validators.RegexValidator( + message="Специализация может содержать: кириллические и латинские символы.", + regex="(^[A-Za-zА-Яа-яЁё]+)\\Z", + ), + ], + verbose_name="Специальность", + ), ), ], options={ - "verbose_name": "Специальность", - "verbose_name_plural": "Специальности", + "verbose_name": "Специалист", + "verbose_name_plural": "Специалисты", }, ), + migrations.AddConstraint( + model_name="section", + constraint=models.UniqueConstraint( + fields=("title", "page_id"), + name="general_section_unique_section_per_page", + ), + ), + migrations.AddConstraint( + model_name="specialist", + constraint=models.UniqueConstraint( + fields=("specialty", "specialization"), + name="general_specialist_unique_specialist", + ), + ), ] diff --git a/src/backend/apps/general/migrations/0002_section_page_id.py b/src/backend/apps/general/migrations/0002_section_page_id.py deleted file mode 100644 index 3fbf8df..0000000 --- a/src/backend/apps/general/migrations/0002_section_page_id.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.0.1 on 2024-02-19 20:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("general", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="section", - name="page_id", - field=models.IntegerField( - default=0, verbose_name="Идентификатор страницы" - ), - ), - ] diff --git a/src/backend/apps/general/models.py b/src/backend/apps/general/models.py index b44c385..a4e81c4 100644 --- a/src/backend/apps/general/models.py +++ b/src/backend/apps/general/models.py @@ -1,12 +1,31 @@ +from django.core.validators import MinLengthValidator, RegexValidator from django.db import models from apps.general.constants import ( + LENGTH_SPECIALIZATION_NAME_ERROR_TEXT, + LENGTH_SPECIALTY_NAME_ERROR_TEXT, + LENGTH_TELEGRAM_NICK_ERROR_TEXT, + MAX_LENGTH_DESCRIPTION, + MAX_LENGTH_EMAIL, + MAX_LENGTH_PHONE_NUMBER, MAX_LENGTH_SKILL_NAME, MAX_LENGTH_SPECIALIZATION_NAME, + MAX_LENGTH_SPECIALTY_NAME, + MAX_LENGTH_TELEGRAM_NICK, + MAX_LENGTH_TITLE, + MIN_LENGTH_SPECIALIZATION_NAME, + MIN_LENGTH_SPECIALTY_NAME, + MIN_LENGTH_TELEGRAM_NICK, + PHONE_NUMBER_REGEX, + PHONE_NUMBER_REGEX_ERROR_TEXT, + REGEX_SPECIALIZATION_NAME, + REGEX_SPECIALIZATION_NAME_ERROR_TEXT, + REGEX_SPECIALTY_NAME, + REGEX_SPECIALTY_NAME_ERROR_TEXT, + REGEX_TELEGRAM_NICK, + REGEX_TELEGRAM_NICK_ERROR_TEXT, ) -from .constants import DESCRIPRION_LENGTH, TITLE_LENGTH - class CreatedModifiedFields(models.Model): """ @@ -21,19 +40,33 @@ class Meta: class Section(models.Model): - """Секции на главной странице""" + """Модель секций страниц.""" title = models.TextField( - verbose_name="Заголовок", max_length=TITLE_LENGTH, null=False + verbose_name="Заголовок", + max_length=MAX_LENGTH_TITLE, + null=False, ) description = models.TextField( - verbose_name="Текст", max_length=DESCRIPRION_LENGTH, null=False + verbose_name="Текст", max_length=MAX_LENGTH_DESCRIPTION, null=False ) - page_id = models.IntegerField( - verbose_name="Идентификатор страницы", null=False, default=0 + page_id = models.PositiveSmallIntegerField( + verbose_name="Идентификатор страницы", null=False ) + class Meta: + verbose_name = "Секция" + verbose_name_plural = "Секции" + constraints = ( + models.constraints.UniqueConstraint( + fields=("title", "page_id"), + name=("%(app_label)s_%(class)s_unique_section_per_page"), + ), + ) + def __str__(self): + """Метод строкового представления объекта секции страницы.""" + return self.title @@ -43,6 +76,7 @@ class Skill(models.Model): name = models.CharField( verbose_name="Название", max_length=MAX_LENGTH_SKILL_NAME, + unique=True, ) class Meta: @@ -50,20 +84,93 @@ class Meta: verbose_name_plural = "Навыки" def __str__(self) -> str: - return self.name + """Метод строкового представления объекта навыка.""" + return self.name -class Specialization(models.Model): - """Модель специальности.""" - name = models.CharField( - verbose_name="Название", +class Specialist(models.Model): + """Модель специалиста.""" + + specialty = models.CharField( + verbose_name="Специализация", + max_length=MAX_LENGTH_SPECIALTY_NAME, + validators=( + MinLengthValidator( + limit_value=MIN_LENGTH_SPECIALTY_NAME, + message=LENGTH_SPECIALTY_NAME_ERROR_TEXT, + ), + RegexValidator( + regex=REGEX_SPECIALTY_NAME, + message=REGEX_SPECIALTY_NAME_ERROR_TEXT, + ), + ), + ) + specialization = models.CharField( + verbose_name="Специальность", max_length=MAX_LENGTH_SPECIALIZATION_NAME, + validators=( + MinLengthValidator( + limit_value=MIN_LENGTH_SPECIALIZATION_NAME, + message=LENGTH_SPECIALIZATION_NAME_ERROR_TEXT, + ), + RegexValidator( + regex=REGEX_SPECIALIZATION_NAME, + message=REGEX_SPECIALIZATION_NAME_ERROR_TEXT, + ), + ), ) class Meta: - verbose_name = "Специальность" - verbose_name_plural = "Специальности" + verbose_name = "Специалист" + verbose_name_plural = "Специалисты" + constraints = ( + models.constraints.UniqueConstraint( + fields=("specialty", "specialization"), + name=("%(app_label)s_%(class)s_unique_specialist"), + ), + ) def __str__(self) -> str: - return self.name + """Метод строкового представления объекта специалиста.""" + + return f"{self.specialty} - {self.specialization}" + + +class ContactsFields(models.Model): + """Абстрактная модель с полями контактов.""" + + phone_number = models.TextField( + max_length=MAX_LENGTH_PHONE_NUMBER, + verbose_name="Номер телефона", + blank=True, + validators=[ + RegexValidator( + regex=PHONE_NUMBER_REGEX, + message=PHONE_NUMBER_REGEX_ERROR_TEXT, + ) + ], + ) + telegram_nick = models.CharField( + max_length=MAX_LENGTH_TELEGRAM_NICK, + verbose_name="Ник в телеграм", + blank=True, + validators=[ + MinLengthValidator( + limit_value=MIN_LENGTH_TELEGRAM_NICK, + message=LENGTH_TELEGRAM_NICK_ERROR_TEXT, + ), + RegexValidator( + regex=REGEX_TELEGRAM_NICK, + message=REGEX_TELEGRAM_NICK_ERROR_TEXT, + ), + ], + ) + email = models.EmailField( + verbose_name="E-mail", + max_length=MAX_LENGTH_EMAIL, + blank=True, + ) + + class Meta: + abstract = True diff --git a/src/backend/apps/project/__init__.py b/src/backend/apps/project/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/backend/apps/project/admin.py b/src/backend/apps/project/admin.py deleted file mode 100644 index 9f2616d..0000000 --- a/src/backend/apps/project/admin.py +++ /dev/null @@ -1,61 +0,0 @@ -from django.contrib import admin - -from .constants import PROJECTS_PER_PAGE -from .models import ( - Project, - ProjectSpecialist, - Skill, - Specialist, - Specialization, -) - - -@admin.register(Skill) -class SkillAdmin(admin.ModelAdmin): - list_display = ("name",) - search_fields = ("name",) - - -@admin.register(Specialist) -class SpecialistAdmin(admin.ModelAdmin): - list_display = ("name", "specialization") - list_filter = ("specialization",) - search_fields = ("name", "specialization__name") - - -@admin.register(Specialization) -class SpecializationAdmin(admin.ModelAdmin): - list_display = ("name",) - search_fields = ("name",) - - -@admin.register(Project) -class ProjectAdmin(admin.ModelAdmin): - list_display = ( - "name", - "description", - "creator", - "started", - "ended", - "contacts", - "busyness", - "recruitment_status", - "status", - "direction", - ) - list_filter = ("busyness", "status") - search_fields = ("name", "description", "purpose", "creator__username") - list_per_page = PROJECTS_PER_PAGE - - -@admin.register(ProjectSpecialist) -class ProjectSpecialistAdmin(admin.ModelAdmin): - list_display = ( - "project", - "specialist", - "level", - "count", - "is_required", - ) - list_filter = ("project",) - search_fields = ("project",) diff --git a/src/backend/apps/project/constants.py b/src/backend/apps/project/constants.py deleted file mode 100644 index 9e6b4d6..0000000 --- a/src/backend/apps/project/constants.py +++ /dev/null @@ -1,23 +0,0 @@ -MAX_LENGTH_CONTACTS = 256 -MAX_LENGTH_DESCRIPTION = 3000 -MAX_LENGTH_PROFESSION_NAME = 100 -MAX_LENGTH_PROJECT_NAME = 100 -MAX_LENGTH_PURPOSE = 100 -BUSYNESS_CHOICES = [ - (1, "10"), - (2, "20"), - (3, "30"), - (4, "40"), -] -DIRECTION_CHOICES = [ - (1, "Десктоп"), - (2, "Веб"), - (3, "Мобильная"), -] -STATUS_CHOICES = [ - (1, "Активен"), - (2, "Завершен"), - (3, "Черновик"), -] - -PROJECTS_PER_PAGE = 10 diff --git a/src/backend/apps/project/migrations/__init__.py b/src/backend/apps/project/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/backend/apps/project/models.py b/src/backend/apps/project/models.py deleted file mode 100644 index 10cad2a..0000000 --- a/src/backend/apps/project/models.py +++ /dev/null @@ -1,144 +0,0 @@ -from django.contrib.auth import get_user_model -from django.db import models - -from apps.general.constants import LEVEL_CHOICES -from apps.general.models import CreatedModifiedFields, Skill, Specialization - -from .constants import ( - BUSYNESS_CHOICES, - DIRECTION_CHOICES, - MAX_LENGTH_CONTACTS, - MAX_LENGTH_DESCRIPTION, - MAX_LENGTH_PROFESSION_NAME, - MAX_LENGTH_PROJECT_NAME, - MAX_LENGTH_PURPOSE, - STATUS_CHOICES, -) - -User = get_user_model() - - -class Specialist(models.Model): - """Модель специалиста.""" - - specialization = models.ForeignKey( - Specialization, - on_delete=models.CASCADE, - related_name="specialists", - verbose_name="Специальность", - ) - name = models.CharField( - verbose_name="Название специализации", - max_length=MAX_LENGTH_PROFESSION_NAME, - ) - - class Meta: - verbose_name = "Специалист" - verbose_name_plural = "Специалисты" - - def __str__(self) -> str: - return f"{self.specialization.name} {self.name}" - - -class Project(CreatedModifiedFields): - """Модель проект.""" - - name = models.CharField( - verbose_name="Название проекта", - max_length=MAX_LENGTH_PROJECT_NAME, - ) - description = models.TextField( - verbose_name="Описание проекта", - max_length=MAX_LENGTH_DESCRIPTION, - ) - creator = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name="created_projects", - verbose_name="Организатор", - ) - owner = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name="owned_projects", - verbose_name="Владелец", - ) - started = models.DateField( - verbose_name="Начало проекта", - null=True, - blank=True, - ) - ended = models.DateField( - verbose_name="Окончание проекта", - null=True, - blank=True, - ) - busyness = models.PositiveSmallIntegerField( - verbose_name="Занятость в часах в неделю", - choices=BUSYNESS_CHOICES, - ) - recruitment_status = models.BooleanField( - verbose_name="Статус набора участников", - default=False, - ) - status = models.PositiveSmallIntegerField( - verbose_name="Статус проекта", - choices=STATUS_CHOICES, - ) - contacts = models.TextField( - verbose_name="Контакты для связи", - max_length=MAX_LENGTH_CONTACTS, - ) - direction = models.PositiveSmallIntegerField( - verbose_name="Направление разработки", - choices=DIRECTION_CHOICES, - ) - participants = models.ManyToManyField( - User, - verbose_name="Участники проекта", - related_name="projects_participated", - blank=True, - ) - - class Meta: - verbose_name = "Проект" - verbose_name_plural = "Проекты" - ordering = ("-created",) - - def __str__(self) -> str: - return self.name - - -class ProjectSpecialist(models.Model): - """Модель количества специалистов необходимых проекту.""" - - project = models.ForeignKey( - Project, - on_delete=models.CASCADE, - verbose_name="Проект", - ) - specialist = models.ForeignKey( - Specialist, - on_delete=models.CASCADE, - verbose_name="Специалист", - ) - skills = models.ManyToManyField( - Skill, - verbose_name="Навыки", - ) - count = models.PositiveSmallIntegerField( - verbose_name="Количество", - ) - level = models.PositiveSmallIntegerField( - verbose_name="Уровень", - choices=LEVEL_CHOICES, - ) - is_required = models.BooleanField( - verbose_name="Требуется для проекта", - default=False, - ) - - class Meta: - verbose_name = "Специалист проекта" - verbose_name_plural = "Специалисты проекта" - default_related_name = "project_specialists" diff --git a/src/__init__.py b/src/backend/apps/projects/__init__.py similarity index 100% rename from src/__init__.py rename to src/backend/apps/projects/__init__.py diff --git a/src/backend/apps/projects/admin.py b/src/backend/apps/projects/admin.py new file mode 100644 index 0000000..fd6abee --- /dev/null +++ b/src/backend/apps/projects/admin.py @@ -0,0 +1,80 @@ +from django.contrib import admin + +from apps.projects.constants import PROJECTS_PER_PAGE +from apps.projects.mixins import RecruitmentStatusMixin +from apps.projects.models import Project, ProjectSpecialist, Skill, Specialist + + +@admin.register(Skill) +class SkillAdmin(admin.ModelAdmin): + list_display = ("name",) + search_fields = ("name",) + + +@admin.register(Specialist) +class SpecialistAdmin(admin.ModelAdmin): + list_display = ("specialty", "specialization") + list_filter = ("specialization",) + search_fields = ("specialty", "specialization") + + +@admin.register(Project) +class ProjectAdmin(RecruitmentStatusMixin, admin.ModelAdmin): + def get_queryset(self, request): + return ( + Project.objects.select_related("creator", "owner") + .only( + "id", + "creator__email", + "owner__email", + "name", + "description", + "started", + "ended", + "phone_number", + "telegram_nick", + "email", + "busyness", + "status", + ) + .prefetch_related( + "project_specialists", + ) + ) + + def recruitment_status(self, obj): + return self.calculate_recruitment_status(obj) + + recruitment_status.short_description = "Статус набора" # type: ignore + + list_display = ( + "name", + "description", + "creator", + "owner", + "started", + "ended", + "phone_number", + "telegram_nick", + "email", + "busyness", + "recruitment_status", + "status", + ) + readonly_fields = ("recruitment_status",) + list_filter = ("busyness", "status") + search_fields = ("name", "description", "creator__username") + list_per_page = PROJECTS_PER_PAGE + + +@admin.register(ProjectSpecialist) +class ProjectSpecialistAdmin(admin.ModelAdmin): + list_display = ( + "project", + "specialist", + "level", + "count", + "is_required", + ) + list_filter = ("project",) + search_fields = ("project",) diff --git a/src/backend/apps/project/apps.py b/src/backend/apps/projects/apps.py similarity index 82% rename from src/backend/apps/project/apps.py rename to src/backend/apps/projects/apps.py index 7fe3abc..cbd9abe 100644 --- a/src/backend/apps/project/apps.py +++ b/src/backend/apps/projects/apps.py @@ -3,4 +3,4 @@ class ProjectConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "apps.project" + name = "apps.projects" diff --git a/src/backend/apps/projects/constants.py b/src/backend/apps/projects/constants.py new file mode 100644 index 0000000..e9a2ffc --- /dev/null +++ b/src/backend/apps/projects/constants.py @@ -0,0 +1,54 @@ +MAX_LENGTH_DESCRIPTION = 750 +MIN_LENGTH_DESCRIPTION = 50 +LENGTH_DESCRIPTION_ERROR_TEXT = ( + f"Длина поля от {MIN_LENGTH_DESCRIPTION} до {MAX_LENGTH_DESCRIPTION} " + "символов." +) + +MAX_LENGTH_PROJECT_NAME = 100 +MIN_LENGTH_PROJECT_NAME = 5 +LENGTH_PROJECT_NAME_ERROR_TEXT = ( + f"Длина поля от {MIN_LENGTH_PROJECT_NAME} до {MAX_LENGTH_PROJECT_NAME} " + "символов." +) +REGEX_PROJECT_NAME = r"(^[+/:,.0-9A-Za-zА-Яа-яЁё\-–—]+)\Z" +REGEX_PROJECT_NAME_ERROR_TEXT = ( + "Название проекта может содержать: кириллические и латинские символы, " + "цифры и символы .,-—+/:" +) + +MAX_LENGTH_DIRECTION_NAME = 20 +MIN_LENGTH_DIRECTION_NAME = 2 +LENGTH_DIRECTION_NAME_ERROR_TEXT = ( + f"Длина поля от {MIN_LENGTH_PROJECT_NAME} до {MAX_LENGTH_PROJECT_NAME} " + "символов." +) +REGEX_DIRECTION_NAME = r"(^[A-Za-zА-Яа-яЁё]+)\Z" +REGEX_DIRECTION_NAME_ERROR_TEXT = "Направление разработки может содержать: кириллические и латинские символы." + +MAX_LENGTH_LINK = 256 +MIN_LENGTH_LINK = 5 +LENGTH_LINK_ERROR_TEXT = ( + f"Длина поля от {MIN_LENGTH_LINK} до {MAX_LENGTH_LINK} символов." +) + +BUSYNESS_CHOICES = ( + (1, 10), + (2, 20), + (3, 30), + (4, 40), +) +STATUS_CHOICES = ( + (1, "Активен"), + (2, "Завершен"), + (3, "Черновик"), +) +LEVEL_CHOICES = ( + (1, "intern"), + (2, "junior"), + (3, "middle"), + (4, "senior"), + (5, "lead"), +) + +PROJECTS_PER_PAGE = 10 diff --git a/src/backend/apps/project/migrations/0001_initial.py b/src/backend/apps/projects/migrations/0001_initial.py similarity index 52% rename from src/backend/apps/project/migrations/0001_initial.py rename to src/backend/apps/projects/migrations/0001_initial.py index 84c6584..ac568b1 100644 --- a/src/backend/apps/project/migrations/0001_initial.py +++ b/src/backend/apps/projects/migrations/0001_initial.py @@ -1,5 +1,6 @@ -# Generated by Django 5.0.1 on 2024-02-21 16:41 +# Generated by Django 5.0.1 on 2024-03-13 13:24 +import django.core.validators import django.db.models.deletion from django.conf import settings from django.db import migrations, models @@ -14,6 +15,42 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name="Direction", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + max_length=20, + unique=True, + validators=[ + django.core.validators.MinLengthValidator( + limit_value=2, + message="Длина поля от 5 до 100 символов.", + ), + django.core.validators.RegexValidator( + message="Направление разработки может содержать: кириллические и латинские символы.", + regex="(^[A-Za-zА-Яа-яЁё]+)\\Z", + ), + ], + verbose_name="Название", + ), + ), + ], + options={ + "verbose_name": "Направление разработки", + "verbose_name_plural": "Направления разработки", + }, + ), migrations.CreateModel( name="Project", fields=[ @@ -28,41 +65,94 @@ class Migration(migrations.Migration): ), ("created", models.DateTimeField(auto_now_add=True)), ("modified", models.DateTimeField(auto_now=True)), + ( + "phone_number", + models.TextField( + blank=True, + max_length=12, + validators=[ + django.core.validators.RegexValidator( + message="Телефон может содержать: цифры, спецсимволы, длина не должна превышать 12 символов", + regex="^\\+7\\(\\d{3}\\)\\d{3}-\\d{2}-\\d{2}$", + ) + ], + verbose_name="Номер телефона", + ), + ), + ( + "telegram_nick", + models.CharField( + blank=True, + max_length=32, + validators=[ + django.core.validators.MinLengthValidator( + limit_value=5, + message="Длина поля от 5 до 32 символов.", + ), + django.core.validators.RegexValidator( + message="Введите корректное имя пользователя. Оно может состоять из латинских букв, цифр и символа подчеркивания.", + regex="^[a-zA-Z0-9_]+$", + ), + ], + verbose_name="Ник в телеграм", + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=256, verbose_name="E-mail" + ), + ), ( "name", models.CharField( - max_length=100, verbose_name="Название проекта" + max_length=100, + validators=[ + django.core.validators.MinLengthValidator( + limit_value=5, + message="Длина поля от 5 до 100 символов.", + ), + django.core.validators.RegexValidator( + message="Название проекта может содержать: кириллические и латинские символы, цифры и символы .,-—+/:", + regex="(^[+/:,.0-9A-Za-zА-Яа-яЁё\\-–—]+)\\Z", + ), + ], + verbose_name="Название", ), ), ( "description", models.TextField( - max_length=3000, verbose_name="Описание проекта" + blank=True, + max_length=750, + validators=[ + django.core.validators.MinLengthValidator( + limit_value=50, + message="Длина поля от 50 до 750 символов.", + ) + ], + verbose_name="Описание", ), ), ( "started", models.DateField( - blank=True, null=True, verbose_name="Начало проекта" + blank=True, null=True, verbose_name="Дата начала" ), ), ( "ended", models.DateField( - blank=True, null=True, verbose_name="Окончание проекта" + blank=True, null=True, verbose_name="Дата завершения" ), ), ( "busyness", models.PositiveSmallIntegerField( + blank=True, choices=[(1, "10"), (2, "20"), (3, "30"), (4, "40")], - verbose_name="Занятость в часах в неделю", - ), - ), - ( - "recruitment_status", - models.BooleanField( - default=False, verbose_name="Статус набора участников" + null=True, + verbose_name="Занятость (час/нед)", ), ), ( @@ -73,20 +163,20 @@ class Migration(migrations.Migration): (2, "Завершен"), (3, "Черновик"), ], - verbose_name="Статус проекта", - ), - ), - ( - "contacts", - models.TextField( - max_length=256, verbose_name="Контакты для связи" + verbose_name="Статус", ), ), ( - "direction", - models.PositiveSmallIntegerField( - choices=[(1, "Десктоп"), (2, "Веб"), (3, "Мобильная")], - verbose_name="Направление разработки", + "link", + models.URLField( + max_length=256, + validators=[ + django.core.validators.MinLengthValidator( + limit_value=5, + message="Длина поля от 5 до 256 символов.", + ) + ], + verbose_name="Ссылка", ), ), ( @@ -98,6 +188,15 @@ class Migration(migrations.Migration): verbose_name="Организатор", ), ), + ( + "directions", + models.ManyToManyField( + blank=True, + related_name="projects_direction", + to="projects.direction", + verbose_name="Направления разработки", + ), + ), ( "owner", models.ForeignKey( @@ -113,7 +212,7 @@ class Migration(migrations.Migration): blank=True, related_name="projects_participated", to=settings.AUTH_USER_MODEL, - verbose_name="Участники проекта", + verbose_name="Участники", ), ), ], @@ -123,39 +222,6 @@ class Migration(migrations.Migration): "ordering": ("-created",), }, ), - migrations.CreateModel( - name="Specialist", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField( - max_length=100, verbose_name="Название специализации" - ), - ), - ( - "specialization", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="specialists", - to="general.specialization", - verbose_name="Специальность", - ), - ), - ], - options={ - "verbose_name": "Специалист", - "verbose_name_plural": "Специалисты", - }, - ), migrations.CreateModel( name="ProjectSpecialist", fields=[ @@ -190,14 +256,14 @@ class Migration(migrations.Migration): ( "is_required", models.BooleanField( - default=False, verbose_name="Требуется для проекта" + default=False, verbose_name="Требуется" ), ), ( "project", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - to="project.project", + to="projects.project", verbose_name="Проект", ), ), @@ -211,7 +277,7 @@ class Migration(migrations.Migration): "specialist", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - to="project.specialist", + to="general.specialist", verbose_name="Специалист", ), ), @@ -222,4 +288,11 @@ class Migration(migrations.Migration): "default_related_name": "project_specialists", }, ), + migrations.AddConstraint( + model_name="project", + constraint=models.UniqueConstraint( + fields=("creator", "name"), + name="projects_project_unique_name_per_creator", + ), + ), ] diff --git a/src/backend/__init__.py b/src/backend/apps/projects/migrations/__init__.py similarity index 100% rename from src/backend/__init__.py rename to src/backend/apps/projects/migrations/__init__.py diff --git a/src/backend/apps/projects/mixins.py b/src/backend/apps/projects/mixins.py new file mode 100644 index 0000000..384b512 --- /dev/null +++ b/src/backend/apps/projects/mixins.py @@ -0,0 +1,10 @@ +class RecruitmentStatusMixin: + def calculate_recruitment_status(self, obj): + """Метод определения статуса набора в проект.""" + + if any( + specialist.is_required + for specialist in obj.project_specialists.all() + ): + return "Набор открыт" + return "Набор закрыт" diff --git a/src/backend/apps/projects/models.py b/src/backend/apps/projects/models.py new file mode 100644 index 0000000..79f7ece --- /dev/null +++ b/src/backend/apps/projects/models.py @@ -0,0 +1,201 @@ +from django.contrib.auth import get_user_model +from django.core.validators import MinLengthValidator, RegexValidator +from django.db import models + +from apps.general.models import ( + ContactsFields, + CreatedModifiedFields, + Skill, + Specialist, +) +from apps.projects.constants import ( + BUSYNESS_CHOICES, + LENGTH_DESCRIPTION_ERROR_TEXT, + LENGTH_DIRECTION_NAME_ERROR_TEXT, + LENGTH_LINK_ERROR_TEXT, + LENGTH_PROJECT_NAME_ERROR_TEXT, + LEVEL_CHOICES, + MAX_LENGTH_DESCRIPTION, + MAX_LENGTH_DIRECTION_NAME, + MAX_LENGTH_LINK, + MAX_LENGTH_PROJECT_NAME, + MIN_LENGTH_DESCRIPTION, + MIN_LENGTH_DIRECTION_NAME, + MIN_LENGTH_LINK, + MIN_LENGTH_PROJECT_NAME, + REGEX_DIRECTION_NAME, + REGEX_DIRECTION_NAME_ERROR_TEXT, + REGEX_PROJECT_NAME, + REGEX_PROJECT_NAME_ERROR_TEXT, + STATUS_CHOICES, +) + +User = get_user_model() + + +class Direction(models.Model): + """Модель направления разработки.""" + + name = models.CharField( + verbose_name="Название", + max_length=MAX_LENGTH_DIRECTION_NAME, + unique=True, + validators=( + MinLengthValidator( + limit_value=MIN_LENGTH_DIRECTION_NAME, + message=LENGTH_DIRECTION_NAME_ERROR_TEXT, + ), + RegexValidator( + regex=REGEX_DIRECTION_NAME, + message=REGEX_DIRECTION_NAME_ERROR_TEXT, + ), + ), + ) + + class Meta: + verbose_name = "Направление разработки" + verbose_name_plural = "Направления разработки" + + def __str__(self) -> str: + """Метод строкового представления объекта направления разработки.""" + + return self.name + + +class Project(CreatedModifiedFields, ContactsFields): + """Модель проекта.""" + + ACTIVE = 1 + ENDED = 2 + DRAFT = 3 + + name = models.CharField( + verbose_name="Название", + max_length=MAX_LENGTH_PROJECT_NAME, + validators=( + MinLengthValidator( + limit_value=MIN_LENGTH_PROJECT_NAME, + message=LENGTH_PROJECT_NAME_ERROR_TEXT, + ), + RegexValidator( + regex=REGEX_PROJECT_NAME, + message=REGEX_PROJECT_NAME_ERROR_TEXT, + ), + ), + ) + description = models.TextField( + verbose_name="Описание", + max_length=MAX_LENGTH_DESCRIPTION, + blank=True, + validators=( + MinLengthValidator( + limit_value=MIN_LENGTH_DESCRIPTION, + message=LENGTH_DESCRIPTION_ERROR_TEXT, + ), + ), + ) + creator = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name="created_projects", + verbose_name="Организатор", + ) + owner = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name="owned_projects", + verbose_name="Владелец", + ) + started = models.DateField( + verbose_name="Дата начала", + null=True, + blank=True, + ) + ended = models.DateField( + verbose_name="Дата завершения", + null=True, + blank=True, + ) + busyness = models.PositiveSmallIntegerField( + verbose_name="Занятость (час/нед)", + choices=BUSYNESS_CHOICES, + null=True, + blank=True, + ) + status = models.PositiveSmallIntegerField( + verbose_name="Статус", + choices=STATUS_CHOICES, + ) + directions = models.ManyToManyField( + Direction, + verbose_name="Направления разработки", + related_name="projects_direction", + blank=True, + ) + link = models.URLField( + verbose_name="Ссылка", + max_length=MAX_LENGTH_LINK, + validators=( + MinLengthValidator( + limit_value=MIN_LENGTH_LINK, + message=LENGTH_LINK_ERROR_TEXT, + ), + ), + ) + participants = models.ManyToManyField( + User, + verbose_name="Участники", + related_name="projects_participated", + blank=True, + ) + + class Meta: + verbose_name = "Проект" + verbose_name_plural = "Проекты" + ordering = ("-created",) + constraints = ( + models.UniqueConstraint( + fields=("creator", "name"), + name=("%(app_label)s_%(class)s_unique_name_per_creator"), + ), + ) + + def __str__(self) -> str: + """Метод строкового представления объекта проекта.""" + + return self.name + + +class ProjectSpecialist(models.Model): + """Модель специалиста необходимого проекту.""" + + project = models.ForeignKey( + Project, + on_delete=models.CASCADE, + verbose_name="Проект", + ) + specialist = models.ForeignKey( + Specialist, + on_delete=models.CASCADE, + verbose_name="Специалист", + ) + skills = models.ManyToManyField( + Skill, + verbose_name="Навыки", + ) + count = models.PositiveSmallIntegerField( + verbose_name="Количество", + ) + level = models.PositiveSmallIntegerField( + verbose_name="Уровень", + choices=LEVEL_CHOICES, + ) + is_required = models.BooleanField( + verbose_name="Требуется", + default=False, + ) + + class Meta: + verbose_name = "Специалист проекта" + verbose_name_plural = "Специалисты проекта" + default_related_name = "project_specialists" diff --git a/src/backend/config/settings/base.py b/src/backend/config/settings/base.py index 190385c..a78f97c 100644 --- a/src/backend/config/settings/base.py +++ b/src/backend/config/settings/base.py @@ -37,7 +37,7 @@ LOCAL_APPS: list = [ "apps.general", "apps.users", - "apps.project", + "apps.projects", ] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS diff --git a/src/backend/config/urls.py b/src/backend/config/urls.py index d0c8c3c..714cd31 100644 --- a/src/backend/config/urls.py +++ b/src/backend/config/urls.py @@ -3,7 +3,7 @@ from django.contrib import admin from django.urls import include, path -urlpatterns: List[str] = [ +urlpatterns = [ path("admin/", admin.site.urls), - path("api/v1/", include("api.v1.urls")), + path("api/", include("api.urls")), ] diff --git a/src/backend/data/fixtures/direction.json b/src/backend/data/fixtures/direction.json new file mode 100644 index 0000000..f2eb7df --- /dev/null +++ b/src/backend/data/fixtures/direction.json @@ -0,0 +1,23 @@ +[ + { + "model": "projects.direction", + "pk": 1, + "fields": { + "name": "Десктоп" + } + }, + { + "model": "projects.direction", + "pk": 2, + "fields": { + "name": "Веб" + } + }, + { + "model": "projects.direction", + "pk": 3, + "fields": { + "name": "Мобильная" + } + } +] diff --git a/src/backend/data/fixtures/skill.json b/src/backend/data/fixtures/skill.json index bb18175..7462b21 100644 --- a/src/backend/data/fixtures/skill.json +++ b/src/backend/data/fixtures/skill.json @@ -1,327 +1,2151 @@ [ -{"model": "project.skill", "pk": 1, "fields": {"name": "Java"}}, -{"model": "project.skill", "pk": 2, "fields": {"name": "Python"}}, -{"model": "project.skill", "pk": 3, "fields": {"name": "JavaScript"}}, -{"model": "project.skill", "pk": 4, "fields": {"name": "C#"}}, -{"model": "project.skill", "pk": 5, "fields": {"name": "C++"}}, -{"model": "project.skill", "pk": 6, "fields": {"name": "Ruby"}}, -{"model": "project.skill", "pk": 7, "fields": {"name": "PHP"}}, -{"model": "project.skill", "pk": 8, "fields": {"name": "Swift"}}, -{"model": "project.skill", "pk": 9, "fields": {"name": "Go"}}, -{"model": "project.skill", "pk": 10, "fields": {"name": "Kotlin"}}, -{"model": "project.skill", "pk": 11, "fields": {"name": "Rust"}}, -{"model": "project.skill", "pk": 12, "fields": {"name": "TypeScript"}}, -{"model": "project.skill", "pk": 13, "fields": {"name": "Objective-C"}}, -{"model": "project.skill", "pk": 14, "fields": {"name": "SQL"}}, -{"model": "project.skill", "pk": 15, "fields": {"name": "HTML"}}, -{"model": "project.skill", "pk": 16, "fields": {"name": "R"}}, -{"model": "project.skill", "pk": 17, "fields": {"name": "MATLAB"}}, -{"model": "project.skill", "pk": 18, "fields": {"name": "Perl"}}, -{"model": "project.skill", "pk": 19, "fields": {"name": "Shell Scripting"}}, -{"model": "project.skill", "pk": 20, "fields": {"name": "Groovy"}}, -{"model": "project.skill", "pk": 21, "fields": {"name": "Scala"}}, -{"model": "project.skill", "pk": 22, "fields": {"name": "Lua"}}, -{"model": "project.skill", "pk": 23, "fields": {"name": "Dart"}}, -{"model": "project.skill", "pk": 24, "fields": {"name": "Julia"}}, -{"model": "project.skill", "pk": 25, "fields": {"name": "Lisp"}}, -{"model": "project.skill", "pk": 26, "fields": {"name": "Haskell"}}, -{"model": "project.skill", "pk": 27, "fields": {"name": "Cobol"}}, -{"model": "project.skill", "pk": 28, "fields": {"name": "Fortran"}}, -{"model": "project.skill", "pk": 29, "fields": {"name": "Ada"}}, -{"model": "project.skill", "pk": 30, "fields": {"name": "Assembly"}}, -{"model": "project.skill", "pk": 31, "fields": {"name": "PowerShell"}}, -{"model": "project.skill", "pk": 32, "fields": {"name": "F#"}}, -{"model": "project.skill", "pk": 33, "fields": {"name": "Prolog"}}, -{"model": "project.skill", "pk": 34, "fields": {"name": "Smalltalk"}}, -{"model": "project.skill", "pk": 35, "fields": {"name": "CoffeeScript"}}, -{"model": "project.skill", "pk": 36, "fields": {"name": "Clojure"}}, -{"model": "project.skill", "pk": 37, "fields": {"name": "Elixir"}}, -{"model": "project.skill", "pk": 38, "fields": {"name": "Erlang"}}, -{"model": "project.skill", "pk": 39, "fields": {"name": "Crystal"}}, -{"model": "project.skill", "pk": 40, "fields": {"name": "D"}}, -{"model": "project.skill", "pk": 41, "fields": {"name": "Scheme"}}, -{"model": "project.skill", "pk": 42, "fields": {"name": "CSS"}}, -{"model": "project.skill", "pk": 43, "fields": {"name": "XML"}}, -{"model": "project.skill", "pk": 44, "fields": {"name": "YAML"}}, -{"model": "project.skill", "pk": 45, "fields": {"name": "Markdown"}}, -{"model": "project.skill", "pk": 46, "fields": {"name": "LaTeX"}}, -{"model": "project.skill", "pk": 47, "fields": {"name": "Bash Scripting"}}, -{"model": "project.skill", "pk": 48, "fields": {"name": "React.js"}}, -{"model": "project.skill", "pk": 49, "fields": {"name": "AngularJS"}}, -{"model": "project.skill", "pk": 50, "fields": {"name": "Vue.js"}}, -{"model": "project.skill", "pk": 51, "fields": {"name": "Node.js"}}, -{"model": "project.skill", "pk": 52, "fields": {"name": "Express.js"}}, -{"model": "project.skill", "pk": 53, "fields": {"name": "Ruby on Rails"}}, -{"model": "project.skill", "pk": 54, "fields": {"name": "Django"}}, -{"model": "project.skill", "pk": 55, "fields": {"name": "Flask"}}, -{"model": "project.skill", "pk": 56, "fields": {"name": "Laravel"}}, -{"model": "project.skill", "pk": 57, "fields": {"name": "ASP.NET"}}, -{"model": "project.skill", "pk": 58, "fields": {"name": "Spring"}}, -{"model": "project.skill", "pk": 59, "fields": {"name": "Ember.js"}}, -{"model": "project.skill", "pk": 60, "fields": {"name": "Backbone.js"}}, -{"model": "project.skill", "pk": 61, "fields": {"name": "Meteor.js"}}, -{"model": "project.skill", "pk": 62, "fields": {"name": "Knockout.js"}}, -{"model": "project.skill", "pk": 63, "fields": {"name": "Redux"}}, -{"model": "project.skill", "pk": 64, "fields": {"name": "Redux-Saga"}}, -{"model": "project.skill", "pk": 65, "fields": {"name": "MobX"}}, -{"model": "project.skill", "pk": 66, "fields": {"name": "Xamarin"}}, -{"model": "project.skill", "pk": 67, "fields": {"name": "Flutter"}}, -{"model": "project.skill", "pk": 68, "fields": {"name": "Ionic"}}, -{"model": "project.skill", "pk": 69, "fields": {"name": "Cordova"}}, -{"model": "project.skill", "pk": 70, "fields": {"name": "PhoneGap"}}, -{"model": "project.skill", "pk": 71, "fields": {"name": "Bootstrap"}}, -{"model": "project.skill", "pk": 72, "fields": {"name": "Material-UI"}}, -{"model": "project.skill", "pk": 73, "fields": {"name": "Semantic UI"}}, -{"model": "project.skill", "pk": 74, "fields": {"name": "Tailwind CSS"}}, -{"model": "project.skill", "pk": 75, "fields": {"name": "Bulma"}}, -{"model": "project.skill", "pk": 76, "fields": {"name": "Electron"}}, -{"model": "project.skill", "pk": 77, "fields": {"name": "Flask-SQLAlchemy"}}, -{"model": "project.skill", "pk": 78, "fields": {"name": "Peewee"}}, -{"model": "project.skill", "pk": 79, "fields": {"name": "PonyORM"}}, -{"model": "project.skill", "pk": 80, "fields": {"name": "SQLAlchemy"}}, -{"model": "project.skill", "pk": 81, "fields": {"name": "Hibernate"}}, -{"model": "project.skill", "pk": 82, "fields": {"name": "Joi"}}, -{"model": "project.skill", "pk": 83, "fields": {"name": "celebrate"}}, -{"model": "project.skill", "pk": 84, "fields": {"name": "Yup"}}, -{"model": "project.skill", "pk": 85, "fields": {"name": "PyTorch"}}, -{"model": "project.skill", "pk": 86, "fields": {"name": "Keras"}}, -{"model": "project.skill", "pk": 87, "fields": {"name": "TensorFlow"}}, -{"model": "project.skill", "pk": 88, "fields": {"name": "MXNet"}}, -{"model": "project.skill", "pk": 89, "fields": {"name": "Pygame"}}, -{"model": "project.skill", "pk": 90, "fields": {"name": "OpenCV"}}, -{"model": "project.skill", "pk": 91, "fields": {"name": "Unity"}}, -{"model": "project.skill", "pk": 92, "fields": {"name": "SDL"}}, -{"model": "project.skill", "pk": 93, "fields": {"name": "Qt"}}, -{"model": "project.skill", "pk": 94, "fields": {"name": "wxWidgets"}}, -{"model": "project.skill", "pk": 95, "fields": {"name": "GTK"}}, -{"model": "project.skill", "pk": 96, "fields": {"name": "Cocoa"}}, -{"model": "project.skill", "pk": 97, "fields": {"name": "UIKit"}}, -{"model": "project.skill", "pk": 98, "fields": {"name": "React Native"}}, -{"model": "project.skill", "pk": 99, "fields": {"name": "NativeScript"}}, -{"model": "project.skill", "pk": 100, "fields": {"name": "Xamarin.Forms"}}, -{"model": "project.skill", "pk": 101, "fields": {"name": "Adobe PhoneGap"}}, -{"model": "project.skill", "pk": 102, "fields": {"name": "NW.js"}}, -{"model": "project.skill", "pk": 103, "fields": {"name": "Flask-SocketIO"}}, -{"model": "project.skill", "pk": 104, "fields": {"name": "Socket.IO"}}, -{"model": "project.skill", "pk": 105, "fields": {"name": "Django Channels"}}, -{"model": "project.skill", "pk": 106, "fields": {"name": "Pusher"}}, -{"model": "project.skill", "pk": 107, "fields": {"name": "Firebase"}}, -{"model": "project.skill", "pk": 108, "fields": {"name": "AWS Amplify"}}, -{"model": "project.skill", "pk": 109, "fields": {"name": "Microsoft Azure"}}, -{"model": "project.skill", "pk": 110, "fields": {"name": "Google Cloud Platform"}}, -{"model": "project.skill", "pk": 111, "fields": {"name": "Heroku"}}, -{"model": "project.skill", "pk": 112, "fields": {"name": "Docker"}}, -{"model": "project.skill", "pk": 113, "fields": {"name": "Kubernetes"}}, -{"model": "project.skill", "pk": 114, "fields": {"name": "OpenStack"}}, -{"model": "project.skill", "pk": 115, "fields": {"name": "Ansible"}}, -{"model": "project.skill", "pk": 116, "fields": {"name": "Chef"}}, -{"model": "project.skill", "pk": 117, "fields": {"name": "Puppet"}}, -{"model": "project.skill", "pk": 118, "fields": {"name": "Terraform"}}, -{"model": "project.skill", "pk": 119, "fields": {"name": "Jenkins"}}, -{"model": "project.skill", "pk": 120, "fields": {"name": "Travis CI"}}, -{"model": "project.skill", "pk": 121, "fields": {"name": "GitLab CI/CD"}}, -{"model": "project.skill", "pk": 122, "fields": {"name": "CircleCI"}}, -{"model": "project.skill", "pk": 123, "fields": {"name": "Argo CD"}}, -{"model": "project.skill", "pk": 124, "fields": {"name": "Sentry"}}, -{"model": "project.skill", "pk": 125, "fields": {"name": "Datadog"}}, -{"model": "project.skill", "pk": 126, "fields": {"name": "New Relic"}}, -{"model": "project.skill", "pk": 127, "fields": {"name": "Elasticsearch"}}, -{"model": "project.skill", "pk": 128, "fields": {"name": "Grafana"}}, -{"model": "project.skill", "pk": 129, "fields": {"name": "Prometheus"}}, -{"model": "project.skill", "pk": 130, "fields": {"name": "Graylog"}}, -{"model": "project.skill", "pk": 131, "fields": {"name": "RabbitMQ"}}, -{"model": "project.skill", "pk": 132, "fields": {"name": "Apache ActiveMQ"}}, -{"model": "project.skill", "pk": 133, "fields": {"name": "Redis"}}, -{"model": "project.skill", "pk": 134, "fields": {"name": "Memcached"}}, -{"model": "project.skill", "pk": 135, "fields": {"name": "MongoDB"}}, -{"model": "project.skill", "pk": 136, "fields": {"name": "CouchDB"}}, -{"model": "project.skill", "pk": 137, "fields": {"name": "Firebase Cloud Firestore"}}, -{"model": "project.skill", "pk": 138, "fields": {"name": "Neo4j"}}, -{"model": "project.skill", "pk": 139, "fields": {"name": "Cassandra"}}, -{"model": "project.skill", "pk": 140, "fields": {"name": "MySQL"}}, -{"model": "project.skill", "pk": 141, "fields": {"name": "MariaDB"}}, -{"model": "project.skill", "pk": 142, "fields": {"name": "PostgreSQL"}}, -{"model": "project.skill", "pk": 143, "fields": {"name": "SQLite"}}, -{"model": "project.skill", "pk": 144, "fields": {"name": "Elasticsearch"}}, -{"model": "project.skill", "pk": 145, "fields": {"name": "Angular Material"}}, -{"model": "project.skill", "pk": 146, "fields": {"name": "Vuetify"}}, -{"model": "project.skill", "pk": 147, "fields": {"name": "Materialize CSS"}}, -{"model": "project.skill", "pk": 148, "fields": {"name": "Ant Design"}}, -{"model": "project.skill", "pk": 149, "fields": {"name": "Tailwind CSS"}}, -{"model": "project.skill", "pk": 150, "fields": {"name": "Bulma"}}, -{"model": "project.skill", "pk": 151, "fields": {"name": "Foundation"}}, -{"model": "project.skill", "pk": 152, "fields": {"name": "Sass"}}, -{"model": "project.skill", "pk": 153, "fields": {"name": "LESS"}}, -{"model": "project.skill", "pk": 154, "fields": {"name": "Framer Motion"}}, -{"model": "project.skill", "pk": 155, "fields": {"name": "GSAP"}}, -{"model": "project.skill", "pk": 156, "fields": {"name": "PixiJS"}}, -{"model": "project.skill", "pk": 157, "fields": {"name": "Chart.js"}}, -{"model": "project.skill", "pk": 158, "fields": {"name": "Highcharts"}}, -{"model": "project.skill", "pk": 159, "fields": {"name": "Leaflet"}}, -{"model": "project.skill", "pk": 160, "fields": {"name": "Mapbox"}}, -{"model": "project.skill", "pk": 161, "fields": {"name": "WebRTC"}}, -{"model": "project.skill", "pk": 162, "fields": {"name": "PeerJS"}}, -{"model": "project.skill", "pk": 163, "fields": {"name": "Socket.IO"}}, -{"model": "project.skill", "pk": 164, "fields": {"name": "Cocos2d-x"}}, -{"model": "project.skill", "pk": 165, "fields": {"name": "Pygame Zero"}}, -{"model": "project.skill", "pk": 166, "fields": {"name": "Godot Engine"}}, -{"model": "project.skill", "pk": 167, "fields": {"name": "Allegro"}}, -{"model": "project.skill", "pk": 168, "fields": {"name": "SFML"}}, -{"model": "project.skill", "pk": 169, "fields": {"name": "SDL"}}, -{"model": "project.skill", "pk": 170, "fields": {"name": "JUnit"}}, -{"model": "project.skill", "pk": 171, "fields": {"name": "NUnit"}}, -{"model": "project.skill", "pk": 172, "fields": {"name": "TestNG"}}, -{"model": "project.skill", "pk": 173, "fields": {"name": "TestCafe"}}, -{"model": "project.skill", "pk": 174, "fields": {"name": "Apache JMeter"}}, -{"model": "project.skill", "pk": 175, "fields": {"name": "Gatling"}}, -{"model": "project.skill", "pk": 176, "fields": {"name": "Cypress"}}, -{"model": "project.skill", "pk": 177, "fields": {"name": "Puppeteer"}}, -{"model": "project.skill", "pk": 178, "fields": {"name": "Selenium"}}, -{"model": "project.skill", "pk": 179, "fields": {"name": "Appium"}}, -{"model": "project.skill", "pk": 180, "fields": {"name": "Postman"}}, -{"model": "project.skill", "pk": 181, "fields": {"name": "SoapUI"}}, -{"model": "project.skill", "pk": 182, "fields": {"name": "REST Assured"}}, -{"model": "project.skill", "pk": 183, "fields": {"name": "Mockito"}}, -{"model": "project.skill", "pk": 184, "fields": {"name": "WireMock"}}, -{"model": "project.skill", "pk": 185, "fields": {"name": "Taiko"}}, -{"model": "project.skill", "pk": 186, "fields": {"name": "WinAppDriver"}}, -{"model": "project.skill", "pk": 187, "fields": {"name": "Robot Framework"}}, -{"model": "project.skill", "pk": 188, "fields": {"name": "Serenity BDD"}}, -{"model": "project.skill", "pk": 189, "fields": {"name": "Cucumber"}}, -{"model": "project.skill", "pk": 190, "fields": {"name": "FitNesse"}}, -{"model": "project.skill", "pk": 191, "fields": {"name": "Gauge"}}, -{"model": "project.skill", "pk": 192, "fields": {"name": "Geb"}}, -{"model": "project.skill", "pk": 193, "fields": {"name": "Chimp"}}, -{"model": "project.skill", "pk": 194, "fields": {"name": "Jasmine"}}, -{"model": "project.skill", "pk": 195, "fields": {"name": "Mocha"}}, -{"model": "project.skill", "pk": 196, "fields": {"name": "Jest"}}, -{"model": "project.skill", "pk": 197, "fields": {"name": "Enzyme"}}, -{"model": "project.skill", "pk": 198, "fields": {"name": "Karma"}}, -{"model": "project.skill", "pk": 199, "fields": {"name": "AVA"}}, -{"model": "project.skill", "pk": 200, "fields": {"name": "Git"}}, -{"model": "project.skill", "pk": 201, "fields": {"name": "Logstash"}}, -{"model": "project.skill", "pk": 202, "fields": {"name": "Kibana"}}, -{"model": "project.skill", "pk": 203, "fields": {"name": "MySQL"}}, -{"model": "project.skill", "pk": 204, "fields": {"name": "PostgreSQL"}}, -{"model": "project.skill", "pk": 205, "fields": {"name": "MongoDB"}}, -{"model": "project.skill", "pk": 206, "fields": {"name": "Oracle"}}, -{"model": "project.skill", "pk": 207, "fields": {"name": "Redis"}}, -{"model": "project.skill", "pk": 208, "fields": {"name": "SQLite"}}, -{"model": "project.skill", "pk": 209, "fields": {"name": "MariaDB"}}, -{"model": "project.skill", "pk": 210, "fields": {"name": "CouchDB"}}, -{"model": "project.skill", "pk": 211, "fields": {"name": "Cassandra"}}, -{"model": "project.skill", "pk": 212, "fields": {"name": "Microsoft SQL Server"}}, -{"model": "project.skill", "pk": 213, "fields": {"name": "Amazon RDS"}}, -{"model": "project.skill", "pk": 214, "fields": {"name": "Google Cloud Spanner"}}, -{"model": "project.skill", "pk": 215, "fields": {"name": "IBM DB2"}}, -{"model": "project.skill", "pk": 216, "fields": {"name": "Apache HBase"}}, -{"model": "project.skill", "pk": 217, "fields": {"name": "Firebase Realtime Database"}}, -{"model": "project.skill", "pk": 218, "fields": {"name": "Neo4j"}}, -{"model": "project.skill", "pk": 219, "fields": {"name": "Apache Couchbase"}}, -{"model": "project.skill", "pk": 220, "fields": {"name": "Firebase Firestore"}}, -{"model": "project.skill", "pk": 221, "fields": {"name": "ArangoDB"}}, -{"model": "project.skill", "pk": 222, "fields": {"name": "InfluxDB"}}, -{"model": "project.skill", "pk": 223, "fields": {"name": "Elasticsearch"}}, -{"model": "project.skill", "pk": 224, "fields": {"name": "Teradata"}}, -{"model": "project.skill", "pk": 225, "fields": {"name": "Informix"}}, -{"model": "project.skill", "pk": 226, "fields": {"name": "Sybase"}}, -{"model": "project.skill", "pk": 227, "fields": {"name": "Amazon DynamoDB"}}, -{"model": "project.skill", "pk": 228, "fields": {"name": "RESTful API"}}, -{"model": "project.skill", "pk": 229, "fields": {"name": "GraphQL"}}, -{"model": "project.skill", "pk": 230, "fields": {"name": "AJAX"}}, -{"model": "project.skill", "pk": 231, "fields": {"name": "JSON"}}, -{"model": "project.skill", "pk": 232, "fields": {"name": "Sass"}}, -{"model": "project.skill", "pk": 233, "fields": {"name": "Less"}}, -{"model": "project.skill", "pk": 234, "fields": {"name": "Bootstrap"}}, -{"model": "project.skill", "pk": 235, "fields": {"name": "CSS Grid"}}, -{"model": "project.skill", "pk": 236, "fields": {"name": "WebRTC"}}, -{"model": "project.skill", "pk": 237, "fields": {"name": "WebSockets"}}, -{"model": "project.skill", "pk": 238, "fields": {"name": "Web Workers"}}, -{"model": "project.skill", "pk": 239, "fields": {"name": "PWA"}}, -{"model": "project.skill", "pk": 240, "fields": {"name": "Service Workers"}}, -{"model": "project.skill", "pk": 241, "fields": {"name": "SPA"}}, -{"model": "project.skill", "pk": 242, "fields": {"name": "Serverless Architecture"}}, -{"model": "project.skill", "pk": 243, "fields": {"name": "Microservices"}}, -{"model": "project.skill", "pk": 244, "fields": {"name": "Web Accessibility"}}, -{"model": "project.skill", "pk": 245, "fields": {"name": "RWD"}}, -{"model": "project.skill", "pk": 246, "fields": {"name": "Cross-Origin Resource Sharing (CORS)"}}, -{"model": "project.skill", "pk": 247, "fields": {"name": "JSON Web Tokens (JWT)"}}, -{"model": "project.skill", "pk": 248, "fields": {"name": "Content Delivery Network (CDN)"}}, -{"model": "project.skill", "pk": 249, "fields": {"name": "Browser Compatibility Testing"}}, -{"model": "project.skill", "pk": 250, "fields": {"name": "Webpack"}}, -{"model": "project.skill", "pk": 251, "fields": {"name": "SEO"}}, -{"model": "project.skill", "pk": 252, "fields": {"name": "Performance Optimization"}}, -{"model": "project.skill", "pk": 253, "fields": {"name": "Browser Storage"}}, -{"model": "project.skill", "pk": 254, "fields": {"name": "Web Animations"}}, -{"model": "project.skill", "pk": 255, "fields": {"name": "Progressive Enhancement"}}, -{"model": "project.skill", "pk": 256, "fields": {"name": "Web Security"}}, -{"model": "project.skill", "pk": 257, "fields": {"name": "Web Typography"}}, -{"model": "project.skill", "pk": 258, "fields": {"name": "Internationalization (i18n)"}}, -{"model": "project.skill", "pk": 259, "fields": {"name": "CMS"}}, -{"model": "project.skill", "pk": 260, "fields": {"name": "Headless CMS"}}, -{"model": "project.skill", "pk": 261, "fields": {"name": "Static Site Generators"}}, -{"model": "project.skill", "pk": 262, "fields": {"name": "Web Scraping"}}, -{"model": "project.skill", "pk": 263, "fields": {"name": "Web Analytics"}}, -{"model": "project.skill", "pk": 264, "fields": {"name": "A/B Testing"}}, -{"model": "project.skill", "pk": 265, "fields": {"name": "Web Testing Frameworks"}}, -{"model": "project.skill", "pk": 266, "fields": {"name": "Responsive Images"}}, -{"model": "project.skill", "pk": 267, "fields": {"name": "Web Fonts"}}, -{"model": "project.skill", "pk": 268, "fields": {"name": "Lazy Loading"}}, -{"model": "project.skill", "pk": 269, "fields": {"name": "Web Accessibility Testing"}}, -{"model": "project.skill", "pk": 270, "fields": {"name": "Web Design Patterns"}}, -{"model": "project.skill", "pk": 271, "fields": {"name": "Web Frameworks"}}, -{"model": "project.skill", "pk": 272, "fields": {"name": "Server-Side Rendering (SSR)"}}, -{"model": "project.skill", "pk": 273, "fields": {"name": "Client-Side Rendering (CSR)"}}, -{"model": "project.skill", "pk": 274, "fields": {"name": "Content Management System (CMS) APIs"}}, -{"model": "project.skill", "pk": 275, "fields": {"name": "API Integration"}}, -{"model": "project.skill", "pk": 276, "fields": {"name": "Web Performance Metrics"}}, -{"model": "project.skill", "pk": 277, "fields": {"name": "Version Control Systems"}}, -{"model": "project.skill", "pk": 278, "fields": {"name": "Gulp"}}, -{"model": "project.skill", "pk": 279, "fields": {"name": "ООП"}}, -{"model": "project.skill", "pk": 280, "fields": {"name": "Управление проектами"}}, -{"model": "project.skill", "pk": 281, "fields": {"name": "Scrum"}}, -{"model": "project.skill", "pk": 282, "fields": {"name": "Kanban"}}, -{"model": "project.skill", "pk": 283, "fields": {"name": "Trello"}}, -{"model": "project.skill", "pk": 284, "fields": {"name": "Waterfall"}}, -{"model": "project.skill", "pk": 285, "fields": {"name": "Разработка ТЗ"}}, -{"model": "project.skill", "pk": 286, "fields": {"name": "Agile"}}, -{"model": "project.skill", "pk": 287, "fields": {"name": "Веб-разработка"}}, -{"model": "project.skill", "pk": 288, "fields": {"name": "Адаптивная верстка"}}, -{"model": "project.skill", "pk": 289, "fields": {"name": "Кроссбраузерная верстка"}}, -{"model": "project.skill", "pk": 290, "fields": {"name": "Валидная верстка"}}, -{"model": "project.skill", "pk": 291, "fields": {"name": "Семантическая верстка"}}, -{"model": "project.skill", "pk": 292, "fields": {"name": "Pixel-perfect"}}, -{"model": "project.skill", "pk": 293, "fields": {"name": "Базы данных"}}, -{"model": "project.skill", "pk": 294, "fields": {"name": "CI/CD"}}, -{"model": "project.skill", "pk": 295, "fields": {"name": "Jira"}}, -{"model": "project.skill", "pk": 296, "fields": {"name": "TestRail"}}, -{"model": "project.skill", "pk": 297, "fields": {"name": "Тестирование ПО"}}, -{"model": "project.skill", "pk": 298, "fields": {"name": "Тестирование сайтов"}}, -{"model": "project.skill", "pk": 299, "fields": {"name": "Тестирование API"}}, -{"model": "project.skill", "pk": 300, "fields": {"name": "Тестирование мобильных приложений"}}, -{"model": "project.skill", "pk": 301, "fields": {"name": "Black box testing"}}, -{"model": "project.skill", "pk": 302, "fields": {"name": "White box testing"}}, -{"model": "project.skill", "pk": 303, "fields": {"name": "Тестирование дизайна"}}, -{"model": "project.skill", "pk": 304, "fields": {"name": "Тестирование методом свободного поиска"}}, -{"model": "project.skill", "pk": 305, "fields": {"name": "Тестирование производительности"}}, -{"model": "project.skill", "pk": 306, "fields": {"name": "Тестирование на проникновение"}}, -{"model": "project.skill", "pk": 307, "fields": {"name": "Автоматизация тестирования"}}, -{"model": "project.skill", "pk": 308, "fields": {"name": "Анализ требований"}}, -{"model": "project.skill", "pk": 309, "fields": {"name": "Анализ данных"}}, -{"model": "project.skill", "pk": 310, "fields": {"name": "Веб аналитика"}}, -{"model": "project.skill", "pk": 311, "fields": {"name": "Построение команды"}}, -{"model": "project.skill", "pk": 312, "fields": {"name": "Управление разработкой"}}, -{"model": "project.skill", "pk": 313, "fields": {"name": "БЭМ"}}, -{"model": "project.skill", "pk": 314, "fields": {"name": "Тест-дизайн"}}, -{"model": "project.skill", "pk": 315, "fields": {"name": "Тест-анализ"}}, -{"model": "project.skill", "pk": 316, "fields": {"name": "Charles"}}, -{"model": "project.skill", "pk": 317, "fields": {"name": "Fidler"}}, -{"model": "project.skill", "pk": 318, "fields": {"name": "DevTools"}}, -{"model": "project.skill", "pk": 319, "fields": {"name": "Kaiten"}}, -{"model": "project.skill", "pk": 320, "fields": {"name": "Cygwin"}}, -{"model": "project.skill", "pk": 321, "fields": {"name": "Figma"}}, -{"model": "project.skill", "pk": 322, "fields": {"name": "1C"}}, -{"model": "project.skill", "pk": 323, "fields": {"name": "Разработка мобильных приложений"}}, -{"model": "project.skill", "pk": 324, "fields": {"name": "Нагрузочное тестирование"}}, -{"model": "project.skill", "pk": 325, "fields": {"name": "Регрессионное тестирование"}} + { + "model": "general.skill", + "pk": 1, + "fields": { + "name": "Java" + } + }, + { + "model": "general.skill", + "pk": 2, + "fields": { + "name": "Python" + } + }, + { + "model": "general.skill", + "pk": 3, + "fields": { + "name": "JavaScript" + } + }, + { + "model": "general.skill", + "pk": 4, + "fields": { + "name": "C#" + } + }, + { + "model": "general.skill", + "pk": 5, + "fields": { + "name": "C++" + } + }, + { + "model": "general.skill", + "pk": 6, + "fields": { + "name": "Ruby" + } + }, + { + "model": "general.skill", + "pk": 7, + "fields": { + "name": "PHP" + } + }, + { + "model": "general.skill", + "pk": 8, + "fields": { + "name": "Swift" + } + }, + { + "model": "general.skill", + "pk": 9, + "fields": { + "name": "Go" + } + }, + { + "model": "general.skill", + "pk": 10, + "fields": { + "name": "Kotlin" + } + }, + { + "model": "general.skill", + "pk": 11, + "fields": { + "name": "Rust" + } + }, + { + "model": "general.skill", + "pk": 12, + "fields": { + "name": "TypeScript" + } + }, + { + "model": "general.skill", + "pk": 13, + "fields": { + "name": "Objective-C" + } + }, + { + "model": "general.skill", + "pk": 14, + "fields": { + "name": "SQL" + } + }, + { + "model": "general.skill", + "pk": 15, + "fields": { + "name": "HTML" + } + }, + { + "model": "general.skill", + "pk": 16, + "fields": { + "name": "R" + } + }, + { + "model": "general.skill", + "pk": 17, + "fields": { + "name": "MATLAB" + } + }, + { + "model": "general.skill", + "pk": 18, + "fields": { + "name": "Perl" + } + }, + { + "model": "general.skill", + "pk": 19, + "fields": { + "name": "Shell Scripting" + } + }, + { + "model": "general.skill", + "pk": 20, + "fields": { + "name": "Groovy" + } + }, + { + "model": "general.skill", + "pk": 21, + "fields": { + "name": "Scala" + } + }, + { + "model": "general.skill", + "pk": 22, + "fields": { + "name": "Lua" + } + }, + { + "model": "general.skill", + "pk": 23, + "fields": { + "name": "Dart" + } + }, + { + "model": "general.skill", + "pk": 24, + "fields": { + "name": "Julia" + } + }, + { + "model": "general.skill", + "pk": 25, + "fields": { + "name": "Lisp" + } + }, + { + "model": "general.skill", + "pk": 26, + "fields": { + "name": "Haskell" + } + }, + { + "model": "general.skill", + "pk": 27, + "fields": { + "name": "Cobol" + } + }, + { + "model": "general.skill", + "pk": 28, + "fields": { + "name": "Fortran" + } + }, + { + "model": "general.skill", + "pk": 29, + "fields": { + "name": "Ada" + } + }, + { + "model": "general.skill", + "pk": 30, + "fields": { + "name": "Assembly" + } + }, + { + "model": "general.skill", + "pk": 31, + "fields": { + "name": "PowerShell" + } + }, + { + "model": "general.skill", + "pk": 32, + "fields": { + "name": "F#" + } + }, + { + "model": "general.skill", + "pk": 33, + "fields": { + "name": "Prolog" + } + }, + { + "model": "general.skill", + "pk": 34, + "fields": { + "name": "Smalltalk" + } + }, + { + "model": "general.skill", + "pk": 35, + "fields": { + "name": "CoffeeScript" + } + }, + { + "model": "general.skill", + "pk": 36, + "fields": { + "name": "Clojure" + } + }, + { + "model": "general.skill", + "pk": 37, + "fields": { + "name": "Elixir" + } + }, + { + "model": "general.skill", + "pk": 38, + "fields": { + "name": "Erlang" + } + }, + { + "model": "general.skill", + "pk": 39, + "fields": { + "name": "Crystal" + } + }, + { + "model": "general.skill", + "pk": 40, + "fields": { + "name": "D" + } + }, + { + "model": "general.skill", + "pk": 41, + "fields": { + "name": "Scheme" + } + }, + { + "model": "general.skill", + "pk": 42, + "fields": { + "name": "CSS" + } + }, + { + "model": "general.skill", + "pk": 43, + "fields": { + "name": "XML" + } + }, + { + "model": "general.skill", + "pk": 44, + "fields": { + "name": "YAML" + } + }, + { + "model": "general.skill", + "pk": 45, + "fields": { + "name": "Markdown" + } + }, + { + "model": "general.skill", + "pk": 46, + "fields": { + "name": "LaTeX" + } + }, + { + "model": "general.skill", + "pk": 47, + "fields": { + "name": "Bash Scripting" + } + }, + { + "model": "general.skill", + "pk": 48, + "fields": { + "name": "React.js" + } + }, + { + "model": "general.skill", + "pk": 49, + "fields": { + "name": "AngularJS" + } + }, + { + "model": "general.skill", + "pk": 50, + "fields": { + "name": "Vue.js" + } + }, + { + "model": "general.skill", + "pk": 51, + "fields": { + "name": "Node.js" + } + }, + { + "model": "general.skill", + "pk": 52, + "fields": { + "name": "Express.js" + } + }, + { + "model": "general.skill", + "pk": 53, + "fields": { + "name": "Ruby on Rails" + } + }, + { + "model": "general.skill", + "pk": 54, + "fields": { + "name": "Django" + } + }, + { + "model": "general.skill", + "pk": 55, + "fields": { + "name": "Flask" + } + }, + { + "model": "general.skill", + "pk": 56, + "fields": { + "name": "Laravel" + } + }, + { + "model": "general.skill", + "pk": 57, + "fields": { + "name": "ASP.NET" + } + }, + { + "model": "general.skill", + "pk": 58, + "fields": { + "name": "Spring" + } + }, + { + "model": "general.skill", + "pk": 59, + "fields": { + "name": "Ember.js" + } + }, + { + "model": "general.skill", + "pk": 60, + "fields": { + "name": "Backbone.js" + } + }, + { + "model": "general.skill", + "pk": 61, + "fields": { + "name": "Meteor.js" + } + }, + { + "model": "general.skill", + "pk": 62, + "fields": { + "name": "Knockout.js" + } + }, + { + "model": "general.skill", + "pk": 63, + "fields": { + "name": "Redux" + } + }, + { + "model": "general.skill", + "pk": 64, + "fields": { + "name": "Redux-Saga" + } + }, + { + "model": "general.skill", + "pk": 65, + "fields": { + "name": "MobX" + } + }, + { + "model": "general.skill", + "pk": 66, + "fields": { + "name": "Xamarin" + } + }, + { + "model": "general.skill", + "pk": 67, + "fields": { + "name": "Flutter" + } + }, + { + "model": "general.skill", + "pk": 68, + "fields": { + "name": "Ionic" + } + }, + { + "model": "general.skill", + "pk": 69, + "fields": { + "name": "Cordova" + } + }, + { + "model": "general.skill", + "pk": 70, + "fields": { + "name": "PhoneGap" + } + }, + { + "model": "general.skill", + "pk": 71, + "fields": { + "name": "Bootstrap" + } + }, + { + "model": "general.skill", + "pk": 72, + "fields": { + "name": "Material-UI" + } + }, + { + "model": "general.skill", + "pk": 73, + "fields": { + "name": "Semantic UI" + } + }, + { + "model": "general.skill", + "pk": 74, + "fields": { + "name": "Tailwind CSS" + } + }, + { + "model": "general.skill", + "pk": 75, + "fields": { + "name": "Bulma" + } + }, + { + "model": "general.skill", + "pk": 76, + "fields": { + "name": "Electron" + } + }, + { + "model": "general.skill", + "pk": 77, + "fields": { + "name": "Flask-SQLAlchemy" + } + }, + { + "model": "general.skill", + "pk": 78, + "fields": { + "name": "Peewee" + } + }, + { + "model": "general.skill", + "pk": 79, + "fields": { + "name": "PonyORM" + } + }, + { + "model": "general.skill", + "pk": 80, + "fields": { + "name": "SQLAlchemy" + } + }, + { + "model": "general.skill", + "pk": 81, + "fields": { + "name": "Hibernate" + } + }, + { + "model": "general.skill", + "pk": 82, + "fields": { + "name": "Joi" + } + }, + { + "model": "general.skill", + "pk": 83, + "fields": { + "name": "celebrate" + } + }, + { + "model": "general.skill", + "pk": 84, + "fields": { + "name": "Yup" + } + }, + { + "model": "general.skill", + "pk": 85, + "fields": { + "name": "PyTorch" + } + }, + { + "model": "general.skill", + "pk": 86, + "fields": { + "name": "Keras" + } + }, + { + "model": "general.skill", + "pk": 87, + "fields": { + "name": "TensorFlow" + } + }, + { + "model": "general.skill", + "pk": 88, + "fields": { + "name": "MXNet" + } + }, + { + "model": "general.skill", + "pk": 89, + "fields": { + "name": "Pygame" + } + }, + { + "model": "general.skill", + "pk": 90, + "fields": { + "name": "OpenCV" + } + }, + { + "model": "general.skill", + "pk": 91, + "fields": { + "name": "Unity" + } + }, + { + "model": "general.skill", + "pk": 92, + "fields": { + "name": "SDL" + } + }, + { + "model": "general.skill", + "pk": 93, + "fields": { + "name": "Qt" + } + }, + { + "model": "general.skill", + "pk": 94, + "fields": { + "name": "wxWidgets" + } + }, + { + "model": "general.skill", + "pk": 95, + "fields": { + "name": "GTK" + } + }, + { + "model": "general.skill", + "pk": 96, + "fields": { + "name": "Cocoa" + } + }, + { + "model": "general.skill", + "pk": 97, + "fields": { + "name": "UIKit" + } + }, + { + "model": "general.skill", + "pk": 98, + "fields": { + "name": "React Native" + } + }, + { + "model": "general.skill", + "pk": 99, + "fields": { + "name": "NativeScript" + } + }, + { + "model": "general.skill", + "pk": 100, + "fields": { + "name": "Xamarin.Forms" + } + }, + { + "model": "general.skill", + "pk": 101, + "fields": { + "name": "Adobe PhoneGap" + } + }, + { + "model": "general.skill", + "pk": 102, + "fields": { + "name": "NW.js" + } + }, + { + "model": "general.skill", + "pk": 103, + "fields": { + "name": "Flask-SocketIO" + } + }, + { + "model": "general.skill", + "pk": 104, + "fields": { + "name": "Socket.IO" + } + }, + { + "model": "general.skill", + "pk": 105, + "fields": { + "name": "Django Channels" + } + }, + { + "model": "general.skill", + "pk": 106, + "fields": { + "name": "Pusher" + } + }, + { + "model": "general.skill", + "pk": 107, + "fields": { + "name": "Firebase" + } + }, + { + "model": "general.skill", + "pk": 108, + "fields": { + "name": "AWS Amplify" + } + }, + { + "model": "general.skill", + "pk": 109, + "fields": { + "name": "Microsoft Azure" + } + }, + { + "model": "general.skill", + "pk": 110, + "fields": { + "name": "Google Cloud Platform" + } + }, + { + "model": "general.skill", + "pk": 111, + "fields": { + "name": "Heroku" + } + }, + { + "model": "general.skill", + "pk": 112, + "fields": { + "name": "Docker" + } + }, + { + "model": "general.skill", + "pk": 113, + "fields": { + "name": "Kubernetes" + } + }, + { + "model": "general.skill", + "pk": 114, + "fields": { + "name": "OpenStack" + } + }, + { + "model": "general.skill", + "pk": 115, + "fields": { + "name": "Ansible" + } + }, + { + "model": "general.skill", + "pk": 116, + "fields": { + "name": "Chef" + } + }, + { + "model": "general.skill", + "pk": 117, + "fields": { + "name": "Puppet" + } + }, + { + "model": "general.skill", + "pk": 118, + "fields": { + "name": "Terraform" + } + }, + { + "model": "general.skill", + "pk": 119, + "fields": { + "name": "Jenkins" + } + }, + { + "model": "general.skill", + "pk": 120, + "fields": { + "name": "Travis CI" + } + }, + { + "model": "general.skill", + "pk": 121, + "fields": { + "name": "GitLab CI/CD" + } + }, + { + "model": "general.skill", + "pk": 122, + "fields": { + "name": "CircleCI" + } + }, + { + "model": "general.skill", + "pk": 123, + "fields": { + "name": "Argo CD" + } + }, + { + "model": "general.skill", + "pk": 124, + "fields": { + "name": "Sentry" + } + }, + { + "model": "general.skill", + "pk": 125, + "fields": { + "name": "Datadog" + } + }, + { + "model": "general.skill", + "pk": 126, + "fields": { + "name": "New Relic" + } + }, + { + "model": "general.skill", + "pk": 127, + "fields": { + "name": "Elasticsearch" + } + }, + { + "model": "general.skill", + "pk": 128, + "fields": { + "name": "Grafana" + } + }, + { + "model": "general.skill", + "pk": 129, + "fields": { + "name": "Prometheus" + } + }, + { + "model": "general.skill", + "pk": 130, + "fields": { + "name": "Graylog" + } + }, + { + "model": "general.skill", + "pk": 131, + "fields": { + "name": "RabbitMQ" + } + }, + { + "model": "general.skill", + "pk": 132, + "fields": { + "name": "Apache ActiveMQ" + } + }, + { + "model": "general.skill", + "pk": 133, + "fields": { + "name": "Redis" + } + }, + { + "model": "general.skill", + "pk": 134, + "fields": { + "name": "Memcached" + } + }, + { + "model": "general.skill", + "pk": 135, + "fields": { + "name": "MongoDB" + } + }, + { + "model": "general.skill", + "pk": 136, + "fields": { + "name": "CouchDB" + } + }, + { + "model": "general.skill", + "pk": 137, + "fields": { + "name": "Firebase Cloud Firestore" + } + }, + { + "model": "general.skill", + "pk": 138, + "fields": { + "name": "Neo4j" + } + }, + { + "model": "general.skill", + "pk": 139, + "fields": { + "name": "Cassandra" + } + }, + { + "model": "general.skill", + "pk": 140, + "fields": { + "name": "MySQL" + } + }, + { + "model": "general.skill", + "pk": 141, + "fields": { + "name": "MariaDB" + } + }, + { + "model": "general.skill", + "pk": 142, + "fields": { + "name": "PostgreSQL" + } + }, + { + "model": "general.skill", + "pk": 143, + "fields": { + "name": "SQLite" + } + }, + { + "model": "general.skill", + "pk": 144, + "fields": { + "name": "Angular Material" + } + }, + { + "model": "general.skill", + "pk": 145, + "fields": { + "name": "Vuetify" + } + }, + { + "model": "general.skill", + "pk": 146, + "fields": { + "name": "Materialize CSS" + } + }, + { + "model": "general.skill", + "pk": 147, + "fields": { + "name": "Ant Design" + } + }, + { + "model": "general.skill", + "pk": 150, + "fields": { + "name": "Foundation" + } + }, + { + "model": "general.skill", + "pk": 151, + "fields": { + "name": "Sass" + } + }, + { + "model": "general.skill", + "pk": 152, + "fields": { + "name": "LESS" + } + }, + { + "model": "general.skill", + "pk": 153, + "fields": { + "name": "Framer Motion" + } + }, + { + "model": "general.skill", + "pk": 154, + "fields": { + "name": "GSAP" + } + }, + { + "model": "general.skill", + "pk": 155, + "fields": { + "name": "PixiJS" + } + }, + { + "model": "general.skill", + "pk": 157, + "fields": { + "name": "Chart.js" + } + }, + { + "model": "general.skill", + "pk": 158, + "fields": { + "name": "Highcharts" + } + }, + { + "model": "general.skill", + "pk": 159, + "fields": { + "name": "Leaflet" + } + }, + { + "model": "general.skill", + "pk": 160, + "fields": { + "name": "Mapbox" + } + }, + { + "model": "general.skill", + "pk": 161, + "fields": { + "name": "WebRTC" + } + }, + { + "model": "general.skill", + "pk": 162, + "fields": { + "name": "PeerJS" + } + }, + { + "model": "general.skill", + "pk": 164, + "fields": { + "name": "Cocos2d-x" + } + }, + { + "model": "general.skill", + "pk": 165, + "fields": { + "name": "Pygame Zero" + } + }, + { + "model": "general.skill", + "pk": 166, + "fields": { + "name": "Godot Engine" + } + }, + { + "model": "general.skill", + "pk": 167, + "fields": { + "name": "Allegro" + } + }, + { + "model": "general.skill", + "pk": 168, + "fields": { + "name": "SFML" + } + }, + { + "model": "general.skill", + "pk": 170, + "fields": { + "name": "JUnit" + } + }, + { + "model": "general.skill", + "pk": 171, + "fields": { + "name": "NUnit" + } + }, + { + "model": "general.skill", + "pk": 172, + "fields": { + "name": "TestNG" + } + }, + { + "model": "general.skill", + "pk": 173, + "fields": { + "name": "TestCafe" + } + }, + { + "model": "general.skill", + "pk": 174, + "fields": { + "name": "Apache JMeter" + } + }, + { + "model": "general.skill", + "pk": 175, + "fields": { + "name": "Gatling" + } + }, + { + "model": "general.skill", + "pk": 176, + "fields": { + "name": "Cypress" + } + }, + { + "model": "general.skill", + "pk": 177, + "fields": { + "name": "Puppeteer" + } + }, + { + "model": "general.skill", + "pk": 178, + "fields": { + "name": "Selenium" + } + }, + { + "model": "general.skill", + "pk": 179, + "fields": { + "name": "Appium" + } + }, + { + "model": "general.skill", + "pk": 180, + "fields": { + "name": "Postman" + } + }, + { + "model": "general.skill", + "pk": 181, + "fields": { + "name": "SoapUI" + } + }, + { + "model": "general.skill", + "pk": 182, + "fields": { + "name": "REST Assured" + } + }, + { + "model": "general.skill", + "pk": 183, + "fields": { + "name": "Mockito" + } + }, + { + "model": "general.skill", + "pk": 184, + "fields": { + "name": "WireMock" + } + }, + { + "model": "general.skill", + "pk": 185, + "fields": { + "name": "Taiko" + } + }, + { + "model": "general.skill", + "pk": 186, + "fields": { + "name": "WinAppDriver" + } + }, + { + "model": "general.skill", + "pk": 187, + "fields": { + "name": "Robot Framework" + } + }, + { + "model": "general.skill", + "pk": 188, + "fields": { + "name": "Serenity BDD" + } + }, + { + "model": "general.skill", + "pk": 189, + "fields": { + "name": "Cucumber" + } + }, + { + "model": "general.skill", + "pk": 190, + "fields": { + "name": "FitNesse" + } + }, + { + "model": "general.skill", + "pk": 191, + "fields": { + "name": "Gauge" + } + }, + { + "model": "general.skill", + "pk": 192, + "fields": { + "name": "Geb" + } + }, + { + "model": "general.skill", + "pk": 193, + "fields": { + "name": "Chimp" + } + }, + { + "model": "general.skill", + "pk": 194, + "fields": { + "name": "Jasmine" + } + }, + { + "model": "general.skill", + "pk": 195, + "fields": { + "name": "Mocha" + } + }, + { + "model": "general.skill", + "pk": 196, + "fields": { + "name": "Jest" + } + }, + { + "model": "general.skill", + "pk": 197, + "fields": { + "name": "Enzyme" + } + }, + { + "model": "general.skill", + "pk": 198, + "fields": { + "name": "Karma" + } + }, + { + "model": "general.skill", + "pk": 199, + "fields": { + "name": "AVA" + } + }, + { + "model": "general.skill", + "pk": 200, + "fields": { + "name": "Git" + } + }, + { + "model": "general.skill", + "pk": 201, + "fields": { + "name": "Logstash" + } + }, + { + "model": "general.skill", + "pk": 202, + "fields": { + "name": "Kibana" + } + }, + { + "model": "general.skill", + "pk": 206, + "fields": { + "name": "Oracle" + } + }, + { + "model": "general.skill", + "pk": 212, + "fields": { + "name": "Microsoft SQL Server" + } + }, + { + "model": "general.skill", + "pk": 213, + "fields": { + "name": "Amazon RDS" + } + }, + { + "model": "general.skill", + "pk": 214, + "fields": { + "name": "Google Cloud Spanner" + } + }, + { + "model": "general.skill", + "pk": 215, + "fields": { + "name": "IBM DB2" + } + }, + { + "model": "general.skill", + "pk": 216, + "fields": { + "name": "Apache HBase" + } + }, + { + "model": "general.skill", + "pk": 217, + "fields": { + "name": "Firebase Realtime Database" + } + }, + { + "model": "general.skill", + "pk": 219, + "fields": { + "name": "Apache Couchbase" + } + }, + { + "model": "general.skill", + "pk": 220, + "fields": { + "name": "Firebase Firestore" + } + }, + { + "model": "general.skill", + "pk": 221, + "fields": { + "name": "ArangoDB" + } + }, + { + "model": "general.skill", + "pk": 222, + "fields": { + "name": "InfluxDB" + } + }, + { + "model": "general.skill", + "pk": 224, + "fields": { + "name": "Teradata" + } + }, + { + "model": "general.skill", + "pk": 225, + "fields": { + "name": "Informix" + } + }, + { + "model": "general.skill", + "pk": 226, + "fields": { + "name": "Sybase" + } + }, + { + "model": "general.skill", + "pk": 227, + "fields": { + "name": "Amazon DynamoDB" + } + }, + { + "model": "general.skill", + "pk": 228, + "fields": { + "name": "RESTful API" + } + }, + { + "model": "general.skill", + "pk": 229, + "fields": { + "name": "GraphQL" + } + }, + { + "model": "general.skill", + "pk": 230, + "fields": { + "name": "AJAX" + } + }, + { + "model": "general.skill", + "pk": 231, + "fields": { + "name": "JSON" + } + }, + { + "model": "general.skill", + "pk": 233, + "fields": { + "name": "Less" + } + }, + { + "model": "general.skill", + "pk": 235, + "fields": { + "name": "CSS Grid" + } + }, + { + "model": "general.skill", + "pk": 237, + "fields": { + "name": "WebSockets" + } + }, + { + "model": "general.skill", + "pk": 238, + "fields": { + "name": "Web Workers" + } + }, + { + "model": "general.skill", + "pk": 239, + "fields": { + "name": "PWA" + } + }, + { + "model": "general.skill", + "pk": 240, + "fields": { + "name": "Service Workers" + } + }, + { + "model": "general.skill", + "pk": 241, + "fields": { + "name": "SPA" + } + }, + { + "model": "general.skill", + "pk": 242, + "fields": { + "name": "Serverless Architecture" + } + }, + { + "model": "general.skill", + "pk": 243, + "fields": { + "name": "Microservices" + } + }, + { + "model": "general.skill", + "pk": 244, + "fields": { + "name": "Web Accessibility" + } + }, + { + "model": "general.skill", + "pk": 245, + "fields": { + "name": "RWD" + } + }, + { + "model": "general.skill", + "pk": 246, + "fields": { + "name": "Cross-Origin Resource Sharing (CORS)" + } + }, + { + "model": "general.skill", + "pk": 247, + "fields": { + "name": "JSON Web Tokens (JWT)" + } + }, + { + "model": "general.skill", + "pk": 248, + "fields": { + "name": "Content Delivery Network (CDN)" + } + }, + { + "model": "general.skill", + "pk": 249, + "fields": { + "name": "Browser Compatibility Testing" + } + }, + { + "model": "general.skill", + "pk": 250, + "fields": { + "name": "Webpack" + } + }, + { + "model": "general.skill", + "pk": 251, + "fields": { + "name": "SEO" + } + }, + { + "model": "general.skill", + "pk": 252, + "fields": { + "name": "Performance Optimization" + } + }, + { + "model": "general.skill", + "pk": 253, + "fields": { + "name": "Browser Storage" + } + }, + { + "model": "general.skill", + "pk": 254, + "fields": { + "name": "Web Animations" + } + }, + { + "model": "general.skill", + "pk": 255, + "fields": { + "name": "Progressive Enhancement" + } + }, + { + "model": "general.skill", + "pk": 256, + "fields": { + "name": "Web Security" + } + }, + { + "model": "general.skill", + "pk": 257, + "fields": { + "name": "Web Typography" + } + }, + { + "model": "general.skill", + "pk": 258, + "fields": { + "name": "Internationalization (i18n)" + } + }, + { + "model": "general.skill", + "pk": 259, + "fields": { + "name": "CMS" + } + }, + { + "model": "general.skill", + "pk": 260, + "fields": { + "name": "Headless CMS" + } + }, + { + "model": "general.skill", + "pk": 261, + "fields": { + "name": "Static Site Generators" + } + }, + { + "model": "general.skill", + "pk": 262, + "fields": { + "name": "Web Scraping" + } + }, + { + "model": "general.skill", + "pk": 263, + "fields": { + "name": "Web Analytics" + } + }, + { + "model": "general.skill", + "pk": 264, + "fields": { + "name": "A/B Testing" + } + }, + { + "model": "general.skill", + "pk": 265, + "fields": { + "name": "Web Testing Frameworks" + } + }, + { + "model": "general.skill", + "pk": 266, + "fields": { + "name": "Responsive Images" + } + }, + { + "model": "general.skill", + "pk": 267, + "fields": { + "name": "Web Fonts" + } + }, + { + "model": "general.skill", + "pk": 268, + "fields": { + "name": "Lazy Loading" + } + }, + { + "model": "general.skill", + "pk": 269, + "fields": { + "name": "Web Accessibility Testing" + } + }, + { + "model": "general.skill", + "pk": 270, + "fields": { + "name": "Web Design Patterns" + } + }, + { + "model": "general.skill", + "pk": 271, + "fields": { + "name": "Web Frameworks" + } + }, + { + "model": "general.skill", + "pk": 272, + "fields": { + "name": "Server-Side Rendering (SSR)" + } + }, + { + "model": "general.skill", + "pk": 273, + "fields": { + "name": "Client-Side Rendering (CSR)" + } + }, + { + "model": "general.skill", + "pk": 274, + "fields": { + "name": "Content Management System (CMS) APIs" + } + }, + { + "model": "general.skill", + "pk": 275, + "fields": { + "name": "API Integration" + } + }, + { + "model": "general.skill", + "pk": 276, + "fields": { + "name": "Web Performance Metrics" + } + }, + { + "model": "general.skill", + "pk": 277, + "fields": { + "name": "Version Control Systems" + } + }, + { + "model": "general.skill", + "pk": 278, + "fields": { + "name": "Gulp" + } + }, + { + "model": "general.skill", + "pk": 279, + "fields": { + "name": "ООП" + } + }, + { + "model": "general.skill", + "pk": 280, + "fields": { + "name": "Управление проектами" + } + }, + { + "model": "general.skill", + "pk": 281, + "fields": { + "name": "Scrum" + } + }, + { + "model": "general.skill", + "pk": 282, + "fields": { + "name": "Kanban" + } + }, + { + "model": "general.skill", + "pk": 283, + "fields": { + "name": "Trello" + } + }, + { + "model": "general.skill", + "pk": 284, + "fields": { + "name": "Waterfall" + } + }, + { + "model": "general.skill", + "pk": 285, + "fields": { + "name": "Разработка ТЗ" + } + }, + { + "model": "general.skill", + "pk": 286, + "fields": { + "name": "Agile" + } + }, + { + "model": "general.skill", + "pk": 287, + "fields": { + "name": "Веб-разработка" + } + }, + { + "model": "general.skill", + "pk": 288, + "fields": { + "name": "Адаптивная верстка" + } + }, + { + "model": "general.skill", + "pk": 289, + "fields": { + "name": "Кроссбраузерная верстка" + } + }, + { + "model": "general.skill", + "pk": 290, + "fields": { + "name": "Валидная верстка" + } + }, + { + "model": "general.skill", + "pk": 291, + "fields": { + "name": "Семантическая верстка" + } + }, + { + "model": "general.skill", + "pk": 292, + "fields": { + "name": "Pixel-perfect" + } + }, + { + "model": "general.skill", + "pk": 293, + "fields": { + "name": "Базы данных" + } + }, + { + "model": "general.skill", + "pk": 294, + "fields": { + "name": "CI/CD" + } + }, + { + "model": "general.skill", + "pk": 295, + "fields": { + "name": "Jira" + } + }, + { + "model": "general.skill", + "pk": 296, + "fields": { + "name": "TestRail" + } + }, + { + "model": "general.skill", + "pk": 297, + "fields": { + "name": "Тестирование ПО" + } + }, + { + "model": "general.skill", + "pk": 298, + "fields": { + "name": "Тестирование сайтов" + } + }, + { + "model": "general.skill", + "pk": 299, + "fields": { + "name": "Тестирование API" + } + }, + { + "model": "general.skill", + "pk": 300, + "fields": { + "name": "Тестирование мобильных приложений" + } + }, + { + "model": "general.skill", + "pk": 301, + "fields": { + "name": "Black box testing" + } + }, + { + "model": "general.skill", + "pk": 302, + "fields": { + "name": "White box testing" + } + }, + { + "model": "general.skill", + "pk": 303, + "fields": { + "name": "Тестирование дизайна" + } + }, + { + "model": "general.skill", + "pk": 304, + "fields": { + "name": "Тестирование методом свободного поиска" + } + }, + { + "model": "general.skill", + "pk": 305, + "fields": { + "name": "Тестирование производительности" + } + }, + { + "model": "general.skill", + "pk": 306, + "fields": { + "name": "Тестирование на проникновение" + } + }, + { + "model": "general.skill", + "pk": 307, + "fields": { + "name": "Автоматизация тестирования" + } + }, + { + "model": "general.skill", + "pk": 308, + "fields": { + "name": "Анализ требований" + } + }, + { + "model": "general.skill", + "pk": 309, + "fields": { + "name": "Анализ данных" + } + }, + { + "model": "general.skill", + "pk": 310, + "fields": { + "name": "Веб аналитика" + } + }, + { + "model": "general.skill", + "pk": 311, + "fields": { + "name": "Построение команды" + } + }, + { + "model": "general.skill", + "pk": 312, + "fields": { + "name": "Управление разработкой" + } + }, + { + "model": "general.skill", + "pk": 313, + "fields": { + "name": "БЭМ" + } + }, + { + "model": "general.skill", + "pk": 314, + "fields": { + "name": "Тест-дизайн" + } + }, + { + "model": "general.skill", + "pk": 315, + "fields": { + "name": "Тест-анализ" + } + }, + { + "model": "general.skill", + "pk": 316, + "fields": { + "name": "Charles" + } + }, + { + "model": "general.skill", + "pk": 317, + "fields": { + "name": "Fidler" + } + }, + { + "model": "general.skill", + "pk": 318, + "fields": { + "name": "DevTools" + } + }, + { + "model": "general.skill", + "pk": 319, + "fields": { + "name": "Kaiten" + } + }, + { + "model": "general.skill", + "pk": 320, + "fields": { + "name": "Cygwin" + } + }, + { + "model": "general.skill", + "pk": 321, + "fields": { + "name": "Figma" + } + }, + { + "model": "general.skill", + "pk": 322, + "fields": { + "name": "1C" + } + }, + { + "model": "general.skill", + "pk": 323, + "fields": { + "name": "Разработка мобильных приложений" + } + }, + { + "model": "general.skill", + "pk": 324, + "fields": { + "name": "Нагрузочное тестирование" + } + }, + { + "model": "general.skill", + "pk": 325, + "fields": { + "name": "Регрессионное тестирование" + } + } ] diff --git a/src/backend/data/fixtures/specialist.json b/src/backend/data/fixtures/specialist.json new file mode 100644 index 0000000..5cdecc6 --- /dev/null +++ b/src/backend/data/fixtures/specialist.json @@ -0,0 +1,130 @@ +[ + { + "model": "general.specialist", + "pk": 1, + "fields": { + "specialty": "Разработка", + "specialization": "Десктоп разработчик / Software Developer" + } + }, + { + "model": "general.specialist", + "pk": 2, + "fields": { + "specialty": "Разработка", + "specialization": "Бэкенд разработчик / Backend Developer" + } + }, + { + "model": "general.specialist", + "pk": 3, + "fields": { + "specialty": "Разработка", + "specialization": "Фронтенд разработчик / Frontend Developer" + } + }, + { + "model": "general.specialist", + "pk": 4, + "fields": { + "specialty": "Разработка", + "specialization": "Фулстек разработчик / Fullstack Developer" + } + }, + { + "model": "general.specialist", + "pk": 5, + "fields": { + "specialty": "Разработка", + "specialization": "Разработчик мобильных приложений / Mobile Application Developer" + } + }, + { + "model": "general.specialist", + "pk": 6, + "fields": { + "specialty": "Тестирование", + "specialization": "Инженер по автоматизации тестирования / Test Automation Engineer" + } + }, + { + "model": "general.specialist", + "pk": 7, + "fields": { + "specialty": "Тестирование", + "specialization": "Инженер по ручному тестированию / Manual Test Engineer" + } + }, + { + "model": "general.specialist", + "pk": 8, + "fields": { + "specialty": "Тестирование", + "specialization": "Инженер по нагрузочному тестированию / Performance Engineer" + } + }, + { + "model": "general.specialist", + "pk": 9, + "fields": { + "specialty": "Администрирование", + "specialization": "Системный администратор / System Administration" + } + }, + { + "model": "general.specialist", + "pk": 10, + "fields": { + "specialty": "Администрирование", + "specialization": "DevOps-инженер / DevOps" + } + }, + { + "model": "general.specialist", + "pk": 11, + "fields": { + "specialty": "Дизайнер", + "specialization": "UI/UX дизайнер / UI/UX Designer" + } + }, + { + "model": "general.specialist", + "pk": 12, + "fields": { + "specialty": "Дизайнер", + "specialization": "Графический дизайнер / Graphic Designer" + } + }, + { + "model": "general.specialist", + "pk": 13, + "fields": { + "specialty": "Менеджмент", + "specialization": "Менеджер проекта / general Manager" + } + }, + { + "model": "general.specialist", + "pk": 14, + "fields": { + "specialty": "Аналитика", + "specialization": "Системный аналитик / Systems Analyst" + } + }, + { + "model": "general.specialist", + "pk": 15, + "fields": { + "specialty": "Аналитика", + "specialization": "Бизнес-аналитик / Business Analyst" + } + }, + { + "model": "general.specialist", + "pk": 16, + "fields": { + "specialty": "Аналитика", + "specialization": "Аналитик по данным / Data Analyst" + } + } +] diff --git a/src/backend/data/fixtures/specialization.json b/src/backend/data/fixtures/specialization.json deleted file mode 100644 index cbae0a5..0000000 --- a/src/backend/data/fixtures/specialization.json +++ /dev/null @@ -1,18 +0,0 @@ -[ -{"model": "project.specialization", "pk": 1, "fields": {"name": "Десктоп разработчик / Software Developer"}}, -{"model": "project.specialization", "pk": 2, "fields": {"name": "Бэкенд разработчик / Backend Developer"}}, -{"model": "project.specialization", "pk": 3, "fields": {"name": "Фронтенд разработчик / Frontend Developer"}}, -{"model": "project.specialization", "pk": 4, "fields": {"name": "Фулстек разработчик / Fullstack Developer"}}, -{"model": "project.specialization", "pk": 5, "fields": {"name": "Разработчик мобильных приложений / Mobile Application Developer"}}, -{"model": "project.specialization", "pk": 6, "fields": {"name": "Инженер по автоматизации тестирования / Test Automation Engineer"}}, -{"model": "project.specialization", "pk": 7, "fields": {"name": "Инженер по ручному тестированию / Manual Test Engineer"}}, -{"model": "project.specialization", "pk": 8, "fields": {"name": "Инженер по нагрузочному тестированию / Performance Engineer"}}, -{"model": "project.specialization", "pk": 9, "fields": {"name": "Системный администратор / System Administration"}}, -{"model": "project.specialization", "pk": 10, "fields": {"name": "DevOps-инженер / DevOps"}}, -{"model": "project.specialization", "pk": 11, "fields": {"name": "UI/UX дизайнер / UI/UX Designer"}}, -{"model": "project.specialization", "pk": 12, "fields": {"name": "Графический дизайнер / Graphic Designer"}}, -{"model": "project.specialization", "pk": 13, "fields": {"name": "Менеджер проекта / Project Manager"}}, -{"model": "project.specialization", "pk": 14, "fields": {"name": "Системный аналитик / Systems Analyst"}}, -{"model": "project.specialization", "pk": 15, "fields": {"name": "Бизнес-аналитик / Business Analyst"}}, -{"model": "project.specialization", "pk": 16, "fields": {"name": "Аналитик по данным / Data Analyst"}} -] diff --git a/src/backend/data/fixtures/user.json b/src/backend/data/fixtures/user.json new file mode 100644 index 0000000..1615c4e --- /dev/null +++ b/src/backend/data/fixtures/user.json @@ -0,0 +1,30 @@ +[ + { + "model": "users.user", + "pk": 1, + "fields": { + "email": "admin@admin.ru", + "username": "admin", + "password": "pbkdf2_sha256$720000$yDMjUkRaCfc23VahJhCosX$H4VZnxw2dGwiwMwEZxCbuH7F6UuwNoBM/GLyk7FZKDk=", + "is_superuser": true, + "is_staff": true, + "is_active": true, + "created": "2024-03-09 13:31:26.594748", + "modified": "2024-03-09 13:31:26.594748" + } + }, + { + "model": "users.user", + "pk": 2, + "fields": { + "email": "test@mail.ru", + "username": "test_user", + "password": "pbkdf2_sha256$720000$3c3o1zAifXJygUrWMW48L8$Te54hj38hdq7elG4KMthkuvo8HvfUfiaZUJp0t9wn+s=", + "is_superuser": false, + "is_staff": false, + "is_active": true, + "created": "2024-03-09 13:31:26.594748", + "modified": "2024-03-09 13:31:26.594748" + } + } +]