Api for @router.post("/job/post-job")

done
pull/2/head
ahmed.mujtaba 2026-08-05 20:41:39 +05:00
parent c79024160c
commit f884ae1d1d
3 changed files with 107 additions and 6 deletions

View File

@ -6,6 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, model_validator
from users.permissions import PermissionTag, require_permission
from job.job_post.views import JobPost
from job.job_post.plugins import PlatformAlias
from dotenv import load_dotenv
load_dotenv()
@ -21,7 +22,9 @@ class JobPostCreate(BaseModel):
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"
due_at: str | None = None
@ -35,6 +38,16 @@ class JobPostCreate(BaseModel):
raise ValueError("due_at is required when mode is customScheduled")
return self
@router.get("/jobs/alias")
async def get_job_alias():
try:
alias_lst=[k.name for k in PlatformAlias]
return JSONResponse(content={"data":alias_lst,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/candidate/cv_upload")
async def cv_upload(

View File

@ -9,6 +9,7 @@ import json
import os
import re
from datetime import datetime
from enum import Enum
import httpx
from dotenv import load_dotenv
@ -39,6 +40,61 @@ def local_status(buffer_status) -> str:
return BUFFER_STATUS_TO_LOCAL.get(buffer_status or "", "scheduled")
class PlatformAlias(str, Enum):
"""Shorthands people type, mapped to Buffer's own `Service` values.
Member name = what arrives in the request, value = what Buffer calls it. This is
spelling tolerance only, *not* the list of supported networks: anything absent here
still resolves, because `resolve_channel` matches against the services Buffer
actually reports. A newly connected network needs no entry.
Names that share a value (ig/insta) become Enum aliases, which is exactly the
intent -- lookup is by member name via `__members__`.
"""
fb = "facebook"
ig = "instagram"
insta = "instagram"
li = "linkedin"
x = "twitter"
tweet = "twitter"
yt = "youtube"
gbp = "googlebusiness"
google = "googlebusiness"
googlebusinessprofile = "googlebusiness"
def normalize_platform(value) -> str:
"""Case/punctuation-insensitive key for comparing a requested platform."""
key = re.sub(r"[^a-z0-9]", "", str(value or "").lower())
alias = PlatformAlias.__members__.get(key)
return alias.value if alias else key
async def resolve_channel(platform, channels=None) -> dict:
"""Return the connected channel for `platform`.
Nothing is special-cased per network: the request is matched against the services
Buffer reports, so any platform Buffer supports and the account has connected will
resolve. Falls back to matching the channel's handle or display name so
"ahmedmujtababaig" works as well as "linkedin".
"""
wanted = normalize_platform(platform)
if not wanted:
raise BufferError("No platform given")
if channels is None:
channels = await list_buffer_channels()
for field in ("service", "name", "displayName"):
for channel in channels:
if normalize_platform(channel.get(field)) == wanted:
return channel
available = sorted({c.get("service") for c in channels if c.get("service")})
raise BufferError(
f"No Buffer channel connected for platform {platform!r}. "
f"Connected: {', '.join(available) if available else 'none'}"
)
def parse_buffer_datetime(value):
"""Buffer sends ISO 8601 with a trailing `Z`, which fromisoformat wants as +00:00."""
if not value:

View File

@ -11,8 +11,10 @@ from job.job_post.plugins import (
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
@ -25,13 +27,33 @@ class JobPost:
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):
channel_id=payload.get("channel_id") or self.channel_id
if not channel_id:
raise HTTPException(status_code=500,detail="BUFFER_CHANNEL_ID is not configured")
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)
row=await JobPosts.insert_job_post(self.session,{
fields={
"title":payload.get("title"),
"employment_type":payload.get("employment_type"),
"location":payload.get("location"),
@ -45,7 +67,14 @@ class JobPost:
"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(
@ -55,8 +84,11 @@ class JobPost:
due_at=payload.get("due_at"),
)
except (httpx.HTTPError,BufferError,RuntimeError) as e:
# Include the reason: Buffer's rejections are actionable (duplicate text,
# daily limit, disconnected channel) and an opaque 502 sends the caller
# digging through job_posts.buffer_error to find out.
await JobPosts.mark_failed(self.session,str(row.id),str(e))
raise HTTPException(status_code=502,detail="Failed to publish job post to Buffer") from e
raise HTTPException(status_code=502,detail=f"Failed to publish job post to Buffer: {e}") from e
# Buffer accepting the mutation is not the same as the network publishing it:
# the default addToQueue mode returns `scheduled`, so the row only reads