Arquitectura organizada en capas
This commit is contained in:
parent
c9982f01e8
commit
ef13c211ad
|
|
@ -5,3 +5,4 @@ backend/venv/**
|
|||
backend/alembic/.DS_Store
|
||||
diseño.md
|
||||
backend/uploads/**
|
||||
**/__pycache__
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ from sqlalchemy import pool
|
|||
|
||||
from alembic import context
|
||||
|
||||
from app.db.database import Base
|
||||
from app.models import *
|
||||
from app.db.base import Base # noqa
|
||||
target_metadata = Base.metadata
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
from typing import Generator
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import jwt, JWTError
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.config import settings
|
||||
from app.db.database import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas.token import TokenData
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(
|
||||
tokenUrl=f"{settings.API_V1_STR}/token"
|
||||
)
|
||||
|
||||
def get_current_user(
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(oauth2_scheme)
|
||||
) -> User:
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
||||
)
|
||||
email: str = payload.get("sub")
|
||||
if email is None:
|
||||
raise credentials_exception
|
||||
token_data = TokenData(email=email)
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
user = db.query(User).filter(User.email == token_data.email).first()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
def get_current_active_user(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
from fastapi import APIRouter
|
||||
from app.api.v1.endpoints import (
|
||||
auth,
|
||||
users,
|
||||
projects,
|
||||
activities,
|
||||
specialties,
|
||||
contractors,
|
||||
transcription,
|
||||
non_conformities,
|
||||
guest
|
||||
)
|
||||
|
||||
from app.api.deps import get_current_active_user
|
||||
from fastapi import Depends
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(auth.router, tags=["Authentication"])
|
||||
api_router.include_router(users.router, prefix="/users", tags=["Users"], dependencies=[Depends(get_current_active_user)])
|
||||
api_router.include_router(projects.router, prefix="/projects", tags=["Projects"], dependencies=[Depends(get_current_active_user)])
|
||||
api_router.include_router(activities.router, prefix="/activities", tags=["Activities"], dependencies=[Depends(get_current_active_user)])
|
||||
api_router.include_router(specialties.router, prefix="/specialties", tags=["Specialties"], dependencies=[Depends(get_current_active_user)])
|
||||
api_router.include_router(contractors.router, prefix="/contractors", tags=["Contractors"], dependencies=[Depends(get_current_active_user)])
|
||||
api_router.include_router(transcription.router, prefix="/transcription", tags=["Transcription"], dependencies=[Depends(get_current_active_user)])
|
||||
api_router.include_router(non_conformities.router, prefix="/non-conformities", tags=["Non Conformities"], dependencies=[Depends(get_current_active_user)])
|
||||
api_router.include_router(guest.router, prefix="/guest", tags=["Guest Access"])
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
from fastapi import APIRouter, Depends, UploadFile, File, BackgroundTasks
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from app.db.database import get_db
|
||||
from app.models.user import User
|
||||
from app.api.deps import get_current_active_user
|
||||
from app.services.activities import ActivityService
|
||||
from app.schemas.activity import Activity, ActivityCreate, ActivityUpdate
|
||||
from app.schemas.evidence import Evidence, EvidenceUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def get_activity_service(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
background_tasks: BackgroundTasks = None
|
||||
) -> ActivityService:
|
||||
return ActivityService(db, current_user, background_tasks)
|
||||
|
||||
@router.post("/", response_model=Activity)
|
||||
def create_activity(
|
||||
activity: ActivityCreate,
|
||||
service: ActivityService = Depends(get_activity_service)
|
||||
):
|
||||
return service.create_activity(activity)
|
||||
|
||||
@router.get("/", response_model=List[Activity])
|
||||
def read_activities(
|
||||
project_id: Optional[int] = None,
|
||||
specialty_id: Optional[int] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
service: ActivityService = Depends(get_activity_service)
|
||||
):
|
||||
return service.get_activities(project_id, specialty_id, skip, limit)
|
||||
|
||||
@router.get("/{activity_id}", response_model=Activity)
|
||||
def read_activity(
|
||||
activity_id: int,
|
||||
service: ActivityService = Depends(get_activity_service)
|
||||
):
|
||||
return service.get_activity(activity_id)
|
||||
|
||||
@router.put("/{activity_id}", response_model=Activity)
|
||||
def update_activity(
|
||||
activity_id: int,
|
||||
activity: ActivityUpdate,
|
||||
service: ActivityService = Depends(get_activity_service)
|
||||
):
|
||||
return service.update_activity(activity_id, activity)
|
||||
|
||||
@router.post("/{activity_id}/upload", response_model=Evidence)
|
||||
async def upload_evidence(
|
||||
activity_id: int,
|
||||
file: UploadFile = File(...),
|
||||
description: Optional[str] = None,
|
||||
captured_at: Optional[str] = None,
|
||||
service: ActivityService = Depends(get_activity_service)
|
||||
):
|
||||
return service.upload_evidence(
|
||||
activity_id,
|
||||
file,
|
||||
description,
|
||||
captured_at
|
||||
)
|
||||
|
||||
@router.post("/evidence/{evidence_id}/retry-transcription", response_model=Evidence)
|
||||
async def retry_transcription(
|
||||
evidence_id: int,
|
||||
service: ActivityService = Depends(get_activity_service)
|
||||
):
|
||||
return service.retry_transcription(evidence_id)
|
||||
|
||||
@router.put("/evidence/{evidence_id}", response_model=Evidence)
|
||||
def update_evidence(
|
||||
evidence_id: int,
|
||||
evidence: EvidenceUpdate,
|
||||
service: ActivityService = Depends(get_activity_service)
|
||||
):
|
||||
return service.update_evidence(evidence_id, evidence)
|
||||
|
||||
@router.delete("/evidence/{evidence_id}")
|
||||
def delete_evidence(
|
||||
evidence_id: int,
|
||||
service: ActivityService = Depends(get_activity_service)
|
||||
):
|
||||
return service.delete_evidence(evidence_id)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.database import get_db
|
||||
from app.services.auth_service import AuthService
|
||||
from app.schemas.token import Token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/token", response_model=Token)
|
||||
def login_for_access_token(
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
user = AuthService.authenticate_user(db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return AuthService.create_user_token(user)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
from fastapi import APIRouter, Depends, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from app.db.database import get_db
|
||||
from app.services.contractors import ContractorService
|
||||
from app.schemas.contractor import Contractor, ContractorCreate, ContractorUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def get_contractor_service(db: Session = Depends(get_db)) -> ContractorService:
|
||||
return ContractorService(db)
|
||||
|
||||
@router.post("/", response_model=Contractor)
|
||||
def create_contractor(
|
||||
contractor: ContractorCreate,
|
||||
service: ContractorService = Depends(get_contractor_service)
|
||||
):
|
||||
return service.create_contractor(contractor)
|
||||
|
||||
@router.get("/", response_model=List[Contractor])
|
||||
def read_contractors(
|
||||
parent_id: Optional[int] = None,
|
||||
only_parents: bool = False,
|
||||
is_active: Optional[bool] = None,
|
||||
service: ContractorService = Depends(get_contractor_service)
|
||||
):
|
||||
return service.get_contractors(parent_id, only_parents, is_active)
|
||||
|
||||
@router.get("/{contractor_id}", response_model=Contractor)
|
||||
def read_contractor(
|
||||
contractor_id: int,
|
||||
service: ContractorService = Depends(get_contractor_service)
|
||||
):
|
||||
return service.get_contractor(contractor_id)
|
||||
|
||||
@router.put("/{contractor_id}", response_model=Contractor)
|
||||
@router.patch("/{contractor_id}", response_model=Contractor)
|
||||
def update_contractor(
|
||||
contractor_id: int,
|
||||
contractor: ContractorUpdate,
|
||||
service: ContractorService = Depends(get_contractor_service)
|
||||
):
|
||||
return service.update_contractor(contractor_id, contractor)
|
||||
|
||||
@router.delete("/{contractor_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_contractor(
|
||||
contractor_id: int,
|
||||
service: ContractorService = Depends(get_contractor_service)
|
||||
):
|
||||
return service.delete_contractor(contractor_id)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
from fastapi import APIRouter, Depends, UploadFile, File
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from app.db.database import get_db
|
||||
from app.services.guest import GuestService
|
||||
from app.schemas.non_conformity import NonConformity, NonConformityUpdate
|
||||
from app.schemas.evidence import Evidence
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def get_guest_service(db: Session = Depends(get_db)) -> GuestService:
|
||||
return GuestService(db)
|
||||
|
||||
@router.get("/nc/{access_hash}", response_model=NonConformity)
|
||||
def read_guest_nc(
|
||||
access_hash: str,
|
||||
service: GuestService = Depends(get_guest_service)
|
||||
):
|
||||
return service.get_nc_by_hash(access_hash)
|
||||
|
||||
@router.patch("/nc/{access_hash}", response_model=NonConformity)
|
||||
def update_guest_nc(
|
||||
access_hash: str,
|
||||
nc_update: NonConformityUpdate,
|
||||
service: GuestService = Depends(get_guest_service)
|
||||
):
|
||||
return service.update_guest_nc(access_hash, nc_update)
|
||||
|
||||
@router.post("/nc/{access_hash}/upload", response_model=Evidence)
|
||||
async def upload_guest_evidence(
|
||||
access_hash: str,
|
||||
file: UploadFile = File(...),
|
||||
description: Optional[str] = None,
|
||||
service: GuestService = Depends(get_guest_service)
|
||||
):
|
||||
return service.upload_guest_evidence(access_hash, file, description)
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
from fastapi import APIRouter, Depends, UploadFile, File
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from app.db.database import get_db
|
||||
from app.services.non_conformities import NonConformityService
|
||||
from app.schemas.non_conformity import NonConformity, NonConformityCreate, NonConformityUpdate
|
||||
from app.schemas.evidence import Evidence
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def get_nc_service(db: Session = Depends(get_db)) -> NonConformityService:
|
||||
return NonConformityService(db)
|
||||
|
||||
@router.post("/", response_model=NonConformity)
|
||||
def create_nc(
|
||||
nc: NonConformityCreate,
|
||||
service: NonConformityService = Depends(get_nc_service)
|
||||
):
|
||||
return service.create_nc(nc)
|
||||
|
||||
@router.get("/", response_model=List[NonConformity])
|
||||
def read_ncs(
|
||||
activity_id: Optional[int] = None,
|
||||
status: Optional[str] = None,
|
||||
service: NonConformityService = Depends(get_nc_service)
|
||||
):
|
||||
return service.get_ncs(activity_id, status)
|
||||
|
||||
@router.get("/{nc_id}", response_model=NonConformity)
|
||||
def read_nc(
|
||||
nc_id: int,
|
||||
service: NonConformityService = Depends(get_nc_service)
|
||||
):
|
||||
return service.get_nc(nc_id)
|
||||
|
||||
@router.put("/{nc_id}", response_model=NonConformity)
|
||||
@router.patch("/{nc_id}", response_model=NonConformity)
|
||||
def update_nc(
|
||||
nc_id: int,
|
||||
nc: NonConformityUpdate,
|
||||
service: NonConformityService = Depends(get_nc_service)
|
||||
):
|
||||
return service.update_nc(nc_id, nc)
|
||||
|
||||
@router.post("/{nc_id}/upload", response_model=Evidence)
|
||||
async def upload_nc_evidence(
|
||||
nc_id: int,
|
||||
file: UploadFile = File(...),
|
||||
description: Optional[str] = None,
|
||||
captured_at: Optional[str] = None,
|
||||
service: NonConformityService = Depends(get_nc_service)
|
||||
):
|
||||
return service.upload_nc_evidence(nc_id, file, description, captured_at)
|
||||
|
||||
@router.delete("/{nc_id}")
|
||||
def delete_nc(
|
||||
nc_id: int,
|
||||
service: NonConformityService = Depends(get_nc_service)
|
||||
):
|
||||
return service.delete_nc(nc_id)
|
||||
|
||||
@router.post("/{nc_id}/notify")
|
||||
def notify_responsible(
|
||||
nc_id: int,
|
||||
service: NonConformityService = Depends(get_nc_service)
|
||||
):
|
||||
return service.notify_responsible(nc_id)
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
from fastapi import APIRouter, Depends, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.db.database import get_db
|
||||
from app.services.projects import ProjectService
|
||||
from app.schemas.project import Project, ProjectCreate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def get_project_service(db: Session = Depends(get_db)) -> ProjectService:
|
||||
return ProjectService(db)
|
||||
|
||||
@router.post("/", response_model=Project)
|
||||
def create_project(
|
||||
project: ProjectCreate,
|
||||
service: ProjectService = Depends(get_project_service)
|
||||
):
|
||||
return service.create_project(project)
|
||||
|
||||
@router.get("/", response_model=List[Project])
|
||||
def read_projects(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
service: ProjectService = Depends(get_project_service)
|
||||
):
|
||||
return service.get_projects(skip=skip, limit=limit)
|
||||
|
||||
@router.get("/{project_id}", response_model=Project)
|
||||
def read_project(
|
||||
project_id: int,
|
||||
service: ProjectService = Depends(get_project_service)
|
||||
):
|
||||
return service.get_project(project_id)
|
||||
|
||||
@router.put("/{project_id}", response_model=Project)
|
||||
def update_project(
|
||||
project_id: int,
|
||||
project: ProjectCreate,
|
||||
service: ProjectService = Depends(get_project_service)
|
||||
):
|
||||
return service.update_project(project_id, project)
|
||||
|
||||
@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_project(
|
||||
project_id: int,
|
||||
service: ProjectService = Depends(get_project_service)
|
||||
):
|
||||
return service.delete_project(project_id)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.db.database import get_db
|
||||
from app.models.specialty import Specialty
|
||||
from app.schemas.specialty import Specialty as SpecialtySchema
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=List[SpecialtySchema])
|
||||
def read_specialties(db: Session = Depends(get_db)):
|
||||
return db.query(Specialty).all()
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
from fastapi import APIRouter, File, UploadFile
|
||||
from app.services.transcription import TranscriptionService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/")
|
||||
async def transcribe_audio(file: UploadFile = File(...)):
|
||||
text = await TranscriptionService.transcribe_audio(file)
|
||||
return {"text": text}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.db.database import get_db
|
||||
from app.models.user import User as UserModel
|
||||
from app.api.deps import get_current_active_user
|
||||
from app.services.users import UserService
|
||||
from app.schemas.user import UserCreate, User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def get_user_service(db: Session = Depends(get_db)) -> UserService:
|
||||
return UserService(db)
|
||||
|
||||
@router.post("/", response_model=User)
|
||||
def create_user(user: UserCreate, service: UserService = Depends(get_user_service)):
|
||||
return service.create_user(user)
|
||||
|
||||
@router.get("/", response_model=List[User])
|
||||
def read_users(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
service: UserService = Depends(get_user_service)
|
||||
):
|
||||
return service.get_users(skip=skip, limit=limit)
|
||||
|
||||
@router.get("/me", response_model=User)
|
||||
def read_users_me(current_user: UserModel = Depends(get_current_active_user)):
|
||||
return current_user
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import os
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import AnyHttpUrl
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from typing import List, Optional
|
||||
|
||||
load_dotenv()
|
||||
|
||||
class Settings(BaseSettings):
|
||||
PROJECT_NAME: str = "SumaQ Backend"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
|
||||
# Security
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "Bt50MaUvRYJ28UOIberyBlRVQCcKiYzVF2JHOFKjbBQq5xoOpowyxjY1tCOEzYEL")
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 8 days
|
||||
|
||||
# Base de Datos
|
||||
# DATABASE_URL: Optional[str] = os.getenv("DATABASE_URL")
|
||||
DATABASE_URL: Optional[str] = (
|
||||
f"postgresql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}"
|
||||
f"@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
|
||||
)
|
||||
|
||||
# Google API
|
||||
GOOGLE_API_KEY: Optional[str] = os.getenv("GOOGLE_API_KEY")
|
||||
|
||||
# Email
|
||||
SMTP_HOST: Optional[str] = os.getenv("SMTP_HOST")
|
||||
SMTP_PORT: Optional[int] = os.getenv("SMTP_PORT")
|
||||
SMTP_USER: Optional[str] = os.getenv("SMTP_USER")
|
||||
SMTP_PASSWORD: Optional[str] = os.getenv("SMTP_PASSWORD")
|
||||
EMAILS_FROM_NAME: str = os.getenv("EMAILS_FROM_NAME", "Sistema SumaQ")
|
||||
EMAILS_FROM_EMAIL: Optional[str] = os.getenv("EMAILS_FROM_EMAIL")
|
||||
|
||||
# Frontend
|
||||
FRONTEND_URL: str = os.getenv("FRONTEND_URL", "http://localhost:4200")
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
case_sensitive=True,
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
extra="ignore"
|
||||
)
|
||||
|
||||
settings = Settings()
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Union, Any
|
||||
from jose import jwt
|
||||
from passlib.context import CryptContext
|
||||
import hashlib
|
||||
from app.core.config import settings
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
# Pre-hash the password with SHA-256 to ensure it's within bcrypt's 72-byte limit
|
||||
pre_hashed = hashlib.sha256(plain_password.encode()).hexdigest()
|
||||
|
||||
# Try verifying the pre-hashed password first
|
||||
if pwd_context.verify(pre_hashed, hashed_password):
|
||||
return True
|
||||
|
||||
# Fallback: Try verifying the raw password (legacy)
|
||||
try:
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
# Pre-hash with SHA-256 to handle any length and stay under bcrypt's 72-byte limit
|
||||
pre_hashed = hashlib.sha256(password.encode()).hexdigest()
|
||||
return pwd_context.hash(pre_hashed)
|
||||
|
||||
def create_access_token(subject: Union[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(
|
||||
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode = {"exp": expire, "sub": str(subject)}
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# Import all the models, so that Base has them before being
|
||||
# imported by Alembic
|
||||
from app.db.database import Base # noqa
|
||||
from app.models.models import * # noqa
|
||||
|
|
@ -1,21 +1,16 @@
|
|||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
from app.core.config import settings
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Using PostgreSQL for production
|
||||
SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:postgresql@localhost:5432/postgres")
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL
|
||||
)
|
||||
# Use settings.DATABASE_URL
|
||||
engine = create_engine(settings.DATABASE_URL)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
def get_db():
|
||||
with SessionLocal() as session:
|
||||
yield session
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from app.db.database import engine, Base, SessionLocal
|
||||
from app.models import User, Project, Specialty, Contractor, Activity, NonConformity, Evidence, UserRole
|
||||
from app.security import get_password_hash # Import hashing function
|
||||
from app.core.security import get_password_hash # Import hashing function from new core
|
||||
import datetime
|
||||
|
||||
def init_db():
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
from .user import User, UserRole
|
||||
from .project import Project, project_specialties, project_contractors
|
||||
from .specialty import Specialty
|
||||
from .contractor import Contractor
|
||||
from .activity import Activity, ActivityType
|
||||
from .non_conformity import NonConformity, NCLevel, NCType
|
||||
from .evidence import Evidence
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Text, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.database import Base
|
||||
import datetime
|
||||
import enum
|
||||
|
||||
class ActivityType(str, enum.Enum):
|
||||
INSPECTION = "inspection"
|
||||
MEETING = "meeting"
|
||||
VIRTUAL_MEETING = "virtual_meeting"
|
||||
COORDINATION = "coordination"
|
||||
TEST = "test"
|
||||
OTHER = "other"
|
||||
|
||||
class Activity(Base):
|
||||
__tablename__ = "activities"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
project_id = Column(Integer, ForeignKey("projects.id"))
|
||||
specialty_id = Column(Integer, ForeignKey("specialties.id"))
|
||||
contractor_id = Column(Integer, ForeignKey("contractors.id"), nullable=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id")) # Reporter
|
||||
|
||||
date = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
end_date = Column(DateTime, nullable=True)
|
||||
type = Column(Enum(ActivityType), default=ActivityType.INSPECTION)
|
||||
area = Column(String) # Frente de obra / Linea
|
||||
description = Column(Text)
|
||||
observations = Column(Text)
|
||||
audio_transcription = Column(Text, nullable=True)
|
||||
status = Column(String, default="completed")
|
||||
|
||||
project = relationship("app.models.project.Project", back_populates="activities")
|
||||
specialty = relationship("app.models.specialty.Specialty")
|
||||
contractor = relationship("app.models.contractor.Contractor")
|
||||
reporter = relationship("app.models.user.User")
|
||||
|
||||
non_conformities = relationship("NonConformity", back_populates="activity")
|
||||
evidences = relationship("Evidence", back_populates="activity")
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.database import Base
|
||||
|
||||
class Contractor(Base):
|
||||
__tablename__ = "contractors"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
ruc = Column(String, unique=True)
|
||||
contact_name = Column(String)
|
||||
email = Column(String, nullable=True)
|
||||
phone = Column(String, nullable=True)
|
||||
address = Column(String, nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
specialty_id = Column(Integer, ForeignKey("specialties.id"), nullable=True)
|
||||
parent_id = Column(Integer, ForeignKey("contractors.id"), nullable=True)
|
||||
|
||||
specialty = relationship("app.models.specialty.Specialty")
|
||||
parent = relationship("Contractor", remote_side=[id], back_populates="subcontractors")
|
||||
subcontractors = relationship("Contractor", back_populates="parent")
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.database import Base
|
||||
import datetime
|
||||
|
||||
class Evidence(Base):
|
||||
__tablename__ = "evidences"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
activity_id = Column(Integer, ForeignKey("activities.id"), nullable=True)
|
||||
non_conformity_id = Column(Integer, ForeignKey("non_conformities.id"), nullable=True)
|
||||
|
||||
file_path = Column(String, nullable=False)
|
||||
media_type = Column(String) # image, video, document
|
||||
description = Column(String)
|
||||
captured_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# Transcription fields for audio
|
||||
transcription = Column(Text, nullable=True)
|
||||
transcription_status = Column(String, default="none") # none, pending, processing, completed, error
|
||||
|
||||
activity = relationship("app.models.activity.Activity", back_populates="evidences")
|
||||
non_conformity = relationship("app.models.non_conformity.NonConformity", back_populates="evidences")
|
||||
|
|
@ -1,177 +1,7 @@
|
|||
from sqlalchemy import Table, Column, Integer, String, Boolean, ForeignKey, DateTime, Text, Enum, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.database import Base
|
||||
import datetime
|
||||
import enum
|
||||
|
||||
# Junction Tables
|
||||
project_specialties = Table(
|
||||
'project_specialties',
|
||||
Base.metadata,
|
||||
Column('project_id', Integer, ForeignKey('projects.id'), primary_key=True),
|
||||
Column('specialty_id', Integer, ForeignKey('specialties.id'), primary_key=True)
|
||||
)
|
||||
|
||||
project_contractors = Table(
|
||||
'project_contractors',
|
||||
Base.metadata,
|
||||
Column('project_id', Integer, ForeignKey('projects.id'), primary_key=True),
|
||||
Column('contractor_id', Integer, ForeignKey('contractors.id'), primary_key=True)
|
||||
)
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
ADMIN = "admin"
|
||||
DIRECTOR = "director"
|
||||
SUPERVISOR = "supervisor"
|
||||
COORDINATOR = "coordinator"
|
||||
CONTRACTOR = "contractor"
|
||||
CLIENT = "client"
|
||||
|
||||
class ActivityType(str, enum.Enum):
|
||||
INSPECTION = "inspection"
|
||||
MEETING = "meeting"
|
||||
VIRTUAL_MEETING = "virtual_meeting"
|
||||
COORDINATION = "coordination"
|
||||
TEST = "test"
|
||||
OTHER = "other"
|
||||
|
||||
class NCLevel(str, enum.Enum):
|
||||
CRITICAL = "critical"
|
||||
MAJOR = "major"
|
||||
MINOR = "minor"
|
||||
|
||||
class NCType(str, enum.Enum):
|
||||
HUMAN_ERROR = "Errores humanos"
|
||||
PROCESS_FAILURE = "Fallas en los procesos"
|
||||
DESIGN_ISSUE = "Problemas de diseño"
|
||||
UNCONTROLLED_CHANGE = "Cambios no controlados"
|
||||
COMMUNICATION_FAILURE = "Falta de comunicación"
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
full_name = Column(String)
|
||||
role = Column(Enum(UserRole), default=UserRole.SUPERVISOR)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
class Project(Base):
|
||||
__tablename__ = "projects"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, index=True, nullable=False)
|
||||
code = Column(String, unique=True, index=True)
|
||||
location = Column(String)
|
||||
start_date = Column(DateTime)
|
||||
end_date = Column(DateTime)
|
||||
status = Column(String, default="active")
|
||||
parent_id = Column(Integer, ForeignKey("projects.id"), nullable=True)
|
||||
|
||||
# Relationships
|
||||
parent = relationship("Project", remote_side=[id], back_populates="subprojects")
|
||||
subprojects = relationship("Project", back_populates="parent")
|
||||
activities = relationship("Activity", back_populates="project")
|
||||
specialties = relationship("Specialty", secondary=project_specialties)
|
||||
contractors = relationship("Contractor", secondary=project_contractors)
|
||||
|
||||
class Specialty(Base):
|
||||
__tablename__ = "specialties"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, unique=True, nullable=False) # Civil, Mecánica, Eléctrica, SSOMA
|
||||
|
||||
class Contractor(Base):
|
||||
__tablename__ = "contractors"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
ruc = Column(String, unique=True)
|
||||
contact_name = Column(String)
|
||||
email = Column(String, nullable=True)
|
||||
phone = Column(String, nullable=True)
|
||||
address = Column(String, nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
specialty_id = Column(Integer, ForeignKey("specialties.id"), nullable=True)
|
||||
parent_id = Column(Integer, ForeignKey("contractors.id"), nullable=True)
|
||||
|
||||
specialty = relationship("Specialty")
|
||||
parent = relationship("Contractor", remote_side=[id], back_populates="subcontractors")
|
||||
subcontractors = relationship("Contractor", back_populates="parent")
|
||||
|
||||
class Activity(Base):
|
||||
__tablename__ = "activities"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
project_id = Column(Integer, ForeignKey("projects.id"))
|
||||
specialty_id = Column(Integer, ForeignKey("specialties.id"))
|
||||
contractor_id = Column(Integer, ForeignKey("contractors.id"), nullable=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id")) # Reporter
|
||||
|
||||
date = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
end_date = Column(DateTime, nullable=True)
|
||||
type = Column(Enum(ActivityType), default=ActivityType.INSPECTION)
|
||||
area = Column(String) # Frente de obra / Linea
|
||||
description = Column(Text)
|
||||
observations = Column(Text)
|
||||
audio_transcription = Column(Text, nullable=True)
|
||||
status = Column(String, default="completed")
|
||||
|
||||
project = relationship("Project", back_populates="activities")
|
||||
specialty = relationship("Specialty")
|
||||
contractor = relationship("Contractor")
|
||||
reporter = relationship("User")
|
||||
|
||||
non_conformities = relationship("NonConformity", back_populates="activity")
|
||||
evidences = relationship("Evidence", back_populates="activity")
|
||||
|
||||
class NonConformity(Base):
|
||||
__tablename__ = "non_conformities"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
activity_id = Column(Integer, ForeignKey("activities.id"))
|
||||
level = Column(Enum(NCLevel), default=NCLevel.MINOR)
|
||||
description = Column(Text, nullable=False)
|
||||
status = Column(String, default="open") # open, closed
|
||||
|
||||
# New Fields
|
||||
due_date = Column(DateTime, nullable=True)
|
||||
responsible_person = Column(String, nullable=True) # Legend/Name
|
||||
responsible_email = Column(String, nullable=True) # For guest access
|
||||
access_hash = Column(String, unique=True, index=True, nullable=True)
|
||||
contractor_id = Column(Integer, ForeignKey("contractors.id"), nullable=True)
|
||||
|
||||
action_checklist = Column(JSON, nullable=True) # List of dicts {text: str, completed: bool}
|
||||
nc_type = Column(Enum(NCType), nullable=True)
|
||||
impact_description = Column(Text, nullable=True)
|
||||
closure_description = Column(Text, nullable=True)
|
||||
guest_actions = Column(Text, nullable=True) # Field for guest to describe actions taken
|
||||
|
||||
parent_id = Column(Integer, ForeignKey("non_conformities.id"), nullable=True)
|
||||
|
||||
activity = relationship("Activity", back_populates="non_conformities")
|
||||
contractor = relationship("Contractor")
|
||||
evidences = relationship("Evidence", back_populates="non_conformity")
|
||||
parent = relationship("NonConformity", remote_side=[id], back_populates="child_ncs")
|
||||
child_ncs = relationship("NonConformity", back_populates="parent")
|
||||
|
||||
class Evidence(Base):
|
||||
__tablename__ = "evidences"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
activity_id = Column(Integer, ForeignKey("activities.id"), nullable=True)
|
||||
non_conformity_id = Column(Integer, ForeignKey("non_conformities.id"), nullable=True)
|
||||
|
||||
file_path = Column(String, nullable=False)
|
||||
media_type = Column(String) # image, video, document
|
||||
description = Column(String)
|
||||
captured_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# Transcription fields for audio
|
||||
transcription = Column(Text, nullable=True)
|
||||
transcription_status = Column(String, default="none") # none, pending, processing, completed, error
|
||||
|
||||
activity = relationship("Activity", back_populates="evidences")
|
||||
non_conformity = relationship("NonConformity", back_populates="evidences")
|
||||
from app.models.user import User, UserRole
|
||||
from app.models.project import Project, project_specialties, project_contractors
|
||||
from app.models.specialty import Specialty
|
||||
from app.models.contractor import Contractor
|
||||
from app.models.activity import Activity, ActivityType
|
||||
from app.models.non_conformity import NonConformity, NCLevel, NCType
|
||||
from app.models.evidence import Evidence
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Text, Enum, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.database import Base
|
||||
import enum
|
||||
|
||||
class NCLevel(str, enum.Enum):
|
||||
CRITICAL = "critical"
|
||||
MAJOR = "major"
|
||||
MINOR = "minor"
|
||||
|
||||
class NCType(str, enum.Enum):
|
||||
HUMAN_ERROR = "Errores humanos"
|
||||
PROCESS_FAILURE = "Fallas en los procesos"
|
||||
DESIGN_ISSUE = "Problemas de diseño"
|
||||
UNCONTROLLED_CHANGE = "Cambios no controlados"
|
||||
COMMUNICATION_FAILURE = "Falta de comunicación"
|
||||
|
||||
class NonConformity(Base):
|
||||
__tablename__ = "non_conformities"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
activity_id = Column(Integer, ForeignKey("activities.id"))
|
||||
level = Column(Enum(NCLevel), default=NCLevel.MINOR)
|
||||
description = Column(Text, nullable=False)
|
||||
status = Column(String, default="open") # open, closed
|
||||
|
||||
# New Fields
|
||||
due_date = Column(DateTime, nullable=True)
|
||||
responsible_person = Column(String, nullable=True) # Legend/Name
|
||||
responsible_email = Column(String, nullable=True) # For guest access
|
||||
access_hash = Column(String, unique=True, index=True, nullable=True)
|
||||
contractor_id = Column(Integer, ForeignKey("contractors.id"), nullable=True)
|
||||
|
||||
action_checklist = Column(JSON, nullable=True) # List of dicts {text: str, completed: bool}
|
||||
nc_type = Column(Enum(NCType), nullable=True)
|
||||
impact_description = Column(Text, nullable=True)
|
||||
closure_description = Column(Text, nullable=True)
|
||||
guest_actions = Column(Text, nullable=True) # Field for guest to describe actions taken
|
||||
|
||||
parent_id = Column(Integer, ForeignKey("non_conformities.id"), nullable=True)
|
||||
|
||||
activity = relationship("app.models.activity.Activity", back_populates="non_conformities")
|
||||
contractor = relationship("app.models.contractor.Contractor")
|
||||
evidences = relationship("Evidence", back_populates="non_conformity")
|
||||
parent = relationship("NonConformity", remote_side=[id], back_populates="child_ncs")
|
||||
child_ncs = relationship("NonConformity", back_populates="parent")
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Table
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.database import Base
|
||||
import datetime
|
||||
|
||||
# Junction Tables
|
||||
project_specialties = Table(
|
||||
'project_specialties',
|
||||
Base.metadata,
|
||||
Column('project_id', Integer, ForeignKey('projects.id'), primary_key=True),
|
||||
Column('specialty_id', Integer, ForeignKey('specialties.id'), primary_key=True)
|
||||
)
|
||||
|
||||
project_contractors = Table(
|
||||
'project_contractors',
|
||||
Base.metadata,
|
||||
Column('project_id', Integer, ForeignKey('projects.id'), primary_key=True),
|
||||
Column('contractor_id', Integer, ForeignKey('contractors.id'), primary_key=True)
|
||||
)
|
||||
|
||||
class Project(Base):
|
||||
__tablename__ = "projects"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, index=True, nullable=False)
|
||||
code = Column(String, unique=True, index=True)
|
||||
location = Column(String)
|
||||
start_date = Column(DateTime)
|
||||
end_date = Column(DateTime)
|
||||
status = Column(String, default="active")
|
||||
parent_id = Column(Integer, ForeignKey("projects.id"), nullable=True)
|
||||
|
||||
# Relationships
|
||||
parent = relationship("Project", remote_side=[id], back_populates="subprojects")
|
||||
subprojects = relationship("Project", back_populates="parent")
|
||||
activities = relationship("Activity", back_populates="project")
|
||||
specialties = relationship("app.models.specialty.Specialty", secondary=project_specialties)
|
||||
contractors = relationship("app.models.contractor.Contractor", secondary=project_contractors)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from sqlalchemy import Column, Integer, String
|
||||
from app.db.database import Base
|
||||
|
||||
class Specialty(Base):
|
||||
__tablename__ = "specialties"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, unique=True, nullable=False) # Civil, Mecánica, Eléctrica, SSOMA
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
from sqlalchemy import Column, Integer, String, Boolean, Enum
|
||||
from app.db.database import Base
|
||||
import enum
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
ADMIN = "admin"
|
||||
DIRECTOR = "director"
|
||||
SUPERVISOR = "supervisor"
|
||||
COORDINATOR = "coordinator"
|
||||
CONTRACTOR = "contractor"
|
||||
CLIENT = "client"
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
full_name = Column(String)
|
||||
role = Column(Enum(UserRole), default=UserRole.SUPERVISOR)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
import os
|
||||
import shutil
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, BackgroundTasks
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
from app.db.database import get_db
|
||||
from app.models.models import Activity, Evidence, User
|
||||
from app.security import get_current_active_user
|
||||
from app.services.activities import ActivityService
|
||||
from app import schemas
|
||||
from app.schemas import ActivityCreate, ActivityUpdate
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/activities",
|
||||
tags=["Activities"]
|
||||
)
|
||||
|
||||
def get_activity_service(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
) -> ActivityService:
|
||||
return ActivityService(db, current_user)
|
||||
|
||||
UPLOAD_DIR = "uploads"
|
||||
if not os.path.exists(UPLOAD_DIR):
|
||||
os.makedirs(UPLOAD_DIR)
|
||||
|
||||
@router.post("/", response_model=schemas.Activity)
|
||||
def create_activity(
|
||||
activity: ActivityCreate,
|
||||
service: ActivityService = Depends(get_activity_service)
|
||||
):
|
||||
return service.create_activity(activity)
|
||||
|
||||
@router.get("/", response_model=List[schemas.Activity])
|
||||
def read_activities(
|
||||
project_id: Optional[int] = None,
|
||||
specialty_id: Optional[int] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
service: ActivityService = Depends(get_activity_service)
|
||||
):
|
||||
db_activities = service.get_activities(project_id, specialty_id, skip, limit)
|
||||
return db_activities
|
||||
|
||||
@router.get("/{activity_id}", response_model=schemas.Activity)
|
||||
def read_activity(
|
||||
activity_id: int,
|
||||
service: ActivityService = Depends(get_activity_service)
|
||||
):
|
||||
return service.get_activity(activity_id)
|
||||
|
||||
@router.put("/{activity_id}", response_model=schemas.Activity)
|
||||
def update_activity(
|
||||
activity_id: int,
|
||||
activity: schemas.ActivityUpdate,
|
||||
service: ActivityService = Depends(get_activity_service)
|
||||
):
|
||||
db_activity = service.update_activity(activity_id, activity)
|
||||
return db_activity
|
||||
|
||||
@router.post("/{activity_id}/upload", response_model=schemas.Evidence)
|
||||
async def upload_evidence(
|
||||
activity_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
file: UploadFile = File(...),
|
||||
description: Optional[str] = None,
|
||||
captured_at: Optional[str] = None,
|
||||
service: ActivityService = Depends(get_activity_service)
|
||||
):
|
||||
|
||||
db_evidence = service.upload_evidence(
|
||||
activity_id,
|
||||
background_tasks,
|
||||
file,
|
||||
description,
|
||||
captured_at
|
||||
)
|
||||
|
||||
return db_evidence
|
||||
|
||||
@router.post("/evidence/{evidence_id}/retry-transcription", response_model=schemas.Evidence)
|
||||
async def retry_transcription(
|
||||
evidence_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
db_evidence = db.query(Evidence).filter(Evidence.id == evidence_id).first()
|
||||
if not db_evidence:
|
||||
raise HTTPException(status_code=404, detail="Evidence not found")
|
||||
|
||||
if not db_evidence.media_type or "audio" not in db_evidence.media_type:
|
||||
raise HTTPException(status_code=400, detail="Only audio evidence can be transcribed")
|
||||
|
||||
# Update status to pending
|
||||
db_evidence.transcription_status = "pending"
|
||||
db_evidence.transcription = None
|
||||
db.commit()
|
||||
db.refresh(db_evidence)
|
||||
|
||||
# Queue transcription task
|
||||
from app.services.transcription_worker import process_transcription
|
||||
background_tasks.add_task(process_transcription, db_evidence.id)
|
||||
|
||||
return db_evidence
|
||||
|
||||
@router.put("/evidence/{evidence_id}", response_model=schemas.Evidence)
|
||||
def update_evidence(
|
||||
evidence_id: int,
|
||||
evidence: schemas.EvidenceUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
db_evidence = db.query(Evidence).filter(Evidence.id == evidence_id).first()
|
||||
if not db_evidence:
|
||||
raise HTTPException(status_code=404, detail="Evidence not found")
|
||||
|
||||
update_data = evidence.dict(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(db_evidence, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_evidence)
|
||||
return db_evidence
|
||||
|
||||
@router.delete("/evidence/{evidence_id}")
|
||||
def delete_evidence(
|
||||
evidence_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
db_evidence = db.query(Evidence).filter(Evidence.id == evidence_id).first()
|
||||
if not db_evidence:
|
||||
raise HTTPException(status_code=404, detail="Evidence not found")
|
||||
|
||||
# Optional: Delete file from disk
|
||||
if db_evidence.file_path and os.path.exists(db_evidence.file_path):
|
||||
try:
|
||||
os.remove(db_evidence.file_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
db.delete(db_evidence)
|
||||
db.commit()
|
||||
return {"detail": "Evidence deleted"}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import timedelta
|
||||
from app.db.database import get_db
|
||||
from app.models.models import User
|
||||
from app.security import verify_password, create_access_token
|
||||
import app.schemas
|
||||
|
||||
router = APIRouter(tags=["Authentication"])
|
||||
|
||||
@router.post("/token", response_model=app.schemas.Token)
|
||||
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
||||
user = db.query(User).filter(User.email == form_data.username).first()
|
||||
if not user or not verify_password(form_data.password, user.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
access_token_expires = timedelta(minutes=300)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.email}, expires_delta=access_token_expires
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from app.db.database import get_db
|
||||
from app.models.models import Contractor
|
||||
from app.security import get_current_active_user
|
||||
import app.schemas
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/contractors",
|
||||
tags=["Contractors"],
|
||||
dependencies=[Depends(get_current_active_user)]
|
||||
)
|
||||
|
||||
@router.post("/", response_model=app.schemas.Contractor)
|
||||
def create_contractor(contractor: app.schemas.ContractorCreate, db: Session = Depends(get_db)):
|
||||
db_contractor = Contractor(**contractor.dict())
|
||||
db.add(db_contractor)
|
||||
db.commit()
|
||||
db.refresh(db_contractor)
|
||||
return db_contractor
|
||||
|
||||
@router.get("/", response_model=List[app.schemas.Contractor])
|
||||
def read_contractors(
|
||||
parent_id: Optional[int] = None,
|
||||
only_parents: bool = False,
|
||||
is_active: Optional[bool] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
query = db.query(Contractor)
|
||||
if only_parents:
|
||||
query = query.filter(Contractor.parent_id == None)
|
||||
elif parent_id is not None:
|
||||
query = query.filter(Contractor.parent_id == parent_id)
|
||||
|
||||
if is_active is not None:
|
||||
query = query.filter(Contractor.is_active == is_active)
|
||||
return query.all()
|
||||
|
||||
@router.get("/{contractor_id}", response_model=app.schemas.Contractor)
|
||||
def read_contractor(contractor_id: int, db: Session = Depends(get_db)):
|
||||
db_contractor = db.query(Contractor).filter(Contractor.id == contractor_id).first()
|
||||
if not db_contractor:
|
||||
raise HTTPException(status_code=404, detail="Contractor not found")
|
||||
return db_contractor
|
||||
|
||||
@router.put("/{contractor_id}", response_model=app.schemas.Contractor)
|
||||
@router.patch("/{contractor_id}", response_model=app.schemas.Contractor)
|
||||
def update_contractor(
|
||||
contractor_id: int,
|
||||
contractor: app.schemas.ContractorUpdate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
db_contractor = db.query(Contractor).filter(Contractor.id == contractor_id).first()
|
||||
if not db_contractor:
|
||||
raise HTTPException(status_code=404, detail="Contractor not found")
|
||||
|
||||
update_data = contractor.dict(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(db_contractor, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_contractor)
|
||||
return db_contractor
|
||||
|
||||
@router.delete("/{contractor_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_contractor(contractor_id: int, db: Session = Depends(get_db)):
|
||||
db_contractor = db.query(Contractor).filter(Contractor.id == contractor_id).first()
|
||||
if not db_contractor:
|
||||
raise HTTPException(status_code=404, detail="Contractor not found")
|
||||
|
||||
# Optional: instead of hard delete, maybe just deactivate
|
||||
db.delete(db_contractor)
|
||||
db.commit()
|
||||
return None
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from app.db.database import get_db
|
||||
from app.models.models import NonConformity, Evidence
|
||||
import app.schemas
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/guest",
|
||||
tags=["Guest Access"]
|
||||
)
|
||||
|
||||
UPLOAD_DIR = "uploads"
|
||||
if not os.path.exists(UPLOAD_DIR):
|
||||
os.makedirs(UPLOAD_DIR)
|
||||
|
||||
@router.get("/nc/{access_hash}", response_model=app.schemas.NonConformity)
|
||||
def read_guest_nc(
|
||||
access_hash: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
db_nc = db.query(NonConformity).filter(NonConformity.access_hash == access_hash).first()
|
||||
if not db_nc:
|
||||
raise HTTPException(status_code=404, detail="Non-Conformity not found or invalid link")
|
||||
return db_nc
|
||||
|
||||
@router.patch("/nc/{access_hash}", response_model=app.schemas.NonConformity)
|
||||
def update_guest_nc(
|
||||
access_hash: str,
|
||||
nc_update: app.schemas.NonConformityUpdate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
db_nc = db.query(NonConformity).filter(NonConformity.access_hash == access_hash).first()
|
||||
if not db_nc:
|
||||
raise HTTPException(status_code=404, detail="Non-Conformity not found or invalid link")
|
||||
|
||||
# Only allow updating specific fields for guest
|
||||
# guest_actions, closure_description? description?
|
||||
# The requirement says: "Describing actions taken." -> guest_actions
|
||||
# "Upload evidence" -> handled by upload endpoint
|
||||
|
||||
# We will trust the validation in schema but we might want to restrict what guests can change.
|
||||
# For now, let's allow updating guest_actions and maybe status if they can close it?
|
||||
# User said: "Add PATCH endpoint to update NC activities/closure by guest."
|
||||
|
||||
if nc_update.guest_actions is not None:
|
||||
db_nc.guest_actions = nc_update.guest_actions
|
||||
|
||||
if nc_update.closure_description is not None:
|
||||
db_nc.closure_description = nc_update.closure_description
|
||||
|
||||
if nc_update.status is not None:
|
||||
# Maybe allow them to mark as "resolved" or something?
|
||||
db_nc.status = nc_update.status
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_nc)
|
||||
return db_nc
|
||||
|
||||
@router.post("/nc/{access_hash}/upload", response_model=app.schemas.Evidence)
|
||||
async def upload_guest_evidence(
|
||||
access_hash: str,
|
||||
file: UploadFile = File(...),
|
||||
description: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
db_nc = db.query(NonConformity).filter(NonConformity.access_hash == access_hash).first()
|
||||
if not db_nc:
|
||||
raise HTTPException(status_code=404, detail="Non-Conformity not found or invalid link")
|
||||
|
||||
# Generate unique filename
|
||||
file_ext = os.path.splitext(file.filename)[1]
|
||||
unique_filename = f"guest_nc_{uuid.uuid4()}{file_ext}"
|
||||
file_path = os.path.join(UPLOAD_DIR, unique_filename)
|
||||
|
||||
# Save file
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# Save to database
|
||||
db_evidence = Evidence(
|
||||
non_conformity_id=db_nc.id,
|
||||
file_path=file_path,
|
||||
media_type=file.content_type,
|
||||
description=description,
|
||||
captured_at=datetime.datetime.utcnow()
|
||||
)
|
||||
db.add(db_evidence)
|
||||
db.commit()
|
||||
db.refresh(db_evidence)
|
||||
|
||||
return db_evidence
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from app.db.database import get_db
|
||||
from app.models.models import NonConformity, User, Activity, Evidence, Contractor
|
||||
from app.security import get_current_active_user
|
||||
import app.schemas
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/non-conformities",
|
||||
tags=["Non-Conformities"]
|
||||
)
|
||||
|
||||
UPLOAD_DIR = "uploads"
|
||||
if not os.path.exists(UPLOAD_DIR):
|
||||
os.makedirs(UPLOAD_DIR)
|
||||
|
||||
@router.post("/", response_model=app.schemas.NonConformity)
|
||||
def create_nc(
|
||||
nc: app.schemas.NonConformityCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
db_activity = db.query(Activity).filter(Activity.id == nc.activity_id).first()
|
||||
if not db_activity:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
|
||||
# Sync responsible email from contractor if not provided
|
||||
if nc.contractor_id and not nc.responsible_email:
|
||||
contractor = db.query(Contractor).filter(Contractor.id == nc.contractor_id).first()
|
||||
if contractor and contractor.email:
|
||||
nc.responsible_email = contractor.email
|
||||
|
||||
db_nc = NonConformity(**nc.dict())
|
||||
db.add(db_nc)
|
||||
db.commit()
|
||||
db.refresh(db_nc)
|
||||
return db_nc
|
||||
|
||||
@router.get("/", response_model=List[app.schemas.NonConformity])
|
||||
def read_ncs(
|
||||
activity_id: Optional[int] = None,
|
||||
status: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
query = db.query(NonConformity)
|
||||
if activity_id:
|
||||
query = query.filter(NonConformity.activity_id == activity_id)
|
||||
if status:
|
||||
query = query.filter(NonConformity.status == status)
|
||||
|
||||
return query.all()
|
||||
|
||||
@router.get("/{nc_id}", response_model=app.schemas.NonConformity)
|
||||
def read_nc(
|
||||
nc_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
db_nc = db.query(NonConformity).filter(NonConformity.id == nc_id).first()
|
||||
if db_nc is None:
|
||||
raise HTTPException(status_code=404, detail="Non-Conformity not found")
|
||||
return db_nc
|
||||
|
||||
@router.put("/{nc_id}", response_model=app.schemas.NonConformity)
|
||||
@router.patch("/{nc_id}", response_model=app.schemas.NonConformity)
|
||||
def update_nc(
|
||||
nc_id: int,
|
||||
nc: app.schemas.NonConformityUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
db_nc = db.query(NonConformity).filter(NonConformity.id == nc_id).first()
|
||||
if not db_nc:
|
||||
raise HTTPException(status_code=404, detail="Non-Conformity not found")
|
||||
|
||||
update_data = nc.dict(exclude_unset=True)
|
||||
|
||||
# Sync responsible email if contractor_id changes and email not explicitly provided
|
||||
if 'contractor_id' in update_data and update_data['contractor_id']:
|
||||
if 'responsible_email' not in update_data or not update_data['responsible_email']:
|
||||
contractor = db.query(Contractor).filter(Contractor.id == update_data['contractor_id']).first()
|
||||
if contractor and contractor.email:
|
||||
update_data['responsible_email'] = contractor.email
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(db_nc, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_nc)
|
||||
return db_nc
|
||||
|
||||
@router.post("/{nc_id}/upload", response_model=app.schemas.Evidence)
|
||||
async def upload_nc_evidence(
|
||||
nc_id: int,
|
||||
file: UploadFile = File(...),
|
||||
description: Optional[str] = None,
|
||||
captured_at: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
# Verify NC exists
|
||||
db_nc = db.query(NonConformity).filter(NonConformity.id == nc_id).first()
|
||||
if not db_nc:
|
||||
raise HTTPException(status_code=404, detail="Non-Conformity not found")
|
||||
|
||||
# Generate unique filename
|
||||
file_ext = os.path.splitext(file.filename)[1]
|
||||
unique_filename = f"nc_{uuid.uuid4()}{file_ext}"
|
||||
file_path = os.path.join(UPLOAD_DIR, unique_filename)
|
||||
|
||||
# Save file
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
db_captured_at = None
|
||||
if captured_at:
|
||||
try:
|
||||
db_captured_at = datetime.datetime.fromisoformat(captured_at.replace('Z', '+00:00'))
|
||||
except:
|
||||
db_captured_at = datetime.datetime.utcnow()
|
||||
else:
|
||||
db_captured_at = datetime.datetime.utcnow()
|
||||
|
||||
# Save to database
|
||||
db_evidence = Evidence(
|
||||
non_conformity_id=nc_id,
|
||||
file_path=file_path,
|
||||
media_type=file.content_type,
|
||||
description=description,
|
||||
captured_at=db_captured_at
|
||||
)
|
||||
db.add(db_evidence)
|
||||
db.commit()
|
||||
db.refresh(db_evidence)
|
||||
|
||||
return db_evidence
|
||||
|
||||
@router.delete("/{nc_id}")
|
||||
def delete_nc(
|
||||
nc_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
db_nc = db.query(NonConformity).filter(NonConformity.id == nc_id).first()
|
||||
if not db_nc:
|
||||
raise HTTPException(status_code=404, detail="Non-Conformity not found")
|
||||
|
||||
db.delete(db_nc)
|
||||
db.commit()
|
||||
return {"detail": "Non-Conformity deleted"}
|
||||
|
||||
@router.post("/{nc_id}/notify")
|
||||
def notify_responsible(
|
||||
nc_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
db_nc = db.query(NonConformity).filter(NonConformity.id == nc_id).first()
|
||||
if not db_nc:
|
||||
raise HTTPException(status_code=404, detail="Non-Conformity not found")
|
||||
|
||||
if not db_nc.responsible_email:
|
||||
raise HTTPException(status_code=400, detail="No responsible email configured for this Non-Conformity")
|
||||
|
||||
# Generate hash if it doesn't exist
|
||||
if not db_nc.access_hash:
|
||||
db_nc.access_hash = str(uuid.uuid4())
|
||||
db.commit()
|
||||
db.refresh(db_nc)
|
||||
|
||||
# Send email
|
||||
from app.services.email_service import EmailService
|
||||
EmailService.send_nc_notification(db_nc.responsible_email, db_nc.access_hash, db_nc.description)
|
||||
|
||||
# Update status to in-checking
|
||||
db_nc.status = 'in-checking'
|
||||
db.commit()
|
||||
db.refresh(db_nc)
|
||||
|
||||
return {"message": "Notification sent successfully", "access_hash": db_nc.access_hash, "status": db_nc.status}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.db.database import get_db
|
||||
from app.models.models import Project, Specialty, Contractor
|
||||
from app.security import get_current_active_user
|
||||
import app.schemas
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/projects",
|
||||
tags=["Projects"],
|
||||
dependencies=[Depends(get_current_active_user)]
|
||||
)
|
||||
|
||||
@router.post("/", response_model=app.schemas.Project)
|
||||
def create_project(project: app.schemas.ProjectCreate, db: Session = Depends(get_db)):
|
||||
db_project = db.query(Project).filter(Project.code == project.code).first()
|
||||
if db_project:
|
||||
raise HTTPException(status_code=400, detail="Project code already exists")
|
||||
|
||||
|
||||
project_data = project.dict(exclude={'specialty_ids', 'contractor_ids'})
|
||||
db_project = Project(**project_data)
|
||||
|
||||
# Handle Parent Project
|
||||
if project.parent_id:
|
||||
parent = db.query(Project).filter(Project.id == project.parent_id).first()
|
||||
if not parent:
|
||||
raise HTTPException(status_code=404, detail="Parent project not found")
|
||||
if project.specialty_ids:
|
||||
specialties = db.query(Specialty).filter(Specialty.id.in_(project.specialty_ids)).all()
|
||||
db_project.specialties = specialties
|
||||
|
||||
# Handle Contractors
|
||||
if project.contractor_ids:
|
||||
contractors = db.query(Contractor).filter(Contractor.id.in_(project.contractor_ids)).all()
|
||||
db_project.contractors = contractors
|
||||
|
||||
db.add(db_project)
|
||||
db.commit()
|
||||
db.refresh(db_project)
|
||||
return db_project
|
||||
|
||||
@router.get("/", response_model=List[app.schemas.Project])
|
||||
def read_projects(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||
projects = db.query(Project).offset(skip).limit(limit).all()
|
||||
return projects
|
||||
|
||||
@router.get("/{project_id}", response_model=app.schemas.Project)
|
||||
def read_project(project_id: int, db: Session = Depends(get_db)):
|
||||
db_project = db.query(Project).filter(Project.id == project_id).first()
|
||||
if db_project is None:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
return db_project
|
||||
|
||||
@router.put("/{project_id}", response_model=app.schemas.Project)
|
||||
def update_project(project_id: int, project: app.schemas.ProjectCreate, db: Session = Depends(get_db)):
|
||||
db_project = db.query(Project).filter(Project.id == project_id).first()
|
||||
if db_project is None:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
# Update simple fields
|
||||
for key, value in project.dict(exclude={'specialty_ids', 'contractor_ids'}).items():
|
||||
setattr(db_project, key, value)
|
||||
|
||||
# Handle Parent Project
|
||||
if project.parent_id is not None:
|
||||
parent = db.query(Project).filter(Project.id == project.parent_id).first()
|
||||
if not parent and project.parent_id != 0: # Allow 0 or null to clear? Actually null is enough
|
||||
raise HTTPException(status_code=404, detail="Parent project not found")
|
||||
db_project.parent_id = project.parent_id if project.parent_id != 0 else None
|
||||
|
||||
# Update Specialties
|
||||
if project.specialty_ids is not None:
|
||||
specialties = db.query(Specialty).filter(Specialty.id.in_(project.specialty_ids)).all()
|
||||
db_project.specialties = specialties
|
||||
|
||||
# Update Contractors
|
||||
if project.contractor_ids is not None:
|
||||
contractors = db.query(Contractor).filter(Contractor.id.in_(project.contractor_ids)).all()
|
||||
db_project.contractors = contractors
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_project)
|
||||
return db_project
|
||||
|
||||
@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_project(project_id: int, db: Session = Depends(get_db)):
|
||||
db_project = db.query(Project).filter(Project.id == project_id).first()
|
||||
if db_project is None:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
db.delete(db_project)
|
||||
db.commit()
|
||||
return None
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.db.database import get_db
|
||||
from app.models.models import Specialty
|
||||
from app.security import get_current_active_user
|
||||
import app.schemas
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/specialties",
|
||||
tags=["Specialties"],
|
||||
dependencies=[Depends(get_current_active_user)]
|
||||
)
|
||||
|
||||
@router.get("/", response_model=List[app.schemas.Specialty])
|
||||
def read_specialties(db: Session = Depends(get_db)):
|
||||
return db.query(Specialty).all()
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import os
|
||||
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
|
||||
import google.generativeai as genai
|
||||
import tempfile
|
||||
from dotenv import load_dotenv
|
||||
from app.security import get_current_active_user
|
||||
|
||||
load_dotenv()
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/transcription",
|
||||
tags=["Transcription"],
|
||||
dependencies=[Depends(get_current_active_user)]
|
||||
)
|
||||
|
||||
# Initialize Google Gemini
|
||||
api_key = os.getenv("GOOGLE_API_KEY")
|
||||
if api_key:
|
||||
genai.configure(api_key=api_key)
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def transcribe_audio(file: UploadFile = File(...)):
|
||||
if not os.getenv("GOOGLE_API_KEY"):
|
||||
# Mock transcription for development if no key is present
|
||||
return {"text": f"[MOCK GEMINI TRANSCRIPTION] Se ha recibido un archivo de audio de tipo {file.content_type}. Configure GOOGLE_API_KEY para transcripción real con Gemini."}
|
||||
|
||||
try:
|
||||
# Create a temporary file to store the upload
|
||||
suffix = os.path.splitext(file.filename)[1] or ".wav"
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
|
||||
content = await file.read()
|
||||
tmp.write(content)
|
||||
tmp_path = tmp.name
|
||||
|
||||
# Upload to Gemini (Media Service)
|
||||
audio_file = genai.upload_file(path=tmp_path, mime_type=file.content_type or "audio/wav")
|
||||
|
||||
# Use Gemini 1.5 Flash for audio-to-text
|
||||
model = genai.GenerativeModel("gemini-2.5-flash-lite")
|
||||
response = model.generate_content([
|
||||
"Por favor, transcribe exactamente lo que se dice en este audio. Solo devuelve el texto transcrito.",
|
||||
audio_file
|
||||
])
|
||||
|
||||
# Cleanup
|
||||
os.unlink(tmp_path)
|
||||
# Gemini files are ephemeral but we can delete explicitly if needed
|
||||
# genai.delete_file(audio_file.name)
|
||||
|
||||
return {"text": response.text}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.db.database import get_db
|
||||
from app.models.models import User
|
||||
from app.security import get_password_hash, get_current_active_user
|
||||
from app.services import users
|
||||
from app.schemas import UserCreate, User
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/users",
|
||||
tags=["Users"],
|
||||
dependencies=[Depends(get_current_active_user)]
|
||||
)
|
||||
|
||||
@router.post("/", response_model=User)
|
||||
def create_user(user: UserCreate, db: Session = Depends(get_db)):
|
||||
return users.create_user(db, user)
|
||||
|
||||
@router.get("/", response_model=List[User])
|
||||
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||
db_users = users.get_users(db, skip, limit)
|
||||
return db_users
|
||||
|
||||
@router.get("/me", response_model=User)
|
||||
def read_users_me(current_user: User = Depends(get_current_active_user)):
|
||||
return current_user
|
||||
|
|
@ -1,202 +1,17 @@
|
|||
from pydantic import BaseModel, EmailStr, field_validator
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from app.models.models import UserRole, ActivityType, NCLevel, NCType
|
||||
from app.schemas.token import Token, TokenData
|
||||
from app.schemas.user import User, UserCreate, UserBase
|
||||
from app.schemas.project import Project, ProjectCreate, ProjectBase
|
||||
from app.schemas.specialty import Specialty, SpecialtyBase
|
||||
from app.schemas.contractor import Contractor, ContractorCreate, ContractorUpdate, ContractorBase
|
||||
from app.schemas.evidence import Evidence, EvidenceUpdate, EvidenceBase
|
||||
from app.schemas.activity import Activity, ActivityCreate, ActivityUpdate, ActivityBase
|
||||
from app.schemas.non_conformity import NonConformity, NonConformityCreate, NonConformityUpdate, NonConformityBase
|
||||
|
||||
# Token
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
class TokenData(BaseModel):
|
||||
email: Optional[str] = None
|
||||
|
||||
# User
|
||||
class UserBase(BaseModel):
|
||||
email: EmailStr
|
||||
full_name: Optional[str] = None
|
||||
role: UserRole = UserRole.SUPERVISOR
|
||||
is_active: bool = True
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
|
||||
class User(UserBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Project
|
||||
class ProjectBase(BaseModel):
|
||||
name: str
|
||||
code: str
|
||||
location: Optional[str] = None
|
||||
start_date: Optional[datetime] = None
|
||||
end_date: Optional[datetime] = None
|
||||
status: str = "active"
|
||||
parent_id: Optional[int] = None
|
||||
|
||||
class ProjectCreate(ProjectBase):
|
||||
specialty_ids: Optional[List[int]] = []
|
||||
contractor_ids: Optional[List[int]] = []
|
||||
|
||||
class Project(ProjectBase):
|
||||
id: int
|
||||
specialties: List['Specialty'] = []
|
||||
contractors: List['Contractor'] = []
|
||||
subprojects: List['Project'] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Specialty
|
||||
class SpecialtyBase(BaseModel):
|
||||
name: str
|
||||
|
||||
class Specialty(SpecialtyBase):
|
||||
id: int
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Contractor
|
||||
class ContractorBase(BaseModel):
|
||||
name: str
|
||||
ruc: Optional[str] = None
|
||||
contact_name: Optional[str] = None
|
||||
email: Optional[EmailStr] = None
|
||||
phone: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
specialty_id: Optional[int] = None
|
||||
parent_id: Optional[int] = None
|
||||
is_active: bool = True
|
||||
|
||||
@field_validator('ruc', 'contact_name', 'email', 'phone', 'address', 'specialty_id', 'parent_id', mode='before')
|
||||
@classmethod
|
||||
def empty_string_to_none(cls, v):
|
||||
if v == "":
|
||||
return None
|
||||
return v
|
||||
|
||||
class ContractorCreate(ContractorBase):
|
||||
pass
|
||||
|
||||
class ContractorUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
ruc: Optional[str] = None
|
||||
contact_name: Optional[str] = None
|
||||
email: Optional[EmailStr] = None
|
||||
phone: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
specialty_id: Optional[int] = None
|
||||
parent_id: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
class Contractor(ContractorBase):
|
||||
id: int
|
||||
specialty: Optional[Specialty] = None
|
||||
subcontractors: List['Contractor'] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Evidence
|
||||
class EvidenceBase(BaseModel):
|
||||
file_path: str
|
||||
media_type: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
captured_at: Optional[datetime] = None
|
||||
transcription: Optional[str] = None
|
||||
transcription_status: str = "none"
|
||||
|
||||
class Evidence(EvidenceBase):
|
||||
id: int
|
||||
activity_id: Optional[int] = None
|
||||
non_conformity_id: Optional[int] = None
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class EvidenceUpdate(BaseModel):
|
||||
description: Optional[str] = None
|
||||
|
||||
# Activity
|
||||
class ActivityBase(BaseModel):
|
||||
project_id: int
|
||||
specialty_id: int
|
||||
contractor_id: Optional[int] = None
|
||||
type: ActivityType = ActivityType.INSPECTION
|
||||
area: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
observations: Optional[str] = None
|
||||
audio_transcription: Optional[str] = None
|
||||
status: str = "completed"
|
||||
date: Optional[datetime] = None
|
||||
end_date: Optional[datetime] = None
|
||||
|
||||
class ActivityUpdate(BaseModel):
|
||||
project_id: Optional[int] = None
|
||||
specialty_id: Optional[int] = None
|
||||
contractor_id: Optional[int] = None
|
||||
area: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
observations: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
date: Optional[datetime] = None
|
||||
end_date: Optional[datetime] = None
|
||||
|
||||
class ActivityCreate(ActivityBase):
|
||||
pass
|
||||
|
||||
# NonConformity
|
||||
class NonConformityBase(BaseModel):
|
||||
level: NCLevel = NCLevel.MINOR
|
||||
description: str
|
||||
status: str = "open"
|
||||
due_date: Optional[datetime] = None
|
||||
responsible_person: Optional[str] = None
|
||||
responsible_email: Optional[str] = None
|
||||
contractor_id: Optional[int] = None
|
||||
access_hash: Optional[str] = None
|
||||
action_checklist: Optional[List[dict]] = None
|
||||
nc_type: Optional[NCType] = None
|
||||
impact_description: Optional[str] = None
|
||||
closure_description: Optional[str] = None
|
||||
guest_actions: Optional[str] = None
|
||||
parent_id: Optional[int] = None
|
||||
|
||||
class NonConformityCreate(NonConformityBase):
|
||||
activity_id: int
|
||||
|
||||
class NonConformityUpdate(BaseModel):
|
||||
due_date: Optional[datetime] = None
|
||||
responsible_person: Optional[str] = None
|
||||
responsible_email: Optional[str] = None
|
||||
contractor_id: Optional[int] = None
|
||||
access_hash: Optional[str] = None
|
||||
action_checklist: Optional[List[dict]] = None
|
||||
nc_type: Optional[NCType] = None
|
||||
impact_description: Optional[str] = None
|
||||
closure_description: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
guest_actions: Optional[str] = None
|
||||
|
||||
class NonConformity(NonConformityBase):
|
||||
id: int
|
||||
activity_id: int
|
||||
evidences: List[Evidence] = []
|
||||
child_ncs: List['NonConformity'] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class Activity(ActivityBase):
|
||||
id: int
|
||||
user_id: int
|
||||
project: Optional[Project] = None
|
||||
evidences: List[Evidence] = []
|
||||
non_conformities: List[NonConformity] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
# Necessary for recursive models
|
||||
from app.schemas.contractor import Contractor
|
||||
from app.schemas.project import Project
|
||||
from app.schemas.non_conformity import NonConformity
|
||||
|
||||
Contractor.model_rebuild()
|
||||
Project.model_rebuild()
|
||||
NonConformity.model_rebuild()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
from .token import Token, TokenData
|
||||
from .user import User, UserCreate
|
||||
from .project import Project, ProjectCreate, ProjectBase
|
||||
from .specialty import Specialty, SpecialtyBase
|
||||
from .contractor import Contractor, ContractorCreate, ContractorUpdate
|
||||
from .evidence import Evidence, EvidenceUpdate
|
||||
from .activity import Activity, ActivityCreate, ActivityUpdate
|
||||
from .non_conformity import NonConformity, NonConformityCreate, NonConformityUpdate
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from app.models.models import ActivityType
|
||||
from .project import Project
|
||||
from .evidence import Evidence
|
||||
from .non_conformity import NonConformity
|
||||
|
||||
class ActivityBase(BaseModel):
|
||||
project_id: int
|
||||
specialty_id: int
|
||||
contractor_id: Optional[int] = None
|
||||
type: ActivityType = ActivityType.INSPECTION
|
||||
area: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
observations: Optional[str] = None
|
||||
audio_transcription: Optional[str] = None
|
||||
status: str = "completed"
|
||||
date: Optional[datetime] = None
|
||||
end_date: Optional[datetime] = None
|
||||
|
||||
class ActivityUpdate(BaseModel):
|
||||
project_id: Optional[int] = None
|
||||
specialty_id: Optional[int] = None
|
||||
contractor_id: Optional[int] = None
|
||||
area: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
observations: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
date: Optional[datetime] = None
|
||||
end_date: Optional[datetime] = None
|
||||
|
||||
class ActivityCreate(ActivityBase):
|
||||
pass
|
||||
|
||||
class Activity(ActivityBase):
|
||||
id: int
|
||||
user_id: int
|
||||
project: Optional[Project] = None
|
||||
evidences: List[Evidence] = []
|
||||
non_conformities: List[NonConformity] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
from pydantic import BaseModel, EmailStr, field_validator
|
||||
from typing import Optional, List
|
||||
from .specialty import Specialty
|
||||
|
||||
class ContractorBase(BaseModel):
|
||||
name: str
|
||||
ruc: Optional[str] = None
|
||||
contact_name: Optional[str] = None
|
||||
email: Optional[EmailStr] = None
|
||||
phone: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
specialty_id: Optional[int] = None
|
||||
parent_id: Optional[int] = None
|
||||
is_active: bool = True
|
||||
|
||||
@field_validator('ruc', 'contact_name', 'email', 'phone', 'address', 'specialty_id', 'parent_id', mode='before')
|
||||
@classmethod
|
||||
def empty_string_to_none(cls, v):
|
||||
if v == "":
|
||||
return None
|
||||
return v
|
||||
|
||||
class ContractorCreate(ContractorBase):
|
||||
pass
|
||||
|
||||
class ContractorUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
ruc: Optional[str] = None
|
||||
contact_name: Optional[str] = None
|
||||
email: Optional[EmailStr] = None
|
||||
phone: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
specialty_id: Optional[int] = None
|
||||
parent_id: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
class Contractor(ContractorBase):
|
||||
id: int
|
||||
specialty: Optional[Specialty] = None
|
||||
subcontractors: List['Contractor'] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
class EvidenceBase(BaseModel):
|
||||
file_path: str
|
||||
media_type: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
captured_at: Optional[datetime] = None
|
||||
transcription: Optional[str] = None
|
||||
transcription_status: str = "none"
|
||||
|
||||
class Evidence(EvidenceBase):
|
||||
id: int
|
||||
activity_id: Optional[int] = None
|
||||
non_conformity_id: Optional[int] = None
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class EvidenceUpdate(BaseModel):
|
||||
description: Optional[str] = None
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from app.models.models import NCLevel, NCType
|
||||
from .evidence import Evidence
|
||||
|
||||
class NonConformityBase(BaseModel):
|
||||
level: NCLevel = NCLevel.MINOR
|
||||
description: str
|
||||
status: str = "open"
|
||||
due_date: Optional[datetime] = None
|
||||
responsible_person: Optional[str] = None
|
||||
responsible_email: Optional[str] = None
|
||||
contractor_id: Optional[int] = None
|
||||
access_hash: Optional[str] = None
|
||||
action_checklist: Optional[List[dict]] = None
|
||||
nc_type: Optional[NCType] = None
|
||||
impact_description: Optional[str] = None
|
||||
closure_description: Optional[str] = None
|
||||
guest_actions: Optional[str] = None
|
||||
parent_id: Optional[int] = None
|
||||
|
||||
class NonConformityCreate(NonConformityBase):
|
||||
activity_id: int
|
||||
|
||||
class NonConformityUpdate(BaseModel):
|
||||
due_date: Optional[datetime] = None
|
||||
responsible_person: Optional[str] = None
|
||||
responsible_email: Optional[str] = None
|
||||
contractor_id: Optional[int] = None
|
||||
access_hash: Optional[str] = None
|
||||
action_checklist: Optional[List[dict]] = None
|
||||
nc_type: Optional[NCType] = None
|
||||
impact_description: Optional[str] = None
|
||||
closure_description: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
guest_actions: Optional[str] = None
|
||||
|
||||
class NonConformity(NonConformityBase):
|
||||
id: int
|
||||
activity_id: int
|
||||
evidences: List[Evidence] = []
|
||||
child_ncs: List['NonConformity'] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from .specialty import Specialty
|
||||
from .contractor import Contractor
|
||||
|
||||
class ProjectBase(BaseModel):
|
||||
name: str
|
||||
code: str
|
||||
location: Optional[str] = None
|
||||
start_date: Optional[datetime] = None
|
||||
end_date: Optional[datetime] = None
|
||||
status: str = "active"
|
||||
parent_id: Optional[int] = None
|
||||
|
||||
class ProjectCreate(ProjectBase):
|
||||
specialty_ids: Optional[List[int]] = []
|
||||
contractor_ids: Optional[List[int]] = []
|
||||
|
||||
class Project(ProjectBase):
|
||||
id: int
|
||||
specialties: List[Specialty] = []
|
||||
contractors: List[Contractor] = []
|
||||
subprojects: List['Project'] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
class SpecialtyBase(BaseModel):
|
||||
name: str
|
||||
|
||||
class Specialty(SpecialtyBase):
|
||||
id: int
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
class TokenData(BaseModel):
|
||||
email: Optional[str] = None
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional
|
||||
from app.models.models import UserRole
|
||||
|
||||
class UserBase(BaseModel):
|
||||
email: EmailStr
|
||||
full_name: Optional[str] = None
|
||||
role: UserRole = UserRole.SUPERVISOR
|
||||
is_active: bool = True
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
|
||||
class User(UserBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
|
@ -8,9 +8,10 @@ from sqlalchemy.orm import Session
|
|||
from app.db.database import get_db
|
||||
from app.models.models import User
|
||||
import app.schemas
|
||||
import hashlib
|
||||
|
||||
# Secret key for JWT (should be in env vars in production)
|
||||
SECRET_KEY = "super_secret_key_for_fritos_fresh_supervision_system"
|
||||
SECRET_KEY = "Bt50MaUvRYJ28UOIberyBlRVQCcKiYzVF2JHOFKjbBQq5xoOpowyxjY1tCOEzYEL"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 300
|
||||
|
||||
|
|
@ -18,10 +19,25 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
# Pre-hash the password with SHA-256 to ensure it's within bcrypt's 72-byte limit
|
||||
pre_hashed = hashlib.sha256(plain_password.encode()).hexdigest()
|
||||
|
||||
# Try verifying the pre-hashed password first (new approach)
|
||||
if pwd_context.verify(pre_hashed, hashed_password):
|
||||
return True
|
||||
|
||||
# Fallback: Try verifying the raw password (legacy approach for existing accounts)
|
||||
try:
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
except ValueError:
|
||||
# If the password was already too long, it would have failed during registration
|
||||
# but just in case, we catch the ValueError from bcrypt
|
||||
return False
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
# Pre-hash with SHA-256 to handle any length and stay under bcrypt's 72-byte limit
|
||||
pre_hashed = hashlib.sha256(password.encode()).hexdigest()
|
||||
return pwd_context.hash(pre_hashed)
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
to_encode = data.copy()
|
||||
|
|
|
|||
|
|
@ -3,24 +3,27 @@ import uuid
|
|||
import shutil
|
||||
import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
from fastapi import HTTPException, File, BackgroundTasks, UploadFile
|
||||
from app.models.models import Activity, User, Evidence
|
||||
from app.schemas import ActivityCreate, ActivityUpdate
|
||||
from app.db.database import get_db
|
||||
from app.security import get_current_active_user
|
||||
from app.models.activity import Activity
|
||||
from app.models.user import User
|
||||
from app.models.evidence import Evidence
|
||||
from app.schemas.activity import ActivityCreate, ActivityUpdate
|
||||
from app.services.transcription_worker import TranscriptionWorker
|
||||
|
||||
UPLOAD_DIR = "uploads"
|
||||
|
||||
class ActivityService:
|
||||
def __init__(self, db: Session, current_user: User):
|
||||
def __init__(self, db: Session, current_user: User, background_tasks: Optional[BackgroundTasks] = None):
|
||||
self._db = db
|
||||
self._current_user = current_user
|
||||
self._background_tasks = background_tasks
|
||||
self._transcriptionWorker = TranscriptionWorker(self._db)
|
||||
|
||||
def get_activities(self, project_id: Optional[int] = None,
|
||||
specialty_id: Optional[int] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100):
|
||||
limit: int = 100) -> List[Activity]:
|
||||
|
||||
query = self._db.query(Activity)
|
||||
if project_id:
|
||||
|
|
@ -28,18 +31,17 @@ class ActivityService:
|
|||
if specialty_id:
|
||||
query = query.filter(Activity.specialty_id == specialty_id)
|
||||
|
||||
activities = query.offset(skip).limit(limit).all()
|
||||
return activities
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
def get_activity(self, activity_id: int):
|
||||
def get_activity(self, activity_id: int) -> Activity:
|
||||
db_activity = self._db.query(Activity).filter(Activity.id == activity_id).first()
|
||||
if db_activity is None:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
return db_activity
|
||||
|
||||
def create_activity(self, activity: ActivityCreate):
|
||||
def create_activity(self, activity: ActivityCreate) -> Activity:
|
||||
db_activity = Activity(
|
||||
**activity.dict(),
|
||||
**activity.model_dump(),
|
||||
user_id=self._current_user.id
|
||||
)
|
||||
self._db.add(db_activity)
|
||||
|
|
@ -47,12 +49,10 @@ class ActivityService:
|
|||
self._db.refresh(db_activity)
|
||||
return db_activity
|
||||
|
||||
def update_activity(self, activity_id: int, activity: ActivityUpdate):
|
||||
db_activity = self._db.query(Activity).filter(Activity.id == activity_id).first()
|
||||
if not db_activity:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
def update_activity(self, activity_id: int, activity: ActivityUpdate) -> Activity:
|
||||
db_activity = self.get_activity(activity_id)
|
||||
|
||||
update_data = activity.dict(exclude_unset=True)
|
||||
update_data = activity.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(db_activity, key, value)
|
||||
|
||||
|
|
@ -63,15 +63,12 @@ class ActivityService:
|
|||
def upload_evidence(
|
||||
self,
|
||||
activity_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
file: UploadFile = File(...),
|
||||
file: UploadFile,
|
||||
description: Optional[str] = None,
|
||||
captured_at: Optional[str] = None):
|
||||
captured_at: Optional[str] = None) -> Evidence:
|
||||
|
||||
# Verify activity exists
|
||||
db_activity = self._db.query(Activity).filter(Activity.id == activity_id).first()
|
||||
if not db_activity:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
self.get_activity(activity_id)
|
||||
|
||||
# Generate unique filename
|
||||
file_ext = os.path.splitext(file.filename)[1]
|
||||
|
|
@ -113,8 +110,56 @@ class ActivityService:
|
|||
self._db.refresh(db_evidence)
|
||||
|
||||
# If it's audio, queue transcription
|
||||
if initial_status == "pending":
|
||||
from app.services.transcription_worker import process_transcription
|
||||
background_tasks.add_task(process_transcription, db_evidence.id)
|
||||
if initial_status == "pending" and self._background_tasks:
|
||||
self._background_tasks.add_task(self._transcriptionWorker.process_transcription, db_evidence.id)
|
||||
|
||||
return db_evidence
|
||||
|
||||
def retry_transcription(self, evidence_id: int) -> Evidence:
|
||||
db_evidence = self._db.query(Evidence).filter(Evidence.id == evidence_id).first()
|
||||
if not db_evidence:
|
||||
raise HTTPException(status_code=404, detail="Evidence not found")
|
||||
|
||||
if not db_evidence.media_type or "audio" not in db_evidence.media_type:
|
||||
raise HTTPException(status_code=400, detail="Only audio evidence can be transcribed")
|
||||
|
||||
# Update status to pending
|
||||
db_evidence.transcription_status = "pending"
|
||||
db_evidence.transcription = None
|
||||
self._db.commit()
|
||||
self._db.refresh(db_evidence)
|
||||
|
||||
# Queue transcription task
|
||||
if self._background_tasks:
|
||||
self._background_tasks.add_task(self._transcriptionWorker.process_transcription, db_evidence.id)
|
||||
|
||||
return db_evidence
|
||||
|
||||
def update_evidence(self, evidence_id: int, evidence_update: any) -> Evidence:
|
||||
db_evidence = self._db.query(Evidence).filter(Evidence.id == evidence_id).first()
|
||||
if not db_evidence:
|
||||
raise HTTPException(status_code=404, detail="Evidence not found")
|
||||
|
||||
update_data = evidence_update.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(db_evidence, key, value)
|
||||
|
||||
self._db.commit()
|
||||
self._db.refresh(db_evidence)
|
||||
return db_evidence
|
||||
|
||||
def delete_evidence(self, evidence_id: int):
|
||||
db_evidence = self._db.query(Evidence).filter(Evidence.id == evidence_id).first()
|
||||
if not db_evidence:
|
||||
raise HTTPException(status_code=404, detail="Evidence not found")
|
||||
|
||||
# Optional: Delete file from disk
|
||||
if db_evidence.file_path and os.path.exists(db_evidence.file_path):
|
||||
try:
|
||||
os.remove(db_evidence.file_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
self._db.delete(db_evidence)
|
||||
self._db.commit()
|
||||
return {"detail": "Evidence deleted"}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from fastapi import HTTPException, status
|
||||
from app.models.user import User
|
||||
from app.core.security import verify_password, create_access_token, get_password_hash
|
||||
from app.core.config import settings
|
||||
|
||||
class AuthService:
|
||||
@staticmethod
|
||||
def authenticate_user(db: Session, email: str, password: str) -> Optional[User]:
|
||||
user = db.query(User).filter(User.email == email).first()
|
||||
if not user:
|
||||
return None
|
||||
if not verify_password(password, user.hashed_password):
|
||||
return None
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def create_user_token(user: User) -> dict:
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
subject=user.email, expires_delta=access_token_expires
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
@staticmethod
|
||||
def hash_password(password: str) -> str:
|
||||
return get_password_hash(password)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from fastapi import HTTPException
|
||||
from app.models.contractor import Contractor
|
||||
from app.schemas.contractor import ContractorCreate, ContractorUpdate
|
||||
|
||||
class ContractorService:
|
||||
def __init__(self, db: Session):
|
||||
self._db = db
|
||||
|
||||
def get_contractors(
|
||||
self,
|
||||
parent_id: Optional[int] = None,
|
||||
only_parents: bool = False,
|
||||
is_active: Optional[bool] = None
|
||||
) -> List[Contractor]:
|
||||
query = self._db.query(Contractor)
|
||||
if only_parents:
|
||||
query = query.filter(Contractor.parent_id == None)
|
||||
elif parent_id is not None:
|
||||
query = query.filter(Contractor.parent_id == parent_id)
|
||||
|
||||
if is_active is not None:
|
||||
query = query.filter(Contractor.is_active == is_active)
|
||||
return query.all()
|
||||
|
||||
def get_contractor(self, contractor_id: int) -> Contractor:
|
||||
db_contractor = self._db.query(Contractor).filter(Contractor.id == contractor_id).first()
|
||||
if not db_contractor:
|
||||
raise HTTPException(status_code=404, detail="Contractor not found")
|
||||
return db_contractor
|
||||
|
||||
def create_contractor(self, contractor: ContractorCreate) -> Contractor:
|
||||
db_contractor = Contractor(**contractor.model_dump())
|
||||
self._db.add(db_contractor)
|
||||
self._db.commit()
|
||||
self._db.refresh(db_contractor)
|
||||
return db_contractor
|
||||
|
||||
def update_contractor(self, contractor_id: int, contractor: ContractorUpdate) -> Contractor:
|
||||
db_contractor = self.get_contractor(contractor_id)
|
||||
|
||||
update_data = contractor.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(db_contractor, key, value)
|
||||
|
||||
self._db.commit()
|
||||
self._db.refresh(db_contractor)
|
||||
return db_contractor
|
||||
|
||||
def delete_contractor(self, contractor_id: int):
|
||||
db_contractor = self.get_contractor(contractor_id)
|
||||
self._db.delete(db_contractor)
|
||||
self._db.commit()
|
||||
return None
|
||||
|
|
@ -1,23 +1,19 @@
|
|||
|
||||
import smtplib
|
||||
import os
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.header import Header
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
from app.core.config import settings
|
||||
|
||||
class EmailService:
|
||||
@staticmethod
|
||||
def send_nc_notification(email: str, access_hash: str, nc_description: str):
|
||||
smtp_host = os.getenv("SMTP_HOST", "smtp.gmail.com")
|
||||
smtp_port = int(os.getenv("SMTP_PORT", 587))
|
||||
smtp_user = os.getenv("SMTP_USER")
|
||||
smtp_password = os.getenv("SMTP_PASSWORD")
|
||||
from_name = os.getenv("EMAILS_FROM_NAME", "Sistema de Supervisión")
|
||||
from_email = os.getenv("EMAILS_FROM_EMAIL", smtp_user)
|
||||
frontend_url = os.getenv("FRONTEND_URL", "http://localhost:4200")
|
||||
smtp_host = settings.SMTP_HOST or "smtp.gmail.com"
|
||||
smtp_port = settings.SMTP_PORT or 587
|
||||
smtp_user = settings.SMTP_USER
|
||||
smtp_password = settings.SMTP_PASSWORD
|
||||
from_name = settings.EMAILS_FROM_NAME
|
||||
from_email = settings.EMAILS_FROM_EMAIL or smtp_user
|
||||
frontend_url = settings.FRONTEND_URL
|
||||
|
||||
if not smtp_user or not smtp_password:
|
||||
print("WARNING: Email credentials not configured. Simulation mode.")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from app.models.non_conformity import NonConformity
|
||||
from app.models.evidence import Evidence
|
||||
from app.schemas.non_conformity import NonConformityUpdate
|
||||
|
||||
UPLOAD_DIR = "uploads"
|
||||
|
||||
class GuestService:
|
||||
def __init__(self, db: Session):
|
||||
self._db = db
|
||||
|
||||
def get_nc_by_hash(self, access_hash: str) -> NonConformity:
|
||||
db_nc = self._db.query(NonConformity).filter(NonConformity.access_hash == access_hash).first()
|
||||
if not db_nc:
|
||||
raise HTTPException(status_code=404, detail="Non-Conformity not found or invalid link")
|
||||
return db_nc
|
||||
|
||||
def update_guest_nc(self, access_hash: str, nc_update: NonConformityUpdate) -> NonConformity:
|
||||
db_nc = self.get_nc_by_hash(access_hash)
|
||||
|
||||
if nc_update.guest_actions is not None:
|
||||
db_nc.guest_actions = nc_update.guest_actions
|
||||
|
||||
if nc_update.closure_description is not None:
|
||||
db_nc.closure_description = nc_update.closure_description
|
||||
|
||||
if nc_update.status is not None:
|
||||
db_nc.status = nc_update.status
|
||||
|
||||
self._db.commit()
|
||||
self._db.refresh(db_nc)
|
||||
return db_nc
|
||||
|
||||
def upload_guest_evidence(
|
||||
self,
|
||||
access_hash: str,
|
||||
file: UploadFile,
|
||||
description: Optional[str] = None
|
||||
) -> Evidence:
|
||||
db_nc = self.get_nc_by_hash(access_hash)
|
||||
|
||||
# Generate unique filename
|
||||
file_ext = os.path.splitext(file.filename)[1]
|
||||
unique_filename = f"guest_nc_{uuid.uuid4()}{file_ext}"
|
||||
file_path = os.path.join(UPLOAD_DIR, unique_filename)
|
||||
|
||||
# Save file
|
||||
if not os.path.exists(UPLOAD_DIR):
|
||||
os.makedirs(UPLOAD_DIR)
|
||||
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# Save to database
|
||||
db_evidence = Evidence(
|
||||
non_conformity_id=db_nc.id,
|
||||
file_path=file_path,
|
||||
media_type=file.content_type,
|
||||
description=description,
|
||||
captured_at=datetime.datetime.utcnow()
|
||||
)
|
||||
self._db.add(db_evidence)
|
||||
self._db.commit()
|
||||
self._db.refresh(db_evidence)
|
||||
|
||||
return db_evidence
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from app.models.non_conformity import NonConformity
|
||||
from app.models.activity import Activity
|
||||
from app.models.evidence import Evidence
|
||||
from app.models.contractor import Contractor
|
||||
from app.schemas.non_conformity import NonConformityCreate, NonConformityUpdate
|
||||
from app.services.email_service import EmailService
|
||||
|
||||
UPLOAD_DIR = "uploads"
|
||||
|
||||
class NonConformityService:
|
||||
def __init__(self, db: Session):
|
||||
self._db = db
|
||||
|
||||
def get_ncs(self, activity_id: Optional[int] = None, status: Optional[str] = None) -> List[NonConformity]:
|
||||
query = self._db.query(NonConformity)
|
||||
if activity_id:
|
||||
query = query.filter(NonConformity.activity_id == activity_id)
|
||||
if status:
|
||||
query = query.filter(NonConformity.status == status)
|
||||
return query.all()
|
||||
|
||||
def get_nc(self, nc_id: int) -> NonConformity:
|
||||
db_nc = self._db.query(NonConformity).filter(NonConformity.id == nc_id).first()
|
||||
if db_nc is None:
|
||||
raise HTTPException(status_code=404, detail="Non-Conformity not found")
|
||||
return db_nc
|
||||
|
||||
def create_nc(self, nc: NonConformityCreate) -> NonConformity:
|
||||
db_activity = self._db.query(Activity).filter(Activity.id == nc.activity_id).first()
|
||||
if not db_activity:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
|
||||
# Sync responsible email from contractor if not provided
|
||||
if nc.contractor_id and not nc.responsible_email:
|
||||
contractor = self._db.query(Contractor).filter(Contractor.id == nc.contractor_id).first()
|
||||
if contractor and contractor.email:
|
||||
nc.responsible_email = contractor.email
|
||||
|
||||
db_nc = NonConformity(**nc.model_dump())
|
||||
self._db.add(db_nc)
|
||||
self._db.commit()
|
||||
self._db.refresh(db_nc)
|
||||
return db_nc
|
||||
|
||||
def update_nc(self, nc_id: int, nc_update: NonConformityUpdate) -> NonConformity:
|
||||
db_nc = self.get_nc(nc_id)
|
||||
update_data = nc_update.model_dump(exclude_unset=True)
|
||||
|
||||
# Sync responsible email if contractor_id changes and email not explicitly provided
|
||||
if 'contractor_id' in update_data and update_data['contractor_id']:
|
||||
if 'responsible_email' not in update_data or not update_data['responsible_email']:
|
||||
contractor = self._db.query(Contractor).filter(Contractor.id == update_data['contractor_id']).first()
|
||||
if contractor and contractor.email:
|
||||
update_data['responsible_email'] = contractor.email
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(db_nc, key, value)
|
||||
|
||||
self._db.commit()
|
||||
self._db.refresh(db_nc)
|
||||
return db_nc
|
||||
|
||||
def upload_nc_evidence(
|
||||
self,
|
||||
nc_id: int,
|
||||
file: UploadFile,
|
||||
description: Optional[str] = None,
|
||||
captured_at: Optional[str] = None
|
||||
) -> Evidence:
|
||||
# Verify NC exists
|
||||
self.get_nc(nc_id)
|
||||
|
||||
# Generate unique filename
|
||||
file_ext = os.path.splitext(file.filename)[1]
|
||||
unique_filename = f"nc_{uuid.uuid4()}{file_ext}"
|
||||
file_path = os.path.join(UPLOAD_DIR, unique_filename)
|
||||
|
||||
# Save file
|
||||
if not os.path.exists(UPLOAD_DIR):
|
||||
os.makedirs(UPLOAD_DIR)
|
||||
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
db_captured_at = None
|
||||
if captured_at:
|
||||
try:
|
||||
db_captured_at = datetime.datetime.fromisoformat(captured_at.replace('Z', '+00:00'))
|
||||
except:
|
||||
db_captured_at = datetime.datetime.utcnow()
|
||||
else:
|
||||
db_captured_at = datetime.datetime.utcnow()
|
||||
|
||||
# Save to database
|
||||
db_evidence = Evidence(
|
||||
non_conformity_id=nc_id,
|
||||
file_path=file_path,
|
||||
media_type=file.content_type,
|
||||
description=description,
|
||||
captured_at=db_captured_at
|
||||
)
|
||||
self._db.add(db_evidence)
|
||||
self._db.commit()
|
||||
self._db.refresh(db_evidence)
|
||||
|
||||
return db_evidence
|
||||
|
||||
def delete_nc(self, nc_id: int):
|
||||
db_nc = self.get_nc(nc_id)
|
||||
self._db.delete(db_nc)
|
||||
self._db.commit()
|
||||
return {"detail": "Non-Conformity deleted"}
|
||||
|
||||
def notify_responsible(self, nc_id: int):
|
||||
db_nc = self.get_nc(nc_id)
|
||||
|
||||
if not db_nc.responsible_email:
|
||||
raise HTTPException(status_code=400, detail="No responsible email configured for this Non-Conformity")
|
||||
|
||||
# Generate hash if it doesn't exist
|
||||
if not db_nc.access_hash:
|
||||
db_nc.access_hash = str(uuid.uuid4())
|
||||
self._db.commit()
|
||||
|
||||
# Send email
|
||||
EmailService.send_nc_notification(db_nc.responsible_email, db_nc.access_hash, db_nc.description)
|
||||
|
||||
# Update status to in-checking
|
||||
db_nc.status = 'in-checking'
|
||||
self._db.commit()
|
||||
self._db.refresh(db_nc)
|
||||
|
||||
return {"message": "Notification sent successfully", "access_hash": db_nc.access_hash, "status": db_nc.status}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from fastapi import HTTPException
|
||||
from app.models.project import Project
|
||||
from app.models.specialty import Specialty
|
||||
from app.models.contractor import Contractor
|
||||
from app.schemas.project import ProjectCreate
|
||||
|
||||
class ProjectService:
|
||||
def __init__(self, db: Session):
|
||||
self._db = db
|
||||
|
||||
def get_projects(self, skip: int = 0, limit: int = 100) -> List[Project]:
|
||||
return self._db.query(Project).offset(skip).limit(limit).all()
|
||||
|
||||
def get_project(self, project_id: int) -> Project:
|
||||
db_project = self._db.query(Project).filter(Project.id == project_id).first()
|
||||
if db_project is None:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
return db_project
|
||||
|
||||
def create_project(self, project: ProjectCreate) -> Project:
|
||||
db_project = self._db.query(Project).filter(Project.code == project.code).first()
|
||||
if db_project:
|
||||
raise HTTPException(status_code=400, detail="Project code already exists")
|
||||
|
||||
project_data = project.model_dump(exclude={'specialty_ids', 'contractor_ids'})
|
||||
db_project = Project(**project_data)
|
||||
|
||||
# Handle Parent Project
|
||||
if project.parent_id:
|
||||
parent = self.get_project(project.parent_id)
|
||||
if not parent:
|
||||
raise HTTPException(status_code=404, detail="Parent project not found")
|
||||
|
||||
# Handle Specialties
|
||||
if project.specialty_ids:
|
||||
specialties = self._db.query(Specialty).filter(Specialty.id.in_(project.specialty_ids)).all()
|
||||
db_project.specialties = specialties
|
||||
|
||||
# Handle Contractors
|
||||
if project.contractor_ids:
|
||||
contractors = self._db.query(Contractor).filter(Contractor.id.in_(project.contractor_ids)).all()
|
||||
db_project.contractors = contractors
|
||||
|
||||
self._db.add(db_project)
|
||||
self._db.commit()
|
||||
self._db.refresh(db_project)
|
||||
return db_project
|
||||
|
||||
def update_project(self, project_id: int, project: ProjectCreate) -> Project:
|
||||
db_project = self.get_project(project_id)
|
||||
|
||||
# Update simple fields
|
||||
for key, value in project.model_dump(exclude={'specialty_ids', 'contractor_ids'}).items():
|
||||
setattr(db_project, key, value)
|
||||
|
||||
# Handle Parent Project
|
||||
if project.parent_id is not None:
|
||||
if project.parent_id != 0:
|
||||
self.get_project(project.parent_id)
|
||||
db_project.parent_id = project.parent_id
|
||||
else:
|
||||
db_project.parent_id = None
|
||||
|
||||
# Update Specialties
|
||||
if project.specialty_ids is not None:
|
||||
specialties = self._db.query(Specialty).filter(Specialty.id.in_(project.specialty_ids)).all()
|
||||
db_project.specialties = specialties
|
||||
|
||||
# Update Contractors
|
||||
if project.contractor_ids is not None:
|
||||
contractors = self._db.query(Contractor).filter(Contractor.id.in_(project.contractor_ids)).all()
|
||||
db_project.contractors = contractors
|
||||
|
||||
self._db.commit()
|
||||
self._db.refresh(db_project)
|
||||
return db_project
|
||||
|
||||
def delete_project(self, project_id: int):
|
||||
db_project = self.get_project(project_id)
|
||||
self._db.delete(db_project)
|
||||
self._db.commit()
|
||||
return None
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import os
|
||||
import google.generativeai as genai
|
||||
import tempfile
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from app.core.config import settings
|
||||
|
||||
class TranscriptionService:
|
||||
@staticmethod
|
||||
async def transcribe_audio(file: UploadFile) -> str:
|
||||
if not settings.GOOGLE_API_KEY:
|
||||
return f"[MOCK GEMINI TRANSCRIPTION] Se ha recibido un archivo de audio de tipo {file.content_type}. Configure GOOGLE_API_KEY para transcripción real con Gemini."
|
||||
|
||||
try:
|
||||
# Create a temporary file to store the upload
|
||||
suffix = os.path.splitext(file.filename)[1] or ".wav"
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
|
||||
content = await file.read()
|
||||
tmp.write(content)
|
||||
tmp_path = tmp.name
|
||||
|
||||
# Configure and upload
|
||||
genai.configure(api_key=settings.GOOGLE_API_KEY)
|
||||
audio_file = genai.upload_file(path=tmp_path, mime_type=file.content_type or "audio/wav")
|
||||
|
||||
# Use Gemini 2.0 Flash Lite
|
||||
model = genai.GenerativeModel("gemini-2.0-flash-lite")
|
||||
response = model.generate_content([
|
||||
"Por favor, transcribe exactamente lo que se dice en este audio. Solo devuelve el texto transcrito.",
|
||||
audio_file
|
||||
])
|
||||
|
||||
# Cleanup
|
||||
os.unlink(tmp_path)
|
||||
|
||||
return response.text
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
@ -2,35 +2,33 @@ import os
|
|||
import time
|
||||
from sqlalchemy.orm import Session
|
||||
import google.generativeai as genai
|
||||
from app.models import Evidence
|
||||
from app.database import SessionLocal
|
||||
from app.models.evidence import Evidence
|
||||
from app.core.config import settings
|
||||
|
||||
def process_transcription(evidence_id: int):
|
||||
class TranscriptionWorker:
|
||||
def __init__(self, db: Session):
|
||||
self._db = db
|
||||
|
||||
def process_transcription(self, evidence_id: int):
|
||||
"""
|
||||
Background task to transcribe audio.
|
||||
In a real scenario, this would call a local model runner like Ollama (Whisper)
|
||||
or an external API like Gemini.
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
evidence = db.query(Evidence).filter(Evidence.id == evidence_id).first()
|
||||
evidence = self._db.query(Evidence).filter(Evidence.id == evidence_id).first()
|
||||
if not evidence:
|
||||
return
|
||||
|
||||
evidence.transcription_status = "processing"
|
||||
db.commit()
|
||||
|
||||
# Simulate local processing or call a local model
|
||||
# For now, we'll try to use Gemini if available, or a mock
|
||||
self._db.commit()
|
||||
|
||||
file_path = evidence.file_path
|
||||
if not os.path.exists(file_path):
|
||||
evidence.transcription_status = "error"
|
||||
evidence.transcription = "Error: File not found"
|
||||
db.commit()
|
||||
self._db.commit()
|
||||
return
|
||||
|
||||
api_key = os.getenv("GOOGLE_API_KEY")
|
||||
api_key = settings.GOOGLE_API_KEY
|
||||
if api_key:
|
||||
try:
|
||||
genai.configure(api_key=api_key)
|
||||
|
|
@ -38,8 +36,8 @@ def process_transcription(evidence_id: int):
|
|||
# Upload to Gemini (Media Service)
|
||||
audio_file = genai.upload_file(path=file_path, mime_type=evidence.media_type or "audio/wav")
|
||||
|
||||
# Use Gemini 1.5 Flash for audio-to-text
|
||||
model = genai.GenerativeModel("gemini-2.5-flash-lite")
|
||||
# Use Gemini 2.0 Flash Lite (as requested in original code, though it said 1.5 in comment)
|
||||
model = genai.GenerativeModel("gemini-2.0-flash-lite")
|
||||
response = model.generate_content([
|
||||
"Por favor, transcribe exactamente lo que se dice en este audio. Solo devuelve el texto transcrito.",
|
||||
audio_file
|
||||
|
|
@ -51,21 +49,19 @@ def process_transcription(evidence_id: int):
|
|||
evidence.transcription_status = "error"
|
||||
evidence.transcription = f"Error: {str(e)}"
|
||||
else:
|
||||
# Mock transcription if no API key (Local Model Simulation)
|
||||
# Mock transcription if no API key
|
||||
time.sleep(5) # Simulate work
|
||||
evidence.transcription = f"[LOCAL MOCK TRANSCRIPTION] Transcripción asíncrona completada para {os.path.basename(file_path)}"
|
||||
evidence.transcription_status = "completed"
|
||||
|
||||
db.commit()
|
||||
self._db.commit()
|
||||
except Exception as e:
|
||||
print(f"Transcription error: {e}")
|
||||
try:
|
||||
evidence = db.query(Evidence).filter(Evidence.id == evidence_id).first()
|
||||
evidence = self._db.query(Evidence).filter(Evidence.id == evidence_id).first()
|
||||
if evidence:
|
||||
evidence.transcription_status = "error"
|
||||
evidence.transcription = f"Unexpected error: {str(e)}"
|
||||
db.commit()
|
||||
self._db.commit()
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
db.close()
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
from sqlalchemy.orm import Session
|
||||
from fastapi import HTTPException
|
||||
from app.models.models import User
|
||||
from app.security import get_password_hash
|
||||
from app.schemas import UserCreate
|
||||
from app.models.user import User
|
||||
from app.core.security import get_password_hash
|
||||
from app.schemas.user import UserCreate
|
||||
|
||||
def get_users(db: Session, skip: int = 0, limit: int = 100):
|
||||
return db.query(User).offset(skip).limit(limit).all()
|
||||
class UserService:
|
||||
def __init__(self, db: Session):
|
||||
self._db = db
|
||||
|
||||
def create_user(db: Session, user: UserCreate):
|
||||
db_user = db.query(User).filter(User.email == user.email).first()
|
||||
def get_users(self, skip: int = 0, limit: int = 100):
|
||||
return self._db.query(User).offset(skip).limit(limit).all()
|
||||
|
||||
def create_user(self, user: UserCreate):
|
||||
db_user = self._db.query(User).filter(User.email == user.email).first()
|
||||
if db_user:
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
hashed_password = get_password_hash(user.password)
|
||||
|
|
@ -18,7 +22,7 @@ def create_user(db: Session, user: UserCreate):
|
|||
full_name=user.full_name,
|
||||
role=user.role
|
||||
)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
self._db.add(db_user)
|
||||
self._db.commit()
|
||||
self._db.refresh(db_user)
|
||||
return db_user
|
||||
|
|
@ -1,49 +1,34 @@
|
|||
import sys
|
||||
# Compatibility patch for importlib.metadata in Python < 3.10
|
||||
# This fixes the AttributeError: module 'importlib.metadata' has no attribute 'packages_distributions'
|
||||
if sys.version_info < (3, 10):
|
||||
try:
|
||||
import importlib_metadata
|
||||
import importlib
|
||||
importlib.metadata = importlib_metadata
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.routers import auth, users, projects, activities, specialties, contractors, transcription, non_conformities, guest
|
||||
import os
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import os
|
||||
from app.api.v1.api import api_router
|
||||
from app.core.config import settings
|
||||
|
||||
app = FastAPI(title="Sistema de Supervision API", version="0.1.0")
|
||||
app = FastAPI(
|
||||
title=settings.PROJECT_NAME,
|
||||
openapi_url=f"{settings.API_V1_STR}/openapi.json"
|
||||
)
|
||||
|
||||
# CORS (allow all for dev)
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[str(origin).strip("/") for origin in [settings.FRONTEND_URL, "*"]],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(users.router)
|
||||
app.include_router(projects.router)
|
||||
app.include_router(activities.router)
|
||||
app.include_router(specialties.router)
|
||||
app.include_router(contractors.router)
|
||||
app.include_router(transcription.router)
|
||||
app.include_router(non_conformities.router)
|
||||
app.include_router(guest.router)
|
||||
app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||
|
||||
# Mount uploads directory to serve files
|
||||
if not os.path.exists("uploads"):
|
||||
os.makedirs("uploads")
|
||||
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|
||||
app.mount("/api/v1/uploads", StaticFiles(directory="uploads"), name="uploads")
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
return {"message": "Sistema de Supervision API is running"}
|
||||
return {"message": f"{settings.PROJECT_NAME} is running"}
|
||||
|
||||
@app.get("/health")
|
||||
def health_check():
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export const environment = {
|
||||
apiUrl: 'http://192.168.1.76:8000'
|
||||
apiUrl: 'http://192.168.1.76:8000/api/v1'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export const environment = {
|
||||
apiUrl: 'http://192.168.1.76:8000'
|
||||
apiUrl: 'http://192.168.1.76:8000/api/v1'
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue