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
+1
View File
@@ -14,3 +14,4 @@ from app.models.template import MessageTemplate # noqa: F401
from app.models.routing_rule import RoutingRule, RoutingRuleCondition, RoutingRuleAction # noqa: F401
from app.models.pec_contact import PecContact # noqa: F401
from app.models.signature import Signature, SignatureAssignment # noqa: F401
from app.models.fascicolo import Fascicolo, FascicoloMessage # noqa: F401
+96
View File
@@ -0,0 +1,96 @@
"""
Modelli Fascicolo e FascicoloMessage — fascicolazione pratiche.
"""
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Enum, ForeignKey, Index, String, Text, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
FascicoloStato = Enum(
"aperto", "chiuso", "archiviato",
name="fascicolo_stato",
create_type=False,
)
class Fascicolo(Base):
__tablename__ = "fascicoli"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
)
titolo: Mapped[str] = mapped_column(String(255), nullable=False)
numero_pratica: Mapped[str | None] = mapped_column(String(100), nullable=True)
stato: Mapped[str] = mapped_column(FascicoloStato, nullable=False, default="aperto")
categoria: Mapped[str | None] = mapped_column(String(100), nullable=True)
responsabile_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
scadenza: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
note: Mapped[str | None] = mapped_column(Text, nullable=True)
created_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
# Relazioni
fascicolo_messages: Mapped[list["FascicoloMessage"]] = relationship(
"FascicoloMessage", back_populates="fascicolo", cascade="all, delete-orphan"
)
messages: Mapped[list] = relationship(
"Message",
secondary="fascicolo_messages",
lazy="select",
viewonly=True,
)
__table_args__ = (
Index("idx_fascicoli_tenant", "tenant_id"),
Index("idx_fascicoli_stato", "stato"),
Index("idx_fascicoli_responsabile", "responsabile_id"),
)
def __repr__(self) -> str:
return f"<Fascicolo {self.id} {self.titolo!r} {self.stato!r}>"
class FascicoloMessage(Base):
__tablename__ = "fascicolo_messages"
fascicolo_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("fascicoli.id", ondelete="CASCADE"),
primary_key=True,
)
message_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("messages.id", ondelete="CASCADE"),
primary_key=True,
)
added_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
added_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
# Relazioni
fascicolo: Mapped["Fascicolo"] = relationship("Fascicolo", back_populates="fascicolo_messages")
__table_args__ = (
Index("idx_fascicolo_messages_fascicolo", "fascicolo_id"),
Index("idx_fascicolo_messages_message", "message_id"),
)
+21 -4
View File
@@ -1,10 +1,17 @@
"""
Modelli Label e MessageLabel tagging messaggi.
Modelli Label e MessageLabel tagging messaggi con supporto tassonomia gerarchica.
Struttura ad albero (Feature N2 Tassonomia di Classificazione Multi-livello):
parent_id = NULL → Livello 0: Ambito (Area Aziendale)
parent_id = ID ambito → Livello 1: Processo
parent_id = ID processo → Livello 2: Classificazione (foglia)
Le label senza parent_id sono label "piatte" classiche (comportamento pre-esistente).
"""
import uuid
from sqlalchemy import CHAR, ForeignKey, Index, String, UniqueConstraint
from sqlalchemy import CHAR, ForeignKey, Index, String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
@@ -20,15 +27,25 @@ class Label(Base):
tenant_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
)
# Tassonomia: se parent_id è None è un nodo radice (Ambito) o label piatta
parent_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("labels.id", ondelete="CASCADE"),
nullable=True,
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
color: Mapped[str | None] = mapped_column(CHAR(7), nullable=True) # hex #RRGGBB
description: Mapped[str | None] = mapped_column(Text, nullable=True)
# Nota: i vincoli di unicità sono gestiti da indici parziali nel DB:
# uq_label_name_root UNIQUE (tenant_id, name) WHERE parent_id IS NULL
# uq_label_name_parent UNIQUE (tenant_id, name, parent_id) WHERE parent_id IS NOT NULL
__table_args__ = (
UniqueConstraint("tenant_id", "name", name="uq_label_name_tenant"),
Index("idx_labels_parent", "parent_id"),
)
def __repr__(self) -> str:
return f"<Label {self.name!r}>"
return f"<Label {self.name!r} parent={self.parent_id}>"
class MessageLabel(Base):
+10
View File
@@ -24,6 +24,12 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
RiskLevel = Enum("low", "medium", "high", "critical", name="risk_level", create_type=False)
ConfidentialityLevel = Enum(
"public", "internal", "confidential", "secret",
name="confidentiality_level",
create_type=False,
)
PecDirection = Enum("inbound", "outbound", name="pec_direction", create_type=False)
PecState = Enum(
"draft", "queued", "sent", "accepted", "delivered", "anomaly", "failed", "received",
@@ -105,6 +111,10 @@ class Message(Base):
is_conserved: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
conserved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
# Rischio e Riservatezza (Feature N3)
risk_level: Mapped[str | None] = mapped_column(RiskLevel, nullable=True)
confidentiality: Mapped[str | None] = mapped_column(ConfidentialityLevel, nullable=True)
raw_eml_path: Mapped[str | None] = mapped_column(Text, nullable=True)
# Full-text search vector (aggiornato da trigger DB + worker per allegati)
+72
View File
@@ -0,0 +1,72 @@
"""
Modello PermissionPreset template riutilizzabili di permessi per casella.
Permette ad admin e supervisor di creare sottoruoli nominati (es. "Operatore Archivio")
con combinazioni predefinite di can_read/can_send/can_manage/can_conserve.
I preset sono per-tenant e vengono applicati opzionalmente al momento
dell'assegnazione di un operatore a una casella.
"""
import uuid
from datetime import datetime
from sqlalchemy import (
Boolean,
DateTime,
ForeignKey,
Index,
String,
Text,
UniqueConstraint,
func,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class PermissionPreset(Base):
__tablename__ = "permission_presets"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
can_read: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
can_send: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
can_manage: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
can_conserve: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
created_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
# Relazioni
creator: Mapped["User"] = relationship( # noqa: F821
"User", foreign_keys=[created_by]
)
__table_args__ = (
UniqueConstraint("tenant_id", "name", name="uq_preset_name_tenant"),
Index("idx_preset_tenant", "tenant_id"),
Index("idx_preset_created_by", "created_by"),
)
def __repr__(self) -> str:
return (
f"<PermissionPreset name={self.name!r} tenant={self.tenant_id} "
f"read={self.can_read} send={self.can_send} "
f"manage={self.can_manage} conserve={self.can_conserve}>"
)