The jury voting system enables jury members to evaluate films through a two-stage process with vote tracking and modification controls.
Vote Data Model
Votes are represented with three possible values and modification tracking:
// /back/src/models/Vote.js:4-35
const Vote = sequelize . define ( 'Vote' , {
id_vote: {
type: DataTypes . INTEGER ,
primaryKey: true ,
autoIncrement: true
},
id_user: {
type: DataTypes . INTEGER ,
allowNull: false
},
id_movie: {
type: DataTypes . INTEGER ,
allowNull: false
},
note: {
type: DataTypes . ENUM ( 'YES' , 'NO' , 'TO DISCUSS' ),
allowNull: false
},
comments: {
type: DataTypes . TEXT ,
allowNull: true
},
modification_count: {
type: DataTypes . INTEGER ,
defaultValue: 0 ,
allowNull: false
}
}, {
tableName: 'votes' ,
timestamps: true // Tracks createdAt and updatedAt
});
Vote Options
Approve Film The jury member recommends the film for selection.
Film quality meets festival standards
Appropriate for the category
Should advance in selection process
Effect on Film Status:
If majority votes YES, film advances
Film remains in current stage for further review
Reject Film The jury member does not recommend the film.
Film does not meet quality standards
Not appropriate for the festival
Should not advance
Effect on Film Status:
If majority votes NO, admin may move to “refused”
Counts against film’s overall score
Needs Discussion The jury member is undecided and wants group discussion.
Film has interesting elements but concerns
Needs other jury opinions
Borderline case requiring debate
Effect on Film Status:
Film moves to “to_discuss” status
Second round of voting opens
All jury members can revise their votes
Two-Stage Voting Process
Stage 1: Initial Review (assigned)
Jury members cast their first vote on assigned films.
// POST /votes/:id_movie/jury
{
"note" : "TO DISCUSS" ,
"comments" : "Interesting concept but execution needs discussion"
}
Film must be in “assigned” status
Jury member must be assigned to the film
Comments are required
One vote per jury member per film
// /back/src/controllers/VoteController.js:165-178
if ( status !== 'assigned' ) {
return res . status ( 409 ). json ({
error: "Le premier vote n'est autorisé qu'en phase assigned"
});
}
const newVote = await Vote . create ({
note: noteFloat ,
comments ,
id_movie ,
id_user ,
modification_count: 0
});
Stage 2: Discussion Phase (to_discuss)
If any jury member votes “TO DISCUSS”, the film enters second round.
// PUT /votes/:id_movie/jury (same endpoint, detects existing vote)
{
"note" : "YES" ,
"comments" : "After discussion, I believe this film deserves selection"
}
Film must be in “to_discuss” status
Jury member must have existing vote
Limited to one modification per jury member
Vote history is preserved
// /back/src/controllers/VoteController.js:119-162
if ( existingVote ) {
const isSecondRound = status === 'to_discuss' ;
if ( ! isSecondRound ) {
return res . status ( 409 ). json ({
error: "Le second vote n'est pas encore ouvert pour ce film"
});
}
if (( existingVote . modification_count || 0 ) >= 1 ) {
return res . status ( 409 ). json ({
error: "Le second vote a déjà été utilisé"
});
}
// Save old vote to history
if ( hasChanges ) {
await VoteHistory . create ({
id_vote: existingVote . id_vote ,
id_movie ,
id_user ,
note: existingVote . note ,
comments: existingVote . comments
});
}
existingVote . note = noteFloat ;
existingVote . comments = normalizedComment ;
existingVote . modification_count = ( existingVote . modification_count || 0 ) + 1 ;
await existingVote . save ();
}
Stage 3: Final Selection (candidate)
Films with positive consensus advance to candidate status.
Voting is closed
Admin reviews jury consensus
Awards may be assigned
Film may be promoted to “awarded” or moved to “refused”
Vote History Tracking
All vote modifications are tracked in VoteHistory:
const VoteHistory = sequelize . define ( 'VoteHistory' , {
id_vote_history: {
type: DataTypes . INTEGER ,
primaryKey: true ,
autoIncrement: true
},
id_vote: {
type: DataTypes . INTEGER ,
allowNull: false
},
id_movie: DataTypes . INTEGER ,
id_user: DataTypes . INTEGER ,
note: DataTypes . ENUM ( 'YES' , 'NO' , 'TO DISCUSS' ),
comments: DataTypes . TEXT
}, {
tableName: 'vote_history' ,
timestamps: true
});
Retrieving Vote History
const vote = await Vote . findOne ({
where: { id_movie , id_user },
include: [
{
model: VoteHistory ,
as: "history" ,
separate: true ,
order: [[ "createdAt" , "ASC" ]]
}
]
});
// Response format:
{
"id_vote" : 123 ,
"note" : "YES" ,
"comments" : "After discussion, I approve" ,
"modification_count" : 1 ,
"history" : [
{
"note" : "TO DISCUSS" ,
"comments" : "Needs discussion" ,
"createdAt" : "2024-01-15T10:30:00Z"
}
]
}
Vote history preserves the complete audit trail of each jury member’s decision-making process.
Jury Assignment
Before voting, admins must assign jury members to films:
// PUT /movies/:id/juries (ADMIN only)
async function updateMovieJuries ( req , res ) {
const { id } = req . params ;
let { juryIds } = req . body ;
const movie = await Movie . findByPk ( id );
const juries = await User . findAll ({
where: {
id_user: juryIds ,
role: "JURY"
}
});
await movie . setJuries ( juries );
// Auto-update status to 'assigned' if not already advanced
const advancedStatuses = [ "to_discuss" , "candidate" , "awarded" , "refused" ];
if ( juries . length > 0 && ! advancedStatuses . includes ( movie . selection_status )) {
movie . selection_status = "assigned" ;
await movie . save ();
}
res . json ({ message: "Jurys assignés" , movie });
}
Assigning jury members automatically moves the film from “submitted” to “assigned” status, triggering the first voting round.
Creating and Updating Votes
Validation Rules
// /back/src/controllers/VoteController.js:86-117
async function createOrUpdateMyVote ( req , res ) {
const id_user = req . user . id_user ;
const { id_movie } = req . params ;
let { note , comments } = req . body ;
// Validate note format
const noteFloat = parseFloat ( note );
if ( isNaN ( noteFloat )) {
return res . status ( 400 ). json ({ error: "Note invalide" });
}
// Comments are required
if ( ! comments || ! String ( comments ). trim ()) {
return res . status ( 400 ). json ({ error: "Commento richiesto" });
}
// Check jury assignment
const assigned = await MovieJury . findOne ({
where: { id_movie , id_user }
});
if ( ! assigned ) {
return res . status ( 403 ). json ({
error: "Film non assigné à ce jury"
});
}
// Check film status
const movie = await Movie . findByPk ( id_movie );
const status = movie . selection_status ;
if ( ! [ 'assigned' , 'to_discuss' ]. includes ( status )) {
return res . status ( 400 ). json ({
error: "Vote non autorisé pour ce statut de film"
});
}
// ... create or update vote
}
Vote Modification Limits
Each jury member can modify their vote only once during the second round. This prevents endless vote changes and ensures timely decision-making.
if (( existingVote . modification_count || 0 ) >= 1 ) {
return res . status ( 409 ). json ({
error: "Le second vote a déjà été utilisé"
});
}
Jury Dashboard Views
Get My Votes
Jury members can view all their votes:
// GET /votes/my
async function getMyVotes ( req , res ) {
const id_user = req . user . id_user ;
const votes = await Vote . findAll ({
where: { id_user },
include: [
{
model: Movie ,
attributes: [ "id_movie" , "title" ]
},
{
model: VoteHistory ,
as: "history" ,
separate: true ,
order: [[ "createdAt" , "ASC" ]]
}
]
});
return res . json ( votes );
}
Get Vote for Specific Film
// GET /votes/my/movie/:id_movie
async function getMyVoteByMovie ( req , res ) {
const id_user = req . user . id_user ;
const { id_movie } = req . params ;
const vote = await Vote . findOne ({
where: { id_user , id_movie },
include: [
{ model: Movie },
{ model: VoteHistory , as: "history" }
]
});
if ( ! vote ) {
return res . status ( 404 ). json ({ error: "Vote non trouvé" });
}
return res . json ( vote );
}
Jury members can directly promote films from “to_discuss” to “candidate”:
// POST /movies/:id/promote-candidate
async function promoteMovieToCandidateByJury ( req , res ) {
const { id } = req . params ;
const { jury_comment } = req . body || {};
const id_user = req . user . id_user ;
const movie = await Movie . findByPk ( id , {
include: [{
model: User ,
as: "Juries" ,
where: { id_user },
required: false
}]
});
// Check assignment
const assignedToJury = movie . Juries . some ( jury => jury . id_user === id_user );
if ( ! assignedToJury ) {
return res . status ( 403 ). json ({
error: "Ce film n'est pas assigné à ce jury."
});
}
// Check status
if ( movie . selection_status !== "to_discuss" ) {
return res . status ( 400 ). json ({
error: "Le film doit être en statut to_discuss pour être promu candidat."
});
}
// Promote
movie . selection_status = "candidate" ;
if ( jury_comment ) {
movie . jury_comment = jury_comment . trim ();
}
await movie . save ();
return res . json ({ message: "Film promu à la candidature" , movie });
}
This feature allows senior jury members or jury leaders to fast-track exceptional films to candidate status after the discussion phase.
Vote Statistics & Analytics
Admins can view aggregated voting data:
// Example: Calculate consensus for a film
async function getFilmVoteConsensus ( id_movie ) {
const votes = await Vote . findAll ({
where: { id_movie },
include: [{ model: User , attributes: [ "first_name" , "last_name" ] }]
});
const summary = {
total: votes . length ,
yes: votes . filter ( v => v . note === "YES" ). length ,
no: votes . filter ( v => v . note === "NO" ). length ,
to_discuss: votes . filter ( v => v . note === "TO DISCUSS" ). length
};
return {
... summary ,
consensus: summary . yes / summary . total ,
needs_discussion: summary . to_discuss > 0
};
}
Voting Workflow Diagram
API Endpoints Summary
Method Endpoint Role Description GET /votesADMIN Get all votes GET /votes/myJURY Get my votes GET /votes/my/movie/:id_movieJURY Get my vote for film POST /votes/:id_movie/juryJURY Create or update vote DELETE /votes/:idADMIN Delete vote DELETE /votes/movie/:id_movieADMIN Delete all votes for film POST /movies/:id/promote-candidateJURY Promote film to candidate
Frontend Integration Example
import { useMutation , useQuery } from "@tanstack/react-query" ;
import { createOrUpdateVote , getMyVoteForMovie } from "./api/votes" ;
function VotingInterface ({ movieId }) {
// Fetch existing vote
const { data : existingVote } = useQuery ({
queryKey: [ "vote" , movieId ],
queryFn : () => getMyVoteForMovie ( movieId )
});
// Submit/update vote
const voteMutation = useMutation ({
mutationFn : ( data ) => createOrUpdateVote ( movieId , data ),
onSuccess : () => {
alert ( "Vote enregistré avec succès" );
}
});
const handleVote = ( note ) => {
const comments = prompt ( "Commentaires (requis):" );
if ( comments ) {
voteMutation . mutate ({ note , comments });
}
};
return (
< div >
< h2 > Votez pour ce film </ h2 >
{ existingVote && (
< div >
< p > Votre vote actuel: { existingVote . note } </ p >
< p > Modifications: { existingVote . modification_count } /1 </ p >
</ div >
) }
< button onClick = { () => handleVote ( "YES" ) } > YES </ button >
< button onClick = { () => handleVote ( "NO" ) } > NO </ button >
< button onClick = { () => handleVote ( "TO DISCUSS" ) } > TO DISCUSS </ button >
{ existingVote ?. history ?. length > 0 && (
< div >
< h3 > Historique </ h3 >
{ existingVote . history . map (( h , i ) => (
< div key = { i } >
< p > { h . note } - { h . comments } </ p >
< small > {new Date ( h . createdAt ). toLocaleString () } </ small >
</ div >
)) }
</ div >
) }
</ div >
);
}
Best Practices
Voting Guidelines for Jury
Always provide detailed comments explaining your vote
Use TO DISCUSS sparingly - only for genuinely borderline cases
Review all assigned films before the deadline
Participate in discussions when films enter second round
Consider the modification carefully - you only get one change
Assign balanced jury panels to each film
Monitor voting progress and send reminders
Facilitate discussions for films in “to_discuss” status
Respect jury consensus when determining final selections
Document decision rationale in admin_comment field
Votes are immutable after modification limit
Vote history is permanently preserved
Comments are required for all votes
Status transitions are automatically validated
Jury assignment is required before voting