194 lines
8.6 KiB
Python
194 lines
8.6 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):
|
|
"""Recruiter ids plus their mapped names, for the payloads that still send the
|
|
flat shape (serialize_job_post, serialize_job_stats)."""
|
|
names = names or {}
|
|
ids = JobPosts.recruiter_ids_of(row)
|
|
mapped = [names.get(i) for i in ids]
|
|
# recruiter_name is the singular legacy field: the first id that resolved.
|
|
recruiter_name = None
|
|
for n in mapped:
|
|
if n:
|
|
recruiter_name = n
|
|
break
|
|
return {
|
|
"current_recruiter_ids": ids,
|
|
"recruiter_name": recruiter_name,
|
|
"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,
|
|
"department_id": str(row.department_id) if row.department_id else 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, *, people=None, applicant_count=0) -> dict:
|
|
"""Requisition view of a job post, for the Jobs screen and the job profile.
|
|
|
|
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.
|
|
|
|
`people` is Users.job_people's shape — {"recruiters": {id: name},
|
|
"hiring_manager": {id: name}} — so the two roles stay apart in the response.
|
|
"""
|
|
req = getattr(row, "requisition", None)
|
|
people = people or {}
|
|
recruiters = people.get("recruiters") or {}
|
|
hiring_manager = people.get("hiring_manager") or None
|
|
return {
|
|
"id": str(row.id),
|
|
"title": row.title,
|
|
# The linked department wins; the free-text column is the fallback, and is
|
|
# still what fetch_jobs filters on, so reads and filters agree.
|
|
"department": (row.department_ref.name if row.department_ref else None) or row.department or None,
|
|
"department_id": str(row.department_id) if row.department_id else None,
|
|
"location": row.location,
|
|
"employment_type": row.employment_type,
|
|
"vacancies": row.vacancies,
|
|
"platform": row.platform or None,
|
|
"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,
|
|
"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,
|
|
"recruiters": recruiters,
|
|
"hiring_manager": hiring_manager,
|
|
}
|
|
|
|
|
|
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,
|
|
}
|
|
|
|
|
|
def serialize_suggested_candidate(row, *, name=None, email=None, candidate=None, source=None, optional_matched=None) -> dict:
|
|
"""One suggested candidate on the job profile: the newest ats_results row for a
|
|
person, with profile fields from its `candidates` row when one exists."""
|
|
score = row.overall_score
|
|
return {
|
|
"id": str(row.id),
|
|
"user_id": str(row.user_id) if row.user_id else None,
|
|
"candidate_id": str(row.candidate_id) if row.candidate_id else None,
|
|
"form_data_id": str(row.form_data_id) if row.form_data_id else None,
|
|
"inbox_id": row.inbox_id,
|
|
"name": name or (candidate.candidate_name if candidate else None) or email or None,
|
|
"email": email or (candidate.candidate_email if candidate else None) or None,
|
|
"current_title": candidate.job_title if candidate else None,
|
|
"current_company": candidate.current_company if candidate else None,
|
|
"years_experience": candidate.years_experience if candidate else None,
|
|
"match_score": round(score) if score is not None else None,
|
|
"band": row.band or None,
|
|
"matched_keywords": list(candidate.matched_keywords or []) if candidate else [],
|
|
"missing_keywords": list(candidate.missing_keywords or []) if candidate else [],
|
|
"optional_matched": list(optional_matched or []),
|
|
"summary": (candidate.summary_critique if candidate else None) or row.professional_summary or None,
|
|
"source": source,
|
|
"scored_candidate_id": str(candidate.id) if candidate else None,
|
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
|
}
|