pedning testinf
parent
e717dbf704
commit
c726bbacbc
|
|
@ -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()
|
||||
|
|
@ -46,34 +45,23 @@ 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"}
|
||||
)
|
||||
favorite: Optional[bool] = Field(default=False)
|
||||
rating: Optional[float] = Field(default=0.0)
|
||||
|
||||
interview_id: Optional[uuid.UUID] = Field(
|
||||
default=None,
|
||||
foreign_key="interviews.id",
|
||||
)
|
||||
interview: List[Interviews] = 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"}
|
||||
sa_relationship_kwargs={"lazy": "selectin"},
|
||||
)
|
||||
|
||||
activity_id: Optional[uuid.UUID] = Field(
|
||||
default=None,
|
||||
foreign_key="activity.id"
|
||||
)
|
||||
activity: Optional[Activity] = Relationship(
|
||||
activity: List["Activity"] = Relationship(
|
||||
back_populates="inbox",
|
||||
sa_relationship_kwargs={"lazy": "selectin"},
|
||||
)
|
||||
Optional[Users] = Relationship(
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue