In una redazione dove più operatori lavorano sullo stesso archivio, lo stato di un contenuto — chi lo vede, chi lo modifica, se è pubblicato — non può restare implicito nel codice dell’applicazione. Va modellato come una macchina a stati, con transizioni vincolate da permessi. Zope CMF (Content Management Framework) lo fa attraverso il prodotto DCWorkflow, e il dettaglio che mi interessa è che il workflow vive fuori dai content type, non dentro.

Contesto

Il Content Management Framework è uno strato che Zope Corporation costruisce sopra Zope per tenere separata la logica di gestione dei contenuti dalla pubblicazione di oggetti vera e propria. Zope, da solo, pubblica oggetti Python persistiti in ZODB (Zope Object Database) attraversando l’albero degli oggetti tramite URL; CMF aggiunge la nozione di portale, di content type tipizzato, e un insieme di tool registrati nel portale che governano i comportamenti trasversali: portal_catalog per l’indicizzazione, portal_membership per gli utenti, portal_workflow per gli stati editoriali.

Il problema concreto che mi riguarda qui è il terzo. Un archivio editoriale tiene contenuti in stati diversi nello stesso momento: una bozza che vede solo l’autore, un articolo in attesa di revisione, un articolo pubblico. Con più redattori sullo stesso ZODB — accesso concorrente garantito dalle transazioni del database e, dove serve scalare, da ZEO (Zope Enterprise Objects), che lascia condividere lo stesso storage a più processi — questi stati vanno resi espliciti e le transizioni controllate, altrimenti il controllo accessi degenera in una manciata di if sparsi nel codice di rendering.

Architettura

DCWorkflow modella ogni workflow come una macchina a stati finiti definita attraverso il web, senza scrivere una riga di Python. Gli ingredienti sono quattro:

  • stati — per esempio private, pending, published;
  • transizioni — per esempio submit (da private a pending), publish (da pending a published), retract (ritorno a private);
  • guardie — la condizione che abilita una transizione, espressa come permesso o ruolo richiesto;
  • variabili e history — valori legati all’istanza di workflow e un registro cronologico delle transizioni applicate a quel contenuto.

Il punto di architettura è che lo stato non è un attributo del content type. Un oggetto Document non sa di essere pending: è portal_workflow a tenere l’associazione tra l’oggetto e il suo stato corrente, e a riscrivere i permessi locali dell’oggetto a ogni transizione. Quando un articolo passa a published, il workflow assegna il permesso View al ruolo Anonymous; quando torna private, lo revoca. Il controllo accessi a runtime resta quello nativo di Zope — la verifica del permesso View contro i ruoli dell’utente corrente — ma la mappa permesso/ruolo dell’oggetto la produce la macchina a stati invece di scriverla a mano.

Ne segue che aggiungere un tipo di contenuto nuovo non costringe a riscrivere la logica di pubblicazione. Il tipo nuovo si registra in portal_types, gli si lega un workflow esistente, ed eredita stati, transizioni e regole di permesso senza una riga di codice di stato. La definizione del workflow è un dato di configurazione, persistito in ZODB come tutto il resto, esportabile e reimportabile.

Il punto critico

Quello che rende solido il modello è che la guardia di transizione e il permesso a runtime sono la stessa primitiva di sicurezza. Una transizione publish è protetta dal permesso Review portal content; quel permesso è assegnato al ruolo Reviewer; la stessa verifica di permesso che decide se un utente può vedere /cartella/articolo decide se può eseguire la transizione publish su di esso. Non ci sono due sistemi di autorizzazione da tenere allineati — uno per il workflow, uno per il rendering — perché ce n’è uno solo.

La history per istanza è l’altra metà. Ogni transizione registra chi l’ha eseguita, quando, da quale stato a quale, con un commento facoltativo. Per un archivio multi-redattore è il sostrato minimo della tracciabilità editoriale: non serve un sistema di audit a parte, perché il percorso di stati di ogni contenuto è già materializzato accanto al contenuto stesso.

Implicazioni

Una conseguenza pratica riguarda la sindacazione. Se gli stati di pubblicazione sono espliciti, il sottoinsieme dei contenuti published è una query ben definita su portal_catalog, e quel sottoinsieme è ciò che si espone all’esterno. La forma naturale per esporlo nel 2001 è RSS 1.0 — RDF Site Summary nella revisione che il RSS-DEV Working Group pubblica nel dicembre 2000 — un’applicazione XML conforme alla specifica RDF del W3C, che porta i metadati descrittivi nel modulo Dublin Core. Gli elementi del Dublin Core Metadata Element Set 1.1 (autore in dc:creator, data in dc:date, titolo, descrizione) corrispondono uno a uno ai metadati che CMF già tiene per ogni contenuto, perché lo stesso modello di metadati di CMF è debitore del Dublin Core. Un articolo che entra nello stato published diventa, senza altro lavoro di modellazione, una rdf:Description esportabile.

L’altra implicazione riguarda la portabilità del processo editoriale. Siccome il workflow è dato e non codice, due installazioni diverse possono condividere lo stesso schema di stati e divergere nelle guardie — la stessa redazione può avere una catena private → pending → published su un sito e una private → published diretta su un altro, senza forkare l’applicazione.

Su questo modello noze costruisce InFlow CMS, sistema Open Source su Zope/CMF con workflow asincrono ed export RDF, descritto nell’insight: https://www.noze.it/insights/inflow-cms/.

Limiti

DCWorkflow modella bene una catena di stati per singolo contenuto. Non modella nativamente le dipendenze tra contenuti: «pubblica l’articolo solo quando anche la sua immagine di copertina è pubblica» non è una transizione, è un vincolo tra istanze, e va espresso come guardia con uno script che interroga altri oggetti, e così rientra nella macchina a stati un po’ di quella logica applicativa che il modello voleva tenere fuori.

C’è poi un costo di concorrenza. Lo storage di ZODB è ottimistico: due redattori che modificano lo stesso oggetto possono incappare in un ConflictError, risolto con un retry della transazione. Per la pubblicazione capita di rado, perché le transizioni di stato sono brevi e di rado sullo stesso contenuto; per l’editing simultaneo dello stesso documento il modello a stati non serve — non è il suo compito — e sopra di esso ci vuole disciplina applicativa (lock espliciti, o una granularità più fine degli oggetti).

Infine il workflow definito attraverso il web è comodo da prototipare e scomodo da versionare. Vivendo in ZODB come configurazione, non sta in nessun file sotto controllo di versione finché non lo si esporta apposta; per un processo editoriale che cambia nel tempo, l’export periodico della definizione è una pratica da imporsi, non un automatismo.


Immagine di copertina: Diagramma di una macchina a stati finiti: due nodi circolari “Opened” e “Closed” collegati da frecce di transizione etichettate “open… — diagramma di Macguy314, reworked by Perhelion, pubblico dominio — https://commons.wikimedia.org/wiki/File:Finite_state_machine_example_with_comments.svg