Skip to main content
The film submission system allows producers to submit their films to the festival with comprehensive metadata, media files, and team credits.

Film Data Model

Films are represented by the Movie model with extensive metadata:
// /back/src/models/Movie.js:27-102
const Movie = sequelize.define('Movie', {
  id_movie: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  title: {
    type: DataTypes.STRING,
    allowNull: false
  },
  description: DataTypes.TEXT,
  duration: DataTypes.INTEGER,  // Duration in seconds
  main_language: DataTypes.STRING,
  release_year: DataTypes.INTEGER,
  nationality: DataTypes.STRING,
  
  // Media files
  display_picture: DataTypes.STRING,
  picture1: DataTypes.STRING,
  picture2: DataTypes.STRING,
  picture3: DataTypes.STRING,
  trailer: DataTypes.STRING,
  youtube_link: DataTypes.STRING(255),
  subtitle: DataTypes.STRING(255),
  thumbnail: DataTypes.STRING(255),
  
  // AI-related metadata
  production: DataTypes.STRING,       // AI classification
  workshop: DataTypes.STRING(255),    // AI methodology
  ai_tool: DataTypes.STRING(255),     // AI tools used
  
  // Bilingual synopsis
  synopsis: DataTypes.TEXT,           // French synopsis
  synopsis_anglais: DataTypes.TEXT,   // English synopsis
  translation: DataTypes.STRING(255), // Translation language
  
  // Review fields
  admin_comment: DataTypes.TEXT,
  jury_comment: DataTypes.TEXT,
  
  // Selection status pipeline
  selection_status: {
    type: DataTypes.ENUM(
      'submitted',   // Initial state after submission
      'assigned',    // Assigned to jury for first round
      'to_discuss',  // Second round voting open
      'candidate',   // Candidate for awards
      'awarded',     // Film received an award
      'refused',     // Rejected by jury
      'selected',    // Legacy: selected status
      'finalist'     // Legacy: finalist status
    ),
    allowNull: false,
    defaultValue: 'submitted'
  },
  
  // Owner
  id_user: {
    type: DataTypes.INTEGER,
    allowNull: false
  }
});

Selection Status Flow

Films progress through a multi-stage selection pipeline:
The status transition rules are enforced in the backend to maintain workflow integrity. Admins can use force_transition to override these rules if needed.

Film Submission Flow

1
Step 1: User Registration
2
Producers can register with or without a film:
3
Option A: Register then submit film
4
POST /auth/register
{
  "first_name": "Jane",
  "last_name": "Smith",
  "email": "jane@example.com",
  "password": "secure123",
  "role": "PRODUCER"
}
5
Option B: Register with film submission (atomic transaction)
6
POST /auth/register-film
Content-Type: multipart/form-data

// User fields
first_name: "Jane"
last_name: "Smith"
email: "jane@example.com"
password: "secure123"

// Film fields
filmTitleOriginal: "AI Dreams"
durationSeconds: 90
filmLanguage: "English"
synopsisOriginal: "A short film about..."
aiClassification: "AI-assisted"
aiStack: "Stable Diffusion, RunwayML"
aiMethodology: "Hybrid workflow"

// File uploads
filmFile: <video file>
thumbnail1: <image file>
thumbnail2: <image file>
subtitlesSrt: <.srt file>
7
Step 2: Film Data Processing
8
// /back/src/controllers/AuthController.js:183-204
const newMovie = await Movie.create({
  title: filmTitleOriginal,
  description: synopsisOriginal,
  duration: durationNumber,
  main_language: filmLanguage,
  release_year: releaseYear || null,
  nationality,
  translation,
  youtube_link: youtubeLink,
  synopsis: synopsisOriginal,
  synopsis_anglais: synopsisEnglish,
  ai_tool: aiStack,
  workshop: aiMethodology,
  production: aiClassification,
  trailer: filmFile,
  subtitle: subtitleFile,
  picture1: thumb1,
  picture2: thumb2,
  picture3: thumb3,
  thumbnail: thumb1,
  id_user: newUser.id_user
}, { transaction });
9
The maximum film duration is 120 seconds (2 minutes). Submissions exceeding this limit are rejected with a 400 error.
10
Step 3: Collaborator Association
11
Add team credits:
12
// /back/src/controllers/AuthController.js:206-240
const collaborators = JSON.parse(req.body.collaborators);

