from inbox.models import Inbox from typing import Any,List,Dict from job.candidate.plugins import documents_from_message, source_from_message_to def _first_file_path(value): if not value: return None return str(value).split(",")[0].strip() or None from job.interviews.serializers import serialize_interview from job.activity.serializers import serialize_activity from job.feedback.serializers import serialize_feedback from job.job_post.serializers import serialize_job_post def serialize_candidate(row) -> dict: return { "id": str(row.id), "job_id": str(row.job_id), "source": row.source, "filename": row.filename, "file_path": row.file_path, "content_sha256": row.content_sha256, "candidate_email": row.candidate_email, "candidate_name": row.candidate_name, "job_title": row.job_title, "current_company": row.current_company, "years_experience": row.years_experience, "match_score": row.match_score, "matched_keywords": list(row.matched_keywords or []), "missing_keywords": list(row.missing_keywords or []), "summary_critique": row.summary_critique, "linkedin_url": row.linkedin_url or None, "status": row.status, "error_code": row.error_code, "error_message": row.error_message, "model": row.model, "created_by": str(row.created_by), "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_matching_candidate(row, job_post=None) -> Dict[str,Any]: """CV-bank origin row for Job Matching. assigned_job_post_id is job_posts.id.""" name=(row.candidate_name or "").strip() or (row.candidate_email or "").strip() or (row.file_name or "").strip() or "Unknown" job_payload=serialize_job_post(job_post) if job_post else None return { "id":str(row.id), "name":name, "email":(row.candidate_email or "").strip() or None, "file_name":(row.file_name or "").strip() or None, "file_path":(row.file_path or "").strip() or None, "resume_text":row.full_text or None, "linkedin_url":row.linkedin_url or None, "apply_via":row.apply_via, "status":row.status or None, "user_id":str(row.user_id) if row.user_id else None, "assigned_job_post_id":str(row.job_post_id) if row.job_post_id else None, "assigned_job_post":job_payload, "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_manual_upload_candidate(row) -> Dict[str,Any]: return { "id":str(row.id) if row.id else None, "candidate_email":row.candidate_email, "candidate_name":row.candidate_name, "candidate_phone":row.candidate_phone, "job_post_id":str(row.job_post_id) if row.job_post_id else None, "full_text":row.full_text, "linkedin_url":row.linkedin_url or None, "current_company":row.current_company, "current_position":row.current_position, "apply_via":row.apply_via, "user_id":str(row.user_id) if row.user_id else None, "platform":row.platform, "created_by":str(row.created_by) if row.created_by else None, "experience":row.experience, "status":row.status, "referral_by":row.referral_by, "file_name":row.file_name, "file_path":row.file_path, "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_candidate_profile( link:Inbox|List[Inbox]|Dict[str,Any]|List[Dict[str,Any]], *, detail:bool=False, ) -> Dict[str,Any]|List[Dict[str,Any]]: if isinstance(link,list): return [serialize_candidate_profile(item,detail=detail) for item in link] if isinstance(link,dict): return link user = link.user message = link.messages payload = { "inbox_id": link.id, "manual_upload_candidate_id": None, "user_id": str(link.user_id) if link.user_id else None, "candidate_id": None, "name": user.name if user else None, "email": user.email if user else None, "linkedin_url": (user.linkedin_url if user else None) or None, "is_active": user.is_active if user else None, "message_id": str(link.message_id) if link.message_id else None, "created_at": link.created_at.isoformat() if link.created_at else None, "application_status": message.application_status if message else None, "experience": message.experience if message else None, "current_employment": message.current_employment if message else None, "current_title": message.current_title if message else None, "resume_text": message.resume_text if message else None, "suggested_job_post_ids": list(message.suggested_job_post_ids or []) if message else [], "assigned_job_post_id": str(message.assigned_job_post_id) if message and message.assigned_job_post_id else None, "match_summary": message.match_summary if message else None, "match_reasoning": message.match_reasoning if message else None, "match_status": message.match_status if message else None, "match_error": message.match_error if message else None, "matched_at": message.matched_at.isoformat() if message and message.matched_at else None, "file_path": _first_file_path(message.file_path if message else None), "job_posts": [], } if not detail: return payload payload.update({ "favorite": link.favorite, "rating": link.rating, "phone": message.candidate_phone_number if message else None, "education": message.candidate_education if message else None, "currentCompany": message.current_employment if message else None, "current_title": message.current_title if message else None, "stage": message.application_status if message else None, "source": source_from_message_to(message.message_to if message else None), "applied": message.message_received_time if message else None, "documents": documents_from_message( message.file_name if message else None, message.file_path if message else None, ), "recruiter": None, "recruiter_id": None, "job_title": None, "ai_score": None, "recommendation": None, "sub_scores": None, "interviews": [serialize_interview(r) for r in (link.interviews or [])], "activity": [serialize_activity(r) for r in (link.activity or [])], "feedback": [serialize_feedback(r) for r in (link.feedback or [])], "notes": [], }) return payload def serialize_manual_candidate_profile(row, user, job_post) -> Dict[str, Any]: """Same key vocabulary as serialize_candidate_profile(detail=True). Manual uploads never go through inbox, so current_position maps onto current_title here and the agent/ATS fields stay empty. """ job_payload = serialize_job_post(job_post) if job_post else None created = row.created_at.isoformat() if row.created_at else None file_name = (row.file_name or "").strip() or None file_path = (row.file_path or "").strip() or None documents = [{"name": file_name or "", "path": file_path or ""}] if (file_name or file_path) else [] company = (row.current_company or "").strip() or None position = (row.current_position or "").strip() or None return { "inbox_id": None, "manual_upload_candidate_id": str(row.id), "user_id": str(user.id) if user else (str(row.user_id) if row.user_id else None), "candidate_id": None, "name": (user.name if user else None) or row.candidate_name or None, "email": (user.email if user else None) or row.candidate_email or None, "linkedin_url": (user.linkedin_url if user else None) or row.linkedin_url or None, "is_active": user.is_active if user else None, "message_id": None, "created_at": created, "application_status": row.status or None, "experience": (row.experience or "").strip() or None, "current_employment": company, "current_title": position, "resume_text": row.full_text or None, "suggested_job_post_ids": [], "assigned_job_post_id": str(row.job_post_id) if row.job_post_id else None, "match_summary": None, "match_reasoning": None, "match_status": None, "match_error": None, "matched_at": None, "job_posts": [job_payload] if job_payload else [], "favorite": None, "rating": None, "phone": (row.candidate_phone or "").strip() or None, "education": None, "currentCompany": company, "stage": row.status or None, "source": (row.platform or "").strip() or None, "applied": created, "file_path": file_path, "documents": documents, "recruiter": job_payload.get("created_by_name") if job_payload else None, "recruiter_id": job_payload.get("created_by") if job_payload else None, "job_title": job_payload.get("title") if job_payload else None, "ai_score": None, "recommendation": None, "sub_scores": None, "interviews": [], "activity": [], "feedback": [], "notes": [], "assigned_job_post": job_payload, "matched_keywords": [], "missing_keywords": [], "summary_critique": None, "scored_at": None, } def serialize_manager_candidate(row, *, source) -> dict: """One application on a hiring-manager's job — list row, not the profile.""" inbox_id = row.get("inbox_id") manual_id = row.get("id") if source == "manual" else None job_post_id = row.get("assigned_job_post_id") or row.get("job_post_id") user_id = row.get("user_id") ats = row.get("ats_result") or {} score = ats.get("overall_score") band = (ats.get("band") or "").strip() or None if score is not None and not band: band = "Strong Match" if score >= 82 else "Potential Match" if score >= 65 else "Weak Match" return { "id": user_id or (f"inbox:{inbox_id}" if inbox_id is not None else f"manual:{manual_id}"), "user_id": user_id, "name": row.get("name"), "email": row.get("email") or row.get("candidate_email"), "job_post_id": job_post_id, "job_title": row.get("title"), "application_status": row.get("application_status"), "inbox_id": inbox_id, "manual_upload_candidate_id": str(manual_id) if manual_id else None, "created_at": row.get("created_at"), "source": source, "ai_score": score, "recommendation": band, } def serialize_application_history_item(row) -> dict: """One prior application / score / sheet row for a reapplicant lookup.""" return { "source": row.get("source"), "inbox_id": row.get("inbox_id"), "message_id": row.get("message_id"), "manual_upload_candidate_id": row.get("manual_upload_candidate_id"), "form_data_id": row.get("form_data_id"), "candidate_id": row.get("candidate_id"), "job_post_id": row.get("job_post_id"), "job_title": row.get("job_title"), "status": row.get("status"), "applied_at": row.get("applied_at"), } def serialize_application_history(email, *, user=None, present_in=None, applications=None) -> dict: items = [serialize_application_history_item(row) for row in (applications or [])] found = bool(user or present_in or items) return { "email": email, "found": found, "present_in": list(present_in or []), "user": ( {"id": str(user.id), "name": user.name, "email": user.email} if user is not None else None ), "is_reapplicant": len(items) > 0, "applications": items, }