import uuid from datetime import datetime, date as Date, timezone from typing import TYPE_CHECKING, Optional from sqlalchemy import DateTime, Enum as SAEnum, JSON, 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 employee_name: Optional[str] = None employee_department: 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 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, 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, 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 "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 "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