419 lines
20 KiB
Python
419 lines
20 KiB
Python
import uuid
|
|
from datetime import datetime, date as Date, timezone
|
|
from typing import TYPE_CHECKING, Optional
|
|
|
|
from sqlalchemy import DateTime, Enum as SAEnum, JSON, and_, func, or_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlmodel import Field, Relationship, SQLModel, select
|
|
|
|
from candidate_forms.enums import EmploymentType
|
|
if TYPE_CHECKING:
|
|
from job.job_post.models import JobPosts
|
|
|
|
|
|
def _now() -> datetime:
|
|
return datetime.now(timezone.utc)
|
|
|
|
class Requisition(SQLModel, table=True):
|
|
__tablename__ = "requisitions"
|
|
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
|
|
|
department: Optional[str] = None
|
|
position_title: Optional[str] = None
|
|
date: Optional[Date] = None
|
|
date_needed: Optional[Date] = None
|
|
employment_type: Optional[EmploymentType] = Field(
|
|
default=None,
|
|
sa_type=SAEnum(
|
|
EmploymentType,
|
|
name="employmenttype",
|
|
schema="app",
|
|
native_enum=True,
|
|
values_callable=lambda enum: [member.value for member in enum],
|
|
),
|
|
)
|
|
job_description: Optional[str] = None
|
|
period_from: Optional[Date] = None
|
|
period_to: Optional[Date] = None
|
|
jd_available: Optional[bool] = None
|
|
|
|
employee_name: Optional[str] = None
|
|
employee_department: Optional[str] = None
|
|
entity: Optional[str] = None
|
|
|
|
to_replace: Optional[str] = None
|
|
grade: Optional[str] = None
|
|
recruitment_title: Optional[str] = None
|
|
date_separated: Optional[Date] = None
|
|
justification: Optional[str] = None
|
|
budget: Optional[str] = None
|
|
recommended_grade: Optional[str] = None
|
|
|
|
initiated_by: Optional[str] = None
|
|
initiated_date: Optional[Date] = None
|
|
recommended_by: Optional[str] = None
|
|
recommended_date: Optional[Date] = None
|
|
approved_by_hr: Optional[bool] = None
|
|
approved_by_date_hr: Optional[Date] = None
|
|
approved_by_vp: Optional[bool] = None
|
|
approved_by_date_vp: Optional[Date] = None
|
|
approved_by_svp: Optional[bool] = None
|
|
approved_by_date_svp: Optional[Date] = None
|
|
created_by: Optional[uuid.UUID] = Field(foreign_key="users.id")
|
|
created_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True))
|
|
updated_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True))
|
|
is_deleted: bool = Field(default=False)
|
|
# Optional 1:1: job_posts.requisition_id points here. uselist=False so a
|
|
# requisition has at most one job post (enforced in DB by the unique FK).
|
|
job_post: Optional["JobPosts"] = Relationship(
|
|
back_populates="requisition",
|
|
sa_relationship_kwargs={"uselist": False, "lazy": "selectin"},
|
|
)
|
|
|
|
@classmethod
|
|
async def get_form_by_id(cls, session: AsyncSession, record_id=None, created_by=None):
|
|
qry = select(cls).where(cls.is_deleted == False) # noqa: E712
|
|
if created_by is not None:
|
|
qry = qry.where(cls.created_by == created_by)
|
|
if record_id not in (None, ""):
|
|
|
|
try:
|
|
uid = uuid.UUID(str(record_id))
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
qry = qry.where(cls.id == uid)
|
|
qry = qry.order_by(cls.created_at.desc(),cls.id.desc())
|
|
result = await session.execute(qry)
|
|
return result.scalars().first()
|
|
result = await session.execute(qry.order_by(cls.created_at.desc(),cls.id.desc()))
|
|
return list(result.scalars().all())
|
|
|
|
@classmethod
|
|
async def search(
|
|
cls,
|
|
session: AsyncSession,
|
|
q: str | None = None,
|
|
*,
|
|
top: int = 50,
|
|
job_post_id=None,
|
|
):
|
|
"""Dropdown rows: match position_title or department (either side).
|
|
|
|
Empty `q` returns the most recent non-deleted rows so the picker has a
|
|
list before the user types. Not scoped to created_by — job creators
|
|
need the org-wide list, not only requisitions they opened themselves.
|
|
|
|
job_posts.requisition_id is 1:1. Hide requisitions already linked to a
|
|
live job post. Pass `job_post_id` when editing so that job's current
|
|
requisition stays in the list until the link is cleared.
|
|
"""
|
|
from job.job_post.models import JobPosts
|
|
|
|
statement = select(cls).where(cls.is_deleted == False) # noqa: E712
|
|
held = select(JobPosts.requisition_id).where(
|
|
JobPosts.requisition_id.is_not(None),
|
|
JobPosts.is_deleted == False, # noqa: E712
|
|
)
|
|
except_uid = JobPosts._as_uuid(job_post_id) if job_post_id else None
|
|
if except_uid is not None:
|
|
held = held.where(JobPosts.id != except_uid)
|
|
statement = statement.where(cls.id.notin_(held))
|
|
term = (q or "").strip()
|
|
if term:
|
|
like = f"%{term}%"
|
|
statement = statement.where(
|
|
or_(cls.position_title.ilike(like), cls.department.ilike(like))
|
|
)
|
|
limit = max(1, min(int(top or 50), 100))
|
|
statement = statement.order_by(cls.created_at.desc(), cls.id.desc()).limit(limit)
|
|
result = await session.execute(statement)
|
|
return list(result.scalars().all())
|
|
|
|
@classmethod
|
|
async def count_open(cls, session: AsyncSession, created_by=None) -> int:
|
|
"""Open requisitions: not linked to a live job post, or linked to one whose
|
|
requisition_status is still open. A closed / on-hold / completed job closes
|
|
its requisition. job_posts.requisition_id is 1:1, so the join never fans out.
|
|
"""
|
|
from job.job_post.enums import RequisitionStatus
|
|
from job.job_post.models import JobPosts
|
|
|
|
statement = (
|
|
select(func.count())
|
|
.select_from(cls)
|
|
.outerjoin(
|
|
JobPosts,
|
|
and_(JobPosts.requisition_id == cls.id, JobPosts.is_deleted == False), # noqa: E712
|
|
)
|
|
.where(
|
|
cls.is_deleted == False, # noqa: E712
|
|
or_(JobPosts.id.is_(None), JobPosts.requisition_status == RequisitionStatus.OPEN.value),
|
|
)
|
|
)
|
|
if created_by is not None:
|
|
statement = statement.where(cls.created_by == created_by)
|
|
result = await session.execute(statement)
|
|
return int(result.scalar_one())
|
|
|
|
@classmethod
|
|
async def insert_form(cls, session: AsyncSession, fields: dict):
|
|
position = fields.get("position") if fields.get("position") else {}
|
|
replacement = fields.get("replacement_for") if fields.get("replacement_for") else {}
|
|
referral = fields.get("refferal_by") if fields.get("refferal_by") else {}
|
|
row = cls(
|
|
department=position.get("department") if position.get("department") else None,
|
|
position_title=position.get("title") if position.get("title") else None,
|
|
date=position.get("date") if position.get("date") else None,
|
|
date_needed=position.get("date_needed") if position.get("date_needed") else None,
|
|
employment_type=EmploymentType(position.get("type")) if position.get("type") else None,
|
|
job_description=position.get("job_description") if position.get("job_description") else None,
|
|
period_from=position.get("period_from") if position.get("period_from") else None,
|
|
period_to=position.get("period_to") if position.get("period_to") else None,
|
|
jd_available=position.get("jd_available") if position.get("jd_available") is not None else None,
|
|
employee_name=referral.get("employee_name") if referral.get("employee_name") else None,
|
|
employee_department=referral.get("employee_department") if referral.get("employee_department") else None,
|
|
entity=referral.get("entity") if referral.get("entity") else None,
|
|
to_replace=replacement.get("to_replace") if replacement.get("to_replace") else None,
|
|
grade=replacement.get("grade") if replacement.get("grade") else None,
|
|
recruitment_title=replacement.get("title") if replacement.get("title") else None,
|
|
date_separated=replacement.get("date_separated") if replacement.get("date_separated") else None,
|
|
justification=replacement.get("justification") if replacement.get("justification") else None,
|
|
budget=replacement.get("budget") if replacement.get("budget") else None,
|
|
recommended_grade=replacement.get("recommended_grade") if replacement.get("recommended_grade") else None,
|
|
initiated_by=fields.get("initiated_by") if fields.get("initiated_by") else None,
|
|
initiated_date=fields.get("initiated_date") if fields.get("initiated_date") else None,
|
|
recommended_by=fields.get("recommended_by") if fields.get("recommended_by") else None,
|
|
recommended_date=fields.get("recommended_date") if fields.get("recommended_date") else None,
|
|
approved_by_hr=fields.get("approved_by_hr") if fields.get("approved_by_hr") is not None else None,
|
|
approved_by_date_hr=fields.get("approved_by_date_hr") if fields.get("approved_by_date_hr") else None,
|
|
approved_by_vp=fields.get("approved_by_vp") if fields.get("approved_by_vp") is not None else None,
|
|
approved_by_date_vp=fields.get("approved_by_date_vp") if fields.get("approved_by_date_vp") else None,
|
|
approved_by_svp=fields.get("approved_by_svp") if fields.get("approved_by_svp") is not None else None,
|
|
approved_by_date_svp=fields.get("approved_by_date_svp") if fields.get("approved_by_date_svp") else None,
|
|
created_by=fields.get("created_by") if fields.get("created_by") else None,
|
|
)
|
|
session.add(row)
|
|
await session.commit()
|
|
return await cls.get_form_by_id(session, row.id)
|
|
|
|
@classmethod
|
|
async def update_form(cls, session: AsyncSession, record_id, fields: dict):
|
|
row = await cls.get_form_by_id(session, record_id)
|
|
if not row:
|
|
return None
|
|
if "position" in fields:
|
|
position = fields.get("position") if fields.get("position") else {}
|
|
if "department" in position:
|
|
row.department = position.get("department") if position.get("department") else None
|
|
if "title" in position:
|
|
row.position_title = position.get("title") if position.get("title") else None
|
|
if "date" in position:
|
|
row.date = position.get("date") if position.get("date") else None
|
|
if "date_needed" in position:
|
|
row.date_needed = position.get("date_needed") if position.get("date_needed") else None
|
|
if "type" in position:
|
|
row.employment_type = EmploymentType(position.get("type")) if position.get("type") else None
|
|
if "job_description" in position:
|
|
row.job_description = position.get("job_description") if position.get("job_description") else None
|
|
if "period_from" in position:
|
|
row.period_from = position.get("period_from") if position.get("period_from") else None
|
|
if "period_to" in position:
|
|
row.period_to = position.get("period_to") if position.get("period_to") else None
|
|
if "jd_available" in position:
|
|
row.jd_available = position.get("jd_available") if position.get("jd_available") is not None else None
|
|
if "replacement_for" in fields:
|
|
replacement = fields.get("replacement_for") if fields.get("replacement_for") else {}
|
|
if "to_replace" in replacement:
|
|
row.to_replace = replacement.get("to_replace") if replacement.get("to_replace") else None
|
|
if "grade" in replacement:
|
|
row.grade = replacement.get("grade") if replacement.get("grade") else None
|
|
if "title" in replacement:
|
|
row.recruitment_title = replacement.get("title") if replacement.get("title") else None
|
|
if "date_separated" in replacement:
|
|
row.date_separated = replacement.get("date_separated") if replacement.get("date_separated") else None
|
|
if "justification" in replacement:
|
|
row.justification = replacement.get("justification") if replacement.get("justification") else None
|
|
if "budget" in replacement:
|
|
row.budget = replacement.get("budget") if replacement.get("budget") else None
|
|
if "recommended_grade" in replacement:
|
|
row.recommended_grade = replacement.get("recommended_grade") if replacement.get("recommended_grade") else None
|
|
if "refferal_by" in fields:
|
|
referral = fields.get("refferal_by") if fields.get("refferal_by") else {}
|
|
if "employee_name" in referral:
|
|
row.employee_name = referral.get("employee_name") if referral.get("employee_name") else None
|
|
if "employee_department" in referral:
|
|
row.employee_department = referral.get("employee_department") if referral.get("employee_department") else None
|
|
if "entity" in referral:
|
|
row.entity = referral.get("entity") if referral.get("entity") else None
|
|
if "initiated_by" in fields:
|
|
row.initiated_by = fields.get("initiated_by") if fields.get("initiated_by") else None
|
|
if "initiated_date" in fields:
|
|
row.initiated_date = fields.get("initiated_date") if fields.get("initiated_date") else None
|
|
if "recommended_by" in fields:
|
|
row.recommended_by = fields.get("recommended_by") if fields.get("recommended_by") else None
|
|
if "recommended_date" in fields:
|
|
row.recommended_date = fields.get("recommended_date") if fields.get("recommended_date") else None
|
|
if "approved_by_hr" in fields:
|
|
row.approved_by_hr = fields.get("approved_by_hr") if fields.get("approved_by_hr") is not None else None
|
|
if "approved_by_date_hr" in fields:
|
|
row.approved_by_date_hr = fields.get("approved_by_date_hr") if fields.get("approved_by_date_hr") else None
|
|
if "approved_by_vp" in fields:
|
|
row.approved_by_vp = fields.get("approved_by_vp") if fields.get("approved_by_vp") is not None else None
|
|
if "approved_by_date_vp" in fields:
|
|
row.approved_by_date_vp = fields.get("approved_by_date_vp") if fields.get("approved_by_date_vp") else None
|
|
if "approved_by_svp" in fields:
|
|
row.approved_by_svp = fields.get("approved_by_svp") if fields.get("approved_by_svp") is not None else None
|
|
if "approved_by_date_svp" in fields:
|
|
row.approved_by_date_svp = fields.get("approved_by_date_svp") if fields.get("approved_by_date_svp") else None
|
|
row.updated_at = _now()
|
|
session.add(row)
|
|
await session.commit()
|
|
await session.refresh(row)
|
|
return row
|
|
|
|
class CandidateForms(SQLModel, table=True):
|
|
"""One digitized hiring form (Annexure A requisition, or one of the two
|
|
Annexure E evaluation forms). Exactly one of inbox_id /
|
|
manual_upload_candidate_id links it to an application; `sections` holds the
|
|
rated grids with server-recomputed averages, `fields` the scalar entries."""
|
|
|
|
__tablename__ = "candidate_forms"
|
|
|
|
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
|
inbox_id: int | None = Field(default=None, index=True, foreign_key="inbox.id")
|
|
manual_upload_candidate_id: uuid.UUID | None = Field(
|
|
default=None, index=True, foreign_key="manual_upload_candidate.id"
|
|
)
|
|
job_post_id: uuid.UUID | None = Field(default=None, foreign_key="job_posts.id")
|
|
form_type: str = Field(index=True)
|
|
interviewer_id: uuid.UUID | None = Field(default=None, foreign_key="users.id")
|
|
form_date: datetime | None = Field(default=None, sa_type=DateTime(timezone=True))
|
|
sections: list | None = Field(default=None, sa_type=JSON)
|
|
fields: dict | None = Field(default=None, sa_type=JSON)
|
|
overall_score: float | None = Field(default=None)
|
|
recommendation: str | None = Field(default=None)
|
|
created_by: uuid.UUID = Field(foreign_key="users.id")
|
|
created_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True))
|
|
updated_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True))
|
|
is_deleted: bool = Field(default=False)
|
|
|
|
@staticmethod
|
|
def _as_uuid(record_id) -> uuid.UUID | None:
|
|
if record_id in (None, ""):
|
|
return None
|
|
try:
|
|
return uuid.UUID(str(record_id))
|
|
except ValueError:
|
|
return None
|
|
|
|
@classmethod
|
|
async def get_form_by_id(cls, session: AsyncSession, record_id):
|
|
uid = cls._as_uuid(record_id)
|
|
if uid is None:
|
|
return None
|
|
result = await session.execute(
|
|
select(cls).where(cls.id == uid, cls.is_deleted == False) # noqa: E712
|
|
)
|
|
return result.scalars().first()
|
|
|
|
@classmethod
|
|
async def fetch_forms(
|
|
cls,
|
|
session: AsyncSession,
|
|
*,
|
|
form_id=None,
|
|
inbox_id=None,
|
|
manual_upload_candidate_id=None,
|
|
job_post_id=None,
|
|
form_type=None,
|
|
top: int | None = None,
|
|
skip: int = 0,
|
|
):
|
|
if form_id:
|
|
row = await cls.get_form_by_id(session, form_id)
|
|
if row is None:
|
|
return [], 0
|
|
return [row], 1
|
|
|
|
statement = select(cls).where(cls.is_deleted == False) # noqa: E712
|
|
if inbox_id is not None:
|
|
statement = statement.where(cls.inbox_id == int(inbox_id))
|
|
if manual_upload_candidate_id is not None:
|
|
uid = cls._as_uuid(manual_upload_candidate_id)
|
|
if uid is None:
|
|
return [], 0
|
|
statement = statement.where(cls.manual_upload_candidate_id == uid)
|
|
if job_post_id is not None:
|
|
uid = cls._as_uuid(job_post_id)
|
|
if uid is None:
|
|
return [], 0
|
|
statement = statement.where(cls.job_post_id == uid)
|
|
if form_type:
|
|
statement = statement.where(cls.form_type == form_type)
|
|
count_statement = select(func.count()).select_from(statement.subquery())
|
|
total = (await session.execute(count_statement)).scalar_one()
|
|
statement = statement.order_by(cls.created_at.desc())
|
|
if skip:
|
|
statement = statement.offset(skip)
|
|
if top is not None:
|
|
statement = statement.limit(top)
|
|
result = await session.execute(statement)
|
|
return list(result.scalars().all()), total
|
|
|
|
@classmethod
|
|
async def insert_form(cls, session: AsyncSession, fields: dict):
|
|
row = cls(
|
|
form_type=fields.get("form_type"),
|
|
inbox_id=fields.get("inbox_id"),
|
|
manual_upload_candidate_id=fields.get("manual_upload_candidate_id"),
|
|
job_post_id=fields.get("job_post_id"),
|
|
interviewer_id=fields.get("interviewer_id"),
|
|
form_date=fields.get("form_date"),
|
|
sections=fields.get("sections"),
|
|
fields=fields.get("fields"),
|
|
overall_score=fields.get("overall_score"),
|
|
recommendation=fields.get("recommendation"),
|
|
created_by=fields.get("created_by"),
|
|
)
|
|
session.add(row)
|
|
await session.commit()
|
|
return await cls.get_form_by_id(session, row.id)
|
|
|
|
@classmethod
|
|
async def update_form(cls, session: AsyncSession, record_id, fields: dict):
|
|
row = await cls.get_form_by_id(session, record_id)
|
|
if not row:
|
|
return None
|
|
if "interviewer_id" in fields:
|
|
row.interviewer_id = fields.get("interviewer_id")
|
|
if "form_date" in fields:
|
|
row.form_date = fields.get("form_date")
|
|
if "sections" in fields:
|
|
row.sections = fields.get("sections")
|
|
if "fields" in fields:
|
|
row.fields = fields.get("fields")
|
|
if "overall_score" in fields:
|
|
row.overall_score = fields.get("overall_score")
|
|
if "recommendation" in fields:
|
|
row.recommendation = fields.get("recommendation")
|
|
row.updated_at = _now()
|
|
session.add(row)
|
|
await session.commit()
|
|
await session.refresh(row)
|
|
return row
|
|
|
|
@classmethod
|
|
async def soft_delete_form(cls, session: AsyncSession, record_id):
|
|
row = await cls.get_form_by_id(session, record_id)
|
|
if not row:
|
|
return None
|
|
row.is_deleted = True
|
|
row.updated_at = _now()
|
|
session.add(row)
|
|
await session.commit()
|
|
return row
|
|
|
|
|
|
import users.models as _users_models # noqa: E402, F401
|