pedning testinf

pull/9/head
ahmed.mujtaba 2026-08-11 17:48:00 +05:00
parent e717dbf704
commit c726bbacbc
16 changed files with 1034 additions and 84 deletions

View File

@ -1,9 +1,8 @@
from ast import List
import logging
import os
import uuid
from datetime import datetime, timezone
from typing import Any, Optional
from typing import Any, List, Optional
from dotenv import load_dotenv
from fastapi import HTTPException
@ -16,8 +15,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlmodel import Field, Relationship, SQLModel, select, true
from job.candidate.models import Interviews
from users.models import Users, Activity, Notes
from job.candidate.models import Activity, Feedback, Interviews
from users.models import Users
from users.plugins import hash_password
load_dotenv()
@ -45,35 +44,24 @@ class Inbox(SQLModel, table=True):
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
favorite:Optional[bool] = Field(default=False)
rating:Optional[float] = Field(default=0.0)
notes_id: Optional[uuid.UUID] = Field(
default=None,
foreign_key="notes.id",
)
notes: List[Notes] = Relationship(
back_populates="inbox",
sa_relationship_kwargs={"lazy": "selectin"}
)
interview_id: Optional[uuid.UUID] = Field(
default=None,
foreign_key="interviews.id",
)
interview: List[Interviews] = Relationship(
back_populates="inbox",
sa_relationship_kwargs={"lazy": "selectin"}
)
favorite: Optional[bool] = Field(default=False)
rating: Optional[float] = Field(default=0.0)
activity_id: Optional[uuid.UUID] = Field(
default=None,
foreign_key="activity.id"
)
activity: Optional[Activity] = Relationship(
# selectin on one-to-many: joined would repeat the inbox row per child
interviews: List["Interviews"] = Relationship(
back_populates="inbox",
sa_relationship_kwargs={"lazy": "selectin"},
)
Optional[Users] = Relationship(
activity: List["Activity"] = Relationship(
back_populates="inbox",
sa_relationship_kwargs={"lazy": "selectin"},
)
feedback: List["Feedback"] = Relationship(
back_populates="inbox",
sa_relationship_kwargs={"lazy": "selectin"},
)
user: Optional["Users"] = Relationship(
back_populates="inbox",
sa_relationship_kwargs={"lazy": "joined"},
)
@ -86,9 +74,16 @@ class Inbox(SQLModel, table=True):
@classmethod
async def get_candidate_profile(cls,session:AsyncSession,user_id:uuid.UUID|None=None,limit:int=10,offset:int=0,search:str|None=None):
try:
options=[selectinload(cls.messages)]
if user_id:
options.extend([
selectinload(cls.interviews),
selectinload(cls.activity),
selectinload(cls.feedback),
])
qry = (
select(cls)
.options(selectinload(cls.messages))
.options(*options)
.join(Users, cls.user_id == Users.id)
.join(Roles, Users.role_id == Roles.id)
.where(Roles.role_name == EnumRoles.CANDIDATE.value)
@ -126,6 +121,52 @@ class Inbox(SQLModel, table=True):
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@classmethod
async def get_inbox_by_id(cls,session:AsyncSession,record_id:int|str|None):
if record_id is None:
return None
try:
iid=int(record_id)
except (TypeError,ValueError):
return None
result=await session.execute(select(cls).where(cls.id==iid))
return result.scalars().first()
@classmethod
async def get_inbox_by_message_id(cls,session:AsyncSession,message_id):
try:
mid=uuid.UUID(str(message_id))
except ValueError:
return None
result=await session.execute(
select(cls).where(cls.message_id==mid).order_by(cls.created_at.desc())
)
return result.scalars().first()
@classmethod
async def get_inbox_by_user_id(cls,session:AsyncSession,user_id):
try:
uid=uuid.UUID(str(user_id))
except ValueError:
return None
result=await session.execute(
select(cls).where(cls.user_id==uid).order_by(cls.created_at.desc())
)
return result.scalars().first()
@classmethod
async def update_inbox(cls,session:AsyncSession,record_id,fields:dict):
row=await cls.get_inbox_by_id(session,record_id)
if not row:
return None
for key,value in fields.items():
setattr(row,key,value)
row.updated_at=datetime.now()
session.add(row)
await session.commit()
await session.refresh(row)
return row
class Inbox_Alerts(SQLModel, table=True):
__tablename__ = "inbox_alerts"

View File

@ -0,0 +1,10 @@
def serialize_activity(row) -> dict:
return {
"id": str(row.id),
"inbox_id": row.inbox_id,
"activity_type": row.activity_type,
"activity_date": row.activity_date.isoformat() if row.activity_date else None,
"activity_time": row.activity_time.isoformat() if row.activity_time else None,
"activity_status": row.activity_status,
"description": row.description,
}

View File

@ -0,0 +1,55 @@
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from inbox.models import Inbox
from job.candidate.models import Activity
from job.activity.serializers import serialize_activity
class ActivityLog:
def __init__(self,session:AsyncSession):
self.session=session
async def _resolve_inbox(self,payload):
if payload.get("inbox_id") is not None:
row=await Inbox.get_inbox_by_id(self.session,payload["inbox_id"])
if not row:
raise HTTPException(status_code=404,detail="Inbox not found")
return row
if payload.get("message_id"):
row=await Inbox.get_inbox_by_message_id(self.session,payload["message_id"])
if not row:
raise HTTPException(status_code=404,detail="Inbox not found for message_id")
return row
if payload.get("user_id"):
row=await Inbox.get_inbox_by_user_id(self.session,payload["user_id"])
if not row:
raise HTTPException(status_code=404,detail="Inbox not found for user_id")
return row
return None
async def get_activity(self,activity_id=None,inbox_id=None):
if activity_id:
row=await Activity.get_activity_by_id(self.session,activity_id)
if not row:
raise HTTPException(status_code=404,detail="Activity not found")
return serialize_activity(row)
if inbox_id is None:
raise HTTPException(status_code=400,detail="activity_id or inbox_id is required")
rows=await Activity.get_activity_by_inbox(self.session,int(inbox_id))
return [serialize_activity(r) for r in rows]
async def create_activity(self,payload):
link=await self._resolve_inbox(payload)
fields={
"activity_type":payload.get("activity_type") or "",
"activity_status":payload.get("activity_status") or "",
"description":payload.get("description"),
"inbox_id":link.id if link else None,
}
if payload.get("activity_date") is not None:
fields["activity_date"]=payload["activity_date"]
if payload.get("activity_time") is not None:
fields["activity_time"]=payload["activity_time"]
row=await Activity.insert_activity(self.session,fields)
return serialize_activity(row)

View File

@ -3,6 +3,10 @@ from fastapi.responses import JSONResponse
from fastapi import HTTPException
from db_setup import get_session
from job.candidate.views import FileRead,CandidateView
from job.interviews.views import Interview
from job.notes.views import Note
from job.activity.views import ActivityLog
from job.feedback.views import FeedbackView
from sqlalchemy.ext.asyncio import AsyncSession
from users.permissions import PermissionTag, require_permission
from job.job_post.views import JobPost,JobPostCreate
@ -11,6 +15,8 @@ from job.job_post.plugins import PlatformAlias
from fastapi import UploadFile, File
from dotenv import load_dotenv
from datetime import datetime, time, timezone
from pydantic import BaseModel
from uuid import UUID
load_dotenv()
logging.basicConfig(level=logging.INFO)
@ -19,6 +25,65 @@ logger = logging.getLogger(__name__)
router = APIRouter()
class CandidateUpdate(BaseModel):
favorite: bool | None = None
rating: float | None = None
class InterviewCreate(BaseModel):
inbox_id: int
interview_date: datetime | None = None
interview_time: datetime | None = None
interview_type: str | None = None
interview_status: str | None = None
class InterviewUpdate(BaseModel):
interview_date: datetime | None = None
interview_time: datetime | None = None
interview_type: str | None = None
interview_status: str | None = None
inbox_id: int | None = None
class NoteCreate(BaseModel):
user_id: UUID
note: str
class NoteUpdate(BaseModel):
note: str | None = None
class ActivityCreate(BaseModel):
message_id: UUID | None = None
user_id: UUID | None = None
inbox_id: int | None = None
activity_type: str | None = None
activity_status: str | None = None
description: str | None = None
activity_date: datetime | None = None
activity_time: datetime | None = None
class FeedbackCreate(BaseModel):
inbox_id: int | None = None
review: str | None = None
financial_status: str | None = None
score: float | None = None
note: str | None = None
reviewed_by: UUID | None = None
class FeedbackUpdate(BaseModel):
review: str | None = None
financial_status: str | None = None
score: float | None = None
note: str | None = None
inbox_id: int | None = None
reviewed_by: UUID | None = None
@router.get("/jobs/alias")
async def get_job_alias():
@ -115,11 +180,215 @@ async def fetch_candidate(
try:
service=CandidateView(session=session)
data=await service.get_candidate(user_id=user_id,limit=limit,offset=offset,search=search)
# total is the RESULT-SET size, not len(data) — a pager cannot be driven
# off the page length. By id stays 1, per the house envelope.
total=await service.count_candidates(user_id=user_id,search=search) if isinstance(data,list) else 1
return JSONResponse(content={"data":data,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.patch("/candidate/update")
async def update_candidate(
user_id:str=Query(...),
payload:CandidateUpdate=...,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=CandidateView(session=session)
data=await service.update_candidate(user_id,payload.model_dump(exclude_unset=True))
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/interview/fetch")
async def fetch_interview(
interview_id:str=Query(None),
inbox_id:int=Query(None),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=Interview(session=session)
data=await service.get_interview(interview_id=interview_id,inbox_id=inbox_id)
total=1 if isinstance(data,dict) else len(data)
return JSONResponse(content={"data":data,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/interview/create")
async def create_interview(
payload:InterviewCreate,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
session: AsyncSession = Depends(get_session),
):
try:
service=Interview(session=session)
data=await service.create_interview(payload.model_dump(exclude_unset=True))
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.patch("/interview/update")
async def update_interview(
interview_id:str=Query(...),
payload:InterviewUpdate=...,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=Interview(session=session)
data=await service.update_interview(interview_id,payload.model_dump(exclude_unset=True))
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/notes/fetch")
async def fetch_notes(
note_id:str=Query(None),
user_id:str=Query(None),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=Note(session=session)
data=await service.get_note(note_id=note_id,user_id=user_id)
total=1 if isinstance(data,dict) else len(data)
return JSONResponse(content={"data":data,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/notes/create")
async def create_note(
payload:NoteCreate,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
session: AsyncSession = Depends(get_session),
):
try:
service=Note(session=session)
data=await service.create_note(payload.model_dump(exclude_unset=True),current_user)
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.patch("/notes/update")
async def update_note(
note_id:str=Query(...),
payload:NoteUpdate=...,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=Note(session=session)
data=await service.update_note(note_id,payload.model_dump(exclude_unset=True))
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/activity/fetch")
async def fetch_activity(
activity_id:str=Query(None),
inbox_id:int=Query(None),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=ActivityLog(session=session)
data=await service.get_activity(activity_id=activity_id,inbox_id=inbox_id)
total=1 if isinstance(data,dict) else len(data)
return JSONResponse(content={"data":data,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/activity/create")
async def create_activity(
payload:ActivityCreate,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
session: AsyncSession = Depends(get_session),
):
try:
service=ActivityLog(session=session)
data=await service.create_activity(payload.model_dump(exclude_unset=True))
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/feedback/fetch")
async def fetch_feedback(
feedback_id:str=Query(None),
inbox_id:int=Query(None),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=FeedbackView(session=session)
data=await service.get_feedback(feedback_id=feedback_id,inbox_id=inbox_id)
total=1 if isinstance(data,dict) else len(data)
return JSONResponse(content={"data":data,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/feedback/create")
async def create_feedback(
payload:FeedbackCreate,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
session: AsyncSession = Depends(get_session),
):
try:
service=FeedbackView(session=session)
data=await service.create_feedback(payload.model_dump(exclude_unset=True),current_user)
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.patch("/feedback/update")
async def update_feedback(
feedback_id:str=Query(...),
payload:FeedbackUpdate=...,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=FeedbackView(session=session)
data=await service.update_feedback(feedback_id,payload.model_dump(exclude_unset=True))
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))

View File

@ -1,67 +1,258 @@
from time import timezone
from token import OP
from sqlmodel import SQLModel, Field
from uuid import UUID, uuid4
from datetime import datetime
from enum import Enum
import uuid
from typing import Optional,List
from sqlmodel import Relationship
from typing import TYPE_CHECKING
from datetime import datetime, timezone
from typing import TYPE_CHECKING, List, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import Field, Relationship, SQLModel, select
if TYPE_CHECKING:
from inbox.models import Inbox
from users.models import Users
class Interviews(SQLModel,table=True):
def _now() -> datetime:
return datetime.now(timezone.utc)
class Interviews(SQLModel, table=True):
__tablename__ = "interviews"
id: UUID = Field(default_factory=uuid4, primary_key=True)
interview_date: datetime = Field(default_factory=datetime.now)
interview_time: datetime = Field(default_factory=datetime.now)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
interview_date: datetime = Field(default_factory=_now)
interview_time: datetime = Field(default_factory=_now)
interview_type: str = Field(default="")
interview_status: str = Field(default="")
inbox_id: Optional[uuid.UUID] = Field(default=None, foreign_key="inbox.id")
inbox: Optional[Inbox] = Relationship(back_populates="interview",
sa_relationship_kwargs={"lazy": "joined"}
inbox_id: int | None = Field(default=None, foreign_key="inbox.id")
inbox: Optional["Inbox"] = Relationship(
back_populates="interviews",
sa_relationship_kwargs={"lazy": "selectin"},
)
@staticmethod
def _as_uuid(record_id) -> uuid.UUID | None:
try:
return uuid.UUID(str(record_id))
except ValueError:
return None
class Notes(SQLModel,table=True):
@classmethod
async def get_interview_by_id(cls, session: AsyncSession, record_id):
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_interviews_by_inbox(cls, session: AsyncSession, inbox_id: int):
result = await session.execute(
select(cls).where(cls.inbox_id == inbox_id).order_by(cls.interview_date.desc())
)
return result.scalars().all()
@classmethod
async def insert_interview(cls, session: AsyncSession, fields: dict):
row = cls(**fields)
session.add(row)
await session.commit()
return await cls.get_interview_by_id(session, row.id)
@classmethod
async def update_interview(cls, session: AsyncSession, record_id, fields: dict):
row = await cls.get_interview_by_id(session, record_id)
if not row:
return None
for key, value in fields.items():
setattr(row, key, value)
session.add(row)
await session.commit()
await session.refresh(row)
return row
class Notes(SQLModel, table=True):
__tablename__ = "notes"
id: UUID = Field(default_factory=uuid4, primary_key=True)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
note: str = Field(default="")
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
inbox_id: Optional[uuid.UUID] = Field(default=None, foreign_key="inbox.id")
inbox: Optional[Inbox] = Relationship(back_populates="notes",
sa_relationship_kwargs={"lazy": "joined"}
created_at: datetime = Field(default_factory=_now)
updated_at: datetime = Field(default_factory=_now)
user_id: uuid.UUID | None = Field(default=None, foreign_key="users.id")
created_by: uuid.UUID | None = Field(default=None, foreign_key="users.id")
user: Optional["Users"] = Relationship(
back_populates="notes",
sa_relationship_kwargs={"lazy": "selectin", "foreign_keys": "[Notes.user_id]"},
)
user_id: Optional[uuid.UUID] = Field(default=None, foreign_key="users.id")
user: Optional[Users] = Relationship(back_populates="notes",
sa_relationship_kwargs={"lazy": "joined"}
author: Optional["Users"] = Relationship(
back_populates="authored_notes",
sa_relationship_kwargs={"lazy": "selectin", "foreign_keys": "[Notes.created_by]"},
)
class Activity(SQLModel,table=True):
@staticmethod
def _as_uuid(record_id) -> uuid.UUID | None:
try:
return uuid.UUID(str(record_id))
except ValueError:
return None
@classmethod
async def get_note_by_id(cls, session: AsyncSession, record_id):
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_notes_by_user(cls, session: AsyncSession, user_id):
uid = cls._as_uuid(user_id)
if uid is None:
return []
result = await session.execute(
select(cls).where(cls.user_id == uid).order_by(cls.created_at.desc())
)
return result.scalars().all()
@classmethod
async def insert_note(cls, session: AsyncSession, fields: dict):
row = cls(**fields)
session.add(row)
await session.commit()
return await cls.get_note_by_id(session, row.id)
@classmethod
async def update_note(cls, session: AsyncSession, record_id, fields: dict):
row = await cls.get_note_by_id(session, record_id)
if not row:
return None
for key, value in fields.items():
setattr(row, key, value)
row.updated_at = _now()
session.add(row)
await session.commit()
await session.refresh(row)
return row
class Activity(SQLModel, table=True):
__tablename__ = "activity"
id: UUID = Field(default_factory=uuid4, primary_key=True)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
activity_type: str = Field(default="")
activity_date: datetime = Field(default_factory=datetime.now)
activity_time: datetime = Field(default_factory=datetime.now)
activity_date: datetime = Field(default_factory=_now)
activity_time: datetime = Field(default_factory=_now)
activity_status: str = Field(default="")
inbox_id: Optional[uuid.UUID] = Field(default=None, foreign_key="inbox.id")
inbox: Optional[Inbox] = Relationship(back_populates="activity",
sa_relationship_kwargs={"lazy": "joined"}
description: str | None = Field(default=None)
inbox_id: int | None = Field(default=None, foreign_key="inbox.id")
inbox: Optional["Inbox"] = Relationship(
back_populates="activity",
sa_relationship_kwargs={"lazy": "selectin"},
)
class Feedback(SQLModel,table=True):
@staticmethod
def _as_uuid(record_id) -> uuid.UUID | None:
try:
return uuid.UUID(str(record_id))
except ValueError:
return None
@classmethod
async def get_activity_by_id(cls, session: AsyncSession, record_id):
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_activity_by_inbox(cls, session: AsyncSession, inbox_id: int):
result = await session.execute(
select(cls).where(cls.inbox_id == inbox_id).order_by(cls.activity_date.desc())
)
return result.scalars().all()
@classmethod
async def insert_activity(cls, session: AsyncSession, fields: dict):
row = cls(**fields)
session.add(row)
await session.commit()
return await cls.get_activity_by_id(session, row.id)
@classmethod
async def update_activity(cls, session: AsyncSession, record_id, fields: dict):
row = await cls.get_activity_by_id(session, record_id)
if not row:
return None
for key, value in fields.items():
setattr(row, key, value)
session.add(row)
await session.commit()
await session.refresh(row)
return row
class Feedback(SQLModel, table=True):
__tablename__ = "feedback"
id: UUID = Field(default_factory=uuid4, primary_key=True)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
review: str = Field(default="")
financial_status: str = Field(default="")
score: float = Field(default=0.0)
note:str=Field(default=None)
created_at:datetime=Field(default_factory=datetime.now(timezone.utc))
updated_at:datetime=Field(default_factory=datetime.now(timezone.utc))
reviewed_by:Optional[uuid.UUID]=Field(default=None,foreign_key="users.id")
user:Optional[Users]=Relationship(back_populates="feedback",
sa_relationship_kwargs={"lazy":"joined"}
)
note: str | None = Field(default=None)
created_at: datetime = Field(default_factory=_now)
updated_at: datetime = Field(default_factory=_now)
reviewed_by: uuid.UUID | None = Field(default=None, foreign_key="users.id")
inbox_id: int | None = Field(default=None, foreign_key="inbox.id")
user: Optional["Users"] = Relationship(
back_populates="feedback",
sa_relationship_kwargs={"lazy": "selectin"},
)
inbox: Optional["Inbox"] = Relationship(
back_populates="feedback",
sa_relationship_kwargs={"lazy": "selectin"},
)
@staticmethod
def _as_uuid(record_id) -> uuid.UUID | None:
try:
return uuid.UUID(str(record_id))
except ValueError:
return None
@classmethod
async def get_feedback_by_id(cls, session: AsyncSession, record_id):
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_feedback_by_inbox(cls, session: AsyncSession, inbox_id: int):
result = await session.execute(
select(cls).where(cls.inbox_id == inbox_id).order_by(cls.created_at.desc())
)
return result.scalars().all()
@classmethod
async def insert_feedback(cls, session: AsyncSession, fields: dict):
row = cls(**fields)
session.add(row)
await session.commit()
return await cls.get_feedback_by_id(session, row.id)
@classmethod
async def update_feedback(cls, session: AsyncSession, record_id, fields: dict):
row = await cls.get_feedback_by_id(session, record_id)
if not row:
return None
for key, value in fields.items():
setattr(row, key, value)
row.updated_at = _now()
session.add(row)
await session.commit()
await session.refresh(row)
return row
import users.models as _users_models # noqa: E402, F401

View File

@ -28,3 +28,46 @@ def normalize_spaced_text(text) -> str:
lines = [re.sub(r" {2,}", " ", line).strip() for line in text.splitlines()]
return re.sub(r"\n{3,}", "\n\n", "\n".join(lines)).strip()
# Mirrors frontend Inbox.jsx sourceFrom — board name is tagged in the To address.
INBOX_SOURCES = (
"Microsoft Outlook",
"Career Portal",
"Manual CV Upload",
"LinkedIn",
"Indeed",
"Rozee",
"Mustakbil",
"Employee Referral",
"Recruitment Agency",
"Campus Hiring",
"Walk-in",
)
def _letters_only(value: str) -> str:
return re.sub(r"[^a-z]", "", (value or "").lower())
def source_from_message_to(message_to: str | None) -> str:
raw = (message_to or "").strip()
if not raw:
return "Unknown"
flat = _letters_only(raw)
for name in INBOX_SOURCES:
if _letters_only(name) and _letters_only(name) in flat:
return name
return raw.split(",")[0].strip()
def documents_from_message(file_name: str | None, file_path: str | None) -> list[dict]:
names = [n.strip() for n in (file_name or "").split(",") if n.strip()]
paths = [p.strip() for p in (file_path or "").split(",") if p.strip()]
out = []
for i, name in enumerate(names):
out.append({"name": name, "path": paths[i] if i < len(paths) else None})
if not out and paths:
for path in paths:
out.append({"name": path.rsplit("/", 1)[-1].rsplit("\\", 1)[-1], "path": path})
return out

View File

@ -1,16 +1,25 @@
from inbox.models import Inbox
from typing import Any,List,Dict
from job.candidate.plugins import documents_from_message, source_from_message_to
from job.interviews.serializers import serialize_interview
from job.activity.serializers import serialize_activity
from job.feedback.serializers import serialize_feedback
def serialize_candidate_profile(link:Inbox|List[Inbox]|Dict[str,Any]|List[Dict[str,Any]]) -> Dict[str,Any]|List[Dict[str,Any]]:
def serialize_candidate_profile(
link:Inbox|List[Inbox]|Dict[str,Any]|List[Dict[str,Any]],
*,
detail:bool=False,
) -> Dict[str,Any]|List[Dict[str,Any]]:
if isinstance(link,list):
return [serialize_candidate_profile(item) for item in link]
return [serialize_candidate_profile(item,detail=detail) for item in link]
if isinstance(link,dict):
return link
user = link.user
message = link.messages
return {
payload = {
"inbox_id": link.id,
"user_id": str(link.user_id) if link.user_id else None,
"name": user.name if user else None,
@ -30,3 +39,31 @@ def serialize_candidate_profile(link:Inbox|List[Inbox]|Dict[str,Any]|List[Dict[s
"matched_at": message.matched_at.isoformat() if message and message.matched_at else None,
"job_posts": [],
}
if not detail:
return payload
payload.update({
"favorite": link.favorite,
"rating": link.rating,
"phone": message.candidate_phone_number if message else None,
"education": message.candidate_education if message else None,
"currentCompany": message.current_employment if message else None,
"stage": message.application_status if message else None,
"source": source_from_message_to(message.message_to if message else None),
"applied": message.message_received_time if message else None,
"documents": documents_from_message(
message.file_name if message else None,
message.file_path if message else None,
),
"recruiter": None,
"recruiter_id": None,
"job_title": None,
"ai_score": None,
"recommendation": None,
"sub_scores": None,
"interviews": [serialize_interview(r) for r in (link.interviews or [])],
"activity": [serialize_activity(r) for r in (link.activity or [])],
"feedback": [serialize_feedback(r) for r in (link.feedback or [])],
"notes": [],
})
return payload

View File

@ -4,10 +4,13 @@ from datetime import datetime,timezone
from fastapi import HTTPException
from pypdf import PdfReader
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from sqlmodel import true
from job.job_post.models import JobPosts
from job.job_post.serializers import serialize_job_post
from job.candidate.serializers import serialize_candidate_profile
from job.candidate.models import Notes
from job.notes.serializers import serialize_note
from inbox.models import Inbox_Messages,Inbox
from job.candidate.plugins import normalize_spaced_text
@ -77,7 +80,12 @@ class CandidateView:
async def get_candidate(self,user_id=None,limit=10,offset=0,search=None):
try:
rows=await Inbox.get_candidate_profile(session=self.session,user_id=user_id,limit=limit,offset=offset,search=search)
detail=bool(user_id)
# Detail mode must see every application for the candidate, not one page.
fetch_limit=1000 if detail else limit
rows=await Inbox.get_candidate_profile(session=self.session,user_id=user_id,limit=fetch_limit,offset=offset,search=search)
if detail:
return await self.attach_profile_detail(rows)
return await self.attach_job_posts(rows)
except HTTPException:
raise
@ -92,6 +100,26 @@ class CandidateView:
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
async def update_candidate(self,user_id,payload):
try:
if not user_id:
raise HTTPException(status_code=400,detail="user_id is required")
fields={k:v for k,v in (payload or {}).items() if k in ("favorite","rating") and v is not None}
if not fields:
raise HTTPException(status_code=400,detail="favorite or rating is required")
links=await Inbox.get_candidate_profile(session=self.session,user_id=user_id,limit=100,offset=0)
records=links if isinstance(links,list) else ([links] if links else [])
if not records:
raise HTTPException(status_code=404,detail="Candidate not found")
for link in records:
await Inbox.update_inbox(self.session,link.id,fields)
refreshed=await Inbox.get_candidate_profile(session=self.session,user_id=user_id,limit=100,offset=0)
return await self.attach_profile_detail(refreshed)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
async def get_job_post_by_id(self,record_id,data=None):
"""Load full job_posts row and optionally append it onto a candidate payload."""
try:
@ -101,6 +129,11 @@ class CandidateView:
payload=serialize_job_post(job_post_data)
if isinstance(data,dict):
data.setdefault("job_posts",[]).append(payload)
if data.get("recruiter") is None and payload.get("created_by_name"):
data["recruiter"]=payload.get("created_by_name")
data["recruiter_id"]=payload.get("created_by")
if data.get("job_title") is None and payload.get("title"):
data["job_title"]=payload.get("title")
return payload
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@ -117,3 +150,63 @@ class CandidateView:
await self.get_job_post_by_id(record_id=job_id,data=payload)
enriched.append(payload)
return enriched[0] if single else enriched
async def attach_profile_detail(self,data):
"""Detail mode: flatten child collections across every Inbox row for the candidate."""
single=not isinstance(data,list)
records=[data] if single else list(data or [])
if not records:
return {} if single else []
interviews=[]
activity=[]
feedback=[]
documents=[]
job_posts=[]
base=None
user_id=None
favorite=None
rating=None
for record in records:
payload=serialize_candidate_profile(record,detail=True)
if base is None:
base=payload
user_id=payload.get("user_id")
favorite=payload.get("favorite")
rating=payload.get("rating")
interviews.extend(payload.get("interviews") or [])
activity.extend(payload.get("activity") or [])
feedback.extend(payload.get("feedback") or [])
documents.extend(payload.get("documents") or [])
for job_id in payload.get("suggested_job_post_ids") or []:
await self.get_job_post_by_id(record_id=job_id,data=payload)
for jp in payload.get("job_posts") or []:
if not any(x.get("id")==jp.get("id") for x in job_posts):
job_posts.append(jp)
if base.get("recruiter") is None and payload.get("recruiter"):
base["recruiter"]=payload.get("recruiter")
base["recruiter_id"]=payload.get("recruiter_id")
if base.get("job_title") is None and payload.get("job_title"):
base["job_title"]=payload.get("job_title")
notes=[]
uid=Notes._as_uuid(user_id) if user_id else None
if uid is not None:
result=await self.session.execute(
select(Notes)
.options(selectinload(Notes.author))
.where(Notes.user_id==uid)
.order_by(Notes.created_at.desc())
)
notes=[serialize_note(r) for r in result.scalars().all()]
activity.sort(key=lambda r:(r.get("activity_date") or ""),reverse=True)
base["favorite"]=favorite
base["rating"]=rating
base["interviews"]=interviews
base["activity"]=activity
base["feedback"]=feedback
base["documents"]=documents
base["notes"]=notes
base["job_posts"]=job_posts or base.get("job_posts") or []
return base

View File

@ -0,0 +1,14 @@
def serialize_feedback(row) -> dict:
reviewer=getattr(row,"user",None)
return {
"id": str(row.id),
"inbox_id": row.inbox_id,
"review": row.review,
"financial_status": row.financial_status,
"score": row.score,
"note": row.note,
"reviewed_by": str(row.reviewed_by) if row.reviewed_by else None,
"reviewed_by_name": reviewer.name if reviewer else None,
"created_at": row.created_at.isoformat() if row.created_at else None,
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
}

View File

@ -0,0 +1,63 @@
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlmodel import select
from job.candidate.models import Feedback
from job.feedback.serializers import serialize_feedback
class FeedbackView:
def __init__(self,session:AsyncSession):
self.session=session
async def _load(self,record_id):
uid=Feedback._as_uuid(record_id)
if uid is None:
return None
result=await self.session.execute(
select(Feedback).options(selectinload(Feedback.user)).where(Feedback.id==uid)
)
return result.scalars().first()
async def get_feedback(self,feedback_id=None,inbox_id=None):
if feedback_id:
row=await self._load(feedback_id)
if not row:
raise HTTPException(status_code=404,detail="Feedback not found")
return serialize_feedback(row)
if inbox_id is None:
raise HTTPException(status_code=400,detail="feedback_id or inbox_id is required")
result=await self.session.execute(
select(Feedback)
.options(selectinload(Feedback.user))
.where(Feedback.inbox_id==int(inbox_id))
.order_by(Feedback.created_at.desc())
)
return [serialize_feedback(r) for r in result.scalars().all()]
async def create_feedback(self,payload,current_user):
fields={
"review":payload.get("review") or "",
"financial_status":payload.get("financial_status") or "",
"score":payload.get("score") if payload.get("score") is not None else 0.0,
"note":payload.get("note"),
"inbox_id":payload.get("inbox_id"),
"reviewed_by":payload.get("reviewed_by") or (
current_user.get("id") if isinstance(current_user,dict) else None
),
}
row=await Feedback.insert_feedback(self.session,fields)
row=await self._load(row.id)
return serialize_feedback(row)
async def update_feedback(self,feedback_id,payload):
allowed=("review","financial_status","score","note","inbox_id","reviewed_by")
fields={k:v for k,v in payload.items() if v is not None and k in allowed}
if not fields:
raise HTTPException(status_code=400,detail="No fields to update")
row=await Feedback.update_feedback(self.session,feedback_id,fields)
if not row:
raise HTTPException(status_code=404,detail="Feedback not found")
row=await self._load(row.id)
return serialize_feedback(row)

View File

@ -0,0 +1,9 @@
def serialize_interview(row) -> dict:
return {
"id": str(row.id),
"inbox_id": row.inbox_id,
"interview_date": row.interview_date.isoformat() if row.interview_date else None,
"interview_time": row.interview_time.isoformat() if row.interview_time else None,
"interview_type": row.interview_type,
"interview_status": row.interview_status,
}

View File

@ -0,0 +1,42 @@
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from job.candidate.models import Interviews
from job.interviews.serializers import serialize_interview
class Interview:
def __init__(self,session:AsyncSession):
self.session=session
async def get_interview(self,interview_id=None,inbox_id=None):
if interview_id:
row=await Interviews.get_interview_by_id(self.session,interview_id)
if not row:
raise HTTPException(status_code=404,detail="Interview not found")
return serialize_interview(row)
if inbox_id is None:
raise HTTPException(status_code=400,detail="interview_id or inbox_id is required")
rows=await Interviews.get_interviews_by_inbox(self.session,int(inbox_id))
return [serialize_interview(r) for r in rows]
async def create_interview(self,payload):
fields={
"interview_date":payload.get("interview_date"),
"interview_time":payload.get("interview_time"),
"interview_type":payload.get("interview_type") or "",
"interview_status":payload.get("interview_status") or "",
"inbox_id":payload.get("inbox_id"),
}
fields={k:v for k,v in fields.items() if v is not None}
row=await Interviews.insert_interview(self.session,fields)
return serialize_interview(row)
async def update_interview(self,interview_id,payload):
fields={k:v for k,v in payload.items() if v is not None}
if not fields:
raise HTTPException(status_code=400,detail="No fields to update")
row=await Interviews.update_interview(self.session,interview_id,fields)
if not row:
raise HTTPException(status_code=404,detail="Interview not found")
return serialize_interview(row)

View File

@ -21,6 +21,7 @@ def serialize_job_post(row) -> dict:
"status": row.status,
"buffer_error": row.buffer_error,
"created_by": str(row.created_by) if row.created_by else None,
"created_by_name": row.user.name if getattr(row, "user", None) else None,
"created_at": row.created_at.isoformat() if row.created_at else None,
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
}

View File

@ -0,0 +1,11 @@
def serialize_note(row) -> dict:
author=getattr(row,"author",None)
return {
"id": str(row.id),
"note": row.note,
"user_id": str(row.user_id) if row.user_id else None,
"created_by": str(row.created_by) if row.created_by else None,
"created_by_name": author.name if author else None,
"created_at": row.created_at.isoformat() if row.created_at else None,
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
}

View File

@ -0,0 +1,62 @@
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlmodel import select
from job.candidate.models import Notes
from job.notes.serializers import serialize_note
class Note:
def __init__(self,session:AsyncSession):
self.session=session
async def _load(self,record_id):
uid=Notes._as_uuid(record_id)
if uid is None:
return None
result=await self.session.execute(
select(Notes).options(selectinload(Notes.author)).where(Notes.id==uid)
)
return result.scalars().first()
async def get_note(self,note_id=None,user_id=None):
if note_id:
row=await self._load(note_id)
if not row:
raise HTTPException(status_code=404,detail="Note not found")
return serialize_note(row)
if not user_id:
raise HTTPException(status_code=400,detail="note_id or user_id is required")
uid=Notes._as_uuid(user_id)
if uid is None:
raise HTTPException(status_code=400,detail="Invalid user_id")
result=await self.session.execute(
select(Notes)
.options(selectinload(Notes.author))
.where(Notes.user_id==uid)
.order_by(Notes.created_at.desc())
)
return [serialize_note(r) for r in result.scalars().all()]
async def create_note(self,payload,current_user):
fields={
"note":payload.get("note") or "",
"user_id":payload.get("user_id"),
"created_by":current_user.get("id") if isinstance(current_user,dict) else None,
}
if not fields["user_id"]:
raise HTTPException(status_code=400,detail="user_id is required")
row=await Notes.insert_note(self.session,fields)
row=await self._load(row.id)
return serialize_note(row)
async def update_note(self,note_id,payload):
fields={k:v for k,v in payload.items() if v is not None and k in ("note",)}
if not fields:
raise HTTPException(status_code=400,detail="No fields to update")
row=await Notes.update_note(self.session,note_id,fields)
if not row:
raise HTTPException(status_code=404,detail="Note not found")
row=await self._load(row.id)
return serialize_note(row)

View File

@ -7,13 +7,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlmodel import Field, Relationship, SQLModel, select
from backend.job.candidate.models import Feedback
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 Notes
from job.candidate.models import Feedback, Notes
class Users(SQLModel, table=True):
__tablename__ = "users"
@ -35,11 +35,17 @@ class Users(SQLModel, table=True):
back_populates="user",
sa_relationship_kwargs={"lazy": "selectin"},
)
feedback:Optional[Feedback]=Relationship(back_populates="user",
sa_relationship_kwargs={"lazy": "selectin"},
feedback: List["Feedback"] = Relationship(
back_populates="user",
sa_relationship_kwargs={"lazy": "selectin"},
)
notes: Optional[Notes] = 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
@ -143,3 +149,6 @@ class Users(SQLModel, table=True):
await session.commit()
await session.refresh(user)
return user
import job.candidate.models as _candidate_models # noqa: E402, F401