Skip to main content
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 FilmThe 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

Two-Stage Voting Process

1
Stage 1: Initial Review (assigned)
2
Jury members cast their first vote on assigned films.
3
Film Status: assigned
4
// POST /votes/:id_movie/jury
{
  "note": "TO DISCUSS",
  "comments": "Interesting concept but execution needs discussion"
}
5
Requirements:
6
  • Film must be in “assigned” status
  • Jury member must be assigned to the film
  • Comments are required
  • One vote per jury member per film
  • 7
    Code Implementation:
    8
    // /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
    });
    
    9
    Stage 2: Discussion Phase (to_discuss)
    10
    If any jury member votes “TO DISCUSS”, the film enters second round.
    11
    Film Status: to_discuss
    12
    // PUT /votes/:id_movie/jury (same endpoint, detects existing vote)
    {
      "note": "YES",
      "comments": "After discussion, I believe this film deserves selection"
    }
    
    13
    Requirements:
    14
  • Film must be in “to_discuss” status
  • Jury member must have existing vote
  • Limited to one modification per jury member
  • Vote history is preserved
  • 15
    Code Implementation:
    16
    // /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();
    }
    
    17
    Stage 3: Final Selection (candidate)
    18
    Films with positive consensus advance to candidate status.
    19
    Film Status: candidate
    20
  • 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);
    }
    

    Promoting Films to Candidate

    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

    MethodEndpointRoleDescription
    GET/votesADMINGet all votes
    GET/votes/myJURYGet my votes
    GET/votes/my/movie/:id_movieJURYGet my vote for film
    POST/votes/:id_movie/juryJURYCreate or update vote
    DELETE/votes/:idADMINDelete vote
    DELETE/votes/movie/:id_movieADMINDelete all votes for film
    POST/movies/:id/promote-candidateJURYPromote 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

    • 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