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, }