215 lines
8.8 KiB
Python
215 lines
8.8 KiB
Python
from pathlib import Path
|
|
|
|
from inbox.models import Inbox_Message_Triage, Inbox_Messages
|
|
|
|
# match_status (inbox/tasks.py) -> the resume badge the inbox tabs render.
|
|
_RESUME_STATUS = {
|
|
"processing": "Parsing",
|
|
"matched": "Parsed",
|
|
"no_text": "Failed",
|
|
"failed": "Failed",
|
|
"dlq": "Failed",
|
|
"skipped": "Pending",
|
|
}
|
|
|
|
|
|
def _sender_name(message: Inbox_Messages, *, light: bool = False) -> str:
|
|
"""Graph's display name when the payload carries one, else the raw address.
|
|
|
|
`light` must not touch `full_email_response` — the list query defers that
|
|
column, and reading it here would lazy-load the whole Graph payload per row.
|
|
"""
|
|
raw = message.message_from or ""
|
|
if light:
|
|
# "Jane Doe <jane@x.com>" → Jane Doe; otherwise the address as stored.
|
|
if "<" in raw and raw.endswith(">"):
|
|
return raw.split("<", 1)[0].strip() or raw
|
|
return raw
|
|
sender_name = raw
|
|
full = message.full_email_response
|
|
if isinstance(full, dict):
|
|
from_block = full.get("from")
|
|
if isinstance(from_block, dict):
|
|
email_address = from_block.get("emailAddress")
|
|
if isinstance(email_address, dict):
|
|
name = email_address.get("name")
|
|
if name:
|
|
sender_name = name
|
|
return sender_name
|
|
|
|
|
|
def _attachment_name(message: Inbox_Messages) -> str | None:
|
|
if message.file_name:
|
|
return message.file_name.split(",")[0].strip() or None
|
|
if message.file_path:
|
|
return Path(message.file_path.split(",")[0].strip()).name or None
|
|
return None
|
|
|
|
|
|
def serialize_message(message: Inbox_Messages, *, linkedin_url=None) -> dict:
|
|
"""inbox_messages row -> the shape the #inbox Email tab renders."""
|
|
sender_name = _sender_name(message)
|
|
attachment_name = _attachment_name(message)
|
|
|
|
return {
|
|
"id": str(message.id),
|
|
"message_id": str(message.message_id) if message.message_id else None,
|
|
"full_email_response": message.full_email_response,
|
|
"sender_name": sender_name,
|
|
"fromEmail": message.message_from,
|
|
"subject": message.message_subject,
|
|
"body": message.message_body,
|
|
"when": message.message_received_time,
|
|
"received": message.message_received_time,
|
|
"created_at": message.created_at.isoformat() if message.created_at else None,
|
|
"unread": not message.message_read,
|
|
"attachment": message.attachment,
|
|
"attachment_name": attachment_name,
|
|
"file_name": message.file_name,
|
|
"message_to": message.message_to,
|
|
"message_cc": message.message_cc,
|
|
"message_bcc": message.message_bcc,
|
|
"message_sent_time": message.message_sent_time,
|
|
"message_reply": message.message_reply,
|
|
"file_path": message.file_path,
|
|
"linkedin_slug": message.linkedin_slug or None,
|
|
"linkedin_url": linkedin_url or None,
|
|
"suggested_job_post_ids": list(message.suggested_job_post_ids or []),
|
|
"assigned_job_post_id": str(message.assigned_job_post_id) if message.assigned_job_post_id else None,
|
|
"match_summary": message.match_summary,
|
|
"match_reasoning": message.match_reasoning,
|
|
"match_status": message.match_status,
|
|
"match_error": message.match_error,
|
|
"matched_at": message.matched_at.isoformat() if message.matched_at else None,
|
|
"resume_text": message.resume_text,
|
|
"ats_score": message.ats_score,
|
|
"ats_band": message.ats_band or None,
|
|
}
|
|
|
|
|
|
def serialize_ats_result(row) -> dict:
|
|
"""One inbox ats_results row — same keys the Sheet Forms cards paint."""
|
|
return {
|
|
"job_post_id": str(row.job_post_id) if row.job_post_id else None,
|
|
"overall_score": row.overall_score,
|
|
"band": row.band or None,
|
|
"computed_at": row.computed_at.isoformat() if row.computed_at else None,
|
|
}
|
|
|
|
|
|
_PROCESSING_LABEL = {
|
|
"unread": "Unread",
|
|
"imported": "Imported",
|
|
"processed": "Processed",
|
|
"rejected": "Rejected",
|
|
}
|
|
|
|
|
|
def serialize_application(message: Inbox_Messages, *, linkedin_url=None, light: bool = False) -> dict:
|
|
"""inbox_messages row -> the shape the #inbox All Applications tab renders.
|
|
|
|
`position` is the mail subject and `source` is the To address, which is where
|
|
the board tag (Rozee, Mustakbil, Employee Referral, ...) lands.
|
|
|
|
The tab also wants ats_score, phone, experience, recruiter, duplicate and a
|
|
processing state beyond read/unread. `processing` prefers imported /
|
|
processed / rejected from the processing_state column so those writes are
|
|
visible; the default `unread` state still follows message_read so existing
|
|
rows keep Read/Unread until someone PATCHes a later state.
|
|
|
|
`light=True` is the list path: skip resume_text / match_reasoning /
|
|
Graph-payload name lookup so we never touch columns the list query defers.
|
|
"""
|
|
state = (message.processing_state or "").strip().lower()
|
|
if state in ("imported", "processed", "rejected"):
|
|
processing = _PROCESSING_LABEL[state]
|
|
else:
|
|
processing = "Read" if message.message_read else "Unread"
|
|
payload = {
|
|
"id": str(message.id),
|
|
"message_id": str(message.message_id) if message.message_id else None,
|
|
"name": _sender_name(message, light=light),
|
|
"email": message.message_from,
|
|
"position": message.message_subject,
|
|
"source": message.message_to,
|
|
"received": message.message_received_time,
|
|
"created_at": message.created_at.isoformat() if message.created_at else None,
|
|
"unread": not message.message_read,
|
|
"processing": processing,
|
|
"application_status": message.application_status,
|
|
"resume_status": _RESUME_STATUS.get(message.match_status, "Pending"),
|
|
"attachment": _attachment_name(message),
|
|
"has_attachment": message.attachment,
|
|
"file_path": message.file_path,
|
|
"linkedin_slug": message.linkedin_slug or None,
|
|
"linkedin_url": linkedin_url or None,
|
|
"suggested_job_post_ids": list(message.suggested_job_post_ids or []),
|
|
"assigned_job_post_id": str(message.assigned_job_post_id) if message.assigned_job_post_id else None,
|
|
"match_summary": message.match_summary,
|
|
"match_status": message.match_status,
|
|
"match_error": message.match_error,
|
|
"matched_at": message.matched_at.isoformat() if message.matched_at else None,
|
|
"ats_score": message.ats_score,
|
|
"ats_band": message.ats_band or None,
|
|
"phone": message.candidate_phone_number,
|
|
"experience": message.experience or "",
|
|
"current_employment": message.current_employment or "",
|
|
"current_title": message.current_title or "",
|
|
"city": message.city or None,
|
|
"recruiter": str(message.recruiter_id) if message.recruiter_id else None,
|
|
"duplicate": message.is_duplicate,
|
|
"processing_state": message.processing_state,
|
|
"source_channel_id": message.source_channel_id,
|
|
}
|
|
if not light:
|
|
payload["resume_text"] = message.resume_text
|
|
payload["match_reasoning"] = message.match_reasoning
|
|
return payload
|
|
|
|
|
|
def serialize_triage(row: Inbox_Message_Triage) -> dict:
|
|
"""inbox_message_triage row -> the intake gate's review shape.
|
|
|
|
No body field exists to expose: the gate stores the verdict, never the mail. A
|
|
reviewer opens the original from the mailbox, or overturns the verdict and lets the
|
|
normal ingestion path re-fetch it.
|
|
"""
|
|
return {
|
|
"id": str(row.id),
|
|
"message_id": row.message_id,
|
|
"is_application": row.is_application,
|
|
"reason_code": row.reason_code,
|
|
"confidence": row.confidence,
|
|
"evidence": row.evidence,
|
|
"status": row.status,
|
|
"error": row.error,
|
|
"model_name": row.model_name or None,
|
|
"subject": row.message_subject,
|
|
"fromEmail": row.message_from,
|
|
"when": row.message_received_time,
|
|
"attachment": row.file_name or None,
|
|
"has_attachment": row.attachment,
|
|
"ingested": row.ingested,
|
|
"overridden_by": str(row.overridden_by_id) if row.overridden_by_id else None,
|
|
"overridden_at": row.overridden_at.isoformat() if row.overridden_at else None,
|
|
"classified_at": row.classified_at.isoformat() if row.classified_at else None,
|
|
}
|
|
|
|
|
|
def serialize_mailbox_sync_run(row) -> dict:
|
|
return {
|
|
"id": str(row.id),
|
|
"status": row.status,
|
|
"task_id": row.task_id,
|
|
"created_by": str(row.created_by) if row.created_by else None,
|
|
"top": row.top,
|
|
"skip": row.skip,
|
|
"test_on": row.test_on,
|
|
"triage": row.triage,
|
|
"entries": row.entries or [],
|
|
"error": row.error,
|
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
|
"started_at": row.started_at.isoformat() if row.started_at else None,
|
|
"finished_at": row.finished_at.isoformat() if row.finished_at else None,
|
|
}
|