128 lines
4.7 KiB
Python
128 lines
4.7 KiB
Python
"""Flags sourced LinkedIn profiles that are already applicants in the ATS.
|
|
|
|
A sourced profile and a CV describe the same person when they carry the same
|
|
/in/<slug>. The slug is persisted on application rows as the CV is processed
|
|
(inbox_messages.linkedin_slug, manual_upload_candidate.linkedin_slug); rows
|
|
that predate those columns are backfilled lazily here in bounded batches, so
|
|
the matching converges over normal use without a migration script.
|
|
|
|
The annotation rides on the profile list/detail payloads as `already_applied`:
|
|
|
|
{"source": "inbox"|"manual", "status", "job_post_id", "candidate",
|
|
"applied_at", "same_job": bool, "applications": N} # or null
|
|
|
|
When the person applied to several jobs, the application for the profile's own
|
|
job wins the summary slot and `same_job` says which case the UI is looking at.
|
|
"""
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from inbox.models import Inbox_Messages
|
|
from job.candidate.models import Manual_UPLOAD_CANDIDATE
|
|
from linkedin_utils import primary_slug_from_text, slug_from_url
|
|
|
|
BACKFILL_BATCH = 200
|
|
|
|
|
|
async def _backfill_slugs(session: AsyncSession) -> None:
|
|
"""Scan a bounded batch of never-scanned CVs (linkedin_slug IS NULL)."""
|
|
changed = False
|
|
inbox_q = (
|
|
select(Inbox_Messages)
|
|
.where(
|
|
Inbox_Messages.linkedin_slug.is_(None),
|
|
Inbox_Messages.resume_text.is_not(None),
|
|
Inbox_Messages.resume_text != "",
|
|
)
|
|
.limit(BACKFILL_BATCH)
|
|
)
|
|
for row in (await session.execute(inbox_q)).scalars().all():
|
|
haystack = row.resume_text or ""
|
|
if row.message_body:
|
|
haystack = f"{haystack}\n{row.message_body}"
|
|
row.linkedin_slug = primary_slug_from_text(haystack)
|
|
session.add(row)
|
|
changed = True
|
|
|
|
manual_q = (
|
|
select(Manual_UPLOAD_CANDIDATE)
|
|
.where(
|
|
Manual_UPLOAD_CANDIDATE.linkedin_slug.is_(None),
|
|
Manual_UPLOAD_CANDIDATE.full_text != "",
|
|
)
|
|
.limit(BACKFILL_BATCH)
|
|
)
|
|
for row in (await session.execute(manual_q)).scalars().all():
|
|
row.linkedin_slug = primary_slug_from_text(row.full_text)
|
|
session.add(row)
|
|
changed = True
|
|
|
|
if changed:
|
|
await session.commit()
|
|
|
|
|
|
async def annotate_applications(session: AsyncSession, profiles: list[dict]) -> list[dict]:
|
|
"""Attach `already_applied` to serialized profile dicts, matched by slug."""
|
|
for profile in profiles:
|
|
profile["already_applied"] = None
|
|
|
|
slug_map: dict[str, list[dict]] = {}
|
|
for profile in profiles:
|
|
slug = slug_from_url(profile.get("linkedin_url"))
|
|
if slug:
|
|
slug_map.setdefault(slug, []).append(profile)
|
|
if not slug_map:
|
|
return profiles
|
|
|
|
await _backfill_slugs(session)
|
|
|
|
matches: dict[str, list[dict]] = {}
|
|
|
|
inbox_q = select(
|
|
Inbox_Messages.linkedin_slug,
|
|
Inbox_Messages.application_status,
|
|
Inbox_Messages.assigned_job_post_id,
|
|
Inbox_Messages.message_from,
|
|
Inbox_Messages.created_at,
|
|
).where(Inbox_Messages.linkedin_slug.in_(list(slug_map)))
|
|
for slug, status, job_id, sender, created in (await session.execute(inbox_q)).all():
|
|
matches.setdefault(slug, []).append({
|
|
"source": "inbox",
|
|
"status": (getattr(status, "value", status) or None),
|
|
"job_post_id": str(job_id) if job_id else None,
|
|
"candidate": sender or None,
|
|
"applied_at": created.isoformat() if created else None,
|
|
})
|
|
|
|
manual_q = select(
|
|
Manual_UPLOAD_CANDIDATE.linkedin_slug,
|
|
Manual_UPLOAD_CANDIDATE.status,
|
|
Manual_UPLOAD_CANDIDATE.job_post_id,
|
|
Manual_UPLOAD_CANDIDATE.candidate_name,
|
|
Manual_UPLOAD_CANDIDATE.created_at,
|
|
).where(Manual_UPLOAD_CANDIDATE.linkedin_slug.in_(list(slug_map)))
|
|
for slug, status, job_id, name, created in (await session.execute(manual_q)).all():
|
|
matches.setdefault(slug, []).append({
|
|
"source": "manual",
|
|
"status": (status or "").strip() or None,
|
|
"job_post_id": str(job_id) if job_id else None,
|
|
"candidate": (name or "").strip() or None,
|
|
"applied_at": created.isoformat() if created else None,
|
|
})
|
|
|
|
for slug, slug_profiles in slug_map.items():
|
|
found = matches.get(slug)
|
|
if not found:
|
|
continue
|
|
for profile in slug_profiles:
|
|
job_id = profile.get("job_post_id")
|
|
same = [m for m in found if m["job_post_id"] and m["job_post_id"] == job_id]
|
|
best = same[0] if same else found[0]
|
|
profile["already_applied"] = {
|
|
**best,
|
|
"same_job": bool(same),
|
|
"applications": len(found),
|
|
}
|
|
return profiles
|