Sondaggi e Questionari: Due Sistemi Complementari per Decisioni Collettive
Organizzare un evento significa prendere decine di decisioni condivise: "Dove andiamo? Cosa mangiamo? Quale data preferiamo?". In Play The Event queste domande trovano risposta attraverso due sistemi distinti ma complementari: i Sondaggi per votazioni rapide su opzioni predefinite e i Questionari per raccogliere feedback strutturati con domande di diverso tipo. Insieme, coprono ogni esigenza di raccolta opinioni nella pianificazione di un evento.
Cosa Troverai in Questo Articolo
- La macchina a stati del sondaggio (BOZZA, ATTIVO, CHIUSO, ANNULLATO)
- Risposte SINGOLA e MULTIPLA con max opzioni selezionabili configurabile
- Votazione anonima e sondaggi pubblici con codice di condivisione UUID
- Template riutilizzabili con deep copy di opzioni
- Storico vincitori con motivazione obbligatoria per ogni cambio
- Sondaggi per entità (LUOGO, DATA, MENU, ATTIVITA, CUSTOM)
- Sistema Questionari con domande tipizzate e compilazioni tracciate
- Visualizzazione risultati in tempo reale
Il Sondaggio come Aggregate Root
Nel domain model di Play The Event, il Sondaggio è un Aggregate Root ricco di comportamento. Non è un semplice contenitore di opzioni: gestisce il proprio ciclo di vita, valida le transizioni di stato, controlla la votazione e mantiene lo storico completo delle decisioni prese.
@Entity
@Table(name = "sondaggi")
public class Sondaggio {
private Long id;
private Evento evento;
private TipoEntitaSondaggio tipoEntita; // LUOGO, DATA, MENU...
private Long entitaId;
private String titolo;
private String descrizione;
private TipoRispostaSondaggio tipoRisposta; // SINGOLA o MULTIPLA
private StatoSondaggio stato; // BOZZA, ATTIVO, CHIUSO, ANNULLATO
private TipoSondaggio tipo; // EVENTO o TEMPLATE
private Long templateOrigineId;
private LocalDateTime dataScadenza;
private Boolean anonimo;
private Boolean permettiModificaVoto;
private Boolean mostraRisultatiPrimaVoto;
private Integer maxOpzioniSelezionabili;
private Boolean pubblico;
private String codiceCondivisione; // UUID per sharing
private Set<OpzioneSondaggio> opzioni;
private OpzioneSondaggio opzioneVincitrice;
private Set<StoricoVincitoreSondaggio> storicoVincitori;
}
Ogni sondaggio è collegato a un evento, ha un tipo di entità che indica a cosa si riferisce (luogo, data, menu, attività), e gestisce internamente le proprie opzioni, risposte e lo storico dei vincitori. Il pattern Aggregate Root garantisce che tutte le invarianti di business siano rispettate in ogni momento.
Macchina a Stati del Sondaggio
Il ciclo di vita di un sondaggio segue una macchina a stati ben definita con quattro stati possibili e transizioni controllate. Ogni transizione è validata dal domain model stesso, rendendo impossibile raggiungere stati invalidi.
public enum StatoSondaggio {
BOZZA("Bozza"), // Creazione e configurazione
ATTIVO("Attivo"), // Aperto alle votazioni
CHIUSO("Chiuso"), // Votazioni terminate
ANNULLATO("Annullato"); // Sondaggio invalidato
}
Le transizioni consentite sono:
- BOZZA → ATTIVO (
pubblica()): richiede almeno 2 opzioni - ATTIVO → CHIUSO (
chiudi()): termina le votazioni - ATTIVO → ANNULLATO (
annulla()): invalida il sondaggio - CHIUSO → ATTIVO (
riapri()): riapre le votazioni
public void pubblica() {
if (this.stato != StatoSondaggio.BOZZA) {
throw new IllegalStateException(
"Solo sondaggi in bozza possono essere pubblicati");
}
if (this.opzioni.size() < 2) {
throw new IllegalStateException(
"Il sondaggio deve avere almeno 2 opzioni");
}
this.stato = StatoSondaggio.ATTIVO;
}
public void riapri() {
if (this.stato != StatoSondaggio.CHIUSO) {
throw new IllegalStateException(
"Solo sondaggi chiusi possono essere riaperti");
}
this.stato = StatoSondaggio.ATTIVO;
}
Perché la Riapertura?
La transizione CHIUSO → ATTIVO è una scelta progettuale importante. Nella vita reale le decisioni cambiano: un luogo diventa non disponibile, arrivano nuovi partecipanti che vogliono votare, o semplicemente servono più voti per prendere una decisione. Invece di creare un nuovo sondaggio e perdere i voti esistenti, la riapertura permette di continuare la raccolta in modo trasparente.
Tipi di Risposta: SINGOLA e MULTIPLA
Il sistema supporta due modalità di votazione che coprono gli scenari più comuni nella pianificazione di eventi.
public enum TipoRispostaSondaggio {
SINGOLA("Singola"), // Radio button - una sola scelta
MULTIPLA("Multipla"); // Checkbox - più' opzioni selezionabili
}
Nel caso di risposta SINGOLA, il campo maxOpzioniSelezionabili
viene impostato automaticamente a 1. Per la risposta MULTIPLA,
il valore predefinito è 3 ma è configurabile dall'organizzatore.
Questo permette scenari come "scegli le tue 3 attività preferite tra 10 proposte".
public static Sondaggio crea(Evento evento, User creatoDa,
TipoEntitaSondaggio tipoEntita, String titolo,
TipoRispostaSondaggio tipoRisposta) {
Sondaggio s = new Sondaggio();
// ... inizializzazione campi ...
s.maxOpzioniSelezionabili =
tipoRisposta == TipoRispostaSondaggio.SINGOLA ? 1 : 3;
s.permettiModificaVoto = true;
s.mostraRisultatiPrimaVoto = false;
return s;
}
Votazione Anonima
Ogni sondaggio può essere configurato come anonimo.
Quando l'anonimato è attivo, le risposte vengono registrate senza
associarle all'utente votante, garantendo la privacy nelle decisioni sensibili.
Il campo anonimo è impostato a false per default:
l'organizzatore deve abilitarlo esplicitamente.
Le risposte anonime utilizzano il factory method creaAnonima()
che non richiede né un utente né un'email. Il tracciamento per
prevenire voti doppi viene gestito separatamente tramite browser fingerprint.
// Voto di un utente registrato
RispostaSondaggio.creaPerUtente(sondaggio, opzione, utente);
// Voto di un esterno via email (con token di modifica)
RispostaSondaggio.creaPerEmail(sondaggio, opzione, email);
// Voto anonimo su sondaggio pubblico
RispostaSondaggio.creaAnonima(sondaggio, opzione);
Sondaggi Pubblici con Codice di Condivisione
Un sondaggio può essere reso pubblico per raccogliere voti anche da persone non registrate sulla piattaforma. Il sistema genera un codice di condivisione UUID univoco che può essere distribuito come link.
public String rendiPubblico() {
if (this.stato == StatoSondaggio.BOZZA) {
throw new IllegalStateException(
"Pubblica il sondaggio prima di renderlo pubblico");
}
if (this.pubblico && this.codiceCondivisione != null) {
return this.codiceCondivisione; // Gia' pubblico
}
this.pubblico = true;
this.codiceCondivisione = UUID.randomUUID().toString();
return this.codiceCondivisione;
}
public String rigeneraCodiceCondivisione() {
// Invalida i link precedenti
this.codiceCondivisione = UUID.randomUUID().toString();
return this.codiceCondivisione;
}
L'entità VotoPubblico traccia i voti anonimi sui sondaggi
pubblici. Utilizza un fingerprint del browser per prevenire
voti duplicati: un vincolo di unicità su (sondaggio_id, fingerprint)
garantisce che ogni dispositivo possa votare una sola volta.
@Entity
@Table(name = "voti_pubblici",
uniqueConstraints = @UniqueConstraint(
columnNames = {"sondaggio_id", "fingerprint"}))
public class VotoPubblico {
private Long id;
private Sondaggio sondaggio;
private String fingerprint; // Hash del browser
private Instant votatoIl;
}
Flusso del Sondaggio Pubblico
- L'organizzatore crea e pubblica il sondaggio (stato ATTIVO)
- Invoca
rendiPubblico()per generare il codice UUID - Il link viene condiviso via messaggio, email o social
- Gli esterni votano senza registrazione, tracciati via fingerprint
- L'organizzatore può rigenerare il codice per invalidare i link precedenti
- Può rendere il sondaggio privato e poi ri-abilitarlo mantenendo il codice
Template Riutilizzabili
Il sistema distingue tra sondaggi di tipo EVENTO (collegati a un evento specifico) e sondaggi di tipo TEMPLATE (riutilizzabili). Un template è un sondaggio non collegato ad alcun evento che serve come modello per creare istanze identiche in contesti diversi.
// Crea un template riutilizzabile
Sondaggio template = Sondaggio.creaTemplate(
utente, "Preferenza Location",
TipoEntitaSondaggio.LUOGO, "Dove facciamo la festa?",
TipoRispostaSondaggio.SINGOLA);
// Istanzia il template per un evento specifico
Sondaggio istanza = Sondaggio.creaDaTemplate(
template, evento, organizzatore);
istanza.copiaOpzioniDa(template);
// L'istanza eredita: titolo, descrizione, tipoRisposta,
// configurazione (anonimo, maxSelezionabili, ecc.)
// ma parte sempre in stato BOZZA
La copia delle opzioni è separata dalla creazione dell'istanza per mantenere
le relazioni JPA corrette. Ogni opzione viene duplicata tramite
OpzioneSondaggio.copiaDa() che crea una nuova entità
con gli stessi dati ma senza ID e senza risposte, garantendo un'istanza
completamente indipendente dal template.
Storico Vincitori con Audit Trail
Per i sondaggi di tipo LUOGO, il sistema supporta la selezione di un'opzione vincitrice dopo la chiusura delle votazioni. Ogni selezione o cambio di vincitore viene tracciato nell'entità StoricoVincitoreSondaggio, creando un audit trail completo.
@Entity
@Table(name = "storico_vincitori_sondaggio")
public class StoricoVincitoreSondaggio {
private Long id;
private Sondaggio sondaggio;
private OpzioneSondaggio opzionePrecedente; // null se prima selezione
private OpzioneSondaggio opzioneNuova;
private String motivazione; // obbligatoria per cambi
private User modificatoDa;
private Instant modificatoIl;
}
La regola chiave è: la motivazione è obbligatoria quando si cambia un vincitore già selezionato. Per la prima selezione non è richiesta. Questo garantisce trasparenza nelle decisioni del gruppo.
public StoricoVincitoreSondaggio selezionaVincitore(
Long opzioneId, User selezionatoDa, String motivazione) {
verificaChiuso(); // Deve essere CHIUSO
verificaTipoLuogo(); // Solo per sondaggi LUOGO
OpzioneSondaggio opzione = trovaNelleOpzioni(opzioneId);
StoricoVincitoreSondaggio storico;
if (this.opzioneVincitrice != null) {
// Cambio vincitore: motivazione OBBLIGATORIA
if (motivazione == null || motivazione.isBlank()) {
throw new IllegalArgumentException(
"La motivazione e' obbligatoria per cambiare il vincitore");
}
storico = StoricoVincitoreSondaggio.creaCambio(
this, this.opzioneVincitrice, opzione,
motivazione, selezionatoDa);
} else {
// Prima selezione
storico = StoricoVincitoreSondaggio.creaPrimaSelezione(
this, opzione, selezionatoDa);
}
this.opzioneVincitrice = opzione;
this.storicoVincitori.add(storico);
return storico;
}
Sondaggi per Entità: Oltre la Semplice Domanda
I sondaggi non sono generici: ogni sondaggio è associato a un tipo di entità che ne definisce il contesto e le funzionalità disponibili.
public enum TipoEntitaSondaggio {
LUOGO("Luogo"), // Votare la venue dell'evento
DATA("Data"), // Scegliere la data
MENU("Menu"), // Preferenze catering
ATTIVITA("Attività"), // Attività da organizzare
CUSTOM("Personalizzato"); // Sondaggio generico
}
Il tipo LUOGO è particolarmente ricco: le opzioni possono
essere collegate direttamente a entità Luogo del sistema tramite
entitaRiferimentoId, con descrizione e immagine dedicata.
Inoltre, solo i sondaggi LUOGO supportano la selezione del vincitore.
@Entity
public class OpzioneSondaggio {
private Long id;
private Sondaggio sondaggio;
private String testo;
private Long entitaRiferimentoId; // ID del luogo collegato
private String descrizione; // Info aggiuntive
private String immagineUrl; // Foto della venue
private Integer ordine;
private Set<RispostaSondaggio> risposte;
}
Il factory method dedicato creaPerLuoghi() semplifica la creazione
di sondaggi per la scelta della location, pre-impostando il tipo entità
a LUOGO e la risposta a SINGOLA.
Opzioni Configurabili
Ogni sondaggio offre tre flag booleani che controllano l'esperienza di voto. Queste configurazioni sono modificabili solo finché il sondaggio è in stato BOZZA.
- maxOpzioniSelezionabili: limita il numero di scelte nelle risposte multiple. Validato per essere almeno 1.
-
mostraRisultatiPrimaVoto: se
true, i partecipanti vedono i risultati parziali prima di votare. Default:falseper evitare effetti bandwagon. -
permettiModificaVoto: se
true, i votanti possono cambiare la propria scelta. Default:true.
public void configura(Boolean anonimo, Boolean permettiModificaVoto,
Boolean mostraRisultatiPrimaVoto,
Integer maxOpzioniSelezionabili,
LocalDateTime dataScadenza) {
verificaModificabile(); // Solo in stato BOZZA
if (maxOpzioniSelezionabili != null) {
if (maxOpzioniSelezionabili < 1) {
throw new IllegalArgumentException(
"Deve essere selezionabile almeno un'opzione");
}
this.maxOpzioniSelezionabili = maxOpzioniSelezionabili;
}
this.dataScadenza = dataScadenza;
// ... altri campi ...
}
La data di scadenza è opzionale: se impostata, il sondaggio
smette automaticamente di accettare voti dopo la scadenza, anche se rimane in
stato ATTIVO. Il metodo isVotabile() controlla entrambe le condizioni.
Il Sistema Questionari
Accanto ai sondaggi, Play The Event offre un sistema di Questionari per raccogliere informazioni più strutturate. Mentre il sondaggio è una votazione su opzioni predefinite, il questionario è un modulo con domande di tipo diverso.
@Entity
@Table(name = "questionari")
public class Questionario {
private Long id;
private Evento evento;
private TipoQuestionario tipo; // TEMPLATE o EVENTO
private Long templateOrigineId;
private String titolo;
private String descrizione;
private String etichetta;
private String coloreEtichetta;
private StatoQuestionario stato; // BOZZA, PUBBLICATO, CHIUSO, ARCHIVIATO
private LocalDateTime dataScadenza;
private Boolean anonimo;
private Boolean richiediEmail;
private Boolean richiediNome;
private Boolean pubblico;
private String codiceCondivisione;
private List<DomandaQuestionario> domande;
}
Il ciclo di vita del questionario è simile a quello del sondaggio ma con uno stato in più: BOZZA → PUBBLICATO → CHIUSO → ARCHIVIATO. Lo stato ARCHIVIATO permette di conservare i questionari completati senza ingombrare la vista attiva.
public enum StatoQuestionario {
BOZZA("Bozza"), // In fase di creazione
PUBBLICATO("Pubblicato"), // Aperto alle compilazioni
CHIUSO("Chiuso"), // Compilazioni terminate
ARCHIVIATO("Archiviato"); // Conservato in archivio
}
Tipi di Domanda nel Questionario
Ogni domanda del questionario ha un tipo che determina come viene visualizzata e come vengono raccolte le risposte.
public enum TipoDomanda {
APERTA("Risposta aperta"), // Testo libero
SCELTA_SINGOLA("Scelta singola"), // Radio button
SCELTA_MULTIPLA("Scelta multipla"); // Checkbox
}
Le domande di tipo APERTA raccolgono testo libero: commenti,
suggerimenti, note particolari. Le domande a SCELTA_SINGOLA e
SCELTA_MULTIPLA richiedono opzioni predefinite tramite
l'entità OpzioneDomanda. Il sistema valida automaticamente
che solo i tipi che richiedono opzioni possano averne.
@Entity
public class DomandaQuestionario {
private Long id;
private Questionario questionario;
private String testo;
private TipoDomanda tipoDomanda;
private Boolean obbligatoria; // Default: true
private Integer ordine;
private String descrizioneAiuto; // Testo di aiuto opzionale
private List<OpzioneDomanda> opzioni;
}
// Validazione nel metodo aggiungiOpzione
public OpzioneDomanda aggiungiOpzione(String testo) {
if (!this.tipoDomanda.richiedeOpzioni()) {
throw new IllegalStateException(
"Le domande di tipo " + this.tipoDomanda
+ " non supportano opzioni");
}
// ...
}
Compilazioni e Risposte
Quando un utente compila un questionario, il sistema crea una CompilazioneQuestionario che raggruppa tutte le risposte. Il questionario supporta sia compilazioni da utenti registrati che compilazioni pubbliche (anonime o con dati forniti volontariamente).
@Entity
public class CompilazioneQuestionario {
private Long id;
private Questionario questionario;
private User utente; // null se compilazione pubblica
private String nomeCompilatore;
private String emailCompilatore;
private String fingerprint; // Per tracciamento anonimo
private List<RispostaQuestionario> risposte;
private Instant compilatoIl;
}
// Due modalità' di compilazione
CompilazioneQuestionario.creaPerUtente(questionario, utente);
CompilazioneQuestionario.creaPubblica(
questionario, nome, email, fingerprint);
Ogni risposta è modellata da RispostaQuestionario, che gestisce sia il testo libero (per domande aperte) sia le opzioni selezionate (serializzate come JSON array per domande a scelta).
Etichette e Organizzazione
I questionari possono essere categorizzati tramite etichette con nome e colore personalizzabile. Il sistema offre etichette predefinite (di sistema) e la possibilità per ogni utente di crearne di personalizzate.
@Entity
public class EtichettaQuestionario {
private Long id;
private String nome; // Max 50 caratteri
private String colore; // Codice esadecimale (#3B82F6)
private Long userId; // null per etichette di sistema
private boolean predefinita; // true = di sistema
}
// Creazione etichetta personalizzata
EtichettaQuestionario.creaPersonalizzata(
userId, "Feedback Post-Evento", "#10B981");
Risultati in Tempo Reale
La visibilità dei risultati è controllata da regole precise sia per
i sondaggi che per i questionari. Per i sondaggi, il metodo
sonoRisultatiVisibili() implementa la logica:
- Se il sondaggio è CHIUSO: risultati sempre visibili a tutti
- Se mostraRisultatiPrimaVoto è attivo: risultati visibili anche senza votare
- Altrimenti: risultati visibili solo dopo aver votato
public boolean sonoRisultatiVisibili(boolean haVotato) {
if (this.stato == StatoSondaggio.CHIUSO) return true;
if (Boolean.TRUE.equals(this.mostraRisultatiPrimaVoto)) return true;
return haVotato;
}
public boolean isVotabile() {
return this.stato == StatoSondaggio.ATTIVO &&
(this.dataScadenza == null ||
LocalDateTime.now().isBefore(this.dataScadenza));
}
I risultati vengono calcolati aggregando le risposte per ogni opzione.
Ogni OpzioneSondaggio espone il conteggio tramite
getNumeroVoti() che conta le risposte nella collection.
Sul frontend, questi dati alimentano grafici a barre e percentuali aggiornati
in tempo reale.
Punti Chiave
- Macchina a stati con 4 transizioni controllate e possibilità di riapertura
- Risposte SINGOLA e MULTIPLA con limite configurabile di opzioni selezionabili
- Votazione anonima con tracciamento via fingerprint per anti-duplicazione
- Sondaggi pubblici con UUID sharing codes per votanti esterni
- Template riutilizzabili con deep copy completa di opzioni
- Audit trail obbligatorio con motivazione per ogni cambio di vincitore
- 5 tipi di entità sondaggio (LUOGO, DATA, MENU, ATTIVITA, CUSTOM)
- Questionari con domande tipizzate (APERTA, SCELTA_SINGOLA, SCELTA_MULTIPLA)
- Etichette personalizzabili con colori per organizzare i questionari
- Visibilità risultati controllata con 3 livelli di accesso
Il codice sorgente del progetto è disponibile su GitHub. Per provare il sistema completo di sondaggi e questionari, visita www.playtheevent.com.







