parent
c79024160c
commit
f884ae1d1d
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue