Nuevos cambios en el servidor

This commit is contained in:
2026-01-27 13:22:13 -07:00
parent 6215fc40a7
commit 7bb3d1a0ac
33 changed files with 8737 additions and 169 deletions

38
backend/alembic.ini Normal file
View File

@@ -0,0 +1,38 @@
[alembic]
script_location = backend/migrations
sqlalchemy.url = postgresql://servicemanager:servicemanager123@172.19.0.3:5432/servicemanager
target_metadata = app.core.database.Base.metadata
default_environment = development
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
[logger_sqlalchemy]
level = WARN
handlers = console
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers = console
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
formatter = generic
[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s

View File

@@ -85,6 +85,9 @@ async def update_tenant(
raise HTTPException(status_code=404, detail="Tenant not found")
update_data = tenant_in.model_dump(exclude_unset=True)
if "status" in update_data:
tenant.is_active = update_data.pop("status") == TenantStatus.active
for field, value in update_data.items():
setattr(tenant, field, value)
@@ -92,3 +95,18 @@ async def update_tenant(
await db.commit()
await db.refresh(tenant)
return tenant
@router.delete("/{tenant_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_tenant(
tenant_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_active_superuser)
):
"""Eliminar un cliente (tenant) por ID."""
tenant = await db.get(Tenant, tenant_id)
if not tenant:
raise HTTPException(status_code=404, detail="Tenant not found")
await db.delete(tenant)
await db.commit()
return {"message": "Tenant deleted successfully"}

View File

@@ -1,8 +1,9 @@
"""
Tickets endpoints - ServiceManagerWeb
Tickets endpoints - ServiceManagerWeb (VERSION FINAL CORREGIDA)
"""
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from typing import List, Optional
@@ -11,10 +12,13 @@ from app.core.database import get_db
from app.api.deps import get_current_user
from app.models.ticket import Ticket, TicketStatus, TicketPriority
from app.models.user import User
from pydantic import BaseModel
from app.models.category import Category
from app.models.system import System
import uuid
import logging
router = APIRouter()
logger = logging.getLogger(__name__)
# ===================================
# SCHEMAS
@@ -24,7 +28,7 @@ class TicketCreate(BaseModel):
subject: str
description: str
category_id: Optional[str] = None
system_id: Optional[str] = None
affected_system_id: Optional[str] = None # Updated field name
priority: str = "MEDIUM"
class TicketUpdate(BaseModel):
@@ -42,7 +46,7 @@ class TicketResponse(BaseModel):
status: str
priority: str
category_id: Optional[str] = None
system_id: Optional[str] = None
affected_system_id: Optional[str] = None # Updated field name
created_by: str
assigned_to: Optional[str] = None
created_at: datetime
@@ -78,8 +82,27 @@ async def create_ticket(
# Convertir IDs de string a UUID si son proporcionados
category_uuid = uuid.UUID(ticket.category_id) if ticket.category_id else None
system_uuid = uuid.UUID(ticket.system_id) if ticket.system_id else None
system_uuid = uuid.UUID(ticket.affected_system_id) if ticket.affected_system_id else None
# ✅ CORRECCIÓN: Validar en la tabla correcta 'categories'
if category_uuid:
category = await db.get(Category, category_uuid)
if not category:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"La categoría con ID {ticket.category_id} no existe."
)
# Validar si el system_id existe en la tabla systems
if system_uuid:
system = await db.get(System, system_uuid)
if not system:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"El sistema con ID {ticket.affected_system_id} no existe."
)
# ✅ CORRECCIÓN: Usar 'affected_system_id' (nombre real en la BD)
db_ticket = Ticket(
id=uuid.uuid4(),
tenant_id=current_user.tenant_id,
@@ -87,7 +110,7 @@ async def create_ticket(
subject=ticket.subject,
description=ticket.description,
category_id=category_uuid,
system_id=system_uuid,
affected_system_id=system_uuid, # Updated field name
priority=TicketPriority[ticket.priority.upper()],
created_by=current_user.id,
status=TicketStatus.NEW,
@@ -99,7 +122,9 @@ async def create_ticket(
await db.commit()
await db.refresh(db_ticket)
# Convertir a respuesta
logger.info(f"Ticket {db_ticket.ticket_number} created by {current_user.email}")
# ✅ CORRECCIÓN: Usar affected_system_id
return {
"id": str(db_ticket.id),
"ticket_number": db_ticket.ticket_number,
@@ -108,7 +133,7 @@ async def create_ticket(
"status": db_ticket.status.value,
"priority": db_ticket.priority.value,
"category_id": str(db_ticket.category_id) if db_ticket.category_id else None,
"system_id": str(db_ticket.system_id) if db_ticket.system_id else None,
"system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORRECTO
"created_by": str(db_ticket.created_by),
"assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None,
"created_at": db_ticket.created_at,
@@ -117,12 +142,17 @@ async def create_ticket(
except ValueError as e:
await db.rollback()
logger.error(f"Invalid UUID format: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid UUID format: {str(e)}"
)
except HTTPException:
await db.rollback()
raise
except Exception as e:
await db.rollback()
logger.error(f"Error creating ticket: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Error creating ticket: {str(e)}"
@@ -140,43 +170,55 @@ async def get_tickets(
"""
Obtener tickets del usuario actual
"""
query = select(Ticket).where(
Ticket.tenant_id == current_user.tenant_id,
Ticket.created_by == current_user.id
)
if status_filter:
try:
status_enum = TicketStatus[status_filter.upper()]
query = query.where(Ticket.status == status_enum)
except KeyError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid status: {status_filter}"
)
query = query.order_by(Ticket.created_at.desc()).offset(skip).limit(limit)
result = await db.execute(query)
tickets = result.scalars().all()
return [
{
"id": str(t.id),
"ticket_number": t.ticket_number,
"subject": t.subject,
"description": t.description,
"status": t.status.value,
"priority": t.priority.value,
"category_id": str(t.category_id) if t.category_id else None,
"system_id": str(t.system_id) if t.system_id else None,
"created_by": str(t.created_by),
"assigned_to": str(t.assigned_to) if t.assigned_to else None,
"created_at": t.created_at,
"updated_at": t.updated_at
}
for t in tickets
]
try:
query = select(Ticket).where(
Ticket.tenant_id == current_user.tenant_id,
Ticket.created_by == current_user.id
)
if status_filter:
try:
status_enum = TicketStatus[status_filter.upper()]
query = query.where(Ticket.status == status_enum)
except KeyError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid status: {status_filter}"
)
query = query.order_by(Ticket.created_at.desc()).offset(skip).limit(limit)
result = await db.execute(query)
tickets = result.scalars().all()
logger.info(f"Listed {len(tickets)} tickets for user {current_user.email}")
# ✅ CORRECCIÓN: Usar affected_system_id
return [
{
"id": str(t.id),
"ticket_number": t.ticket_number,
"subject": t.subject,
"description": t.description,
"status": t.status.value,
"priority": t.priority.value,
"category_id": str(t.category_id) if t.category_id else None,
"system_id": str(t.affected_system_id) if t.affected_system_id else None, # ✅ CORRECTO
"created_by": str(t.created_by),
"assigned_to": str(t.assigned_to) if t.assigned_to else None,
"created_at": t.created_at,
"updated_at": t.updated_at
}
for t in tickets
]
except HTTPException:
raise
except Exception as e:
logger.error(f"Error listing tickets: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error listing tickets: {str(e)}"
)
@router.get("/{ticket_id}", response_model=TicketResponse)
@@ -196,35 +238,45 @@ async def get_ticket(
detail="Invalid ticket ID format"
)
query = select(Ticket).where(
Ticket.id == ticket_uuid,
Ticket.tenant_id == current_user.tenant_id,
Ticket.created_by == current_user.id
)
result = await db.execute(query)
ticket = result.scalars().first()
if not ticket:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Ticket {ticket_id} not found"
try:
query = select(Ticket).where(
Ticket.id == ticket_uuid,
Ticket.tenant_id == current_user.tenant_id,
Ticket.created_by == current_user.id
)
result = await db.execute(query)
ticket = result.scalars().first()
if not ticket:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Ticket {ticket_id} not found"
)
# ✅ CORRECCIÓN: Usar affected_system_id
return {
"id": str(ticket.id),
"ticket_number": ticket.ticket_number,
"subject": ticket.subject,
"description": ticket.description,
"status": ticket.status.value,
"priority": ticket.priority.value,
"category_id": str(ticket.category_id) if ticket.category_id else None,
"system_id": str(ticket.affected_system_id) if ticket.affected_system_id else None, # ✅ CORRECTO
"created_by": str(ticket.created_by),
"assigned_to": str(ticket.assigned_to) if ticket.assigned_to else None,
"created_at": ticket.created_at,
"updated_at": ticket.updated_at
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting ticket: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting ticket: {str(e)}"
)
return {
"id": str(ticket.id),
"ticket_number": ticket.ticket_number,
"subject": ticket.subject,
"description": ticket.description,
"status": ticket.status.value,
"priority": ticket.priority.value,
"category_id": str(ticket.category_id) if ticket.category_id else None,
"system_id": str(ticket.system_id) if ticket.system_id else None,
"created_by": str(ticket.created_by),
"assigned_to": str(ticket.assigned_to) if ticket.assigned_to else None,
"created_at": ticket.created_at,
"updated_at": ticket.updated_at
}
@router.patch("/{ticket_id}", response_model=TicketResponse)
@@ -245,22 +297,22 @@ async def update_ticket(
detail="Invalid ticket ID format"
)
query = select(Ticket).where(
Ticket.id == ticket_uuid,
Ticket.tenant_id == current_user.tenant_id,
Ticket.created_by == current_user.id
)
result = await db.execute(query)
db_ticket = result.scalars().first()
if not db_ticket:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Ticket {ticket_id} not found"
)
try:
query = select(Ticket).where(
Ticket.id == ticket_uuid,
Ticket.tenant_id == current_user.tenant_id,
Ticket.created_by == current_user.id
)
result = await db.execute(query)
db_ticket = result.scalars().first()
if not db_ticket:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Ticket {ticket_id} not found"
)
update_data = ticket_update.dict(exclude_unset=True)
for field, value in update_data.items():
@@ -278,6 +330,9 @@ async def update_ticket(
await db.commit()
await db.refresh(db_ticket)
logger.info(f"Ticket {db_ticket.ticket_number} updated by {current_user.email}")
# ✅ CORRECCIÓN: Usar affected_system_id
return {
"id": str(db_ticket.id),
"ticket_number": db_ticket.ticket_number,
@@ -286,15 +341,19 @@ async def update_ticket(
"status": db_ticket.status.value,
"priority": db_ticket.priority.value,
"category_id": str(db_ticket.category_id) if db_ticket.category_id else None,
"system_id": str(db_ticket.system_id) if db_ticket.system_id else None,
"system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORRECTO
"created_by": str(db_ticket.created_by),
"assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None,
"created_at": db_ticket.created_at,
"updated_at": db_ticket.updated_at
}
except HTTPException:
await db.rollback()
raise
except Exception as e:
await db.rollback()
logger.error(f"Error updating ticket: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Error updating ticket: {str(e)}"
@@ -319,28 +378,31 @@ async def close_ticket(
detail="Invalid ticket ID format"
)
query = select(Ticket).where(
Ticket.id == ticket_uuid,
Ticket.tenant_id == current_user.tenant_id,
Ticket.created_by == current_user.id
)
result = await db.execute(query)
db_ticket = result.scalars().first()
if not db_ticket:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Ticket {ticket_id} not found"
)
try:
query = select(Ticket).where(
Ticket.id == ticket_uuid,
Ticket.tenant_id == current_user.tenant_id,
Ticket.created_by == current_user.id
)
result = await db.execute(query)
db_ticket = result.scalars().first()
if not db_ticket:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Ticket {ticket_id} not found"
)
db_ticket.status = TicketStatus.CLOSED
db_ticket.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(db_ticket)
logger.info(f"Ticket {db_ticket.ticket_number} closed by {current_user.email}")
# ✅ CORRECCIÓN: Usar affected_system_id
return {
"id": str(db_ticket.id),
"ticket_number": db_ticket.ticket_number,
@@ -349,15 +411,19 @@ async def close_ticket(
"status": db_ticket.status.value,
"priority": db_ticket.priority.value,
"category_id": str(db_ticket.category_id) if db_ticket.category_id else None,
"system_id": str(db_ticket.system_id) if db_ticket.system_id else None,
"system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORRECTO
"created_by": str(db_ticket.created_by),
"assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None,
"created_at": db_ticket.created_at,
"updated_at": db_ticket.updated_at
}
except HTTPException:
await db.rollback()
raise
except Exception as e:
await db.rollback()
logger.error(f"Error closing ticket: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Error closing ticket: {str(e)}"

View File

@@ -5,6 +5,7 @@ Router principal para la API v1
"""
from fastapi import APIRouter
from app.api.v1.endpoints import auth, health, tenants, users, systems, categories, tickets
api_router = APIRouter()
@@ -51,4 +52,5 @@ api_router.include_router(
tickets.router,
prefix="/tickets",
tags=["tickets"]
)
)

View File

@@ -139,4 +139,8 @@ def get_settings() -> Settings:
Using lru_cache to create a singleton pattern for settings.
"""
return Settings()
return Settings()
# Crear una instancia global de Settings
settings = Settings()

View File

@@ -1,24 +1,29 @@
"""
Category Model - ServiceManagerWeb
"""
from sqlalchemy import String, Text, Boolean, ForeignKey
from sqlalchemy import String, Text, Boolean, ForeignKey, Column, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID
from typing import List, Optional
import uuid
from app.core.database import Base
from app.models.tenant import Tenant
from app.models.ticket import Ticket
class Category(Base):
__tablename__ = "categories"
__tablename__ = "ticket_categories" # Updated table name
name: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
# Optional: Tenant specific categories?
tenant_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True)
# Updated tenant_id to be required
tenant_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=False
)
# Relationships
tickets: Mapped[List["Ticket"]] = relationship("Ticket", back_populates="category")

View File

@@ -1,4 +1,3 @@
"""
System Model - ServiceManagerWeb
"""
@@ -19,7 +18,11 @@ class System(Base):
# Relationships
# If we want tickets to link to systems, we will add relationship in Ticket later or now.
# We will assume Ticket links to System.
tickets: Mapped[List["Ticket"]] = relationship("Ticket", back_populates="system")
tickets: Mapped[List["Ticket"]] = relationship(
"Ticket",
back_populates="affected_system",
foreign_keys="Ticket.affected_system_id"
)
def __repr__(self) -> str:
return f"<System(id={self.id}, name='{self.name}')>"

View File

@@ -54,15 +54,12 @@ class Tenant(Base):
String(20),
default=TenantStatus.ACTIVE
)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
# Relaciones
users: Mapped[List["User"]] = relationship("User", back_populates="tenant")
tickets: Mapped[List["Ticket"]] = relationship("Ticket", back_populates="tenant")
categories: Mapped[List["Category"]] = relationship("Category", back_populates="tenant")
def __repr__(self) -> str:
return f"<Tenant(id={self.id}, name='{self.name}', slug='{self.slug}')>"
@property
def is_active(self) -> bool:
"""Check if tenant is active."""
return self.status == TenantStatus.ACTIVE
return f"<Tenant(id={self.id}, name='{self.name}', slug='{self.slug}')>"

View File

@@ -4,11 +4,17 @@ Ticket Model - ServiceManagerWeb
from sqlalchemy import String, ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID, ENUM
from typing import Optional
from typing import Optional, TYPE_CHECKING
import enum
import uuid
from app.core.database import Base
from .tenant import Tenant
from .system import System
from .user import User
if TYPE_CHECKING:
from app.models.category import Category
class TicketStatus(str, enum.Enum):
NEW = "NEW"
@@ -43,7 +49,7 @@ class Ticket(Base):
created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
assigned_to: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
system_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("systems.id"), nullable=True)
affected_system_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("systems.id"), nullable=True)
category_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("categories.id"), nullable=True)
# Relationships

View File

@@ -0,0 +1 @@
Generic single-database configuration.

View File

@@ -0,0 +1,38 @@
[alembic]
script_location = backend/migrations
sqlalchemy.url = postgresql://servicemanager:servicemanager123@172.19.0.3:5432/servicemanager
target_metadata = app.core.database.Base.metadata
default_environment = development
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
[logger_sqlalchemy]
level = WARN
handlers = console
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers = console
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
formatter = generic
[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s

View File

@@ -0,0 +1,50 @@
# backend/migrations/env.py
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.ext.asyncio import create_async_engine
from alembic import context
import asyncio
# Importa tus settings
from app.core.config import settings
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# metadata de tus modelos
from app.core.database import Base
target_metadata = Base.metadata
def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection):
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online():
connectable = create_async_engine(
settings.DATABASE_URL, poolclass=pool.NullPool
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
if context.is_offline_mode():
run_migrations_offline()
else:
asyncio.run(run_migrations_online())

View File

@@ -0,0 +1,26 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,30 @@
"""Fix ambiguous foreign key in System.tickets relationship
Revision ID: 1b894c060a94
Revises: d77aa93a2cf2
Create Date: 2026-01-27 19:20:19.670006
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '1b894c060a94'
down_revision: Union[str, None] = 'd77aa93a2cf2'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
pass # No changes needed for the database schema
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
pass # No changes needed for the database schema
# ### end Alembic commands ###

View File

@@ -0,0 +1,37 @@
"""Fix affected_system relationship in Ticket model
Revision ID: d77aa93a2cf2
Revises:
Create Date: 2026-01-27 19:18:04.005182
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = 'd77aa93a2cf2'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_foreign_key(
'fk_tickets_affected_system_id',
'tickets',
'systems',
['affected_system_id'],
['id'],
ondelete='SET NULL'
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('fk_tickets_affected_system_id', 'tickets', type_='foreignkey')
# ### end Alembic commands ###

View File

@@ -0,0 +1,23 @@
from sqlalchemy import create_engine
from alembic import command
from alembic.config import Config
# Cargar la URL de la base de datos desde el entorno
from os import getenv
DATABASE_URL = getenv("DATABASE_URL", "postgresql://servicemanager:servicemanager123@postgres:5432/servicemanager")
# Cambiar el dialecto a uno síncrono
DATABASE_URL = DATABASE_URL.replace("+asyncpg", "")
# Crear un motor síncrono
engine = create_engine(DATABASE_URL)
# Configurar Alembic
alembic_cfg = Config("backend/migrations/alembic.ini")
alembic_cfg.attributes['connection'] = engine.connect()
# Ejecutar las migraciones
if __name__ == "__main__":
print("Ejecutando migraciones...")
command.upgrade(alembic_cfg, "head")
print("Migraciones aplicadas correctamente.")

74
backend/migrations/env.py Normal file
View File

@@ -0,0 +1,74 @@
from logging.config import fileConfig
import os
from sqlalchemy import create_engine, pool
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
from sqlalchemy.engine import engine_from_config
from alembic import context
import asyncio
# Import Base and all models
from app.core.database import Base
from app.models import tenant # Import all models explicitly
# Alembic Config object
config = context.config
# Logging configuration
if config.config_file_name:
fileConfig(config.config_file_name)
# Get DATABASE_URL and convert to synchronous
DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL:
raise RuntimeError("DATABASE_URL environment variable is not set")
SYNC_DATABASE_URL = DATABASE_URL.replace("+asyncpg", "")
# Metadata for autogenerate
target_metadata = Base.metadata
def run_migrations_offline():
"""
Run migrations in 'offline' mode.
"""
context.configure(
url=SYNC_DATABASE_URL,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online():
"""
Run migrations in 'online' mode.
"""
# Fetch the URL from Alembic configuration
alembic_config = config.get_section(config.config_ini_section)
alembic_config["sqlalchemy.url"] = DATABASE_URL # Use async URL
connectable: AsyncEngine = create_async_engine(
DATABASE_URL,
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(
lambda sync_connection: context.configure(
connection=sync_connection, target_metadata=target_metadata,
compare_type=True # Ensure column types are compared
)
)
await connection.run_sync(context.run_migrations())
asyncio.run(do_run_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

28
backend/run_migrations.py Normal file
View File

@@ -0,0 +1,28 @@
import asyncio
from alembic.config import Config
from alembic.script import ScriptDirectory
from alembic.runtime.environment import EnvironmentContext
from sqlalchemy.ext.asyncio import create_async_engine
async def run_async_migrations():
alembic_cfg = Config("alembic.ini")
script = ScriptDirectory.from_config(alembic_cfg)
engine = create_async_engine("postgresql+asyncpg://servicemanager:servicemanager123@172.19.0.3:5432/servicemanager")
async with engine.connect() as connection:
def do_run_migrations(connection):
with EnvironmentContext(
alembic_cfg,
script,
connection=connection,
fn=lambda rev, context: script.run_env()
) as context:
context.configure(connection=connection, target_metadata=None)
with context.begin_transaction():
context.run_migrations()
await connection.run_sync(do_run_migrations)
if __name__ == "__main__":
asyncio.run(run_async_migrations())

View File

@@ -0,0 +1,23 @@
from sqlalchemy import create_engine
from alembic import command
from alembic.config import Config
# Cargar la URL de la base de datos desde el entorno
from os import getenv
DATABASE_URL = getenv("DATABASE_URL", "postgresql://servicemanager:servicemanager123@postgres:5432/servicemanager")
# Cambiar el dialecto a uno síncrono
DATABASE_URL = DATABASE_URL.replace("+asyncpg", "")
# Crear un motor síncrono
engine = create_engine(DATABASE_URL)
# Configurar Alembic
alembic_cfg = Config("backend/migrations/alembic.ini")
alembic_cfg.attributes['connection'] = engine.connect()
# Ejecutar las migraciones
if __name__ == "__main__":
print("Ejecutando migraciones...")
command.upgrade(alembic_cfg, "head")
print("Migraciones aplicadas correctamente.")

View File

@@ -0,0 +1,19 @@
import asyncio
import asyncpg
async def test_connection():
try:
conn = await asyncpg.connect(
user="servicemanager",
password="servicemanager123",
database="servicemanager",
host="postgres",
port=5432
)
print("Connection successful!")
await conn.close()
except Exception as e:
print(f"Connection failed: {e}")
if __name__ == "__main__":
asyncio.run(test_connection())