HR-ATS-Portal/backend/job/job_post/views.py

135 lines
5.1 KiB
Python

from datetime import date, time
import os
import httpx
from dotenv import load_dotenv
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, model_validator
from job.job_post.models import JobPosts
from job.job_post.plugins import (
BufferError,
create_buffer_post,
list_buffer_channels,
local_status,
normalize_platform,
parse_buffer_datetime,
render_job_post,
resolve_channel,
)
from job.job_post.serializers import serialize_job_post
load_dotenv()
class JobPostCreate(BaseModel):
title: str
experience_min: int | None = None
experience_max: int | None = None
requirements: list[str] = []
optional_skills: list[str] = []
salary: str = "Anonymous"
location: str | None = None
employment_type: str | None = None
platform: str = "linkedin"
description: str | None = None
platform: str | None = None
channel_id: str | None = None
mode: str = "addToQueue"
scheduler_time: time | None = time(0, 0, 0)
scheduler_date: date | None = None
due_at: str | None = None
@model_validator(mode="after")
def validate_mode_and_due_at(self):
allowed = {"addToQueue", "shareNow", "customScheduled"}
if self.mode not in allowed:
raise ValueError(f"mode must be one of {sorted(allowed)}")
if self.mode == "customScheduled" and not self.due_at:
raise ValueError("due_at is required when mode is customScheduled")
return self
class JobPost:
def __init__(self,session:AsyncSession):
self.session=session
self.buffer_api=os.getenv("BUFFER_API")
self.channel_id=os.getenv("BUFFER_CHANNEL_ID")
async def _resolve_target(self,payload):
"""Pick the Buffer channel to post to, and the service it belongs to.
Precedence: an explicit channel_id, then the requested platform, then the
configured default channel. Returns (channel_id, service) where service is
None if we did not have to look the channel up.
"""
if payload.get("channel_id"):
return payload["channel_id"],None
if payload.get("platform"):
channel=await resolve_channel(payload["platform"])
return channel["id"],channel.get("service")
if self.channel_id:
return self.channel_id,None
raise HTTPException(
status_code=400,
detail="Provide channel_id or platform, or configure BUFFER_CHANNEL_ID",
)
async def post_job(self,payload,current_user):
try:
channel_id,service=await self._resolve_target(payload)
except (httpx.HTTPError,BufferError,RuntimeError) as e:
raise HTTPException(status_code=502,detail=f"Failed to resolve Buffer channel: {e}") from e
text=render_job_post(payload)
fields={
"title":payload.get("title"),
"employment_type":payload.get("employment_type"),
"location":payload.get("location"),
"experience_min":payload.get("experience_min"),
"experience_max":payload.get("experience_max"),
"requirements":list(payload.get("requirements") or []),
"optional_skills":list(payload.get("optional_skills") or []),
"salary":payload.get("salary") or "Anonymous",
"description":payload.get("description"),
"post_text":text,
"channel_id":channel_id,
"status":"draft",
"created_by":current_user["id"],
}
# Only set platform when it is actually known: passing None would override the
# column default and break the NOT NULL constraint. Buffer's channelService
# replaces this with the authoritative value once the post is created.
known_platform=service or normalize_platform(payload.get("platform"))
if known_platform:
fields["platform"]=known_platform
row=await JobPosts.insert_job_post(self.session,fields)
try:
post=await create_buffer_post(
text,
channel_id,
mode=payload.get("mode") or "addToQueue",
due_at=payload.get("due_at"),
)
except (httpx.HTTPError,BufferError,RuntimeError) as e:
await JobPosts.mark_failed(self.session,str(row.id),str(e))
raise HTTPException(status_code=502,detail=f"Failed to publish job post to Buffer: {e}") from e
saved=await JobPosts.mark_buffer_result(
self.session,
str(row.id),
buffer_post_id=post["id"],
status=local_status(post.get("status")),
external_link=post.get("externalLink"),
sent_at=parse_buffer_datetime(post.get("sentAt")),
platform=post.get("channelService"),
)
return serialize_job_post(saved)
async def list_channels(self):
try:
return await list_buffer_channels()
except (httpx.HTTPError,BufferError,RuntimeError) as e:
raise HTTPException(status_code=502,detail="Failed to list Buffer channels") from e