Spaces:
Sleeping
Sleeping
Merge pull request #23 from yuvabe-ai-labs/feat/chatbot
Browse files- alembic/versions/d9bb355538fd_add_is_read_to_leave_table.py +34 -0
- src/main.py +3 -9
- src/profile/models.py +2 -0
- src/profile/service.py +105 -105
alembic/versions/d9bb355538fd_add_is_read_to_leave_table.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""add is_read to leave table
|
| 2 |
+
|
| 3 |
+
Revision ID: d9bb355538fd
|
| 4 |
+
Revises: e95f62f91348
|
| 5 |
+
Create Date: 2025-11-25 16:43:45.584602
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
import sqlmodel.sql.sqltypes
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# revision identifiers, used by Alembic.
|
| 16 |
+
revision: str = 'd9bb355538fd'
|
| 17 |
+
down_revision: Union[str, Sequence[str], None] = 'e95f62f91348'
|
| 18 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 19 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def upgrade():
|
| 23 |
+
op.add_column(
|
| 24 |
+
"leave",
|
| 25 |
+
sa.Column(
|
| 26 |
+
"is_read",
|
| 27 |
+
sa.Boolean(),
|
| 28 |
+
nullable=False,
|
| 29 |
+
server_default=sa.false()
|
| 30 |
+
)
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
def downgrade():
|
| 34 |
+
op.drop_column("leave", "is_read")
|
src/main.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
| 1 |
-
from src.profile.router import router as profile
|
| 2 |
from fastapi import FastAPI
|
| 3 |
|
| 4 |
import os
|
| 5 |
from src.auth.router import router as auth_router
|
| 6 |
-
from src.chatbot.router import router as
|
| 7 |
from src.core.database import init_db
|
| 8 |
from src.home.router import router as home_router
|
| 9 |
from src.notifications.router import router as notifications_router
|
| 10 |
-
|
| 11 |
from fastapi.staticfiles import StaticFiles
|
| 12 |
|
| 13 |
|
|
@@ -18,16 +17,11 @@ app.include_router(home_router, prefix="/home", tags=["Home"])
|
|
| 18 |
|
| 19 |
# init_db()
|
| 20 |
|
| 21 |
-
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 22 |
-
AUDIO_DIR = os.path.join(BASE_DIR, "static", "audio")
|
| 23 |
-
|
| 24 |
app.include_router(profile)
|
| 25 |
-
app.mount("/static/audio", StaticFiles(directory=AUDIO_DIR), name="audio")
|
| 26 |
-
print("Serving audio from:", AUDIO_DIR)
|
| 27 |
|
| 28 |
app.include_router(auth_router)
|
| 29 |
|
| 30 |
-
app.include_router(
|
| 31 |
|
| 32 |
app.include_router(notifications_router)
|
| 33 |
|
|
|
|
|
|
|
| 1 |
from fastapi import FastAPI
|
| 2 |
|
| 3 |
import os
|
| 4 |
from src.auth.router import router as auth_router
|
| 5 |
+
from src.chatbot.router import router as chatbot_router
|
| 6 |
from src.core.database import init_db
|
| 7 |
from src.home.router import router as home_router
|
| 8 |
from src.notifications.router import router as notifications_router
|
| 9 |
+
from src.profile.router import router as profile
|
| 10 |
from fastapi.staticfiles import StaticFiles
|
| 11 |
|
| 12 |
|
|
|
|
| 17 |
|
| 18 |
# init_db()
|
| 19 |
|
|
|
|
|
|
|
|
|
|
| 20 |
app.include_router(profile)
|
|
|
|
|
|
|
| 21 |
|
| 22 |
app.include_router(auth_router)
|
| 23 |
|
| 24 |
+
app.include_router(chatbot_router)
|
| 25 |
|
| 26 |
app.include_router(notifications_router)
|
| 27 |
|
src/profile/models.py
CHANGED
|
@@ -15,6 +15,7 @@ class LeaveType(str, Enum):
|
|
| 15 |
class LeaveStatus(str, Enum):
|
| 16 |
APPROVED = "Approved"
|
| 17 |
REJECTED = "Rejected"
|
|
|
|
| 18 |
PENDING = "Pending"
|
| 19 |
|
| 20 |
class Leave(SQLModel, table=True):
|
|
@@ -30,6 +31,7 @@ class Leave(SQLModel, table=True):
|
|
| 30 |
reason: str = Field(nullable=True)
|
| 31 |
status: LeaveStatus = Field(default=LeaveStatus.PENDING)
|
| 32 |
is_delivered: bool = Field(default= False)
|
|
|
|
| 33 |
requested_at: date = Field(default_factory=date.today)
|
| 34 |
updated_at: date = Field(default_factory=date.today)
|
| 35 |
reject_reason: Optional[str] = None
|
|
|
|
| 15 |
class LeaveStatus(str, Enum):
|
| 16 |
APPROVED = "Approved"
|
| 17 |
REJECTED = "Rejected"
|
| 18 |
+
CANCELLED = "Cancelled"
|
| 19 |
PENDING = "Pending"
|
| 20 |
|
| 21 |
class Leave(SQLModel, table=True):
|
|
|
|
| 31 |
reason: str = Field(nullable=True)
|
| 32 |
status: LeaveStatus = Field(default=LeaveStatus.PENDING)
|
| 33 |
is_delivered: bool = Field(default= False)
|
| 34 |
+
is_read: bool = Field(default=False)
|
| 35 |
requested_at: date = Field(default_factory=date.today)
|
| 36 |
updated_at: date = Field(default_factory=date.today)
|
| 37 |
reject_reason: Optional[str] = None
|
src/profile/service.py
CHANGED
|
@@ -21,13 +21,13 @@ from src.profile.schemas import CreateLeaveRequest, LeaveStatus, ApproveRejectRe
|
|
| 21 |
from src.notifications.fcm import send_fcm
|
| 22 |
|
| 23 |
|
| 24 |
-
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 25 |
|
| 26 |
-
# src/profile/service.py
|
| 27 |
|
| 28 |
-
# Leave limits (you can move to config)
|
| 29 |
-
SICK_LIMIT = getattr(settings, "SICK_LEAVE_LIMIT", 10)
|
| 30 |
-
CASUAL_LIMIT = getattr(settings, "CASUAL_LEAVE_LIMIT", 10)
|
| 31 |
|
| 32 |
|
| 33 |
async def _get_team_roles(session: AsyncSession, user_id: uuid.UUID):
|
|
@@ -135,7 +135,7 @@ async def create_leave(session, user_id, body):
|
|
| 135 |
leave.lead_id,
|
| 136 |
)
|
| 137 |
|
| 138 |
-
|
| 139 |
|
| 140 |
|
| 141 |
async def mentor_decide_leave(session, mentor_id, leave_id, body):
|
|
@@ -172,7 +172,7 @@ async def mentor_decide_leave(session, mentor_id, leave_id, body):
|
|
| 172 |
{"type": "leave_status"},
|
| 173 |
)
|
| 174 |
|
| 175 |
-
|
| 176 |
|
| 177 |
|
| 178 |
# async def apply_leave(session: AsyncSession, user_id, payload: ApplyLeaveRequest):
|
|
@@ -377,25 +377,25 @@ async def mentor_decide_leave(session, mentor_id, leave_id, body):
|
|
| 377 |
# return leave
|
| 378 |
|
| 379 |
|
| 380 |
-
async def add_device_token(session: AsyncSession, user_id, device_token: str):
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
|
| 398 |
-
|
| 399 |
|
| 400 |
|
| 401 |
# async def get_leave_balance(session: AsyncSession, user_id) -> List[dict]:
|
|
@@ -421,106 +421,106 @@ async def add_device_token(session: AsyncSession, user_id, device_token: str):
|
|
| 421 |
# ]
|
| 422 |
|
| 423 |
|
| 424 |
-
# In production, replace with DB storage
|
| 425 |
-
USER_TOKEN_STORE = {} # {google_user_id: {tokens}}
|
| 426 |
|
| 427 |
|
| 428 |
-
async def send_email_service(req: SendMailRequest):
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
|
| 433 |
-
|
| 434 |
-
|
| 435 |
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
|
| 441 |
-
|
| 442 |
-
|
| 443 |
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
|
| 452 |
-
|
| 453 |
-
|
| 454 |
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
|
| 460 |
-
|
| 461 |
-
|
| 462 |
|
| 463 |
-
|
| 464 |
|
| 465 |
|
| 466 |
-
async def update_user_profile(session, user_id: str, data):
|
| 467 |
-
|
| 468 |
|
| 469 |
-
|
| 470 |
-
|
| 471 |
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
|
| 504 |
-
|
| 505 |
-
|
| 506 |
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
async def list_user_assets(session: AsyncSession, user_id: str) -> List[Assets]:
|
| 525 |
-
|
| 526 |
-
|
|
|
|
| 21 |
from src.notifications.fcm import send_fcm
|
| 22 |
|
| 23 |
|
| 24 |
+
# pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 25 |
|
| 26 |
+
# # src/profile/service.py
|
| 27 |
|
| 28 |
+
# # Leave limits (you can move to config)
|
| 29 |
+
# SICK_LIMIT = getattr(settings, "SICK_LEAVE_LIMIT", 10)
|
| 30 |
+
# CASUAL_LIMIT = getattr(settings, "CASUAL_LEAVE_LIMIT", 10)
|
| 31 |
|
| 32 |
|
| 33 |
async def _get_team_roles(session: AsyncSession, user_id: uuid.UUID):
|
|
|
|
| 135 |
leave.lead_id,
|
| 136 |
)
|
| 137 |
|
| 138 |
+
# return leave
|
| 139 |
|
| 140 |
|
| 141 |
async def mentor_decide_leave(session, mentor_id, leave_id, body):
|
|
|
|
| 172 |
{"type": "leave_status"},
|
| 173 |
)
|
| 174 |
|
| 175 |
+
# return leave
|
| 176 |
|
| 177 |
|
| 178 |
# async def apply_leave(session: AsyncSession, user_id, payload: ApplyLeaveRequest):
|
|
|
|
| 377 |
# return leave
|
| 378 |
|
| 379 |
|
| 380 |
+
# async def add_device_token(session: AsyncSession, user_id, device_token: str):
|
| 381 |
+
# """
|
| 382 |
+
# Add FCM token to Users.device_tokens ARRAY.
|
| 383 |
+
# Avoid duplicates.
|
| 384 |
+
# """
|
| 385 |
|
| 386 |
+
# # 1) Fetch user
|
| 387 |
+
# user = await session.get(Users, user_id)
|
| 388 |
+
# if not user:
|
| 389 |
+
# raise HTTPException(404, "User not found")
|
| 390 |
|
| 391 |
+
# # 2) If token not present -> add it
|
| 392 |
+
# if device_token not in user.device_tokens:
|
| 393 |
+
# user.device_tokens.append(device_token)
|
| 394 |
+
# session.add(user)
|
| 395 |
+
# await session.commit()
|
| 396 |
+
# await session.refresh(user)
|
| 397 |
|
| 398 |
+
# return user.device_tokens
|
| 399 |
|
| 400 |
|
| 401 |
# async def get_leave_balance(session: AsyncSession, user_id) -> List[dict]:
|
|
|
|
| 421 |
# ]
|
| 422 |
|
| 423 |
|
| 424 |
+
# # In production, replace with DB storage
|
| 425 |
+
# USER_TOKEN_STORE = {} # {google_user_id: {tokens}}
|
| 426 |
|
| 427 |
|
| 428 |
+
# async def send_email_service(req: SendMailRequest):
|
| 429 |
+
# record = USER_TOKEN_STORE.get(req.user_id)
|
| 430 |
+
# if not record:
|
| 431 |
+
# raise HTTPException(404, "User not logged in with Google OAuth")
|
| 432 |
|
| 433 |
+
# access_token = record["access_token"]
|
| 434 |
+
# refresh_token = record.get("refresh_token")
|
| 435 |
|
| 436 |
+
# if not access_token and refresh_token:
|
| 437 |
+
# new_tokens = await refresh_access_token(refresh_token)
|
| 438 |
+
# access_token = new_tokens["access_token"]
|
| 439 |
+
# record["access_token"] = access_token
|
| 440 |
|
| 441 |
+
# if not access_token:
|
| 442 |
+
# raise HTTPException(400, "Re-auth required")
|
| 443 |
|
| 444 |
+
# raw = build_raw_message(
|
| 445 |
+
# to_email=req.to,
|
| 446 |
+
# subject=req.subject,
|
| 447 |
+
# body=req.body,
|
| 448 |
+
# from_name=req.from_name,
|
| 449 |
+
# from_email=record["email"],
|
| 450 |
+
# )
|
| 451 |
|
| 452 |
+
# url = "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
|
| 453 |
+
# payload = {"raw": raw}
|
| 454 |
|
| 455 |
+
# async with httpx.AsyncClient() as client:
|
| 456 |
+
# r = await client.post(
|
| 457 |
+
# url, json=payload, headers={"Authorization": f"Bearer {access_token}"}
|
| 458 |
+
# )
|
| 459 |
|
| 460 |
+
# if r.status_code >= 400:
|
| 461 |
+
# raise HTTPException(500, f"Gmail error: {r.text}")
|
| 462 |
|
| 463 |
+
# return r.json()
|
| 464 |
|
| 465 |
|
| 466 |
+
# async def update_user_profile(session, user_id: str, data):
|
| 467 |
+
# user = await session.get(Users, uuid.UUID(user_id))
|
| 468 |
|
| 469 |
+
# if not user:
|
| 470 |
+
# raise HTTPException(status_code=404, detail="User not found")
|
| 471 |
|
| 472 |
+
# # --- Update Name ---
|
| 473 |
+
# if data.name:
|
| 474 |
+
# user.user_name = data.name
|
| 475 |
|
| 476 |
+
# # --- Update Email ---
|
| 477 |
+
# if data.email:
|
| 478 |
+
# user.email_id = data.email
|
| 479 |
|
| 480 |
+
# # --- Update DOB ---
|
| 481 |
+
# if data.dob:
|
| 482 |
+
# try:
|
| 483 |
+
# # Convert DD.MM.YYYY → Python date
|
| 484 |
+
# parsed_date = datetime.strptime(data.dob, "%d.%m.%Y").date()
|
| 485 |
+
# user.dob = parsed_date
|
| 486 |
+
# except:
|
| 487 |
+
# raise HTTPException(
|
| 488 |
+
# status_code=400, detail="DOB must be in DD.MM.YYYY format"
|
| 489 |
+
# )
|
| 490 |
|
| 491 |
+
# # --- Update Address ---
|
| 492 |
+
# if data.address:
|
| 493 |
+
# user.address = data.address
|
| 494 |
|
| 495 |
+
# # --- Change Password ---
|
| 496 |
+
# if data.new_password:
|
| 497 |
+
# if not data.current_password:
|
| 498 |
+
# raise HTTPException(status_code=400, detail="Current password required")
|
| 499 |
|
| 500 |
+
# # Verify old password
|
| 501 |
+
# if not pwd_context.verify(data.current_password, user.password):
|
| 502 |
+
# raise HTTPException(status_code=400, detail="Incorrect current password")
|
| 503 |
|
| 504 |
+
# # Set new password
|
| 505 |
+
# user.password = pwd_context.hash(data.new_password)
|
| 506 |
|
| 507 |
+
# # Commit changes
|
| 508 |
+
# await session.commit()
|
| 509 |
+
# await session.refresh(user)
|
| 510 |
+
|
| 511 |
+
# return {
|
| 512 |
+
# "message": "Profile updated successfully",
|
| 513 |
+
# "user": {
|
| 514 |
+
# "id": str(user.id),
|
| 515 |
+
# "name": user.user_name,
|
| 516 |
+
# "email": user.email_id,
|
| 517 |
+
# "dob": user.dob.isoformat() if user.dob else None,
|
| 518 |
+
# "address": user.address,
|
| 519 |
+
# "is_verified": user.is_verified,
|
| 520 |
+
# },
|
| 521 |
+
# }
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
# async def list_user_assets(session: AsyncSession, user_id: str) -> List[Assets]:
|
| 525 |
+
# q = await session.exec(select(Assets).where(Assets.user_id == uuid.UUID(user_id)))
|
| 526 |
+
# return q.all()
|