Bckn funcion
This commit is contained in:
@@ -1,48 +0,0 @@
|
||||
import asyncio
|
||||
from app.core.database import AsyncSessionLocal, Base, engine
|
||||
from app.models.ticket import Ticket
|
||||
from sqlalchemy import String, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from app.core.database import GUID
|
||||
import uuid
|
||||
|
||||
# Agregar modelo al archivo ticket.py
|
||||
new_model = '''
|
||||
|
||||
class TicketParticipant(Base):
|
||||
"""Participantes asignados a un ticket"""
|
||||
__tablename__ = "ticket_participants"
|
||||
|
||||
ticket_id: Mapped[uuid.UUID] = mapped_column(
|
||||
GUID(),
|
||||
ForeignKey("tickets.id", ondelete="CASCADE"),
|
||||
nullable=False
|
||||
)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
GUID(),
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False
|
||||
)
|
||||
role: Mapped[str] = mapped_column(String(50), nullable=False, default="participant")
|
||||
|
||||
ticket: Mapped["Ticket"] = relationship("Ticket", back_populates="participants")
|
||||
user: Mapped["User"] = relationship("User")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("ticket_id", "user_id", name="uq_ticket_participant"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TicketParticipant(ticket={self.ticket_id}, user={self.user_id})>"
|
||||
'''
|
||||
|
||||
with open("/app/app/models/ticket.py", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
if "TicketParticipant" in content:
|
||||
print("Ya existe TicketParticipant")
|
||||
else:
|
||||
content += new_model
|
||||
with open("/app/app/models/ticket.py", "w") as f:
|
||||
f.write(content)
|
||||
print("OK: TicketParticipant agregado")
|
||||
@@ -1,21 +0,0 @@
|
||||
with open("/app/app/models/ticket.py", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
old = ' def __repr__(self) -> str:\n return f"<Ticket(id={self.id}, number=\'{self.ticket_number}\', status={self.status})>"'
|
||||
new = ''' participants: Mapped[list["TicketParticipant"]] = relationship(
|
||||
"TicketParticipant",
|
||||
back_populates="ticket",
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Ticket(id={self.id}, number=\'{self.ticket_number}\', status={self.status})>"'''
|
||||
|
||||
if old in content:
|
||||
content = content.replace(old, new)
|
||||
print("OK: relacion participants agregada")
|
||||
else:
|
||||
print("ERROR: bloque no encontrado")
|
||||
|
||||
with open("/app/app/models/ticket.py", "w") as f:
|
||||
f.write(content)
|
||||
53
backend/app/api/schemas/issue.py
Normal file
53
backend/app/api/schemas/issue.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Schemas para TicketIssue"""
|
||||
from pydantic import BaseModel, field_validator
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
|
||||
class TaggedUserBasic(BaseModel):
|
||||
id: uuid.UUID
|
||||
full_name: str
|
||||
email: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class IssueCreate(BaseModel):
|
||||
content: str
|
||||
priority: str = "MEDIUM"
|
||||
tagged_user_ids: List[uuid.UUID] = []
|
||||
|
||||
@field_validator("priority")
|
||||
@classmethod
|
||||
def validate_priority(cls, v: str) -> str:
|
||||
valid = {"LOW", "MEDIUM", "HIGH", "URGENT"}
|
||||
if v.upper() not in valid:
|
||||
raise ValueError(f"priority debe ser uno de: {valid}")
|
||||
return v.upper()
|
||||
|
||||
@field_validator("content")
|
||||
@classmethod
|
||||
def validate_content(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("content no puede estar vacío")
|
||||
return v.strip()
|
||||
|
||||
|
||||
class IssueResponse(BaseModel):
|
||||
id: uuid.UUID
|
||||
ticket_id: uuid.UUID
|
||||
tenant_id: uuid.UUID
|
||||
content: str
|
||||
priority: str
|
||||
created_by: uuid.UUID
|
||||
created_by_name: str
|
||||
tagged_users: List[TaggedUserBasic] = []
|
||||
attachment_filename: Optional[str] = None
|
||||
attachment_mime_type: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -7,6 +7,8 @@ from sqlalchemy.orm import selectinload
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
from app.models.issue import TicketIssue, ticket_issue_tagged_users
|
||||
from app.api.schemas.issue import IssueCreate, IssueResponse
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.api.deps import get_current_user, get_current_tenant
|
||||
@@ -411,3 +413,179 @@ async def download_attachment(ticket_id: str, attachment_id: str, db: AsyncSessi
|
||||
|
||||
logger.info(f"Returning file: {attachment.original_filename}")
|
||||
return FileResponse(path=file_path, filename=attachment.original_filename, media_type=attachment.mime_type)
|
||||
|
||||
@router.get("/{ticket_id}/issues", response_model=List[IssueResponse])
|
||||
async def get_ticket_issues(
|
||||
ticket_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Obtener asuntos de un ticket."""
|
||||
ticket_uuid = validate_uuid_param(ticket_id, "ticket ID")
|
||||
|
||||
# Verificar que el ticket existe y pertenece al tenant
|
||||
query = select(Ticket).where(
|
||||
Ticket.id == ticket_uuid,
|
||||
Ticket.tenant_id == current_user.tenant_id,
|
||||
)
|
||||
if current_user.role.is_client:
|
||||
query = query.where(Ticket.created_by == current_user.id)
|
||||
|
||||
result = await db.execute(query)
|
||||
ticket_obj = result.scalars().first()
|
||||
if not ticket_obj:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ticket no encontrado")
|
||||
|
||||
issues_query = (
|
||||
select(TicketIssue)
|
||||
.where(TicketIssue.ticket_id == ticket_uuid)
|
||||
.options(
|
||||
selectinload(TicketIssue.created_by_user),
|
||||
selectinload(TicketIssue.tagged_users),
|
||||
)
|
||||
.order_by(TicketIssue.created_at.desc())
|
||||
)
|
||||
result = await db.execute(issues_query)
|
||||
issues = result.scalars().all()
|
||||
|
||||
return [
|
||||
IssueResponse(
|
||||
id=issue.id,
|
||||
ticket_id=issue.ticket_id,
|
||||
tenant_id=issue.tenant_id,
|
||||
content=issue.content,
|
||||
priority=issue.priority.value,
|
||||
created_by=issue.created_by,
|
||||
created_by_name=f"{issue.created_by_user.first_name} {issue.created_by_user.last_name}",
|
||||
tagged_users=[
|
||||
{"id": u.id, "full_name": f"{u.first_name} {u.last_name}", "email": u.email}
|
||||
for u in issue.tagged_users
|
||||
],
|
||||
attachment_filename=issue.attachment_filename,
|
||||
attachment_mime_type=issue.attachment_mime_type,
|
||||
created_at=issue.created_at,
|
||||
updated_at=issue.updated_at,
|
||||
)
|
||||
for issue in issues
|
||||
]
|
||||
|
||||
|
||||
@router.post("/{ticket_id}/issues", response_model=IssueResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_ticket_issue(
|
||||
ticket_id: str,
|
||||
issue: IssueCreate,
|
||||
file: Optional[UploadFile] = File(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_tenant: Tenant = Depends(get_current_tenant),
|
||||
):
|
||||
"""
|
||||
Crear un asunto de escalación en un ticket.
|
||||
|
||||
Solo puede crearlo el dueño del ticket o el CLIENT_ADMIN del tenant.
|
||||
"""
|
||||
ticket_uuid = validate_uuid_param(ticket_id, "ticket ID")
|
||||
|
||||
# 1. Verificar que el ticket existe y pertenece al tenant
|
||||
result = await db.execute(
|
||||
select(Ticket).where(
|
||||
Ticket.id == ticket_uuid,
|
||||
Ticket.tenant_id == current_user.tenant_id,
|
||||
)
|
||||
)
|
||||
ticket_obj = result.scalars().first()
|
||||
if not ticket_obj:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ticket no encontrado")
|
||||
|
||||
# 2. Verificar permisos: solo el creador del ticket o CLIENT_ADMIN
|
||||
is_ticket_owner = ticket_obj.created_by == current_user.id
|
||||
is_client_admin = current_user.role == UserRole.CLIENT_ADMIN
|
||||
|
||||
if not is_ticket_owner and not is_client_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Solo el creador del ticket o el administrador del tenant pueden crear asuntos",
|
||||
)
|
||||
|
||||
# 3. Validar usuarios etiquetados — deben pertenecer al mismo tenant
|
||||
tagged_users = []
|
||||
if issue.tagged_user_ids:
|
||||
result = await db.execute(
|
||||
select(User).where(
|
||||
User.id.in_(issue.tagged_user_ids),
|
||||
User.tenant_id == current_user.tenant_id,
|
||||
User.is_active == True,
|
||||
)
|
||||
)
|
||||
tagged_users = result.scalars().all()
|
||||
|
||||
found_ids = {u.id for u in tagged_users}
|
||||
missing = [str(uid) for uid in issue.tagged_user_ids if uid not in found_ids]
|
||||
if missing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Usuarios no encontrados en el tenant: {missing}",
|
||||
)
|
||||
|
||||
# 4. Manejar adjunto opcional
|
||||
attachment_path = None
|
||||
attachment_filename = None
|
||||
attachment_mime_type = None
|
||||
if file and file.filename:
|
||||
file_metadata = await file_handler.save_upload(file, current_tenant.id, ticket_uuid)
|
||||
attachment_path = file_metadata["file_path"]
|
||||
attachment_filename = file_metadata["original_filename"]
|
||||
attachment_mime_type = file_metadata["mime_type"]
|
||||
|
||||
# 5. Crear el asunto
|
||||
new_issue = TicketIssue(
|
||||
id=uuid.uuid4(),
|
||||
ticket_id=ticket_uuid,
|
||||
tenant_id=current_user.tenant_id,
|
||||
created_by=current_user.id,
|
||||
content=issue.content,
|
||||
priority=TicketPriority[issue.priority],
|
||||
attachment_path=attachment_path,
|
||||
attachment_filename=attachment_filename,
|
||||
attachment_mime_type=attachment_mime_type,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow(),
|
||||
)
|
||||
new_issue.tagged_users = tagged_users
|
||||
db.add(new_issue)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(new_issue, ["created_by_user", "tagged_users"])
|
||||
|
||||
# 6. Audit log
|
||||
await safe_audit_log(
|
||||
db=db,
|
||||
tenant_id=current_user.tenant_id,
|
||||
user_id=current_user.id,
|
||||
action="ticket.issue.create",
|
||||
resource_type="ticket_issue",
|
||||
resource_id=new_issue.id,
|
||||
new_values={
|
||||
"ticket_id": str(ticket_uuid),
|
||||
"priority": issue.priority,
|
||||
"tagged_users": [str(uid) for uid in issue.tagged_user_ids],
|
||||
},
|
||||
)
|
||||
|
||||
return IssueResponse(
|
||||
id=new_issue.id,
|
||||
ticket_id=new_issue.ticket_id,
|
||||
tenant_id=new_issue.tenant_id,
|
||||
content=new_issue.content,
|
||||
priority=new_issue.priority.value,
|
||||
created_by=new_issue.created_by,
|
||||
created_by_name=f"{new_issue.created_by_user.first_name} {new_issue.created_by_user.last_name}",
|
||||
tagged_users=[
|
||||
{"id": u.id, "full_name": f"{u.first_name} {u.last_name}", "email": u.email}
|
||||
for u in new_issue.tagged_users
|
||||
],
|
||||
attachment_filename=new_issue.attachment_filename,
|
||||
attachment_mime_type=new_issue.attachment_mime_type,
|
||||
created_at=new_issue.created_at,
|
||||
updated_at=new_issue.updated_at,
|
||||
)
|
||||
@@ -35,6 +35,7 @@ from app.middleware.tenant import TenantMiddleware
|
||||
from app.middleware.correlation_id import CorrelationIDMiddleware
|
||||
from app.core.cache import cache
|
||||
from app.core.limiter import limiter
|
||||
from app.models.issue import TicketIssue, ticket_issue_tagged_users
|
||||
|
||||
settings = get_settings()
|
||||
setup_logging()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from .user import User
|
||||
from .tenant import Tenant
|
||||
from .ticket import Ticket
|
||||
from .issue import TicketIssue, ticket_issue_tagged_users
|
||||
from .comment import TicketComment
|
||||
from .system import System
|
||||
from .category import Category
|
||||
@@ -15,6 +16,7 @@ __all__ = [
|
||||
"User",
|
||||
"Tenant",
|
||||
"Ticket",
|
||||
"TicketIssue",
|
||||
"TicketComment",
|
||||
"System",
|
||||
"Category",
|
||||
|
||||
102
backend/app/models/issue.py
Normal file
102
backend/app/models/issue.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
TicketIssue Model - ServiceManagerWeb
|
||||
|
||||
Asuntos de escalación asociados a tickets.
|
||||
Creados por el dueño del ticket o el CLIENT_ADMIN del tenant.
|
||||
"""
|
||||
from sqlalchemy import String, ForeignKey, Text, Table, Column
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from app.core.database import Base, GUID
|
||||
from app.models.ticket import TicketPriority
|
||||
from sqlalchemy import Enum as SAEnum
|
||||
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
|
||||
|
||||
|
||||
# Tabla de relación N:M entre TicketIssue y User (usuarios etiquetados)
|
||||
ticket_issue_tagged_users = Table(
|
||||
"ticket_issue_tagged_users",
|
||||
Base.metadata,
|
||||
Column(
|
||||
"issue_id",
|
||||
GUID(),
|
||||
ForeignKey("ticket_issues.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
nullable=False,
|
||||
),
|
||||
Column(
|
||||
"user_id",
|
||||
GUID(),
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TicketIssue(Base):
|
||||
"""
|
||||
Asunto de escalación de un ticket.
|
||||
|
||||
Solo puede crearlo:
|
||||
- El usuario que creó el ticket (created_by del Ticket)
|
||||
- El CLIENT_ADMIN del mismo tenant
|
||||
"""
|
||||
__tablename__ = "ticket_issues"
|
||||
|
||||
# Multi-tenancy — siempre filtrar por esto
|
||||
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
||||
GUID(),
|
||||
ForeignKey("tenants.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Relación con el ticket padre
|
||||
ticket_id: Mapped[uuid.UUID] = mapped_column(
|
||||
GUID(),
|
||||
ForeignKey("tickets.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Quién lo creó
|
||||
created_by: Mapped[uuid.UUID] = mapped_column(
|
||||
GUID(),
|
||||
ForeignKey("users.id"),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Contenido
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
|
||||
priority: Mapped[TicketPriority] = mapped_column(
|
||||
SAEnum(TicketPriority, name="ticket_priority_enum", native_enum=False).with_variant(
|
||||
PG_ENUM(TicketPriority, name="ticket_priority_enum", create_type=True),
|
||||
"postgresql",
|
||||
),
|
||||
default=TicketPriority.MEDIUM,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Adjunto opcional (reutiliza file_handler igual que TicketAttachment)
|
||||
attachment_path: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
||||
attachment_filename: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
attachment_mime_type: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
|
||||
# Relationships
|
||||
ticket: Mapped["Ticket"] = relationship("Ticket", back_populates="issues")
|
||||
|
||||
created_by_user: Mapped["User"] = relationship(
|
||||
"User",
|
||||
foreign_keys=[created_by],
|
||||
)
|
||||
|
||||
tagged_users: Mapped[List["User"]] = relationship(
|
||||
"User",
|
||||
secondary=ticket_issue_tagged_users,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<TicketIssue(id={self.id}, ticket={self.ticket_id}, priority={self.priority})>"
|
||||
@@ -163,6 +163,12 @@ class Ticket(Base):
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
issues: Mapped[list["TicketIssue"]] = relationship(
|
||||
"TicketIssue",
|
||||
back_populates="ticket",
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Ticket(id={self.id}, number='{self.ticket_number}', status={self.status})>"
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""add_ticket_issues
|
||||
|
||||
Revision ID: b2dcb926e091
|
||||
Revises: 46bccd948688
|
||||
Create Date: 2026-03-17 16:25:34.452826
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision = 'b2dcb926e091'
|
||||
down_revision = '46bccd948688'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
existing_tables = inspector.get_table_names()
|
||||
|
||||
if 'ticket_issues' not in existing_tables:
|
||||
op.create_table(
|
||||
'ticket_issues',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('tenant_id', sa.UUID(), nullable=False),
|
||||
sa.Column('ticket_id', sa.UUID(), nullable=False),
|
||||
sa.Column('created_by', sa.UUID(), nullable=False),
|
||||
sa.Column('content', sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
'priority',
|
||||
postgresql.ENUM('LOW', 'MEDIUM', 'HIGH', 'URGENT',
|
||||
name='ticket_priority_enum', create_type=False),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column('attachment_path', sa.String(500), nullable=True),
|
||||
sa.Column('attachment_filename', sa.String(255), nullable=True),
|
||||
sa.Column('attachment_mime_type', sa.String(100), nullable=True),
|
||||
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True),
|
||||
server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True),
|
||||
server_default=sa.text('now()'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['users.id']),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
|
||||
if 'ticket_issue_tagged_users' not in existing_tables:
|
||||
op.create_table(
|
||||
'ticket_issue_tagged_users',
|
||||
sa.Column('issue_id', sa.UUID(), nullable=False),
|
||||
sa.Column('user_id', sa.UUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['issue_id'], ['ticket_issues.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('issue_id', 'user_id'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table('ticket_issue_tagged_users')
|
||||
op.drop_table('ticket_issues')
|
||||
@@ -1,3 +0,0 @@
|
||||
with open("/app/app/api/schemas/category.py", "r") as f:
|
||||
content = f.read()
|
||||
print(content)
|
||||
@@ -1,19 +0,0 @@
|
||||
import asyncio
|
||||
from app.core.database import AsyncSessionLocal
|
||||
from app.models.user import User
|
||||
from app.models.tenant import Tenant
|
||||
from sqlalchemy import select
|
||||
|
||||
async def check():
|
||||
async with AsyncSessionLocal() as db:
|
||||
result = await db.execute(
|
||||
select(User, Tenant).join(Tenant).where(User.email == 'javier@ventas.com')
|
||||
)
|
||||
user, tenant = result.one()
|
||||
print(f"user.tenant_id: {user.tenant_id}")
|
||||
print(f"tenant.id: {tenant.id}")
|
||||
print(f"tenant.slug: {tenant.slug}")
|
||||
print(f"user.role: {user.role}")
|
||||
print(f"role.is_client: {user.role.is_client}")
|
||||
|
||||
asyncio.run(check())
|
||||
@@ -1,28 +0,0 @@
|
||||
with open("/app/app/api/v1/endpoints/categories.py", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
old = ''' # ✅ CORREGIDO: Asignar tenant_id del usuario actual
|
||||
db_category = Category(
|
||||
**category.model_dump(),
|
||||
tenant_id=current_user.tenant_id # ✅ Multi-tenancy automático
|
||||
)'''
|
||||
|
||||
new = ''' # ADMIN global puede especificar tenant_id; otros usan el suyo
|
||||
from app.models.user import UserRole as _R
|
||||
data = category.model_dump()
|
||||
if current_user.role == _R.ADMIN and data.get("tenant_id"):
|
||||
target_tenant_id = data["tenant_id"]
|
||||
else:
|
||||
target_tenant_id = current_user.tenant_id
|
||||
data["tenant_id"] = target_tenant_id
|
||||
|
||||
db_category = Category(**data)'''
|
||||
|
||||
if old in content:
|
||||
content = content.replace(old, new)
|
||||
print("OK: fix aplicado")
|
||||
else:
|
||||
print("ERROR: bloque no encontrado")
|
||||
|
||||
with open("/app/app/api/v1/endpoints/categories.py", "w") as f:
|
||||
f.write(content)
|
||||
@@ -1,15 +0,0 @@
|
||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
old = ' # Verificar permisos - solo ADMIN puede eliminar\n if current_user.role != UserRole.ADMIN:\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail="Only admins can delete users"\n )'
|
||||
|
||||
new = ' # Verificar permisos - ADMIN global o CLIENT_ADMIN pueden eliminar\n allowed = [UserRole.ADMIN, UserRole.CLIENT_ADMIN]\n if current_user.role not in allowed:\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail="Only admins can delete users"\n )'
|
||||
|
||||
if old in content:
|
||||
content = content.replace(old, new)
|
||||
print("OK: permisos delete actualizados")
|
||||
else:
|
||||
print("ERROR: bloque no encontrado")
|
||||
|
||||
with open("/app/app/api/v1/endpoints/users.py", "w") as f:
|
||||
f.write(content)
|
||||
@@ -1,14 +0,0 @@
|
||||
with open("/app/app/api/v1/endpoints/users.py", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
old = " # Soft delete\n db_user.is_active = False\n await db.commit()"
|
||||
new = " # Hard delete\n await db.delete(db_user)\n await db.commit()"
|
||||
|
||||
if old in content:
|
||||
content = content.replace(old, new)
|
||||
print("OK: hard delete aplicado")
|
||||
else:
|
||||
print("ERROR: bloque no encontrado")
|
||||
|
||||
with open("/app/app/api/v1/endpoints/users.py", "w") as f:
|
||||
f.write(content)
|
||||
@@ -1,34 +0,0 @@
|
||||
with open("/app/app/api/v1/router.py", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
old = "from app.api.v1.endpoints import auth, health, tenants, users, systems, categories, tickets, client_profile, audit, sla, reports"
|
||||
new = "from app.api.v1.endpoints import auth, health, tenants, users, systems, categories, tickets, client_profile, audit, sla, reports, participants"
|
||||
|
||||
old2 = "# Tickets routes\napi_router.include_router(\n tickets.router,\n prefix=\"/tickets\",\n tags=[\"tickets\"]\n)"
|
||||
new2 = """# Tickets routes
|
||||
api_router.include_router(
|
||||
tickets.router,
|
||||
prefix="/tickets",
|
||||
tags=["tickets"]
|
||||
)
|
||||
# Participants routes (nested under tickets)
|
||||
api_router.include_router(
|
||||
participants.router,
|
||||
prefix="/tickets",
|
||||
tags=["participants"]
|
||||
)"""
|
||||
|
||||
if old in content:
|
||||
content = content.replace(old, new)
|
||||
print("OK: import agregado")
|
||||
else:
|
||||
print("ERROR: import no encontrado")
|
||||
|
||||
if old2 in content:
|
||||
content = content.replace(old2, new2)
|
||||
print("OK: router agregado")
|
||||
else:
|
||||
print("ERROR: router no encontrado")
|
||||
|
||||
with open("/app/app/api/v1/router.py", "w") as f:
|
||||
f.write(content)
|
||||
@@ -1,30 +0,0 @@
|
||||
with open("/app/app/api/schemas/category.py", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
old = '''class CategoryCreate(BaseModel):
|
||||
"""Schema para crear categoría. No incluye tenant_id (se asigna automáticamente)."""
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
sla_response_hours: int = 24
|
||||
sla_resolution_hours: int = 72
|
||||
auto_assign_to: Optional[uuid.UUID] = None'''
|
||||
|
||||
new = '''class CategoryCreate(BaseModel):
|
||||
"""Schema para crear categoría. tenant_id opcional para ADMIN global."""
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
sla_response_hours: int = 24
|
||||
sla_resolution_hours: int = 72
|
||||
auto_assign_to: Optional[uuid.UUID] = None
|
||||
tenant_id: Optional[uuid.UUID] = None'''
|
||||
|
||||
if old in content:
|
||||
content = content.replace(old, new)
|
||||
print("OK")
|
||||
else:
|
||||
print("ERROR: bloque no encontrado")
|
||||
|
||||
with open("/app/app/api/schemas/category.py", "w") as f:
|
||||
f.write(content)
|
||||
@@ -1,11 +0,0 @@
|
||||
with open("/app/src/routes/usuarios/+page.svelte", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
content = content.replace(
|
||||
"{#if (user as any).can_manage_users && user.role !== 'CLIENT_ADMIN'}",
|
||||
"{#if user.can_manage_users && user.role !== 'CLIENT_ADMIN'}"
|
||||
)
|
||||
|
||||
with open("/app/src/routes/usuarios/+page.svelte", "w") as f:
|
||||
f.write(content)
|
||||
print("Listo")
|
||||
Reference in New Issue
Block a user