const collaboratorRecords = await Promise.all(
  collaborators
    .filter(collab => collab?.email)
    .map(async (collab) => {
      const [record] = await Collaborator.findOrCreate({
        where: { email: collab.email },
        defaults: {
          first_name: collab.first_name || "",
          last_name: collab.last_name || "",
          email: collab.email,
          job: collab.job || null
        },
        transaction
      });
      return record;
    })
);

await newMovie.setCollaborators(collaboratorRecords, { transaction });
13
Step 4: Confirmation Email
14
Users receive confirmation after successful submission:
15
// /back/src/controllers/AuthController.js:265-286
await sendTemplateEmail({
  to: newUser.email,
  templateId: process.env.BREVO_TEMPLATE_FILM_CANDIDACY_CONFIRMATION,
  params: {
    first_name: newUser.first_name,
    last_name: newUser.last_name,
    movie_title: newMovie.title,
    email: newUser.email
  }
});

Media File Management

Supported File Types

The platform accepts multiple media types:
Film/Trailer Upload
  • Field: filmFile or trailer
  • Stored in: /uploads/ directory
  • Database column: trailer
const files = req.files || {};
const filmFile = files.filmFile?.[0]?.filename || null;

if (filmFile) {
  await movie.update({ trailer: filmFile });
}

File Upload Configuration

Files are handled with Multer middleware:
import multer from "multer";
import path from "path";

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "uploads/");
  },
  filename: (req, file, cb) => {
    const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
    cb(null, uniqueSuffix + path.extname(file.originalname));
  }
});

const upload = multer({ storage });

Category Management

Category Model

// /back/src/models/Categorie.js:5-15
const Categorie = sequelize.define('Categorie', {
  id_categorie: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  name: DataTypes.STRING
});

Many-to-Many Relationship

Films can belong to multiple categories:
// /back/src/models/Movie.js:134-138
Movie.belongsToMany(models.Categorie, {
  through: 'movies_categories',
  foreignKey: 'id_movie',
  otherKey: 'id_categorie'
});

Assigning Categories

During Submission:
const categories = [1, 3, 5];  // Category IDs
await newMovie.setCategories(categories);
Admin Updates:
// PUT /movies/:id/categories (ADMIN only)
async function updateMovieCategories(req, res) {
  const { id } = req.params;
  let { categories } = req.body;

  const movie = await Movie.findByPk(id);
  await movie.setCategories(categoryIds);

  const updatedMovie = await Movie.findByPk(id, {
    include: [{ model: Categorie }]
  });

  res.json({ message: "Catégories mises à jour", movie: updatedMovie });
}

Collaborator Credits

Collaborator Model

Track team members:
const Collaborator = sequelize.define('Collaborator', {
  id_collaborator: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  first_name: DataTypes.STRING,
  last_name: DataTypes.STRING,
  email: {
    type: DataTypes.STRING,
    unique: true,
    allowNull: false
  },
  job: DataTypes.STRING  // Role in production
});

Adding Collaborators

// POST /movies/:id/collaborators
const collaborators = [
  {
    first_name: "John",
    last_name: "Doe",
    email: "john@example.com",
    job: "DIRECTOR"
  },
  {
    first_name: "Jane",
    last_name: "Smith",
    email: "jane@example.com",
    job: "ACTOR"
  }
];

await movie.setCollaborators(collaboratorRecords);
Collaborators are identified by email. If a collaborator already exists in the database, their information is updated rather than creating a duplicate.

Retrieving Films

Get All Films (Admin/Jury)

// GET /movies
async function getMovies(req, res) {
  const movies = await Movie.findAll({
    include: [
      {
        model: Categorie,
        through: { attributes: [] }
      },
      {
        model: Collaborator,
        through: { attributes: [] }
      },
      {
        model: Award,
        required: false
      },
      {
        model: User,
        as: "Producer",
        attributes: ["id_user", "first_name", "last_name"]
      },
      {
        model: User,
        as: "Juries",
        attributes: ["id_user", "first_name", "last_name", "email"],
        through: { attributes: [] },
        required: false
      }
    ]
  });

  res.json(movies);
}

Get My Films (Producer)

// GET /movies/my
async function getMyMovies(req, res) {
  const id_user = req.user.id_user;
  const movies = await Movie.findAll({
    where: { id_user },
    include: [
      { model: Categorie },
      { model: Collaborator }
    ]
  });
  res.json(movies);
}

Get Assigned Films (Jury)

// GET /movies/assigned
async function getAssignedMovies(req, res) {
  const id_user = req.user.id_user;

  const movies = await Movie.findAll({
    where: {
      selection_status: {
        [Op.in]: ["assigned", "to_discuss", "candidate"]
      }
    },
    include: [
      {
        model: User,
        as: "Juries",
        where: { id_user },  // Only films assigned to this jury
        through: { attributes: [] }
      }
    ]
  });

  res.json(movies);
}

Updating Films

Producer Updates

Producers can update their own films:
// PUT /movies/:id (PRODUCER or ADMIN)
if (req.user.role !== "ADMIN" && movie.id_user !== req.user.id_user) {
  return res.status(403).json({ error: "Accès refusé" });
}

await movie.update({
  title: req.body.title,
  description: req.body.description,
  // ... other fields
});

Admin Status Updates

// PUT /movies/:id/status (ADMIN only)
async function updateMovieStatus(req, res) {
  const { id } = req.params;
  const { selection_status, jury_comment, force_transition } = req.body;

  const movie = await Movie.findByPk(id);
  const previousStatus = movie.selection_status;

  // Validate transition
  const transitionMap = {
    submitted: ["assigned", "candidate", "refused"],
    assigned: ["to_discuss", "candidate", "refused"],
    to_discuss: ["candidate", "refused"],
    candidate: ["awarded", "refused"],
    awarded: [],
    refused: []
  };

  const allowedTargets = transitionMap[previousStatus] || [];
  if (!force_transition && !allowedTargets.includes(selection_status)) {
    return res.status(400).json({
      error: `Transition invalide: ${previousStatus} -> ${selection_status}`
    });
  }

  movie.selection_status = selection_status;
  if (jury_comment) {
    movie.jury_comment = jury_comment.trim();
  }
  await movie.save();

  res.json({ message: "Statut mis à jour", movie });
}
When a film is moved to “refused” status, an automatic rejection email is sent to the producer.

Validation Rules

  • title - Film title (required)
  • description or synopsis - Film description (required)
  • duration - Duration in seconds (optional but recommended)
  • id_user - Owner/producer ID (automatically set)
if (durationSeconds && movieDuration > 120) {
  return res.status(400).json({
    error: "La durée maximale est de 120 secondes"
  });
}
Maximum duration: 120 seconds (2 minutes)
  • Video file: Required for complete submission
  • At least one thumbnail: Required (or auto-generated)
  • Subtitles: Optional
  • All files stored in /uploads/ directory

API Endpoints Summary

MethodEndpointRoleDescription
POST/auth/register-filmPublicRegister user + submit film (atomic)
POST/moviesPRODUCERSubmit new film
GET/moviesADMINGet all films
GET/movies/myPRODUCERGet my films
GET/movies/assignedJURYGet assigned films
GET/movies/:idALLGet single film details
PUT/movies/:idADMINUpdate film metadata
PUT/movies/:id/statusADMINUpdate selection status
PUT/movies/:id/categoriesADMINAssign categories
PUT/movies/:id/juriesADMINAssign jury members
PUT/movies/:id/collaboratorsPRODUCER/ADMINUpdate collaborators
DELETE/movies/:idADMINDelete film

Example: Complete Submission

// Frontend form submission
const formData = new FormData();

// User data
formData.append("first_name", "Jane");
formData.append("last_name", "Smith");
formData.append("email", "jane@example.com");
formData.append("password", "secure123");

// Film data
formData.append("filmTitleOriginal", "AI Dreams");
formData.append("durationSeconds", 90);
formData.append("filmLanguage", "English");
formData.append("synopsisOriginal", "A mesmerizing exploration...");
formData.append("synopsisEnglish", "A mesmerizing exploration...");
formData.append("aiClassification", "AI-assisted");
formData.append("aiStack", "Stable Diffusion, Midjourney");
formData.append("aiMethodology", "Hybrid human-AI workflow");
formData.append("nationality", "USA");
formData.append("releaseYear", 2024);

// Files
formData.append("filmFile", videoFile);
formData.append("thumbnail1", thumb1);
formData.append("thumbnail2", thumb2);
formData.append("subtitlesSrt", srtFile);

// Collaborators
formData.append("collaborators", JSON.stringify([
  { first_name: "John", last_name: "Doe", email: "john@example.com", job: "DIRECTOR" },
  { first_name: "Alice", last_name: "Johnson", email: "alice@example.com", job: "ACTOR" }
]));

// Submit
const response = await fetch("/auth/register-film", {
  method: "POST",
  body: formData
});