import uuid from datetime import datetime, timezone from typing import TYPE_CHECKING, Optional from sqlalchemy import DateTime, JSON 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 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