HR-ATS-Portal/backend/job/job_post/serializers.py

158 lines
6.7 KiB
Python

from job.job_post.enums import RequisitionStatus
from job.job_post.models import JobPosts
def _status_label(value):
if value in (None, ""):
return None
parsed = RequisitionStatus.parse(value)
return parsed.label if parsed else value
def serialize_job_post_title(row) -> dict:
"""Inbox suggestion rail — title only until the recruiter expands the card."""
return {
"id": str(row.id),
"title": row.title,
"status": row.status,
"is_active": row.is_active,
"is_deleted": row.is_deleted,
}
def _recruiter_payload(row, names=None):
"""List of recruiter ids plus mapped names; first id stays the legacy pointer."""
names = names or {}
ids = JobPosts.recruiter_ids_of(row)
mapped = [names.get(i) for i in ids]
first = ids[0] if ids else None
return {
"current_recruiter_id": first,
"current_recruiter_ids": ids,
"recruiter_name": next((n for n in mapped if n), None),
"recruiter_names": [n for n in mapped if n],
"recruiters": [{"id": i, "name": names.get(i)} for i in ids],
}
def serialize_job_post(row, *, names=None) -> dict:
return {
"id": str(row.id),
"title": row.title,
# Talent Pool / candidate filters key off attached job_posts.department.
"department": row.department or None,
"employment_type": row.employment_type,
"location": row.location,
"experience_min": row.experience_min,
"experience_max": row.experience_max,
"requirements": list(row.requirements or []),
"optional_skills": list(row.optional_skills or []),
"salary": row.salary,
"description": row.description,
"post_text": row.post_text,
"channel_id": row.channel_id,
"platform": row.platform,
"is_active": row.is_active,
"is_deleted": row.is_deleted,
"buffer_post_id": row.buffer_post_id,
"buffer_external_link": row.buffer_external_link,
"buffer_sent_at": row.buffer_sent_at.isoformat() if row.buffer_sent_at else None,
"status": row.status,
"buffer_error": row.buffer_error,
"created_by": str(row.created_by) if row.created_by else None,
"created_by_name": row.user.name if getattr(row, "user", None) 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,
"requisition_id": str(row.requisition_id) if getattr(row, "requisition_id", None) else None,
**_recruiter_payload(row, names),
}
def serialize_job_row(row, *, names=None, recruiter_name=None, hiring_manager_name=None, applicant_count=0) -> dict:
"""Requisition view of a job post, for the Jobs screen.
Deliberately separate from serialize_job_post: that payload is shared by the
inbox, candidate and matching paths. department is the one shared field —
talent-pool filters key off it on attached job_posts.
"""
req = getattr(row, "requisition", None)
payload = _recruiter_payload(row, names)
if recruiter_name and not payload["recruiter_name"]:
payload["recruiter_name"] = recruiter_name
return {
"id": str(row.id),
"title": row.title,
"department": row.department or None,
"location": row.location,
"employment_type": row.employment_type,
"vacancies": row.vacancies,
"platform": row.platform or None,
# Two different lifecycles, never conflate: requisition_status is hiring
# (open/closed/on_hold), status is Buffer publishing (draft/scheduled/...).
"requisition_status": row.requisition_status,
"status": row.status,
"experience_min": row.experience_min,
"experience_max": row.experience_max,
"salary": row.salary,
"requirements": list(row.requirements or []),
"optional_skills": list(row.optional_skills or []),
"description": row.description,
"is_active": row.is_active,
"closed_at": row.closed_at.isoformat() if row.closed_at else None,
**payload,
"hiring_manager_id": str(row.hiring_manager_id) if row.hiring_manager_id else None,
"hiring_manager_name": hiring_manager_name,
"requisition_id": str(row.requisition_id) if getattr(row, "requisition_id", None) else None,
"requisition_title": req.position_title if req else None,
"requisition_department": req.department if req else None,
"applicant_count": applicant_count,
"created_by": str(row.created_by) if row.created_by else None,
"created_by_name": row.user.name if getattr(row, "user", None) 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,
}
def serialize_job_stats(row, *, names=None) -> dict:
"""One job post's live pipeline-stage counts. `row` is a mapping from fetch_job_stats."""
created_at = row.get("created_at")
payload = _recruiter_payload(row, names)
if not payload["recruiter_name"] and row.get("recruiter_name"):
payload["recruiter_name"] = row.get("recruiter_name")
return {
"job_post_id": str(row["job_post_id"]),
"title": row["title"],
"department": row["department"] or None,
"location": row["location"],
"requisition_status": row["requisition_status"],
**payload,
# Frontend computes days-open vs client clock; no server days_open field.
"created_at": created_at.isoformat() if created_at else None,
"total_applicants": int(row["total_applicants"] or 0),
"shortlisting": int(row["shortlisting"] or 0),
"screened": int(row["screened"] or 0),
"assessment": int(row["assessment"] or 0),
"interviewed": int(row["interviewed"] or 0),
"offered": int(row["offered"] or 0),
"on_hold": int(row["on_hold"] or 0),
"rejected": int(row["rejected"] or 0),
"approved": int(row["approved"] or 0),
"hired": int(row["hired"] or 0),
"reapplied": int(row.get("reapplied") or 0),
}
def serialize_status_history(row, *, changed_by_name=None) -> dict:
return {
"id": str(row.id),
"job_post_id": str(row.job_post_id) if row.job_post_id else None,
"from_status": row.from_status,
"from_label": _status_label(row.from_status),
"to_status": row.to_status,
"to_label": _status_label(row.to_status),
"changed_by": str(row.changed_by) if row.changed_by else None,
"changed_by_name": changed_by_name,
"actor_kind": row.actor_kind,
"created_at": row.created_at.isoformat() if row.created_at else None,
}