244 lines
8.8 KiB
Python
244 lines
8.8 KiB
Python
from optparse import Option
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from typing import TYPE_CHECKING,List,Optional
|
|
|
|
from sqlalchemy import DateTime, func, or_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import selectinload
|
|
from sqlmodel import Field, Relationship, SQLModel, select
|
|
|
|
from role.models import Roles
|
|
from job.job_post.models import JobPosts
|
|
|
|
if TYPE_CHECKING: # runtime import would cycle: inbox.models imports this module
|
|
from inbox.models import Inbox
|
|
from job.candidate.models import Feedback, Notes
|
|
|
|
|
|
def _now() -> datetime:
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
class Users(SQLModel, table=True):
|
|
__tablename__ = "users"
|
|
|
|
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
|
name: str
|
|
email: str = Field(unique=True)
|
|
role_id: int | None = Field(nullable=True, foreign_key="roles.id")
|
|
role: Roles | None = Relationship(back_populates="users",
|
|
sa_relationship_kwargs={"lazy": "selectin"}
|
|
)
|
|
# selectin, not joined: this is a one-to-many, so a joined load would repeat the
|
|
# user row once per post. Without an explicit strategy the default is a lazy load,
|
|
# which raises MissingGreenlet the moment anything touches it under asyncio.
|
|
# foreign_keys must match the other side: job_posts.current_recruiter_id is a
|
|
# second FK into this table, so this relation has to say it means created_by.
|
|
job_posts: List[JobPosts] = Relationship(
|
|
back_populates="user",
|
|
sa_relationship_kwargs={"lazy": "selectin", "foreign_keys": "[JobPosts.created_by]"},
|
|
)
|
|
inbox: List["Inbox"] = Relationship(
|
|
back_populates="user",
|
|
sa_relationship_kwargs={"lazy": "selectin"},
|
|
)
|
|
feedback: List["Feedback"] = Relationship(
|
|
back_populates="user",
|
|
sa_relationship_kwargs={"lazy": "selectin"},
|
|
)
|
|
notes: List["Notes"] = Relationship(
|
|
back_populates="user",
|
|
sa_relationship_kwargs={"lazy": "selectin", "foreign_keys": "[Notes.user_id]"},
|
|
)
|
|
authored_notes: List["Notes"] = Relationship(
|
|
back_populates="author",
|
|
sa_relationship_kwargs={"lazy": "selectin", "foreign_keys": "[Notes.created_by]"},
|
|
)
|
|
|
|
password: str
|
|
# Public profile URL extracted from a CV at ingest. NULL until a CV
|
|
# mentions LinkedIn; never overwrite a stored value with empty.
|
|
linkedin_url: str | None = Field(default=None)
|
|
created_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True))
|
|
updated_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True))
|
|
is_active: bool = Field(default=False)
|
|
is_approved: bool = Field(default=False)
|
|
is_deleted: bool = Field(default=False)
|
|
|
|
@classmethod
|
|
async def get_user_id(cls, session: AsyncSession, user_id: str):
|
|
uid = cls._as_uuid(user_id)
|
|
if uid is None:
|
|
return None
|
|
statement = select(cls).where(cls.id == uid)
|
|
result = await session.execute(statement)
|
|
return result.scalars().first()
|
|
|
|
@classmethod
|
|
def _search_filter(cls, search: str):
|
|
pattern = f"%{search}%"
|
|
return or_(
|
|
cls.name.ilike(pattern),
|
|
cls.email.ilike(pattern),
|
|
)
|
|
|
|
@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_users(
|
|
cls, session: AsyncSession,
|
|
top: Optional[int]=None,
|
|
skip: Optional[int]=None,
|
|
search: Optional[str]=None,
|
|
role_id:Optional[int]=None
|
|
):
|
|
statement = (
|
|
select(cls)
|
|
.options(selectinload(cls.role))
|
|
.where(cls.is_deleted == False)
|
|
.order_by(cls.created_at.desc())
|
|
)
|
|
if search:
|
|
statement = statement.where(cls._search_filter(search))
|
|
if skip:
|
|
statement = statement.offset(skip)
|
|
if top is not None:
|
|
statement = statement.limit(top)
|
|
if role_id:
|
|
statement = statement.where(cls.role_id == role_id)
|
|
if not role_id:
|
|
statement = statement.where(cls.role_id != 8)
|
|
result = await session.execute(statement)
|
|
return result.scalars().all()
|
|
|
|
@classmethod
|
|
async def names_by_ids(cls, session: AsyncSession, user_ids) -> dict[str, str]:
|
|
"""Resolve {user_id: name} in a single query.
|
|
|
|
COLUMN select, not the Users entity: `select(cls)` would pull the five
|
|
selectin relations (role, job_posts, inbox, feedback, notes) for a
|
|
two-column lookup.
|
|
"""
|
|
uids = {u for u in (user_ids or []) if u}
|
|
if not uids:
|
|
return {}
|
|
result = await session.execute(
|
|
select(cls.id, cls.name).where(cls.id.in_(uids))
|
|
)
|
|
return {str(uid): name for uid, name in result.all()}
|
|
|
|
@classmethod
|
|
async def get_user_by_id(cls, session: AsyncSession, record_id: str):
|
|
uid = cls._as_uuid(record_id)
|
|
if uid is None:
|
|
return None
|
|
statement = select(cls).options(selectinload(cls.role)).where(cls.id == uid,cls.role_id != 8)
|
|
result = await session.execute(statement)
|
|
return result.scalars().first()
|
|
|
|
@classmethod
|
|
async def get_user_by_email(cls, session: AsyncSession, email: str):
|
|
statement = select(cls).options(selectinload(cls.role)).where(cls.email == email)
|
|
result = await session.execute(statement)
|
|
return result.scalars().first()
|
|
|
|
@classmethod
|
|
async def count_users(cls, session: AsyncSession, search: str | None = None, role_id: Optional[int] = None):
|
|
statement = (
|
|
select(func.count())
|
|
.select_from(cls)
|
|
.where(cls.is_deleted == False) # noqa: E712
|
|
)
|
|
if role_id:
|
|
statement = statement.where(cls.role_id == role_id)
|
|
else:
|
|
statement = statement.where(cls.role_id != 8)
|
|
if search:
|
|
statement = statement.where(cls._search_filter(search))
|
|
result = await session.execute(statement)
|
|
return result.scalar_one()
|
|
|
|
@classmethod
|
|
async def set_linkedin_url_if_empty(cls, session: AsyncSession, *, user_id=None, email=None, url=None) -> bool:
|
|
"""Write linkedin_url only when the user has none yet. Caller commits."""
|
|
value = (url or "").strip() or None
|
|
if not value:
|
|
return False
|
|
statement = select(cls)
|
|
if user_id is not None:
|
|
uid = cls._as_uuid(user_id)
|
|
if uid is None:
|
|
return False
|
|
statement = statement.where(cls.id == uid)
|
|
elif email:
|
|
statement = statement.where(func.lower(cls.email) == str(email).strip().lower())
|
|
else:
|
|
return False
|
|
user = (await session.execute(statement)).scalars().first()
|
|
if user is None or (user.linkedin_url or "").strip():
|
|
return False
|
|
user.linkedin_url = value
|
|
user.updated_at = _now()
|
|
session.add(user)
|
|
return True
|
|
|
|
@classmethod
|
|
async def get_pending_approvals(cls, session: AsyncSession):
|
|
"""Email-confirmed staff accounts waiting on an admin to set is_approved."""
|
|
statement = (
|
|
select(cls)
|
|
.options(selectinload(cls.role))
|
|
.where(
|
|
cls.is_deleted == False, # noqa: E712
|
|
cls.is_active == True, # noqa: E712
|
|
cls.is_approved == False, # noqa: E712
|
|
cls.role_id != 8,
|
|
)
|
|
.order_by(cls.created_at.desc())
|
|
)
|
|
result = await session.execute(statement)
|
|
return result.scalars().all()
|
|
|
|
@classmethod
|
|
async def insert_user(cls, session: AsyncSession, fields: dict):
|
|
"""`fields["password"]` is expected to be hashed already — see users.plugins."""
|
|
user = cls(**fields)
|
|
session.add(user)
|
|
await session.commit()
|
|
return await cls.get_user_by_id(session, user.id)
|
|
|
|
@classmethod
|
|
async def update_user(cls, session: AsyncSession, record_id: str, fields: dict):
|
|
user = await cls.get_user_by_id(session, record_id)
|
|
if not user:
|
|
return None
|
|
for key, value in fields.items():
|
|
setattr(user, key, value)
|
|
user.updated_at = _now()
|
|
session.add(user)
|
|
await session.commit()
|
|
await session.refresh(user)
|
|
return await cls.get_user_by_id(session, user.id)
|
|
|
|
@classmethod
|
|
async def soft_delete_user(cls, session: AsyncSession, record_id: str):
|
|
user = await cls.get_user_by_id(session, record_id)
|
|
if not user:
|
|
return None
|
|
user.is_deleted = True
|
|
user.is_active = False
|
|
user.updated_at = _now()
|
|
session.add(user)
|
|
await session.commit()
|
|
await session.refresh(user)
|
|
return user
|
|
|
|
|
|
import job.candidate.models as _candidate_models # noqa: E402, F401
|