From 381d7a7bdd9c8ebc2944da8bcbb20bdc0d38f2ac Mon Sep 17 00:00:00 2001 From: "ahmed.mujtaba" Date: Fri, 11 Sep 2026 16:18:01 +0500 Subject: [PATCH] committed --- backend/g_sheet/app.py | 22 ++++++++++++++++++++++ backend/g_sheet/enums.py | 2 ++ backend/g_sheet/models.py | 19 +++++++++++++++++++ backend/g_sheet/views.py | 9 +++++++++ backend/job/app.py | 22 ++++++++++++++++++++++ backend/job/candidate/models.py | 19 +++++++++++++++++++ backend/job/candidate/serializers.py | 2 ++ backend/job/candidate/views.py | 14 ++++++++++++++ 8 files changed, 109 insertions(+) diff --git a/backend/g_sheet/app.py b/backend/g_sheet/app.py index b98227f..a18fe53 100644 --- a/backend/g_sheet/app.py +++ b/backend/g_sheet/app.py @@ -198,6 +198,11 @@ class FormDuplicateBody(BaseModel): is_duplicate: bool +class FormDataRatingBody(BaseModel): + favorite: bool | None = None + rating: float | None = None + + @router.get("/sheet/form-data/sheets") async def fetch_form_data_sheets( current_user: dict = Depends(_FORM_DATA_READ), @@ -364,6 +369,23 @@ async def set_form_duplicate( raise HTTPException(status_code=500,detail=str(e)) +@router.patch("/sheet/form-data/{record_id}/rating") +async def set_form_rating( + record_id: str, + payload: FormDataRatingBody, + current_user: dict = Depends(_FORM_DATA_EDIT), + session: AsyncSession = Depends(get_session), +): + try: + service=SheetFormData(session=session) + data=await service.set_rating(record_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.delete("/sheet/form-data/{tab}/delete") async def delete_form_data_sheet( tab: str, diff --git a/backend/g_sheet/enums.py b/backend/g_sheet/enums.py index 5ee5910..847edd8 100644 --- a/backend/g_sheet/enums.py +++ b/backend/g_sheet/enums.py @@ -225,6 +225,8 @@ class FormDataColumn(str, Enum): # Same vocabulary as inbox_messages — Import / Shortlist / Reject / Duplicate. PROCESSING_STATE = "processing_state" IS_DUPLICATE = "is_duplicate" + FAVORITE = "favorite" + RATING = "rating" REAPPLIED = "reapplied" RAW_RECORD = "raw_record" IMPORTED_AT = "imported_at" diff --git a/backend/g_sheet/models.py b/backend/g_sheet/models.py index 4a5995b..c2ce38c 100644 --- a/backend/g_sheet/models.py +++ b/backend/g_sheet/models.py @@ -107,6 +107,10 @@ class FormData(SQLModel, table=True): processing_state: str = Field(default="unread", sa_column_kwargs={"server_default": "unread"}) is_duplicate: bool = Field(default=False, sa_column_kwargs={"server_default": "false"}) + # Recruiter-set, same shape as inbox.favorite/inbox.rating. + favorite: bool | None = Field(default=False) + rating: float | None = Field(default=0.0) + raw_record: dict | None = Field(default=None, sa_column=Column(JSONB)) imported_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True)) created_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True)) @@ -439,6 +443,21 @@ class FormData(SQLModel, table=True): await session.refresh(row) return row + @classmethod + async def set_rating_favorite(cls, session: AsyncSession, record_id, favorite=None, rating=None): + row = await cls.get_form_data_by_id(session, record_id) + if not row: + return None + if favorite is not None: + row.favorite = bool(favorite) + if rating is not None: + row.rating = float(rating) + row.updated_at = _now() + session.add(row) + await session.commit() + await session.refresh(row) + return row + @classmethod async def link_manual_upload(cls, session: AsyncSession, record_id, manual_upload_candidate_id, *, commit: bool = True): row = await cls.get_form_data_by_id(session, record_id) diff --git a/backend/g_sheet/views.py b/backend/g_sheet/views.py index f95ff5d..f78b346 100644 --- a/backend/g_sheet/views.py +++ b/backend/g_sheet/views.py @@ -603,6 +603,15 @@ class SheetFormData(Sheet): raise HTTPException(status_code=404,detail="Form data not found") return await self.get_form_data_by_id(record_id) + async def set_rating(self,record_id,payload): + 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") + updated=await FormData.set_rating_favorite(self._require_session(),record_id,**fields) + if not updated: + raise HTTPException(status_code=404,detail="Form data not found") + return await self.get_form_data_by_id(record_id) + async def get_counts(self,sheet=None,search=None,has_linkedin=None,has_resume=None,city=None,source=None,assigned=None,job_post_ids=None): return await FormData.count_processing( self._require_session(),sheet=sheet,search=search, diff --git a/backend/job/app.py b/backend/job/app.py index 0f4eec9..b434aa5 100644 --- a/backend/job/app.py +++ b/backend/job/app.py @@ -57,6 +57,11 @@ class CandidateUpdate(BaseModel): rating: float | None = None +class ManualUploadRatingUpdate(BaseModel): + favorite: bool | None = None + rating: float | None = None + + class InterviewCreate(BaseModel): inbox_id: int interview_date: datetime | None = None @@ -1115,6 +1120,23 @@ async def update_candidate( raise HTTPException(status_code=500,detail=str(e)) +@router.patch("/candidate/manual-upload/rating") +async def update_manual_upload_rating( + id:str=Query(...), + payload:ManualUploadRatingUpdate=..., + current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)), + session: AsyncSession = Depends(get_session), +): + try: + service=CandidateView(session=session) + data=await service.update_manual_upload_rating(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("/candidate/history/fetch") async def fetch_candidate_history( user_id:str=Query(...), diff --git a/backend/job/candidate/models.py b/backend/job/candidate/models.py index e1d56de..846c39e 100644 --- a/backend/job/candidate/models.py +++ b/backend/job/candidate/models.py @@ -72,6 +72,10 @@ class Manual_UPLOAD_CANDIDATE(SQLModel, table=True): referral_by: str = Field(default="", sa_column_kwargs={"server_default": ""}) file_name: str = Field(default="", sa_column_kwargs={"server_default": ""}) file_path: str = Field(default="", sa_column_kwargs={"server_default": ""}) + # Recruiter-set, same shape as inbox.favorite/inbox.rating — this row's own + # copy, not derived from Inbox (manual_upload_candidate has no inbox link). + favorite: Optional[bool] = Field(default=False) + rating: Optional[float] = Field(default=0.0) created_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True)) updated_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True)) @@ -395,6 +399,21 @@ class Manual_UPLOAD_CANDIDATE(SQLModel, table=True): result = await session.execute(select(cls).where(cls.id == uid)) return result.scalars().first() + @classmethod + async def set_rating_favorite(cls, session: AsyncSession, record_id, favorite=None, rating=None): + row=await cls.get_by_id(session,record_id) + if not row: + return None + if favorite is not None: + row.favorite=favorite + if rating is not None: + row.rating=rating + row.updated_at=_now() + session.add(row) + await session.commit() + await session.refresh(row) + return row + @classmethod async def set_full_text(cls, session: AsyncSession, record_id, text, *, only_if_empty=True): """Keep extracted CV text on the bank/manual row (Inbox uses resume_text).""" diff --git a/backend/job/candidate/serializers.py b/backend/job/candidate/serializers.py index f00bc98..5dde2aa 100644 --- a/backend/job/candidate/serializers.py +++ b/backend/job/candidate/serializers.py @@ -202,6 +202,8 @@ def serialize_manual_upload_candidate(row) -> Dict[str,Any]: "referral_by":row.referral_by, "file_name":row.file_name, "file_path":row.file_path, + "favorite":row.favorite, + "rating":row.rating, "created_at":row.created_at.isoformat() if row.created_at else None, "updated_at":row.updated_at.isoformat() if row.updated_at else None, } diff --git a/backend/job/candidate/views.py b/backend/job/candidate/views.py index 37ca323..34529dc 100644 --- a/backend/job/candidate/views.py +++ b/backend/job/candidate/views.py @@ -1605,6 +1605,20 @@ class CandidateView: except Exception as e: raise HTTPException(status_code=500,detail=str(e)) + async def update_manual_upload_rating(self,record_id,payload): + try: + 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") + row=await Manual_UPLOAD_CANDIDATE.set_rating_favorite(self.session,record_id,**fields) + if not row: + raise HTTPException(status_code=404,detail="Candidate not found") + return serialize_manual_upload_candidate(row) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500,detail=str(e)) + @staticmethod def _attach_job_post(data,payload,*,as_assigned=False): """Merge one serialized job post onto a candidate payload.