diff --git a/backend/alembic/versions/f4bc1e54902e_vacancy_salary.py b/backend/alembic/versions/f4bc1e54902e_vacancy_salary.py new file mode 100644 index 0000000000000000000000000000000000000000..313291d8aef5690b62ab360e2470c161d4ffbf74 --- /dev/null +++ b/backend/alembic/versions/f4bc1e54902e_vacancy_salary.py @@ -0,0 +1,32 @@ +"""vacancy_salary + +Revision ID: f4bc1e54902e +Revises: 8a728645b7b9 +Create Date: 2024-10-06 12:19:28.876405 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f4bc1e54902e' +down_revision: Union[str, None] = '8a728645b7b9' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('vacancies', sa.Column('salary_high', sa.Integer(), server_default='30000', nullable=False)) + op.add_column('vacancies', sa.Column('salary_low', sa.Integer(), server_default='70000', nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('vacancies', 'salary_low') + op.drop_column('vacancies', 'salary_high') + # ### end Alembic commands ### diff --git a/backend/src/database.py b/backend/src/database.py index 2b14ef73a53aec4caf2523db59e82151b988ce6d..b328a281070974578c89aaa9a60ce0ef20dc17d5 100644 --- a/backend/src/database.py +++ b/backend/src/database.py @@ -2,13 +2,9 @@ import typing as tp from asyncio import current_task import sqlalchemy as sa -from sqlalchemy.ext.asyncio import ( - AsyncEngine, - AsyncSession, - async_scoped_session, - async_sessionmaker, - create_async_engine, -) +from sqlalchemy.ext.asyncio import (AsyncEngine, AsyncSession, + async_scoped_session, async_sessionmaker, + create_async_engine) from sqlalchemy.orm import DeclarativeBase, declared_attr from src.config import app_config diff --git a/backend/src/hr/models.py b/backend/src/hr/models.py index 0e18221cd563749fe07e4a53dc08f9d801fe1bf7..e42864a48f15994131a6f88b8a5e800de1ea3a36 100644 --- a/backend/src/hr/models.py +++ b/backend/src/hr/models.py @@ -1,10 +1,11 @@ import datetime as dt +from enum import unique import typing as tp - import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSON from sqlalchemy.orm import Mapped, mapped_column, relationship +from src.database import Base from src.database import Base from src.models import TimestampMixin @@ -32,6 +33,18 @@ class Vacancy(Base, TimestampMixin): recruiter_id: Mapped[int] = mapped_column( sa.Integer, sa.ForeignKey("users.id"), nullable=True ) + salary_high: Mapped[int] = mapped_column( + sa.Integer, + nullable=False, + default=30_000, + server_default="30000", + ) + salary_low: Mapped[int] = mapped_column( + sa.Integer, + nullable=False, + default=70_000, + server_default="70000", + ) hr_id: Mapped[int] = mapped_column( sa.Integer, sa.ForeignKey("users.id"), nullable=True ) diff --git a/backend/src/hr/router.py b/backend/src/hr/router.py index 2b2bf57219988fe231caddf49f6b6bf993d2a742..31723f9d1b1565a3f6be5b61064c2172e0e7b055 100644 --- a/backend/src/hr/router.py +++ b/backend/src/hr/router.py @@ -10,13 +10,22 @@ from sqlalchemy.ext.asyncio import AsyncSession from src.dependencies import get_db -from .models import Skill, Vacancy -from .schemas import (EXAMPLE_ALL_ACTIVE, EXAMPLE_STAGES, - EXAMPLES_ALL_DECLINED, EXAMPLES_ALL_POTENTIAL, - AllCandidatesDeclinedDto, AllCandidatesPotentialDto, - AllCandidatesVacancyDto, CandidateDto, RoadmapDto, - SkillCreate, SkillSearchResult, VacancyCreate, - VacancyDTO) +from .models import Skill, Vacancy, Roadmap, RoadmapStage +from .schemas import ( + EXAMPLE_ALL_ACTIVE, + EXAMPLE_STAGES, + EXAMPLES_ALL_DECLINED, + EXAMPLES_ALL_POTENTIAL, + AllCandidatesDeclinedDto, + AllCandidatesPotentialDto, + AllCandidatesVacancyDto, + CandidateDto, + RoadmapDto, + SkillCreate, + SkillSearchResult, + VacancyCreate, + VacancyDTO, +) router = APIRouter( prefix="/vacancies", @@ -90,10 +99,13 @@ async def create_vacancy( experience_to=vacancy.experience_to, education=vacancy.education, quantity=vacancy.quantity, + salary_high=vacancy.salary_high, + salary_low=vacancy.salary_low, direction=vacancy.direction, description=vacancy.description, type_of_employment=vacancy.type_of_employment, ) + db.add(db_vacancy) stmt = sa.select(Skill).where(Skill.id.in_(vacancy.key_skills)) required_skills = (await db.execute(stmt)).scalars() @@ -103,11 +115,25 @@ async def create_vacancy( additional_skills = (await db.execute(stmt)).scalars() db_vacancy.vacancy_skills = list(required_skills) + list(additional_skills) # type: ignore + db_vacancy.vacancy_skills = list(required_skills) + list(additional_skills) # type: ignore + await db.flush() + await db.refresh(db_vacancy, attribute_names=["id"]) + + db_roadmap = Roadmap(vacancy_id=db_vacancy.id) + db.add(db_roadmap) + await db.flush() + await db.refresh(db_roadmap, ["id"]) + for stage in vacancy.stages: + db_stage = RoadmapStage( + name=stage.name, + order=stage.order, + roadmap_id=db_roadmap.id, + duration=stage.duration, + ) + db.add(db_stage) try: - db.add(db_vacancy) await db.commit() - await db.refresh(db_vacancy, attribute_names=["vacancy_skills"]) except Exception as e: logger.error(f"Error: {e}") await db.rollback() @@ -171,22 +197,34 @@ async def get_active_vacancies( stmt = ( sa.select(Vacancy) .options( - orm.joinedload( - Vacancy.vacancy_skills, - ), - orm.joinedload( - Vacancy.vacancy_candidates, - ), + orm.joinedload(Vacancy.vacancy_skills), + orm.joinedload(Vacancy.vacancy_candidates), ) .filter(Vacancy.id == vacancy_id) ) - db_vacancy = await db.execute(stmt) - result: Vacancy | None = db_vacancy.unique().scalar_one_or_none() - - if not result: - raise HTTPException(status_code=404, detail="Vacancy not found") + try: + db_vacancy = await db.execute(stmt) + except Exception as e: + logger.error(e) + raise HTTPException(status_code=400, detail="User not found") + + vacancy: Vacancy | None = db_vacancy.unique().scalar_one_or_none() + candidates = [] + # for candidate in vacancy.vacancy_candidates: + # candidates.append( + # CandidateVacancyDto( + # source=candidate.src, + # candidate_id=candidate.id, + # date_of_accept: dt.datetime = Field(..., description="The education level required") + # stage_name: str = Field(..., description="") + # similarity: int = Field(..., description="") + # ) + # ) + logger.error(candidates) + # if not result: + # raise HTTPException(status_code=404, detail="Vacancy not found") return AllCandidatesVacancyDto( - vacancy=VacancyDTO.model_validate(result), candidates=EXAMPLE_ALL_ACTIVE + vacancy=VacancyDTO.model_validate(vacancy), candidates=EXAMPLE_ALL_ACTIVE ) @@ -199,12 +237,8 @@ async def get_declined_vacancies( stmt = ( sa.select(Vacancy) .options( - orm.joinedload( - Vacancy.vacancy_skills, - ), - orm.joinedload( - Vacancy.vacancy_candidates, - ), + orm.joinedload(Vacancy.vacancy_skills), + orm.joinedload(Vacancy.vacancy_candidates), ) .filter(Vacancy.id == vacancy_id) ) diff --git a/backend/src/hr/schemas.py b/backend/src/hr/schemas.py index e69addbab0e9cbe8d3cf939a1ae6fd830b29eafa..ebddf73a4fbb6248c79ca43f9314c7f065ea6c1c 100644 --- a/backend/src/hr/schemas.py +++ b/backend/src/hr/schemas.py @@ -6,6 +6,12 @@ from pydantic import BaseModel, ConfigDict, Field from src.auth.schemas import UserDto +class RoadmapStageCreate(BaseModel): + order: int = Field(1, description="ПорÑдок Ñтапа в воронке") + name: str = Field("HR Ñкрининг", description="Ðазвание Ñтапа") + duration: int = Field(1, description="ПродолжительноÑть Ñтапа в днÑÑ…") + + class SkillBase(BaseModel): """SkillBase""" @@ -62,6 +68,9 @@ class VacancyCreate(BaseModel): # направление в компании direction: str = Field("Ðналитика", title="Ðаправление") + salary_high: int = Field(100000, title="ВерхнÑÑ Ð³Ñ€Ð°Ð½Ð¸Ñ†Ð° зп") + salary_low: int = Field(150000, title="ÐижнÑÑ Ð³Ñ€Ð°Ð½Ð¸Ñ†Ð° зп") + # дополнительные навыки additional_skills: list[int] = Field([3], title="Дополнительные навыки") @@ -69,6 +78,14 @@ class VacancyCreate(BaseModel): type_of_employment: str = Field("ÐŸÐ¾Ð»Ð½Ð°Ñ Ð·Ð°Ð½ÑтоÑть", title="Тип занÑтоÑти") + salary_low: int = Field(30000, title="ÐижнÑÑ Ð³Ñ€Ð°Ð½Ð¸Ñ†Ð° зп") + salary_high: int = Field(150000, title="ВерхнÑÑ Ð³Ñ€Ð°Ð½Ð¸Ñ†Ð° зп") + + stages: list[RoadmapStageCreate] = Field( + [RoadmapStageCreate()], # type: ignore + title="Ðтапы воронки", + ) + class CandidateDto(BaseModel): id: int = Field(..., description="") @@ -110,10 +127,14 @@ class VacancyDTO(BaseModel): quantity: int = Field(..., description="The number of vacancies available") description: str = Field(..., description="The description of the vacancy") type_of_employment: str = Field(..., description="The type of employment") + + salary_high: int = Field(..., title="Higher salary border") + salary_low: int = Field(..., title="Lower salary border") + vacancy_skills: list[SkillSearchResult] = Field( None, description="List of skill IDs associated with the vacancy" ) - vacancy_candidates: list[CandidateDto] = Field( + vacancy_candidates: list[CandidateDto] | None = Field( None, description="List of candidates IDs associated with vacancy" )