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