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

175 lines
6.2 KiB
Python

import uuid
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Optional
from sqlalchemy import DateTime, JSON, func, or_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import Field, Relationship, SQLModel, select
if TYPE_CHECKING: # runtime import would be circular: users.models imports this module
from users.models import Users
def _now() -> datetime:
return datetime.now(timezone.utc)
class JobPosts(SQLModel, table=True):
__tablename__ = "job_posts"
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
title: str = Field(index=True)
user: Optional["Users"] = Relationship(
back_populates="job_posts",
sa_relationship_kwargs={"lazy": "joined"},
)
platform: str = Field(default="linkedin")
is_active: bool = Field(default=True)
is_deleted: bool = Field(default=False)
employment_type: str | None = Field(default=None)
location: str | None = Field(default=None)
experience_min: int | None = Field(default=None)
experience_max: int | None = Field(default=None)
requirements: list[str] = Field(default_factory=list, sa_type=JSON)
optional_skills: list[str] = Field(default_factory=list, sa_type=JSON)
salary: str = Field(default="Anonymous")
description: str | None = Field(default=None)
post_text: str
channel_id: str
buffer_post_id: str | None = Field(default=None)
buffer_external_link: str | None = Field(default=None)
buffer_sent_at: datetime | None = Field(default=None, sa_type=DateTime(timezone=True))
status: str = Field(default="draft")
buffer_error: str | None = Field(default=None)
created_by: uuid.UUID = Field(foreign_key="users.id")
created_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True))
updated_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True))
@staticmethod
def _as_uuid(record_id: str) -> uuid.UUID | None:
try:
return uuid.UUID(str(record_id))
except ValueError:
return None
@classmethod
async def get_job_post_by_id(cls, session: AsyncSession, record_id: str):
uid = cls._as_uuid(record_id)
if uid is None:
return None
result = await session.execute(select(cls).where(cls.id == uid))
return result.scalars().first()
@classmethod
async def get_active_job_posts(cls, session: AsyncSession):
result = await session.execute(
select(cls).where(cls.is_active == True, cls.is_deleted == False) # noqa: E712
)
return result.scalars().all()
@classmethod
async def get_by_ids(cls, session: AsyncSession, ids: list[str], *, active_only: bool = True):
uids = []
for raw in ids or []:
uid = cls._as_uuid(raw)
if uid is not None:
uids.append(uid)
if not uids:
return []
statement = select(cls).where(cls.id.in_(uids))
if active_only:
statement = statement.where(cls.is_active == True, cls.is_deleted == False) # noqa: E712
result = await session.execute(statement)
rows = list(result.scalars().all())
by_id = {str(r.id): r for r in rows}
# Preserve request order so suggestion ranks stay stable.
return [by_id[str(u)] for u in uids if str(u) in by_id]
@classmethod
async def fetch_job_posts(
cls,
session: AsyncSession,
*,
search: str | None = None,
top: int | None = None,
skip: int = 0,
ids: list[str] | None = None,
active_only: bool = True,
):
if ids:
rows = await cls.get_by_ids(session, ids, active_only=active_only)
return rows, len(rows)
statement = select(cls)
if active_only:
statement = statement.where(cls.is_active == True, cls.is_deleted == False) # noqa: E712
if search:
like = f"%{search.strip()}%"
statement = statement.where(
or_(cls.title.ilike(like), cls.location.ilike(like))
)
count_statement = select(func.count()).select_from(statement.subquery())
total = (await session.execute(count_statement)).scalar_one()
statement = statement.order_by(cls.created_at.desc())
if skip:
statement = statement.offset(skip)
if top is not None:
statement = statement.limit(top)
result = await session.execute(statement)
return list(result.scalars().all()), total
@classmethod
async def insert_job_post(cls, session: AsyncSession, fields: dict):
row = cls(**fields)
session.add(row)
await session.commit()
return await cls.get_job_post_by_id(session, row.id)
@classmethod
async def mark_buffer_result(
cls,
session: AsyncSession,
record_id: str,
*,
buffer_post_id: str,
status: str,
external_link: str | None = None,
sent_at: datetime | None = None,
platform: str | None = None,
):
"""Record what Buffer reported.
`status` is the mapped Buffer PostStatus, not an assumption: a queued post lands
here as "scheduled" and only becomes "published" once Buffer says `sent`.
"""
row = await cls.get_job_post_by_id(session, record_id)
if not row:
return None
row.status = status
row.buffer_post_id = buffer_post_id
row.buffer_external_link = external_link
row.buffer_sent_at = sent_at
if platform:
row.platform = platform
row.buffer_error = None
row.updated_at = _now()
session.add(row)
await session.commit()
await session.refresh(row)
return row
@classmethod
async def mark_failed(cls, session: AsyncSession, record_id: str, error: str):
row = await cls.get_job_post_by_id(session, record_id)
if not row:
return None
row.status = "failed"
row.buffer_error = error
row.updated_at = _now()
session.add(row)
await session.commit()
await session.refresh(row)
return row
import users.models as _users_models