Fascicoli+Tassonomia+permessi

This commit is contained in:
2026-06-17 21:47:46 +02:00
parent e31676d22e
commit 3fd3c72f06
42 changed files with 4554 additions and 99 deletions
@@ -0,0 +1,115 @@
"""
Migrazione 0016: tabelle fascicoli e fascicolo_messages.
Fascicolazione / Dossier — raggruppa piu' comunicazioni PEC in una pratica.
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = "0016"
down_revision = "0015"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ─── Tabella fascicoli ─────────────────────────────────────────────────────
# L'ENUM fascicolo_stato viene creato automaticamente da SQLAlchemy
# come parte di op.create_table (no op.execute separato)
op.create_table(
"fascicoli",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column(
"tenant_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("titolo", sa.String(255), nullable=False),
sa.Column("numero_pratica", sa.String(100), nullable=True),
sa.Column(
"stato",
sa.Enum("aperto", "chiuso", "archiviato", name="fascicolo_stato"),
nullable=False,
server_default="aperto",
),
sa.Column("categoria", sa.String(100), nullable=True),
sa.Column(
"responsabile_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column("scadenza", sa.DateTime(timezone=True), nullable=True),
sa.Column("note", sa.Text, nullable=True),
sa.Column(
"created_by",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
)
op.create_index("idx_fascicoli_tenant", "fascicoli", ["tenant_id"])
op.create_index("idx_fascicoli_stato", "fascicoli", ["stato"])
op.create_index("idx_fascicoli_responsabile", "fascicoli", ["responsabile_id"])
# ─── Tabella fascicolo_messages (N:M) ──────────────────────────────────────
op.create_table(
"fascicolo_messages",
sa.Column(
"fascicolo_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("fascicoli.id", ondelete="CASCADE"),
primary_key=True,
),
sa.Column(
"message_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("messages.id", ondelete="CASCADE"),
primary_key=True,
),
sa.Column(
"added_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
sa.Column(
"added_by",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
),
)
op.create_index(
"idx_fascicolo_messages_fascicolo", "fascicolo_messages", ["fascicolo_id"]
)
op.create_index(
"idx_fascicolo_messages_message", "fascicolo_messages", ["message_id"]
)
def downgrade() -> None:
op.drop_index("idx_fascicolo_messages_message", table_name="fascicolo_messages")
op.drop_index("idx_fascicolo_messages_fascicolo", table_name="fascicolo_messages")
op.drop_table("fascicolo_messages")
op.drop_index("idx_fascicoli_responsabile", table_name="fascicoli")
op.drop_index("idx_fascicoli_stato", table_name="fascicoli")
op.drop_index("idx_fascicoli_tenant", table_name="fascicoli")
op.drop_table("fascicoli")
op.execute("DROP TYPE fascicolo_stato")
@@ -0,0 +1,78 @@
"""
Migrazione 0017: Tassonomia di Classificazione Multi-livello (Feature N2).
Estende la tabella `labels` con:
- parent_id: FK self-referenziale (nullable) per struttura ad albero
- description: testo descrittivo opzionale
Struttura tassonomica:
Livello 0 (radice) = Ambito (Area Aziendale) — parent_id IS NULL
Livello 1 = Processo — parent_id = ID Ambito
Livello 2 (foglia) = Classificazione — parent_id = ID Processo
I vincoli di unicità vengono sostituiti con indici parziali per supportare
nomi identici a diversi livelli dell'albero.
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = "0017"
down_revision = "0016"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ── Aggiunge parent_id (FK self-referenziale nullable) ─────────────────────
op.add_column(
"labels",
sa.Column(
"parent_id",
postgresql.UUID(as_uuid=True),
nullable=True,
),
)
op.create_foreign_key(
"fk_labels_parent",
"labels",
"labels",
["parent_id"],
["id"],
ondelete="CASCADE",
)
# ── Aggiunge description ────────────────────────────────────────────────────
op.add_column(
"labels",
sa.Column("description", sa.Text(), nullable=True),
)
# ── Indice su parent_id per query gerarchiche ───────────────────────────────
op.create_index("idx_labels_parent", "labels", ["parent_id"])
# ── Sostituisce il vincolo di unicità con indici parziali ───────────────────
# Il vecchio vincolo (tenant_id, name) non supporta nomi uguali a livelli diversi
op.drop_constraint("uq_label_name_tenant", "labels")
# Nodi radice: nome unico per tenant (parent_id IS NULL)
op.execute(
"CREATE UNIQUE INDEX uq_label_name_root "
"ON labels (tenant_id, name) WHERE parent_id IS NULL"
)
# Nodi non-radice: nome unico per (tenant, parent) — stesso nome ammesso sotto parent diversi
op.execute(
"CREATE UNIQUE INDEX uq_label_name_parent "
"ON labels (tenant_id, name, parent_id) WHERE parent_id IS NOT NULL"
)
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS uq_label_name_parent")
op.execute("DROP INDEX IF EXISTS uq_label_name_root")
op.create_unique_constraint("uq_label_name_tenant", "labels", ["tenant_id", "name"])
op.drop_index("idx_labels_parent", table_name="labels")
op.drop_constraint("fk_labels_parent", "labels", type_="foreignkey")
op.drop_column("labels", "description")
op.drop_column("labels", "parent_id")
@@ -0,0 +1,80 @@
"""
Migrazione 0018: Livello di Rischio e Riservatezza per Comunicazione (Feature N3).
Aggiunge alla tabella `messages`:
- risk_level ENUM(low, medium, high, critical) nullable
- confidentiality ENUM(public, internal, confidential, secret) nullable
Entrambi i campi sono nullable: rimangono NULL finche' non vengono
impostati manualmente o da una regola di smistamento (set_risk_level /
set_confidentiality).
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = "0018"
down_revision = "0017"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ── Crea i nuovi ENUM types ─────────────────────────────────────────────────
risk_level_enum = postgresql.ENUM(
"low", "medium", "high", "critical",
name="risk_level",
create_type=True,
)
confidentiality_enum = postgresql.ENUM(
"public", "internal", "confidential", "secret",
name="confidentiality_level",
create_type=True,
)
risk_level_enum.create(op.get_bind(), checkfirst=True)
confidentiality_enum.create(op.get_bind(), checkfirst=True)
# ── Aggiunge le colonne a messages ──────────────────────────────────────────
op.add_column(
"messages",
sa.Column(
"risk_level",
sa.Enum("low", "medium", "high", "critical", name="risk_level", create_type=False),
nullable=True,
),
)
op.add_column(
"messages",
sa.Column(
"confidentiality",
sa.Enum("public", "internal", "confidential", "secret", name="confidentiality_level", create_type=False),
nullable=True,
),
)
# ── Indici per filtrare rapidamente per rischio/riservatezza ────────────────
op.create_index(
"idx_messages_risk_level",
"messages",
["tenant_id", "risk_level"],
postgresql_where=sa.text("risk_level IS NOT NULL"),
)
op.create_index(
"idx_messages_confidentiality",
"messages",
["tenant_id", "confidentiality"],
postgresql_where=sa.text("confidentiality IS NOT NULL"),
)
def downgrade() -> None:
op.drop_index("idx_messages_confidentiality", table_name="messages")
op.drop_index("idx_messages_risk_level", table_name="messages")
op.drop_column("messages", "confidentiality")
op.drop_column("messages", "risk_level")
risk_level_enum = postgresql.ENUM(name="risk_level")
confidentiality_enum = postgresql.ENUM(name="confidentiality_level")
risk_level_enum.drop(op.get_bind(), checkfirst=True)
confidentiality_enum.drop(op.get_bind(), checkfirst=True)
@@ -0,0 +1,77 @@
"""
Migrazione 0019: Tabella permission_presets (sottoruoli/template permessi).
Aggiunge la tabella `permission_presets` che consente ad admin e supervisor
di definire template nominati di permessi riutilizzabili (es. "Operatore Archivio",
"Operatore Invio", ecc.) da applicare agli operatori sulle caselle PEC.
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = "0019"
down_revision = "0018"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"permission_presets",
sa.Column(
"id",
postgresql.UUID(as_uuid=True),
primary_key=True,
server_default=sa.text("gen_random_uuid()"),
nullable=False,
),
sa.Column(
"tenant_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("can_read", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("can_send", sa.Boolean(), nullable=False, server_default="false"),
sa.Column("can_manage", sa.Boolean(), nullable=False, server_default="false"),
sa.Column("can_conserve", sa.Boolean(), nullable=False, server_default="false"),
sa.Column(
"created_by",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text("now()"),
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text("now()"),
),
)
# Unicita' nome per tenant
op.create_unique_constraint(
"uq_preset_name_tenant",
"permission_presets",
["tenant_id", "name"],
)
# Indici
op.create_index("idx_preset_tenant", "permission_presets", ["tenant_id"])
op.create_index("idx_preset_created_by", "permission_presets", ["created_by"])
def downgrade() -> None:
op.drop_index("idx_preset_created_by", table_name="permission_presets")
op.drop_index("idx_preset_tenant", table_name="permission_presets")
op.drop_constraint("uq_preset_name_tenant", "permission_presets", type_="unique")
op.drop_table("permission_presets")