172 lines
7.7 KiB
Python
172 lines
7.7 KiB
Python
from fastapi import HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
import logging
|
|
|
|
from job.assignment.models import ApplicationAssignments, JobAssignments
|
|
from job.assignment.serializers import serialize_application_assignment, serialize_job_assignment
|
|
from job.job_post.models import JobPosts
|
|
from role.models import EnumRoles, Roles
|
|
from users.models import Users
|
|
|
|
# job_assignments.assignment_role → the users.role that may hold it.
|
|
# primary_recruiter is swappable; hiring_manager is the requisition owner.
|
|
JOB_ASSIGNMENT_ROLES = {
|
|
"primary_recruiter": EnumRoles.RECRUITER,
|
|
"hiring_manager": EnumRoles.HIRING_MANAGER,
|
|
}
|
|
JOB_OWNER_COLUMN = {
|
|
"primary_recruiter": "current_recruiter_ids",
|
|
"hiring_manager": "hiring_manager_id",
|
|
}
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Assignment:
|
|
def __init__(self,session:AsyncSession):
|
|
self.session=session
|
|
|
|
async def require_role(self,user_id,role_enum,field_name):
|
|
role=await Roles.get_role_by_name(self.session,role_enum.value)
|
|
user=await Users.get_user_by_id(self.session,user_id)
|
|
if not role or not user or user.role_id!=role.id:
|
|
raise HTTPException(
|
|
status_code=422,
|
|
detail=f"{field_name} must be a {role_enum.value}",
|
|
)
|
|
if not user.is_active or user.is_deleted:
|
|
raise HTTPException(status_code=422,detail=f"{field_name} is not an active user")
|
|
return user
|
|
|
|
def _job_role(self,raw):
|
|
key=(raw or "primary_recruiter").strip()
|
|
if key=="recruiter":
|
|
key="primary_recruiter"
|
|
if key not in JOB_ASSIGNMENT_ROLES:
|
|
allowed=", ".join(sorted(JOB_ASSIGNMENT_ROLES))
|
|
raise HTTPException(
|
|
status_code=422,
|
|
detail=f"assignment_role must be one of {allowed}",
|
|
)
|
|
return key
|
|
|
|
async def record_job_owner(self,job_post_id,user_id,assignment_role,assigned_by):
|
|
"""Close the open interval of this role, then open a new one.
|
|
|
|
user_id None = unassign. No-ops when the same person already holds the
|
|
open interval. Does not touch job_posts columns.
|
|
"""
|
|
role=self._job_role(assignment_role)
|
|
job_uid=JobAssignments._as_uuid(job_post_id)
|
|
by_uid=JobAssignments._as_uuid(assigned_by)
|
|
if not job_uid or not by_uid:
|
|
raise HTTPException(status_code=422,detail="Invalid job_post_id or assigned_by")
|
|
current=await JobAssignments.fetch_by_job(
|
|
self.session,job_uid,current_only=True,assignment_role=role,
|
|
)
|
|
if user_id is None:
|
|
await JobAssignments.close_current(self.session,job_uid,role)
|
|
return None
|
|
user_uid=JobAssignments._as_uuid(user_id)
|
|
if not user_uid:
|
|
raise HTTPException(status_code=422,detail="Invalid user_id")
|
|
if current and str(current[0].user_id)==str(user_uid):
|
|
return current[0]
|
|
await JobAssignments.close_current(self.session,job_uid,role)
|
|
return await JobAssignments.insert_assignment(self.session,{
|
|
"job_post_id":job_uid,
|
|
"user_id":user_uid,
|
|
"assignment_role":role,
|
|
"assigned_by":by_uid,
|
|
})
|
|
|
|
async def record_job_recruiters(self,job_post_id,user_ids,assigned_by):
|
|
"""Keep open primary_recruiter intervals in sync with the JSON list."""
|
|
job_uid=JobAssignments._as_uuid(job_post_id)
|
|
by_uid=JobAssignments._as_uuid(assigned_by)
|
|
if not job_uid or not by_uid:
|
|
raise HTTPException(status_code=422,detail="Invalid job_post_id or assigned_by")
|
|
return await JobAssignments.sync_open(
|
|
self.session,job_uid,"primary_recruiter",user_ids,by_uid,
|
|
)
|
|
|
|
async def list_job_assignments(self,job_post_id,current_only=True,assignment_role=None):
|
|
if not job_post_id:
|
|
raise HTTPException(status_code=400,detail="job_post_id is required")
|
|
role=self._job_role(assignment_role) if assignment_role else None
|
|
rows=await JobAssignments.fetch_by_job(
|
|
self.session,job_post_id,current_only=current_only,assignment_role=role,
|
|
)
|
|
names=await Users.names_by_ids(
|
|
self.session,[r.user_id for r in rows]+[r.assigned_by for r in rows],
|
|
)
|
|
return [serialize_job_assignment(r,names=names) for r in rows]
|
|
|
|
async def create_job_assignment(self,payload,current_user):
|
|
user_id=payload.get("user_id")
|
|
job_post_id=payload.get("job_post_id")
|
|
if not user_id or not job_post_id:
|
|
raise HTTPException(status_code=422,detail="user_id and job_post_id are required")
|
|
job=await JobPosts.get_job_post_by_id(self.session,job_post_id)
|
|
if not job or job.is_deleted:
|
|
raise HTTPException(status_code=404,detail="Job post not found")
|
|
role=self._job_role(payload.get("assignment_role"))
|
|
await self.require_role(user_id,JOB_ASSIGNMENT_ROLES[role],"user_id")
|
|
assigned_by=current_user.get("id") if isinstance(current_user,dict) else None
|
|
row=await self.record_job_owner(job_post_id,user_id,role,assigned_by)
|
|
column=JOB_OWNER_COLUMN[role]
|
|
patch={column:user_id}
|
|
if role=="primary_recruiter":
|
|
patch["current_recruiter_ids"]=[str(user_id)]
|
|
updated=await JobPosts.update_job_post(self.session,job_post_id,patch)
|
|
if updated:
|
|
try:
|
|
from notifications.views import notify_job_assignment
|
|
label="hiring manager" if role=="hiring_manager" else "recruiter"
|
|
previous=(
|
|
[job.hiring_manager_id]
|
|
if role=="hiring_manager"
|
|
else JobPosts.recruiter_ids_of(job)
|
|
)
|
|
await notify_job_assignment(
|
|
self.session,updated,
|
|
role_label=label,
|
|
actor_id=assigned_by,
|
|
previous_ids=previous,
|
|
)
|
|
except Exception as exc:
|
|
logger.warning("notification insert skipped: %s", exc)
|
|
names=await Users.names_by_ids(
|
|
self.session,[row.user_id,row.assigned_by] if row else [],
|
|
)
|
|
return serialize_job_assignment(row,names=names) if row else None
|
|
|
|
async def list_application_assignments(self,inbox_id):
|
|
if inbox_id is None:
|
|
raise HTTPException(status_code=400,detail="inbox_id is required")
|
|
rows=await ApplicationAssignments.fetch_by_inbox(self.session,int(inbox_id))
|
|
names=await Users.names_by_ids(
|
|
self.session,[r.user_id for r in rows]+[r.assigned_by for r in rows],
|
|
)
|
|
return [serialize_application_assignment(r,names=names) for r in rows]
|
|
|
|
async def create_application_assignment(self,payload,current_user):
|
|
user_id=payload.get("user_id")
|
|
inbox_id=payload.get("inbox_id")
|
|
if not user_id or inbox_id is None:
|
|
raise HTTPException(status_code=422,detail="user_id and inbox_id are required")
|
|
await self.require_role(user_id,EnumRoles.RECRUITER,"user_id")
|
|
fields={
|
|
"inbox_id":int(inbox_id),
|
|
"user_id":ApplicationAssignments._as_uuid(user_id),
|
|
"assignment_role":payload.get("assignment_role") or "primary_recruiter",
|
|
"assigned_by":ApplicationAssignments._as_uuid(
|
|
current_user.get("id") if isinstance(current_user,dict) else None
|
|
),
|
|
}
|
|
if not fields["user_id"] or not fields["assigned_by"]:
|
|
raise HTTPException(status_code=422,detail="Invalid user_id or assigned_by")
|
|
row=await ApplicationAssignments.insert_assignment(self.session,fields)
|
|
names=await Users.names_by_ids(self.session,[row.user_id,row.assigned_by])
|
|
return serialize_application_assignment(row,names=names)
|