Edit_Job_Profile
parent
51bd8b56f2
commit
1fa25f2854
|
|
@ -1359,7 +1359,7 @@ class Inbox_Messages(SQLModel, table=True):
|
||||||
return row
|
return row
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def counts_by_job_post_ids(cls, session: AsyncSession, job_post_ids) -> dict[str, int]:
|
async def counts_by_job_post_ids(cls, session: AsyncSession, job_post_ids,search=None,top=None,limit=None) -> dict[str, int]:
|
||||||
"""Resolve {job_post_id: applicant_count} for a page of rows in a single query.
|
"""Resolve {job_post_id: applicant_count} for a page of rows in a single query.
|
||||||
|
|
||||||
Counts Inbox_Messages rows, not Inbox rows: one message fans out to several
|
Counts Inbox_Messages rows, not Inbox rows: one message fans out to several
|
||||||
|
|
@ -1373,6 +1373,12 @@ class Inbox_Messages(SQLModel, table=True):
|
||||||
.where(cls.assigned_job_post_id.in_(uids))
|
.where(cls.assigned_job_post_id.in_(uids))
|
||||||
.group_by(cls.assigned_job_post_id)
|
.group_by(cls.assigned_job_post_id)
|
||||||
)
|
)
|
||||||
|
if search:
|
||||||
|
result = result.where(cls.message_from.ilike(f"%{search}%"))
|
||||||
|
if top:
|
||||||
|
result = result.limit(top)
|
||||||
|
if limit:
|
||||||
|
result = result.limit(limit)
|
||||||
return {str(job_id): int(n) for job_id, n in result.all()}
|
return {str(job_id): int(n) for job_id, n in result.all()}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,6 @@ class JobUpdate(BaseModel):
|
||||||
experience_min: int | None = None
|
experience_min: int | None = None
|
||||||
experience_max: int | None = None
|
experience_max: int | None = None
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
current_recruiter_id: UUID | None = None
|
|
||||||
current_recruiter_ids: list[UUID] | None = None
|
current_recruiter_ids: list[UUID] | None = None
|
||||||
hiring_manager_id: UUID | None = None
|
hiring_manager_id: UUID | None = None
|
||||||
requisition_id: UUID | None = None
|
requisition_id: UUID | None = None
|
||||||
|
|
@ -987,6 +986,9 @@ async def fetch_jobs(
|
||||||
@router.get("/jobs/profile/fetch")
|
@router.get("/jobs/profile/fetch")
|
||||||
async def fetch_job_profile(
|
async def fetch_job_profile(
|
||||||
job_post_id: str = Query(...),
|
job_post_id: str = Query(...),
|
||||||
|
search: str | None = Query(None),
|
||||||
|
top: int | None = Query(None, ge=1),
|
||||||
|
limit: int | None = Query(None, ge=1),
|
||||||
current_user: dict = Depends(require_permission(PermissionTag.JOBS_VIEW)),
|
current_user: dict = Depends(require_permission(PermissionTag.JOBS_VIEW)),
|
||||||
session: AsyncSession = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
):
|
):
|
||||||
|
|
@ -994,7 +996,7 @@ async def fetch_job_profile(
|
||||||
per person, best score first) and the Suggested / Top Match header stats."""
|
per person, best score first) and the Suggested / Top Match header stats."""
|
||||||
try:
|
try:
|
||||||
service=JobPost(session=session)
|
service=JobPost(session=session)
|
||||||
data=await service.fetch_job_profile(job_post_id,current_user=current_user)
|
data=await service.fetch_job_profile(job_post_id,current_user=current_user,search=search,top=top,limit=limit)
|
||||||
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ JOB_ASSIGNMENT_ROLES = {
|
||||||
"hiring_manager": EnumRoles.HIRING_MANAGER,
|
"hiring_manager": EnumRoles.HIRING_MANAGER,
|
||||||
}
|
}
|
||||||
JOB_OWNER_COLUMN = {
|
JOB_OWNER_COLUMN = {
|
||||||
"primary_recruiter": "current_recruiter_id",
|
"primary_recruiter": "current_recruiter_ids",
|
||||||
"hiring_manager": "hiring_manager_id",
|
"hiring_manager": "hiring_manager_id",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1301,9 +1301,7 @@ class Interviews(SQLModel, table=True):
|
||||||
interview_type: str = Field(default="")
|
interview_type: str = Field(default="")
|
||||||
interview_status: str = Field(default="")
|
interview_status: str = Field(default="")
|
||||||
inbox_id: int | None = Field(default=None, foreign_key="inbox.id")
|
inbox_id: int | None = Field(default=None, foreign_key="inbox.id")
|
||||||
# Optional denorm so Recruiter Hub can join interviews → job_posts.current_recruiter_id
|
|
||||||
# without walking inbox. Filled on create from the application's assigned job;
|
|
||||||
# migration 011 added the columns. user_id is the candidate, not the recruiter.
|
|
||||||
user_id: uuid.UUID | None = Field(default=None, foreign_key="users.id")
|
user_id: uuid.UUID | None = Field(default=None, foreign_key="users.id")
|
||||||
job_post_id: uuid.UUID | None = Field(default=None, foreign_key="job_posts.id")
|
job_post_id: uuid.UUID | None = Field(default=None, foreign_key="job_posts.id")
|
||||||
graph_event_id: str | None = Field(default=None)
|
graph_event_id: str | None = Field(default=None)
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,7 @@ class JobPosts(SQLModel, table=True):
|
||||||
department: str = Field(default="", sa_column_kwargs={"server_default": ""})
|
department: str = Field(default="", sa_column_kwargs={"server_default": ""})
|
||||||
vacancies: int = Field(default=1, sa_column_kwargs={"server_default": "1"})
|
vacancies: int = Field(default=1, sa_column_kwargs={"server_default": "1"})
|
||||||
closed_at: datetime | None = Field(default=None, sa_type=DateTime(timezone=True))
|
closed_at: datetime | None = Field(default=None, sa_type=DateTime(timezone=True))
|
||||||
# Who is working the req now (swappable). History lives in job_assignments
|
|
||||||
# with assignment_role=primary_recruiter; this column is the first / primary
|
|
||||||
# pointer so existing joins keep working. current_recruiter_ids is the full
|
|
||||||
# list (UUID strings) so more than one recruiter can sit on the same job.
|
|
||||||
current_recruiter_id: uuid.UUID | None = Field(default=None, foreign_key="users.id")
|
|
||||||
current_recruiter_ids: list[str] = Field(
|
current_recruiter_ids: list[str] = Field(
|
||||||
default_factory=list, sa_type=JSONB, sa_column_kwargs={"server_default": "[]"},
|
default_factory=list, sa_type=JSONB, sa_column_kwargs={"server_default": "[]"},
|
||||||
)
|
)
|
||||||
|
|
@ -92,35 +88,35 @@ class JobPosts(SQLModel, table=True):
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
# @staticmethod
|
||||||
def recruiter_ids_of(row) -> list[str]:
|
# def recruiter_ids_of(row) -> list[str]:
|
||||||
"""UUID strings currently assigned as recruiters on a job row or mapping.
|
# """UUID strings currently assigned as recruiters on a job row or mapping.
|
||||||
|
|
||||||
Prefers current_recruiter_ids; falls back to current_recruiter_id so a
|
# Prefers current_recruiter_ids; falls back to current_recruiter_id so a
|
||||||
row that has not been backfilled still maps to one person.
|
# row that has not been backfilled still maps to one person.
|
||||||
"""
|
# """
|
||||||
if isinstance(row, dict):
|
# if isinstance(row, dict):
|
||||||
raw = row.get("current_recruiter_ids")
|
# raw = row.get("current_recruiter_ids")
|
||||||
fallback = row.get("current_recruiter_id")
|
# fallback = row.get("current_recruiter_id")
|
||||||
else:
|
# else:
|
||||||
raw = getattr(row, "current_recruiter_ids", None)
|
# raw = getattr(row, "current_recruiter_ids", None)
|
||||||
fallback = getattr(row, "current_recruiter_id", None)
|
# fallback = getattr(row, "current_recruiter_id", None)
|
||||||
out: list[str] = []
|
# out: list[str] = []
|
||||||
seen: set[str] = set()
|
# seen: set[str] = set()
|
||||||
for item in raw or []:
|
# for item in raw or []:
|
||||||
uid = JobPosts._as_uuid(item)
|
# uid = JobPosts._as_uuid(item)
|
||||||
if uid is None:
|
# if uid is None:
|
||||||
continue
|
# continue
|
||||||
key = str(uid)
|
# key = str(uid)
|
||||||
if key in seen:
|
# if key in seen:
|
||||||
continue
|
# continue
|
||||||
seen.add(key)
|
# seen.add(key)
|
||||||
out.append(key)
|
# out.append(key)
|
||||||
if not out:
|
# if not out:
|
||||||
uid = JobPosts._as_uuid(fallback)
|
# uid = JobPosts._as_uuid(fallback)
|
||||||
if uid is not None:
|
# if uid is not None:
|
||||||
out.append(str(uid))
|
# out.append(str(uid))
|
||||||
return out
|
# return out
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def has_recruiter(cls, recruiter_id):
|
def has_recruiter(cls, recruiter_id):
|
||||||
|
|
@ -129,7 +125,6 @@ class JobPosts(SQLModel, table=True):
|
||||||
if uid is None:
|
if uid is None:
|
||||||
return false()
|
return false()
|
||||||
return or_(
|
return or_(
|
||||||
cls.current_recruiter_id == uid,
|
|
||||||
cls.current_recruiter_ids.contains([str(uid)]),
|
cls.current_recruiter_ids.contains([str(uid)]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -137,7 +132,7 @@ class JobPosts(SQLModel, table=True):
|
||||||
def no_recruiters(cls):
|
def no_recruiters(cls):
|
||||||
"""SQL: neither the pointer nor the JSON list names anyone."""
|
"""SQL: neither the pointer nor the JSON list names anyone."""
|
||||||
return and_(
|
return and_(
|
||||||
cls.current_recruiter_id.is_(None),
|
cls.current_recruiter_ids.is_(None),
|
||||||
func.coalesce(func.jsonb_array_length(cls.current_recruiter_ids), 0) == 0,
|
func.coalesce(func.jsonb_array_length(cls.current_recruiter_ids), 0) == 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -146,7 +141,7 @@ class JobPosts(SQLModel, table=True):
|
||||||
uid = cls._as_uuid(record_id)
|
uid = cls._as_uuid(record_id)
|
||||||
if uid is None:
|
if uid is None:
|
||||||
return None
|
return None
|
||||||
result = await session.execute(select(cls).where(cls.id == uid))
|
result = await session.execute(select(cls).where(cls.id == uid).order_by(cls.created_at.desc(),cls.id.desc()))
|
||||||
return result.scalars().first()
|
return result.scalars().first()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -517,7 +512,6 @@ class JobPosts(SQLModel, table=True):
|
||||||
cls.department,
|
cls.department,
|
||||||
cls.location,
|
cls.location,
|
||||||
cls.requisition_status,
|
cls.requisition_status,
|
||||||
cls.current_recruiter_id,
|
|
||||||
cls.current_recruiter_ids,
|
cls.current_recruiter_ids,
|
||||||
cls.created_at,
|
cls.created_at,
|
||||||
Recruiter.name.label("recruiter_name"),
|
Recruiter.name.label("recruiter_name"),
|
||||||
|
|
@ -536,7 +530,7 @@ class JobPosts(SQLModel, table=True):
|
||||||
.select_from(cls)
|
.select_from(cls)
|
||||||
.outerjoin(stats, stats.c.job_post_id == cls.id)
|
.outerjoin(stats, stats.c.job_post_id == cls.id)
|
||||||
.outerjoin(reapplied_stats, reapplied_stats.c.job_post_id == cls.id)
|
.outerjoin(reapplied_stats, reapplied_stats.c.job_post_id == cls.id)
|
||||||
.outerjoin(Recruiter, Recruiter.id == cls.current_recruiter_id)
|
.outerjoin(Recruiter, Recruiter.id.in_(cls.current_recruiter_ids))
|
||||||
.where(cls.is_deleted == False) # noqa: E712
|
.where(cls.is_deleted == False) # noqa: E712
|
||||||
)
|
)
|
||||||
if active_only:
|
if active_only:
|
||||||
|
|
|
||||||
|
|
@ -20,19 +20,17 @@ def serialize_job_post_title(row) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _recruiter_payload(row, names=None):
|
# def _recruiter_payload(row, names=None):
|
||||||
"""List of recruiter ids plus mapped names; first id stays the legacy pointer."""
|
# """List of recruiter ids plus mapped names; first id stays the legacy pointer."""
|
||||||
names = names or {}
|
# names = names or []
|
||||||
ids = JobPosts.recruiter_ids_of(row)
|
# # ids = JobPosts.recruiter_ids_of(row)
|
||||||
mapped = [names.get(i) for i in ids]
|
# mapped = [names.get(i) for i in ids]
|
||||||
first = ids[0] if ids else None
|
# return {
|
||||||
return {
|
# "current_recruiter_ids": ids,
|
||||||
"current_recruiter_id": first,
|
# "recruiter_name": next((n for n in mapped if n), None),
|
||||||
"current_recruiter_ids": ids,
|
# "recruiter_names": [n for n in mapped if n],
|
||||||
"recruiter_name": next((n for n in mapped if n), None),
|
# "recruiters": [{"id": i, "name": names.get(i)} for i in ids],
|
||||||
"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:
|
def serialize_job_post(row, *, names=None) -> dict:
|
||||||
|
|
@ -77,9 +75,8 @@ def serialize_job_row(row, *, names=None, recruiter_name=None, hiring_manager_na
|
||||||
talent-pool filters key off it on attached job_posts.
|
talent-pool filters key off it on attached job_posts.
|
||||||
"""
|
"""
|
||||||
req = getattr(row, "requisition", None)
|
req = getattr(row, "requisition", None)
|
||||||
payload = _recruiter_payload(row, names)
|
recruiter_names=names.get("recruiters")
|
||||||
if recruiter_name and not payload["recruiter_name"]:
|
hiring_manager_name=names.get("hiring_manager",None)
|
||||||
payload["recruiter_name"] = recruiter_name
|
|
||||||
return {
|
return {
|
||||||
"id": str(row.id),
|
"id": str(row.id),
|
||||||
"title": row.title,
|
"title": row.title,
|
||||||
|
|
@ -89,8 +86,6 @@ def serialize_job_row(row, *, names=None, recruiter_name=None, hiring_manager_na
|
||||||
"employment_type": row.employment_type,
|
"employment_type": row.employment_type,
|
||||||
"vacancies": row.vacancies,
|
"vacancies": row.vacancies,
|
||||||
"platform": row.platform or None,
|
"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,
|
"requisition_status": row.requisition_status,
|
||||||
"status": row.status,
|
"status": row.status,
|
||||||
"experience_min": row.experience_min,
|
"experience_min": row.experience_min,
|
||||||
|
|
@ -101,9 +96,6 @@ def serialize_job_row(row, *, names=None, recruiter_name=None, hiring_manager_na
|
||||||
"description": row.description,
|
"description": row.description,
|
||||||
"is_active": row.is_active,
|
"is_active": row.is_active,
|
||||||
"closed_at": row.closed_at.isoformat() if row.closed_at else None,
|
"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_id": str(row.requisition_id) if getattr(row, "requisition_id", None) else None,
|
||||||
"requisition_title": req.position_title if req else None,
|
"requisition_title": req.position_title if req else None,
|
||||||
"requisition_department": req.department if req else None,
|
"requisition_department": req.department if req else None,
|
||||||
|
|
@ -112,6 +104,8 @@ def serialize_job_row(row, *, names=None, recruiter_name=None, hiring_manager_na
|
||||||
"created_by_name": row.user.name if getattr(row, "user", None) 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,
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
||||||
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
|
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
|
||||||
|
"recruiters": recruiter_names,
|
||||||
|
"hiring_manager": hiring_manager_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
from datetime import date, time
|
from datetime import date, time
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
@ -45,28 +48,14 @@ MAX_JOB_IMAGE_BYTES=5*1024*1024
|
||||||
|
|
||||||
|
|
||||||
def _payload_recruiter_ids(payload):
|
def _payload_recruiter_ids(payload):
|
||||||
"""Prefer current_recruiter_ids; fall back to current_recruiter_id. None = omitted."""
|
if payload.get("current_recruiter_ids"):
|
||||||
has_list="current_recruiter_ids" in payload and payload.get("current_recruiter_ids") is not None
|
raw=payload.get("current_recruiter_ids")
|
||||||
has_one="current_recruiter_id" in payload
|
return raw
|
||||||
if has_list:
|
|
||||||
raw=payload.get("current_recruiter_ids") or []
|
|
||||||
if not isinstance(raw,(list,tuple)):
|
|
||||||
raw=[raw]
|
|
||||||
ids=list(raw)
|
|
||||||
if not ids and has_one and payload.get("current_recruiter_id") not in (None,""):
|
|
||||||
ids=[payload.get("current_recruiter_id")]
|
|
||||||
return ids
|
|
||||||
if has_one:
|
|
||||||
raw=payload.get("current_recruiter_id")
|
|
||||||
return [] if raw in (None,"") else [raw]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _recruiter_fields(users):
|
def _recruiter_fields(users):
|
||||||
ids=[str(u.id) for u in users]
|
ids=[str(u.id) for u in users]
|
||||||
return {
|
return {
|
||||||
"current_recruiter_ids": ids,
|
"current_recruiter_ids": ids,
|
||||||
"current_recruiter_id": users[0].id if users else None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -97,7 +86,6 @@ class JobPostCreate(BaseModel):
|
||||||
scheduler_date: date | None = None
|
scheduler_date: date | None = None
|
||||||
due_at: str | None = None
|
due_at: str | None = None
|
||||||
hiring_manager_id: UUID | None = None
|
hiring_manager_id: UUID | None = None
|
||||||
current_recruiter_id: UUID | None = None
|
|
||||||
current_recruiter_ids: list[UUID] | None = None
|
current_recruiter_ids: list[UUID] | None = None
|
||||||
requisition_id: UUID | None = None
|
requisition_id: UUID | None = None
|
||||||
|
|
||||||
|
|
@ -423,7 +411,7 @@ class JobPost:
|
||||||
for r in rows
|
for r in rows
|
||||||
],total
|
],total
|
||||||
|
|
||||||
async def fetch_job_profile(self,job_post_id,current_user=None):
|
async def fetch_job_profile(self,job_post_id,current_user=None,search=None,top=None,limit=None):
|
||||||
"""Job profile page: the requisition row, its suggested candidates and the
|
"""Job profile page: the requisition row, its suggested candidates and the
|
||||||
Suggested / Top Match header stats — one round trip.
|
Suggested / Top Match header stats — one round trip.
|
||||||
|
|
||||||
|
|
@ -431,24 +419,29 @@ class JobPost:
|
||||||
and keywords come from that score's candidates row; a user- or form-identity
|
and keywords come from that score's candidates row; a user- or form-identity
|
||||||
score has none, so it borrows the newest completed row for the same email."""
|
score has none, so it borrows the newest completed row for the same email."""
|
||||||
uid=JobPosts._as_uuid(job_post_id)
|
uid=JobPosts._as_uuid(job_post_id)
|
||||||
|
|
||||||
if uid is None:
|
if uid is None:
|
||||||
raise HTTPException(status_code=422,detail="job_post_id must be a UUID")
|
raise HTTPException(status_code=422,detail="job_post_id must be a UUID")
|
||||||
|
|
||||||
|
# it's a system admin job profile page, so we don't need to restrict the ids
|
||||||
restrict=await self._restrict_ids_for_requisition_scope(current_user)
|
restrict=await self._restrict_ids_for_requisition_scope(current_user)
|
||||||
|
|
||||||
if restrict is not None and str(uid) not in {str(i) for i in restrict}:
|
if restrict is not None and str(uid) not in {str(i) for i in restrict}:
|
||||||
raise HTTPException(status_code=404,detail="Job post not found")
|
raise HTTPException(status_code=404,detail="Job post not found")
|
||||||
|
|
||||||
job=await JobPosts.get_job_post_by_id(self.session,uid)
|
job=await JobPosts.get_job_post_by_id(self.session,uid)
|
||||||
if not job or job.is_deleted:
|
if not job or job.is_deleted:
|
||||||
raise HTTPException(status_code=404,detail="Job post not found")
|
raise HTTPException(status_code=404,detail="Job post not found")
|
||||||
|
|
||||||
names=await Users.names_by_ids(
|
recruiter_lst=job.current_recruiter_ids or []
|
||||||
self.session,
|
|
||||||
JobPosts.recruiter_ids_of(job)+[job.hiring_manager_id],
|
recruiter_x_manager_names=await Users.names_by_ids(self.session,recruiter_lst,job.hiring_manager_id,search,top,limit)
|
||||||
)
|
|
||||||
counts=await Inbox_Messages.counts_by_job_post_ids(self.session,[job.id])
|
counts=await Inbox_Messages.counts_by_job_post_ids(self.session,[job.id],search,top,limit)
|
||||||
job_payload=serialize_job_row(
|
job_payload=serialize_job_row(
|
||||||
job,
|
job,
|
||||||
names=names,
|
names=recruiter_x_manager_names["recruiters"],
|
||||||
hiring_manager_name=names.get(str(job.hiring_manager_id)),
|
hiring_manager_name=recruiter_x_manager_names["hiring_manager"],
|
||||||
applicant_count=counts.get(str(job.id),0),
|
applicant_count=counts.get(str(job.id),0),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -216,9 +216,6 @@ async def job_recruiter_ids(session, job):
|
||||||
ids = set()
|
ids = set()
|
||||||
if job is None:
|
if job is None:
|
||||||
return ids
|
return ids
|
||||||
uid = _as_uuid(getattr(job, "current_recruiter_id", None))
|
|
||||||
if uid is not None:
|
|
||||||
ids.add(uid)
|
|
||||||
for raw in getattr(job, "current_recruiter_ids", None) or []:
|
for raw in getattr(job, "current_recruiter_ids", None) or []:
|
||||||
extra = _as_uuid(raw)
|
extra = _as_uuid(raw)
|
||||||
if extra is not None:
|
if extra is not None:
|
||||||
|
|
|
||||||
|
|
@ -154,20 +154,37 @@ class Users(SQLModel, table=True):
|
||||||
return [row[0] for row in result.all()]
|
return [row[0] for row in result.all()]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def names_by_ids(cls, session: AsyncSession, user_ids) -> dict[str, str]:
|
async def names_by_ids(cls, session: AsyncSession, user_ids,manager_id=None,search=None,top=None,limit=None) -> dict[str, str]:
|
||||||
"""Resolve {user_id: name} in a single query.
|
|
||||||
|
|
||||||
COLUMN select, not the Users entity: `select(cls)` would pull the five
|
data = {"recruiters": {}, "hiring_manager": {}}
|
||||||
selectin relations (role, job_posts, inbox, feedback, notes) for a
|
|
||||||
two-column lookup.
|
|
||||||
"""
|
|
||||||
uids = {u for u in (user_ids or []) if u}
|
uids = {u for u in (user_ids or []) if u}
|
||||||
if not uids:
|
if not uids:
|
||||||
return {}
|
return {}
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(cls.id, cls.name).where(cls.id.in_(uids))
|
select(cls.id, cls.name).where(cls.id.in_(uids))
|
||||||
)
|
)
|
||||||
return {str(uid): name for uid, name in result.all()}
|
if search:
|
||||||
|
result = result.where(cls.name.ilike(f"%{search}%"))
|
||||||
|
if top:
|
||||||
|
result = result.limit(top)
|
||||||
|
if limit:
|
||||||
|
result = result.limit(limit)
|
||||||
|
|
||||||
|
recruiter_result=result.all()
|
||||||
|
data["recruiters"] = {str(uid): name for uid, name in recruiter_result}
|
||||||
|
if manager_id:
|
||||||
|
result=await session.execute(
|
||||||
|
select(cls.id, cls.name).where(cls.id==manager_id,cls.role_id==4,cls.is_deleted==False)
|
||||||
|
)
|
||||||
|
if search:
|
||||||
|
result = result.where(cls.name.ilike(f"%{search}%"))
|
||||||
|
if top:
|
||||||
|
result = result.limit(top)
|
||||||
|
if limit:
|
||||||
|
result = result.limit(limit)
|
||||||
|
manager_result=result.all()
|
||||||
|
data["hiring_manager"] = {str(uid): name for uid, name in manager_result}
|
||||||
|
return data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_by_ids(cls, session: AsyncSession, ids):
|
async def get_by_ids(cls, session: AsyncSession, ids):
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,24 @@ function experienceLabel(min, max) {
|
||||||
return `${min ?? max}+ years`
|
return `${min ?? max}+ years`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Job ownership arrives in one of two shapes. serialize_job_row (GET /jobs/fetch,
|
||||||
|
GET /jobs/profile/fetch) sends the two roles as separate {id: name} objects:
|
||||||
|
|
||||||
|
"recruiters": {"ed9e…": "Nida Khan"}, "hiring_manager": {"77aa…": "Amara Osei"}
|
||||||
|
|
||||||
|
serialize_job_post — the inbox / matching / picker payload — still sends the
|
||||||
|
older flat keys (current_recruiter_ids, recruiter_names, recruiters as an
|
||||||
|
ARRAY of {id, name}, hiring_manager_id / hiring_manager_name). Both are read
|
||||||
|
here so one mapper serves every caller; the Array check is what tells the two
|
||||||
|
`recruiters` shapes apart. */
|
||||||
|
function recruiterMap(row) {
|
||||||
|
const value = row?.recruiters
|
||||||
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : null
|
||||||
|
}
|
||||||
|
|
||||||
function recruiterIdsFrom(row) {
|
function recruiterIdsFrom(row) {
|
||||||
|
const map = recruiterMap(row)
|
||||||
|
if (map) return Object.keys(map).map(String)
|
||||||
const ids = Array.isArray(row?.current_recruiter_ids)
|
const ids = Array.isArray(row?.current_recruiter_ids)
|
||||||
? row.current_recruiter_ids.filter(Boolean).map(String)
|
? row.current_recruiter_ids.filter(Boolean).map(String)
|
||||||
: []
|
: []
|
||||||
|
|
@ -73,6 +90,8 @@ function recruiterIdsFrom(row) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function recruiterNamesFrom(row) {
|
function recruiterNamesFrom(row) {
|
||||||
|
const map = recruiterMap(row)
|
||||||
|
if (map) return Object.values(map).filter(Boolean)
|
||||||
if (Array.isArray(row?.recruiter_names) && row.recruiter_names.length) {
|
if (Array.isArray(row?.recruiter_names) && row.recruiter_names.length) {
|
||||||
return row.recruiter_names.filter(Boolean)
|
return row.recruiter_names.filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
@ -82,10 +101,24 @@ function recruiterNamesFrom(row) {
|
||||||
return row?.recruiter_name ? [row.recruiter_name] : []
|
return row?.recruiter_name ? [row.recruiter_name] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {id, name} of the hiring manager, from either payload shape. */
|
||||||
|
function hiringManagerFrom(row) {
|
||||||
|
const value = row?.hiring_manager
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
const [id, name] = Object.entries(value)[0] ?? []
|
||||||
|
if (id) return { id: String(id), name: name ?? null }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: row?.hiring_manager_id ? String(row.hiring_manager_id) : null,
|
||||||
|
name: row?.hiring_manager_name ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** API row -> what the Jobs table and detail modal render. */
|
/** API row -> what the Jobs table and detail modal render. */
|
||||||
export function toJobView(row) {
|
export function toJobView(row) {
|
||||||
const recruiterIds = recruiterIdsFrom(row)
|
const recruiterIds = recruiterIdsFrom(row)
|
||||||
const recruiterNames = recruiterNamesFrom(row)
|
const recruiterNames = recruiterNamesFrom(row)
|
||||||
|
const manager = hiringManagerFrom(row)
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
title: row.title,
|
title: row.title,
|
||||||
|
|
@ -101,8 +134,8 @@ export function toJobView(row) {
|
||||||
recruiterId: recruiterIds[0] || null,
|
recruiterId: recruiterIds[0] || null,
|
||||||
recruiterIds,
|
recruiterIds,
|
||||||
recruiterNames,
|
recruiterNames,
|
||||||
hiringManager: row.hiring_manager_name,
|
hiringManager: manager.name,
|
||||||
hiringManagerId: row.hiring_manager_id,
|
hiringManagerId: manager.id,
|
||||||
createdByName: row.created_by_name,
|
createdByName: row.created_by_name,
|
||||||
applicantCount: row.applicant_count ?? 0,
|
applicantCount: row.applicant_count ?? 0,
|
||||||
// A real Date, so the existing `sortValue: j => j.created.getTime()` keeps working.
|
// A real Date, so the existing `sortValue: j => j.created.getTime()` keeps working.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue