Compare commits
5 Commits
3838634e82
...
1493af9fea
| Author | SHA1 | Date |
|---|---|---|
|
|
1493af9fea | |
|
|
a023562110 | |
|
|
3095fb338f | |
|
|
626eb56942 | |
|
|
6bbf33346f |
|
|
@ -25,6 +25,7 @@ from candidate_forms.app import router as candidate_forms_router
|
|||
from g_sheet.app import router as g_sheet_router
|
||||
from s3.app import router as s3_router
|
||||
from department.app import router as department_router
|
||||
from verification_check.app import router as verification_check_router
|
||||
|
||||
logging.basicConfig(level=logging.INFO,format="%(levelname)-8s %(name)s: %(message)s")
|
||||
logger=logging.getLogger("main")
|
||||
|
|
@ -138,3 +139,4 @@ app.include_router(candidate_forms_router)
|
|||
app.include_router(g_sheet_router)
|
||||
app.include_router(s3_router)
|
||||
app.include_router(department_router)
|
||||
app.include_router(verification_check_router)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
-- 042_verification_check.sql
|
||||
-- Annexure E - Employment Verification Check Form as its own table
|
||||
-- (backend/verification_check/models.py, VerificationCheck). One row per referee
|
||||
-- call, so a candidate can have several. recruiter_id and candidate_id both
|
||||
-- point at users.id (recruiters and candidates are users in this system);
|
||||
-- created_by is the logged-in user who filled the form.
|
||||
--
|
||||
-- The table name is mixed case at the user's request, so it must stay quoted
|
||||
-- ("Verification_Check") in any raw SQL. Index / FK names follow the db_setup
|
||||
-- NAMING_CONVENTION so a dev DB that autogenerated first is a no-op.
|
||||
--
|
||||
-- Idempotent, applied at startup by alembic_setup.run_manual_sql(). Needed
|
||||
-- because prod boots with DB_AUTOGENERATE=false and never autogenerates tables.
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_type t
|
||||
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||
WHERE t.typname = 'rehireeligibility' AND n.nspname = 'app'
|
||||
) THEN
|
||||
CREATE TYPE app.rehireeligibility AS ENUM ('yes', 'no', 'conditional');
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app."Verification_Check" (
|
||||
id uuid PRIMARY KEY,
|
||||
recruiter_id uuid,
|
||||
candidate_id uuid,
|
||||
position_applied varchar,
|
||||
department_id uuid,
|
||||
department varchar,
|
||||
date_time timestamptz,
|
||||
confirmed_job_title varchar,
|
||||
confirmed_from date,
|
||||
confirmed_to date,
|
||||
reason_for_leaving varchar,
|
||||
technical_knowledge_rating integer,
|
||||
technical_knowledge_notes varchar,
|
||||
reliability_rating integer,
|
||||
reliability_notes varchar,
|
||||
communication_rating integer,
|
||||
communication_notes varchar,
|
||||
problem_solving_rating integer,
|
||||
problem_solving_notes varchar,
|
||||
strengths varchar,
|
||||
areas_for_growth varchar,
|
||||
rehire_eligibility app.rehireeligibility,
|
||||
rehire_comments varchar,
|
||||
referee_full_name varchar,
|
||||
referee_current_title varchar,
|
||||
referee_company varchar,
|
||||
referee_phone varchar,
|
||||
referee_email varchar,
|
||||
created_by uuid,
|
||||
created_at timestamptz NOT NULL DEFAULT NOW(),
|
||||
updated_at timestamptz NOT NULL DEFAULT NOW(),
|
||||
is_deleted boolean NOT NULL DEFAULT false,
|
||||
CONSTRAINT "fk_Verification_Check_recruiter_id_users"
|
||||
FOREIGN KEY (recruiter_id) REFERENCES app.users (id),
|
||||
CONSTRAINT "fk_Verification_Check_candidate_id_users"
|
||||
FOREIGN KEY (candidate_id) REFERENCES app.users (id),
|
||||
CONSTRAINT "fk_Verification_Check_department_id_departments"
|
||||
FOREIGN KEY (department_id) REFERENCES app.departments (id),
|
||||
CONSTRAINT "fk_Verification_Check_created_by_users"
|
||||
FOREIGN KEY (created_by) REFERENCES app.users (id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "ix_Verification_Check_recruiter_id"
|
||||
ON app."Verification_Check" (recruiter_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "ix_Verification_Check_candidate_id"
|
||||
ON app."Verification_Check" (candidate_id);
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from db_setup import get_session
|
||||
from users.permissions import PermissionTag, require_permission
|
||||
from verification_check.enums import (
|
||||
CompetencyEvaluation,
|
||||
EmploymentVerification,
|
||||
QualitativeEvaluation,
|
||||
RecordOverview,
|
||||
RefereeDetails,
|
||||
)
|
||||
from verification_check.views import VerificationCheckForm
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class VerificationCheckCreate(BaseModel):
|
||||
recruiter_id:uuid.UUID
|
||||
candidate_id:uuid.UUID
|
||||
record_overview:Optional[RecordOverview]
|
||||
employment_verification:Optional[EmploymentVerification]
|
||||
competency_evaluation:Optional[CompetencyEvaluation]
|
||||
qualitative_evaluation:Optional[QualitativeEvaluation]
|
||||
referee:Optional[RefereeDetails]
|
||||
|
||||
|
||||
class VerificationCheckUpdate(BaseModel):
|
||||
recruiter_id:Optional[uuid.UUID]=None
|
||||
candidate_id:Optional[uuid.UUID]=None
|
||||
record_overview:Optional[RecordOverview]=None
|
||||
employment_verification:Optional[EmploymentVerification]=None
|
||||
competency_evaluation:Optional[CompetencyEvaluation]=None
|
||||
qualitative_evaluation:Optional[QualitativeEvaluation]=None
|
||||
referee:Optional[RefereeDetails]=None
|
||||
|
||||
|
||||
@router.get("/verification-check/fetch")
|
||||
async def fetch_verification_check(
|
||||
current_user:dict=Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
|
||||
form_id:str=Query(None),
|
||||
candidate_id:str=Query(None),
|
||||
session:AsyncSession=Depends(get_session),
|
||||
):
|
||||
"""One row by form_id, else the list (optionally one candidate's referees).
|
||||
Admins see every row; other roles stay scoped to created_by."""
|
||||
try:
|
||||
service=VerificationCheckForm(session=session)
|
||||
data=await service.get_form_by_id(form_id,candidate_id,current_user)
|
||||
return JSONResponse(content={"data":data,"status_code":200})
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/verification-check/create")
|
||||
async def create_verification_check(
|
||||
payload: VerificationCheckCreate,
|
||||
current_user:dict=Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
|
||||
session:AsyncSession=Depends(get_session),
|
||||
):
|
||||
try:
|
||||
service = VerificationCheckForm(session=session)
|
||||
data = await service.create_form(payload.model_dump(exclude_unset=True), current_user)
|
||||
return JSONResponse(content={"data": data, "status_code": 200})
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.patch("/verification-check/update")
|
||||
async def update_verification_check(
|
||||
payload: VerificationCheckUpdate,
|
||||
current_user:dict=Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
|
||||
form_id:str=Query(...),
|
||||
session:AsyncSession=Depends(get_session),
|
||||
):
|
||||
try:
|
||||
service=VerificationCheckForm(session=session)
|
||||
data=await service.update_form(form_id,payload.model_dump(exclude_unset=True),current_user)
|
||||
return JSONResponse(content={"data":data,"status_code":200})
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500,detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/verification-check/delete")
|
||||
async def delete_verification_check(
|
||||
current_user:dict=Depends(require_permission(PermissionTag.CANDIDATES_DELETE)),
|
||||
form_id:str=Query(...),
|
||||
session:AsyncSession=Depends(get_session),
|
||||
):
|
||||
try:
|
||||
service=VerificationCheckForm(session=session)
|
||||
data=await service.delete_form(form_id,current_user)
|
||||
return JSONResponse(content={"data":data,"status_code":200})
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500,detail=str(e))
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
from enum import Enum
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import date, datetime
|
||||
import uuid
|
||||
|
||||
|
||||
class CompetencyRating(int, Enum):
|
||||
NEEDS_IMPROVEMENT = 1
|
||||
SATISFACTORY = 2
|
||||
GOOD = 3
|
||||
EXCELLENT = 4
|
||||
|
||||
|
||||
class ReHireEligibility(str, Enum):
|
||||
YES = "yes"
|
||||
NO = "no"
|
||||
CONDITIONAL = "conditional"
|
||||
|
||||
|
||||
# SECTION 1: INFORMATION & RECORD OVERVIEW
|
||||
class RecordOverview(BaseModel):
|
||||
position_applied:Optional[str]=None
|
||||
department_id:Optional[uuid.UUID]=None
|
||||
department:Optional[str]=None
|
||||
date_time:Optional[datetime]=None
|
||||
|
||||
|
||||
# SECTION 2: EMPLOYMENT VERIFICATION & DUTIES
|
||||
class EmploymentVerification(BaseModel):
|
||||
confirmed_job_title:Optional[str]=None
|
||||
confirmed_from:Optional[date]=None
|
||||
confirmed_to:Optional[date]=None
|
||||
reason_for_leaving:Optional[str]=None
|
||||
|
||||
|
||||
# SECTION 3: one row of the competency grid (1-4 tick + Notes column)
|
||||
class CompetencyScore(BaseModel):
|
||||
rating:Optional[CompetencyRating]=None
|
||||
notes:Optional[str]=None
|
||||
|
||||
|
||||
class CompetencyEvaluation(BaseModel):
|
||||
technical_knowledge:Optional[CompetencyScore]=None
|
||||
reliability:Optional[CompetencyScore]=None
|
||||
communication:Optional[CompetencyScore]=None
|
||||
problem_solving:Optional[CompetencyScore]=None
|
||||
|
||||
|
||||
# SECTION 4: QUALITATIVE EVALUATION & KEY INSIGHTS
|
||||
class QualitativeEvaluation(BaseModel):
|
||||
strengths:Optional[str]=None
|
||||
areas_for_growth:Optional[str]=None
|
||||
rehire_eligibility:Optional[ReHireEligibility]=None
|
||||
rehire_comments:Optional[str]=None
|
||||
|
||||
|
||||
# SECTION 5: REFEREE CONTACT & RELATIONSHIP VERIFICATION
|
||||
class RefereeDetails(BaseModel):
|
||||
full_name:Optional[str]=None
|
||||
current_title:Optional[str]=None
|
||||
company:Optional[str]=None
|
||||
phone:Optional[str]=None
|
||||
email:Optional[str]=None
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
import uuid
|
||||
from datetime import datetime, date as Date, timezone
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import DateTime, Enum as SAEnum
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import Field, SQLModel, select
|
||||
|
||||
from verification_check.enums import ReHireEligibility
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class VerificationCheck(SQLModel, table=True):
|
||||
"""Annexure E - Employment Verification Check Form. One row per referee call,
|
||||
so a candidate can hold several. recruiter_id / candidate_id are both users.id."""
|
||||
|
||||
__tablename__ = "Verification_Check"
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
|
||||
recruiter_id: Optional[uuid.UUID] = Field(default=None, foreign_key="users.id", index=True)
|
||||
candidate_id: Optional[uuid.UUID] = Field(default=None, foreign_key="users.id", index=True)
|
||||
|
||||
position_applied: Optional[str] = None
|
||||
department_id: Optional[uuid.UUID] = Field(default=None, foreign_key="departments.id")
|
||||
department: Optional[str] = None
|
||||
date_time: Optional[datetime] = Field(default=None, sa_type=DateTime(timezone=True))
|
||||
|
||||
confirmed_job_title: Optional[str] = None
|
||||
confirmed_from: Optional[Date] = None
|
||||
confirmed_to: Optional[Date] = None
|
||||
reason_for_leaving: Optional[str] = None
|
||||
|
||||
technical_knowledge_rating: Optional[int] = None
|
||||
technical_knowledge_notes: Optional[str] = None
|
||||
reliability_rating: Optional[int] = None
|
||||
reliability_notes: Optional[str] = None
|
||||
communication_rating: Optional[int] = None
|
||||
communication_notes: Optional[str] = None
|
||||
problem_solving_rating: Optional[int] = None
|
||||
problem_solving_notes: Optional[str] = None
|
||||
|
||||
strengths: Optional[str] = None
|
||||
areas_for_growth: Optional[str] = None
|
||||
rehire_eligibility: Optional[ReHireEligibility] = Field(
|
||||
default=None,
|
||||
sa_type=SAEnum(
|
||||
ReHireEligibility,
|
||||
name="rehireeligibility",
|
||||
schema="app",
|
||||
native_enum=True,
|
||||
values_callable=lambda enum: [member.value for member in enum],
|
||||
),
|
||||
)
|
||||
rehire_comments: Optional[str] = None
|
||||
|
||||
referee_full_name: Optional[str] = None
|
||||
referee_current_title: Optional[str] = None
|
||||
referee_company: Optional[str] = None
|
||||
referee_phone: Optional[str] = None
|
||||
referee_email: Optional[str] = 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)
|
||||
|
||||
@classmethod
|
||||
async def get_form_by_id(cls, session: AsyncSession, record_id=None, created_by=None, candidate_id=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 candidate_id is not None:
|
||||
qry = qry.where(cls.candidate_id == candidate_id)
|
||||
if record_id not in (None, ""):
|
||||
try:
|
||||
uid = uuid.UUID(str(record_id))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
qry = qry.where(cls.id == uid)
|
||||
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 insert_form(cls, session: AsyncSession, fields: dict):
|
||||
overview = fields.get("record_overview") if fields.get("record_overview") else {}
|
||||
employment = fields.get("employment_verification") if fields.get("employment_verification") else {}
|
||||
competency = fields.get("competency_evaluation") if fields.get("competency_evaluation") else {}
|
||||
technical = competency.get("technical_knowledge") if competency.get("technical_knowledge") else {}
|
||||
reliability = competency.get("reliability") if competency.get("reliability") else {}
|
||||
communication = competency.get("communication") if competency.get("communication") else {}
|
||||
problem_solving = competency.get("problem_solving") if competency.get("problem_solving") else {}
|
||||
qualitative = fields.get("qualitative_evaluation") if fields.get("qualitative_evaluation") else {}
|
||||
referee = fields.get("referee") if fields.get("referee") else {}
|
||||
row = cls(
|
||||
recruiter_id=fields.get("recruiter_id") if fields.get("recruiter_id") else None,
|
||||
candidate_id=fields.get("candidate_id") if fields.get("candidate_id") else None,
|
||||
position_applied=overview.get("position_applied") if overview.get("position_applied") else None,
|
||||
department_id=overview.get("department_id") if overview.get("department_id") else None,
|
||||
department=overview.get("department") if overview.get("department") else None,
|
||||
date_time=overview.get("date_time") if overview.get("date_time") else None,
|
||||
confirmed_job_title=employment.get("confirmed_job_title") if employment.get("confirmed_job_title") else None,
|
||||
confirmed_from=employment.get("confirmed_from") if employment.get("confirmed_from") else None,
|
||||
confirmed_to=employment.get("confirmed_to") if employment.get("confirmed_to") else None,
|
||||
reason_for_leaving=employment.get("reason_for_leaving") if employment.get("reason_for_leaving") else None,
|
||||
technical_knowledge_rating=int(technical.get("rating")) if technical.get("rating") else None,
|
||||
technical_knowledge_notes=technical.get("notes") if technical.get("notes") else None,
|
||||
reliability_rating=int(reliability.get("rating")) if reliability.get("rating") else None,
|
||||
reliability_notes=reliability.get("notes") if reliability.get("notes") else None,
|
||||
communication_rating=int(communication.get("rating")) if communication.get("rating") else None,
|
||||
communication_notes=communication.get("notes") if communication.get("notes") else None,
|
||||
problem_solving_rating=int(problem_solving.get("rating")) if problem_solving.get("rating") else None,
|
||||
problem_solving_notes=problem_solving.get("notes") if problem_solving.get("notes") else None,
|
||||
strengths=qualitative.get("strengths") if qualitative.get("strengths") else None,
|
||||
areas_for_growth=qualitative.get("areas_for_growth") if qualitative.get("areas_for_growth") else None,
|
||||
rehire_eligibility=ReHireEligibility(qualitative.get("rehire_eligibility")) if qualitative.get("rehire_eligibility") else None,
|
||||
rehire_comments=qualitative.get("rehire_comments") if qualitative.get("rehire_comments") else None,
|
||||
referee_full_name=referee.get("full_name") if referee.get("full_name") else None,
|
||||
referee_current_title=referee.get("current_title") if referee.get("current_title") else None,
|
||||
referee_company=referee.get("company") if referee.get("company") else None,
|
||||
referee_phone=referee.get("phone") if referee.get("phone") else None,
|
||||
referee_email=referee.get("email") if referee.get("email") 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 "recruiter_id" in fields:
|
||||
row.recruiter_id = fields.get("recruiter_id") if fields.get("recruiter_id") else None
|
||||
if "candidate_id" in fields:
|
||||
row.candidate_id = fields.get("candidate_id") if fields.get("candidate_id") else None
|
||||
if "record_overview" in fields:
|
||||
overview = fields.get("record_overview") if fields.get("record_overview") else {}
|
||||
if "position_applied" in overview:
|
||||
row.position_applied = overview.get("position_applied") if overview.get("position_applied") else None
|
||||
if "department_id" in overview:
|
||||
row.department_id = overview.get("department_id") if overview.get("department_id") else None
|
||||
if "department" in overview:
|
||||
row.department = overview.get("department") if overview.get("department") else None
|
||||
if "date_time" in overview:
|
||||
row.date_time = overview.get("date_time") if overview.get("date_time") else None
|
||||
if "employment_verification" in fields:
|
||||
employment = fields.get("employment_verification") if fields.get("employment_verification") else {}
|
||||
if "confirmed_job_title" in employment:
|
||||
row.confirmed_job_title = employment.get("confirmed_job_title") if employment.get("confirmed_job_title") else None
|
||||
if "confirmed_from" in employment:
|
||||
row.confirmed_from = employment.get("confirmed_from") if employment.get("confirmed_from") else None
|
||||
if "confirmed_to" in employment:
|
||||
row.confirmed_to = employment.get("confirmed_to") if employment.get("confirmed_to") else None
|
||||
if "reason_for_leaving" in employment:
|
||||
row.reason_for_leaving = employment.get("reason_for_leaving") if employment.get("reason_for_leaving") else None
|
||||
if "competency_evaluation" in fields:
|
||||
competency = fields.get("competency_evaluation") if fields.get("competency_evaluation") else {}
|
||||
if "technical_knowledge" in competency:
|
||||
technical = competency.get("technical_knowledge") if competency.get("technical_knowledge") else {}
|
||||
if "rating" in technical:
|
||||
row.technical_knowledge_rating = int(technical.get("rating")) if technical.get("rating") else None
|
||||
if "notes" in technical:
|
||||
row.technical_knowledge_notes = technical.get("notes") if technical.get("notes") else None
|
||||
if "reliability" in competency:
|
||||
reliability = competency.get("reliability") if competency.get("reliability") else {}
|
||||
if "rating" in reliability:
|
||||
row.reliability_rating = int(reliability.get("rating")) if reliability.get("rating") else None
|
||||
if "notes" in reliability:
|
||||
row.reliability_notes = reliability.get("notes") if reliability.get("notes") else None
|
||||
if "communication" in competency:
|
||||
communication = competency.get("communication") if competency.get("communication") else {}
|
||||
if "rating" in communication:
|
||||
row.communication_rating = int(communication.get("rating")) if communication.get("rating") else None
|
||||
if "notes" in communication:
|
||||
row.communication_notes = communication.get("notes") if communication.get("notes") else None
|
||||
if "problem_solving" in competency:
|
||||
problem_solving = competency.get("problem_solving") if competency.get("problem_solving") else {}
|
||||
if "rating" in problem_solving:
|
||||
row.problem_solving_rating = int(problem_solving.get("rating")) if problem_solving.get("rating") else None
|
||||
if "notes" in problem_solving:
|
||||
row.problem_solving_notes = problem_solving.get("notes") if problem_solving.get("notes") else None
|
||||
if "qualitative_evaluation" in fields:
|
||||
qualitative = fields.get("qualitative_evaluation") if fields.get("qualitative_evaluation") else {}
|
||||
if "strengths" in qualitative:
|
||||
row.strengths = qualitative.get("strengths") if qualitative.get("strengths") else None
|
||||
if "areas_for_growth" in qualitative:
|
||||
row.areas_for_growth = qualitative.get("areas_for_growth") if qualitative.get("areas_for_growth") else None
|
||||
if "rehire_eligibility" in qualitative:
|
||||
row.rehire_eligibility = ReHireEligibility(qualitative.get("rehire_eligibility")) if qualitative.get("rehire_eligibility") else None
|
||||
if "rehire_comments" in qualitative:
|
||||
row.rehire_comments = qualitative.get("rehire_comments") if qualitative.get("rehire_comments") else None
|
||||
if "referee" in fields:
|
||||
referee = fields.get("referee") if fields.get("referee") else {}
|
||||
if "full_name" in referee:
|
||||
row.referee_full_name = referee.get("full_name") if referee.get("full_name") else None
|
||||
if "current_title" in referee:
|
||||
row.referee_current_title = referee.get("current_title") if referee.get("current_title") else None
|
||||
if "company" in referee:
|
||||
row.referee_company = referee.get("company") if referee.get("company") else None
|
||||
if "phone" in referee:
|
||||
row.referee_phone = referee.get("phone") if referee.get("phone") else None
|
||||
if "email" in referee:
|
||||
row.referee_email = referee.get("email") if referee.get("email") else None
|
||||
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
|
||||
import department.models as _department_models # noqa: E402, F401
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
def _date(value):
|
||||
return value.isoformat() if value else None
|
||||
|
||||
|
||||
def _enum(value):
|
||||
if value is None:
|
||||
return None
|
||||
return getattr(value, "value", value)
|
||||
|
||||
|
||||
def serialize_verification_check(row, names=None) -> dict:
|
||||
"""`names` is {user_id: name} from one batched Users.names_by_ids lookup in
|
||||
views, so the recruiter / candidate search dropdowns can preselect on edit."""
|
||||
names = names or {}
|
||||
return {
|
||||
"id": str(row.id) if row.id else None,
|
||||
"recruiter_id": str(row.recruiter_id) if row.recruiter_id else None,
|
||||
"recruiter_name": names.get(str(row.recruiter_id)) if row.recruiter_id else None,
|
||||
"candidate_id": str(row.candidate_id) if row.candidate_id else None,
|
||||
"candidate_name": names.get(str(row.candidate_id)) if row.candidate_id else None,
|
||||
"record_overview": {
|
||||
"position_applied": row.position_applied,
|
||||
"department_id": str(row.department_id) if row.department_id else None,
|
||||
"department": row.department,
|
||||
"date_time": _date(row.date_time),
|
||||
},
|
||||
"employment_verification": {
|
||||
"confirmed_job_title": row.confirmed_job_title,
|
||||
"confirmed_from": _date(row.confirmed_from),
|
||||
"confirmed_to": _date(row.confirmed_to),
|
||||
"reason_for_leaving": row.reason_for_leaving,
|
||||
},
|
||||
"competency_evaluation": {
|
||||
"technical_knowledge": {
|
||||
"rating": row.technical_knowledge_rating,
|
||||
"notes": row.technical_knowledge_notes,
|
||||
},
|
||||
"reliability": {
|
||||
"rating": row.reliability_rating,
|
||||
"notes": row.reliability_notes,
|
||||
},
|
||||
"communication": {
|
||||
"rating": row.communication_rating,
|
||||
"notes": row.communication_notes,
|
||||
},
|
||||
"problem_solving": {
|
||||
"rating": row.problem_solving_rating,
|
||||
"notes": row.problem_solving_notes,
|
||||
},
|
||||
},
|
||||
"qualitative_evaluation": {
|
||||
"strengths": row.strengths,
|
||||
"areas_for_growth": row.areas_for_growth,
|
||||
"rehire_eligibility": _enum(row.rehire_eligibility),
|
||||
"rehire_comments": row.rehire_comments,
|
||||
},
|
||||
"referee": {
|
||||
"full_name": row.referee_full_name,
|
||||
"current_title": row.referee_current_title,
|
||||
"company": row.referee_company,
|
||||
"phone": row.referee_phone,
|
||||
"email": row.referee_email,
|
||||
},
|
||||
"created_by": str(row.created_by) if row.created_by else None,
|
||||
"created_by_name": names.get(str(row.created_by)) if row.created_by else None,
|
||||
"created_at": row.created_at.isoformat() if row.created_at else None,
|
||||
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
from fastapi import HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from candidate_forms.plugins import _as_uuid, _aware, _user_id
|
||||
from users.models import Users
|
||||
from users.permissions import is_admin
|
||||
from verification_check.models import VerificationCheck
|
||||
from verification_check.serializers import serialize_verification_check
|
||||
|
||||
|
||||
class VerificationCheckForm:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def _serialize_rows(self, rows):
|
||||
user_ids = set()
|
||||
for row in rows:
|
||||
user_ids |= {row.recruiter_id, row.candidate_id, row.created_by}
|
||||
names = await Users.names_by_ids(self.session, user_ids)
|
||||
return [serialize_verification_check(row, names) for row in rows]
|
||||
|
||||
async def create_form(self, payload, current_user):
|
||||
if payload.get("record_overview") and "date_time" in payload["record_overview"]:
|
||||
payload["record_overview"]["date_time"] = _aware(payload["record_overview"]["date_time"])
|
||||
payload["created_by"] = _user_id(current_user)
|
||||
row = await VerificationCheck.insert_form(self.session, payload)
|
||||
return (await self._serialize_rows([row]))[0]
|
||||
|
||||
async def update_form(self, form_id, payload, current_user):
|
||||
_user_id(current_user)
|
||||
row = await VerificationCheck.get_form_by_id(self.session, form_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Verification check not found")
|
||||
if not payload:
|
||||
raise HTTPException(status_code=400, detail="No fields to update")
|
||||
if payload.get("record_overview") and "date_time" in payload["record_overview"]:
|
||||
payload["record_overview"]["date_time"] = _aware(payload["record_overview"]["date_time"])
|
||||
updated = await VerificationCheck.update_form(self.session, form_id, payload)
|
||||
if not updated:
|
||||
raise HTTPException(status_code=404, detail="Verification check not found")
|
||||
return (await self._serialize_rows([updated]))[0]
|
||||
|
||||
async def get_form_by_id(self, form_id, candidate_id, current_user):
|
||||
created_by = None if is_admin(current_user) else _user_id(current_user)
|
||||
if form_id:
|
||||
row = await VerificationCheck.get_form_by_id(self.session, record_id=form_id, created_by=created_by)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Verification check not found")
|
||||
return (await self._serialize_rows([row]))[0]
|
||||
rows = await VerificationCheck.get_form_by_id(
|
||||
self.session, created_by=created_by, candidate_id=_as_uuid(candidate_id)
|
||||
)
|
||||
return await self._serialize_rows(rows)
|
||||
|
||||
async def delete_form(self, form_id, current_user):
|
||||
_user_id(current_user)
|
||||
row = await VerificationCheck.soft_delete_form(self.session, form_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Verification check not found")
|
||||
return {"id": str(row.id), "deleted": True}
|
||||
|
|
@ -349,6 +349,7 @@ function toPayload(f) {
|
|||
const body = {
|
||||
position: {
|
||||
department_id: emptyToNull(f.department_id),
|
||||
department: emptyToNull(f.department_name),
|
||||
title: emptyToNull(f.title),
|
||||
date: emptyToNull(f.date),
|
||||
date_needed: emptyToNull(f.date_needed),
|
||||
|
|
@ -458,7 +459,10 @@ function RequisitionEditor({ row, allowed, onClose, onSaved, toast }) {
|
|||
<label>From (Dept.)</label>
|
||||
<DepartmentSelect
|
||||
value={fields.department_id}
|
||||
onChange={(id) => set('department_id', id)}
|
||||
onChange={(id, name) => {
|
||||
set('department_id', id)
|
||||
set('department_name', name)
|
||||
}}
|
||||
fallbackName={fields.department_name}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
DepartmentSelect — the "From (Dept.)" / "Department" picker.
|
||||
|
||||
Wraps SearchSelect over GET /department/names (`{data:[{id,name}]}`) and
|
||||
stores the department's id, never its name. The server filters on `search`,
|
||||
hands back both the department's id and its name. The server filters on `search`,
|
||||
so typing is debounced and SearchSelect is told not to filter again locally.
|
||||
============================================================ */
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ import * as departmentsApi from '../api/departments'
|
|||
|
||||
/**
|
||||
* @param value selected department id, '' when none
|
||||
* @param onChange (id) => void — receives '' when cleared
|
||||
* @param onChange (id, name) => void — receives ('', '') when cleared
|
||||
* @param fallbackName name of the saved department, used only to label it when
|
||||
* the list does not contain it (see below)
|
||||
*/
|
||||
|
|
@ -53,7 +53,7 @@ export default function DepartmentSelect({ value, onChange, fallbackName = '', d
|
|||
<SearchSelect
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onChange={(id) => onChange(id, options.find((d) => String(d.id) === String(id))?.name ?? '')}
|
||||
onQueryChange={setQ}
|
||||
placeholder="Search departments…"
|
||||
disabled={disabled}
|
||||
|
|
|
|||
Loading…
Reference in New Issue