Section 13.1.1 : Terminal et ligne de commande
🎯 Objectif pédagogique
Maîtriser le terminal comme outil principal de développement. À la fin de cette section, vous naviguerez dans votre système de fichiers, manipulerez des fichiers et exécuterez des commandes complexes avec confiance.
Le terminal : votre cockpit de développeur
Tout développeur professionnel passe une partie significative de son temps dans le terminal. C'est l'interface textuelle qui vous donne un contrôle total sur votre machine, vos fichiers, vos programmes et vos serveurs. Si l'interface graphique est le volant d'une voiture, le terminal est le cockpit d'un avion : plus complexe, mais infiniment plus puissant.
Le terminal (aussi appelé shell, console ou CLI — Command Line Interface) est un interpréteur qui exécute des commandes textuelles. Sur macOS et Linux, vous utiliserez principalement Bash ou Zsh. Sur Windows, PowerShell ou le Windows Terminal avec WSL (Windows Subsystem for Linux).
Anatomie d'une commande
Une commande suit toujours la même structure :
commande [options] [arguments]
# Exemples :
ls -la Documents/
# │ │ └── argument (le dossier cible)
# │ └── options (-l pour détaillé, -a pour fichiers cachés)
# └── commande (lister les fichiers)
Navigation dans le système de fichiers
Le système de fichiers est organisé en arborescence. Votre position actuelle est le répertoire courant (working directory). Toute commande s'exécute relativement à cette position.
# Où suis-je ?
pwd # Print Working Directory
# Que contient ce dossier ?
ls # Liste simple
ls -l # Détaillé (permissions, taille, date)
ls -la # Avec fichiers cachés (.gitignore, .env)
ls -lah # Avec tailles lisibles (Ko, Mo, Go)
# Se déplacer
cd Documents # Entrer dans Documents
cd .. # Remonter d'un niveau
cd ../.. # Remonter de deux niveaux
cd ~ # Aller au répertoire home
cd - # Retourner au dernier répertoire
Astuce productivité
Utilisez la touche Tab pour l'autocomplétion. Tapez les premières lettres d'un nom de fichier ou dossier, puis appuyez sur Tab. Le terminal complète automatiquement. En cas d'ambiguïté, appuyez deux fois sur Tab pour voir les options.
Manipulation de fichiers et dossiers
# Créer
touch index.html # Créer un fichier vide
mkdir mon-projet # Créer un dossier
mkdir -p src/components/ui # Créer une arborescence complète
# Copier
cp fichier.txt copie.txt # Copier un fichier
cp -r dossier/ backup/ # Copier un dossier récursivement
# Déplacer / Renommer
mv ancien.txt nouveau.txt # Renommer
mv fichier.txt Documents/ # Déplacer
# Supprimer (ATTENTION : pas de corbeille !)
rm fichier.txt # Supprimer un fichier
rm -r dossier/ # Supprimer un dossier
rm -ri dossier/ # Supprimer avec confirmation
# Lire le contenu
cat fichier.txt # Afficher tout le contenu
head -20 fichier.txt # Les 20 premières lignes
tail -20 fichier.txt # Les 20 dernières lignes
less fichier.txt # Navigation pagée (q pour quitter)
Recherche et filtrage
# Chercher du texte dans des fichiers
grep 'def initialize' app/models/*.rb # Chercher dans les fichiers Ruby
grep -r 'TODO' src/ # Recherche récursive
grep -rn 'function' --include='*.js' # Avec numéros de ligne
# Trouver des fichiers
find . -name '*.rb' # Tous les fichiers .rb
find . -type d -name 'node_modules' # Trouver des dossiers
# Compter
wc -l fichier.txt # Nombre de lignes
wc -w fichier.txt # Nombre de mots
Le pipe : la puissance de la composition
Le pipe (|) connecte la sortie d'une commande à l'entrée de la suivante, permettant de créer des pipelines de traitement de données :
# Compter les erreurs dans un log
cat server.log | grep 'ERROR' | wc -l
# Extraire les emails uniques d'un CSV
cut -d',' -f3 contacts.csv | sort | uniq | grep '@'
# Top 5 des plus gros fichiers
find . -type f -exec ls -la {} \; | sort -k5 -rn | head -5
Variables d'environnement
Les variables d'environnement sont des valeurs globales accessibles par tous les programmes :
echo $HOME # Votre répertoire home
echo $PATH # Dossiers où chercher les commandes
echo $USER # Votre nom d'utilisateur
# Définir une variable
export API_KEY='sk-abc123'
# Persistance dans .zshrc
echo 'export API_KEY="sk-abc123"' >> ~/.zshrc
source ~/.zshrc
Exercice pratique : créer un projet web
# 1. Créer la structure
mkdir -p mon-site/css mon-site/js mon-site/images
touch mon-site/index.html mon-site/css/style.css mon-site/js/app.js
# 2. Vérifier
find mon-site -type f
# 3. Archiver
tar -czf mon-site.tar.gz mon-site/
ls -lh mon-site.tar.gz
Section 13.1.2 : Git et GitHub
🎯 Objectif pédagogique
Comprendre et utiliser Git pour le versionnement de code et GitHub pour la collaboration. Vous saurez créer des repositories, committer, brancher, merger et travailler en équipe.
Pourquoi Git est indispensable
Imaginez que vous écrivez un roman. Vous faites des modifications, revenez en arrière, essayez des variantes... Sans système de versionnement, vous finiriez avec des fichiers comme roman_v2_final_VRAIMENT_FINAL_ok.docx. En développement, ce chaos serait catastrophique.
Git est un système de contrôle de version distribué créé en 2005 par Linus Torvalds (le créateur de Linux). Il enregistre chaque modification dans un historique permanent, vous permettant de revenir à n'importe quelle version, travailler en parallèle sur plusieurs fonctionnalités via des branches, et collaborer sans conflits.
Les concepts fondamentaux
Git organise votre travail en trois zones : le Working Directory (vos fichiers actuels), la Staging Area (modifications sélectionnées pour le prochain commit), et le Repository (historique complet des commits).
Configuration initiale
git config --global user.name 'Alice Martin'
git config --global user.email 'alice@example.com'
git config --global core.editor 'code --wait'
git config --global color.ui auto
git config --list
Votre premier repository
mkdir mon-projet && cd mon-projet
git init
touch index.html style.css app.js
echo '# Mon Projet' > README.md
git status
git add .
git commit -m 'Initial commit: structure du projet'
Le cycle de travail quotidien
# 1. Modifier des fichiers
echo '<h1>Hello World</h1>' >> index.html
# 2. Voir les modifications
git status # Fichiers modifiés
git diff # Détail ligne par ligne
# 3. Stager
git add index.html
# 4. Committer
git commit -m 'feat: add heading to homepage'
# 5. Historique
git log --oneline --graph
Conventional Commits
Adoptez cette convention dès le début — feat: nouvelle fonctionnalité, fix: correction, docs: documentation, style: formatage, refactor: restructuration, test: tests. Exemple : feat: add user authentication with Devise
Les branches : travailler en parallèle
Les branches sont la fonctionnalité qui rend Git indispensable en équipe. Une branche est une copie indépendante de votre code pour expérimenter sans casser la version stable.
git checkout -b feature/navbar
# ... coder la fonctionnalité ...
git add .
git commit -m 'feat: add navigation bar'
git checkout main
git merge feature/navbar
git branch -d feature/navbar
GitHub : collaborer dans le cloud
GitHub ajoute à Git des fonctionnalités essentielles : hébergement de repos, pull requests, issues, CI/CD, et un profil qui fait office de CV technique.
git remote add origin https://github.com/alice/mon-projet.git
git push -u origin main
git pull origin main
git clone https://github.com/lewagon/fullstack-challenges.git
Pull Requests et code review
Le workflow professionnel repose sur les Pull Requests : créer une branche, coder, pousser, ouvrir une PR, l'équipe review, merger dans main après approbation.
.gitignore
node_modules/ # Dépendances npm
.env # Variables secrètes
*.log # Logs
.DS_Store # Fichiers macOS
dist/ # Fichiers compilés
Section 13.1.3 : Ruby — Variables, types et boucles
🎯 Objectif pédagogique
Découvrir le langage Ruby, comprendre ses types de données fondamentaux et maîtriser les structures de contrôle. Ruby est le langage au cœur de Ruby on Rails — sa philosophie de « developer happiness » en fait un excellent premier langage.
Pourquoi Ruby ?
Ruby est un langage créé en 1995 par Yukihiro « Matz » Matsumoto au Japon. Sa philosophie : « Ruby is designed to make programmers happy ». Le langage privilégie la lisibilité et l'expressivité, ce qui en fait un excellent choix pour apprendre la programmation.
Variables et types de données
En Ruby, pas besoin de déclarer le type — le langage le déduit automatiquement (typage dynamique).
# Strings
prenom = 'Alice'
nom = "Martin"
message = "Bonjour, #{prenom} #{nom} !" # Interpolation
# Nombres
age = 28 # Integer
prix = 19.99 # Float
quantite = 1_000_000 # Underscore pour lisibilité
# Booléens
majeur = true
inscrit = false
# Nil
resultat = nil
# Symboles
statut = :actif
role = :admin
Opérations sur les strings
nom = 'alice martin'
nom.upcase # 'ALICE MARTIN'
nom.capitalize # 'Alice martin'
nom.length # 12
nom.include?('alice') # true
nom.gsub('alice', 'bob') # 'bob martin'
nom.split(' ') # ['alice', 'martin']
nom.reverse # 'nitram ecila'
nom.empty? # false
nom.start_with?('alice') # true
Opérations sur les nombres
10 + 3 # 13
10 - 3 # 7
10 * 3 # 30
10 / 3 # 3 (division entière !)
10.0 / 3 # 3.333...
10 % 3 # 1 (modulo)
2 ** 10 # 1024 (puissance)
'42'.to_i # 42
42.to_s # '42'
42.to_f # 42.0
-5.abs # 5
42.even? # true
Structures conditionnelles
age = 25
if age >= 18
puts 'Vous êtes majeur'
elsif age >= 16
puts 'Presque majeur'
else
puts 'Vous êtes mineur'
end
# Opérateur ternaire
statut = age >= 18 ? 'majeur' : 'mineur'
# unless (inverse de if)
unless age >= 18
puts 'Accès refusé'
end
# Condition postfix
puts 'Bienvenue !' if age >= 18
# case / when
role = :admin
case role
when :admin then puts 'Accès total'
when :editor then puts 'Accès écriture'
when :viewer then puts 'Accès lecture'
else puts 'Rôle inconnu'
end
Boucles
# while
compteur = 0
while compteur < 5
puts "Tour #{compteur}"
compteur += 1
end
# times (la manière Ruby)
5.times do |i|
puts "Itération #{i}"
end
# upto / downto
1.upto(5) { |i| puts i }
5.downto(1) { |i| puts i }
# loop avec break
loop do
puts 'Entrez votre nom :'
nom = gets.chomp
break if nom.length >= 2
puts 'Nom trop court !'
end
Arrays (tableaux)
fruits = ['pomme', 'banane', 'cerise']
nombres = [1, 2, 3, 4, 5]
fruits[0] # 'pomme'
fruits[-1] # 'cerise'
fruits << 'mangue'
fruits.push('kiwi')
fruits.delete('banane')
fruits.each { |fruit| puts fruit }
nombres.map { |n| n * 2 } # [2, 4, 6, 8, 10]
nombres.select { |n| n.even? } # [2, 4]
nombres.reject { |n| n > 3 } # [1, 2, 3]
nombres.reduce(:+) # 15
nombres.min # 1
nombres.max # 5
Section 13.1.4 : Ruby — Hash, méthodes et blocs
🎯 Objectif pédagogique
Maîtriser les Hash (dictionnaires), écrire des méthodes Ruby propres et comprendre les blocs — le mécanisme le plus élégant et distinctif du langage Ruby.
Hash : les dictionnaires Ruby
Un Hash est une collection de paires clé-valeur. Contrairement à un Array indexé par position, un Hash permet d'accéder aux valeurs par une clé nommée.
# Syntaxe moderne (clés symboles - PRÉFÉRÉ)
personne = { nom: 'Alice', age: 28, ville: 'Paris' }
personne[:nom] # 'Alice'
personne[:age] # 28
# Ajouter / Modifier
personne[:email] = 'alice@example.com'
personne[:age] = 29
# Supprimer
personne.delete(:ville)
# Vérifications
personne.key?(:nom) # true
personne.empty? # false
personne.length # 3
Symboles vs Strings comme clés
Utilisez toujours des symboles (:nom) comme clés de Hash. Les symboles sont plus rapides (mêmes objets en mémoire) et plus idiomatiques en Ruby. La syntaxe nom: 'valeur' est du sucre syntaxique pour :nom => 'valeur'.
Itération sur les Hash
personne = { nom: 'Alice', age: 28, email: 'alice@example.com' }
personne.each do |cle, valeur|
puts "#{cle}: #{valeur}"
end
personne.keys # [:nom, :age, :email]
personne.values # ['Alice', 28, 'alice@example.com']
personne.map { |k, v| "#{k}=#{v}" }
personne.select { |k, v| v.is_a?(String) }
Hash imbriqués
utilisateurs = {
alice: {
nom: 'Alice Martin',
age: 28,
competences: ['Ruby', 'JavaScript', 'SQL'],
adresse: { ville: 'Paris', cp: '75001' }
},
bob: {
nom: 'Bob Dupont',
age: 32,
competences: ['Python', 'Docker', 'AWS'],
adresse: { ville: 'Lyon', cp: '69001' }
}
}
utilisateurs[:alice][:nom] # 'Alice Martin'
utilisateurs[:alice][:adresse][:ville] # 'Paris'
utilisateurs.dig(:alice, :adresse, :ville) # 'Paris' (accès sécurisé)
utilisateurs.dig(:charlie, :nom) # nil (pas d erreur)
Les méthodes
En Ruby, les méthodes retournent implicitement la dernière expression évaluée — pas besoin de return.
def saluer(prenom, langue = 'fr')
case langue
when 'fr' then "Bonjour, #{prenom} !"
when 'en' then "Hello, #{prenom}!"
when 'es' then "Hola, #{prenom}!"
else "Hi, #{prenom}!"
end
end
saluer('Alice') # Bonjour, Alice !
saluer('Bob', 'en') # Hello, Bob!
# Keyword arguments
def creer_utilisateur(nom:, email:, role: :viewer)
{ nom: nom, email: email, role: role }
end
creer_utilisateur(nom: 'Alice', email: 'alice@test.com')
# Méthodes prédicats (?), destructives (!)
def majeur?(age)
age >= 18
end
Les blocs : la signature de Ruby
Les blocs sont des morceaux de code anonymes passés à une méthode. Deux syntaxes :
# do...end (multi-ligne)
[1, 2, 3].each do |nombre|
puts nombre * 2
end
# { } (une ligne)
[1, 2, 3].each { |nombre| puts nombre * 2 }
Créer ses propres méthodes avec yield
def avec_timing
debut = Time.now
yield
duree = Time.now - debut
puts "Exécuté en #{duree.round(3)} secondes"
end
avec_timing do
sleep(1)
puts 'Travail en cours...'
end
Procs et Lambdas
carre = Proc.new { |n| n ** 2 }
carre.call(5) # 25
[1, 2, 3].map(&carre) # [1, 4, 9]
double = ->(n) { n * 2 }
double.call(5) # 10
[1, 2, 3].map(&double) # [2, 4, 6]
Section 13.1.5 : Ruby — Classes et TDD avec Rake
🎯 Objectif pédagogique
Comprendre la programmation orientée objet en Ruby via les classes, et adopter le TDD (Test-Driven Development) avec Rake pour écrire du code fiable dès le départ.
Les classes : modéliser le monde réel
La programmation orientée objet (OOP) modélise votre programme comme un ensemble d'objets qui interagissent. Une classe est le plan de construction, un objet est une instance concrète.
class Utilisateur
attr_accessor :nom, :email
attr_reader :id
def initialize(nom, email)
@id = rand(1000..9999)
@nom = nom
@email = email
@date_inscription = Time.now
end
def presentation
"#{@nom} (#{@email}) - inscrit le #{@date_inscription.strftime('%d/%m/%Y')}"
end
def email_valide?
@email.include?('@') && @email.include?('.')
end
def to_s
presentation
end
end
alice = Utilisateur.new('Alice Martin', 'alice@example.com')
puts alice.nom # Alice Martin
puts alice.email_valide? # true
Variables d'instance vs de classe
class Compteur
@@total = 0 # Variable de classe (partagée)
def initialize(nom)
@nom = nom # Variable d instance (unique)
@@total += 1
end
def self.total
@@total
end
end
Compteur.new('A')
Compteur.new('B')
Compteur.total # 2
Encapsulation : public, private, protected
class CompteBancaire
def initialize(titulaire, solde)
@titulaire = titulaire
@solde = solde
end
def deposer(montant)
raise ArgumentError, 'Montant invalide' unless montant > 0
@solde += montant
historiser("Dépôt de #{montant} EUR")
end
def retirer(montant)
raise ArgumentError, 'Montant invalide' unless montant > 0
raise 'Solde insuffisant' unless solde_suffisant?(montant)
@solde -= montant
end
def afficher_solde
"Solde de #{@titulaire}: #{@solde} EUR"
end
private
def solde_suffisant?(montant)
@solde >= montant
end
def historiser(operation)
puts "[#{Time.now}] #{operation}"
end
end
TDD : Test-Driven Development
Le TDD suit un cycle en trois étapes : Red (test qui échoue), Green (code minimal), Refactor (améliorer).
Minitest en pratique
# test/calculatrice_test.rb
require 'minitest/autorun'
require_relative '../lib/calculatrice'
class CalculatriceTest < Minitest::Test
def setup
@calc = Calculatrice.new
end
def test_addition
assert_equal 5, @calc.additionner(2, 3)
end
def test_division_par_zero
assert_raises(ZeroDivisionError) do
@calc.diviser(10, 0)
end
end
end
# lib/calculatrice.rb
class Calculatrice
def additionner(a, b)
a + b
end
def diviser(a, b)
raise ZeroDivisionError if b.zero?
a / b
end
end
Rake : automatiser les tâches
# Rakefile
require 'rake/testtask'
Rake::TestTask.new do |t|
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
task default: :test
rake # Lance tous les tests
rake test # Idem, explicitement
Exercice : classe Restaurant
class Restaurant
attr_reader :nom, :ville
def initialize(nom, ville, cuisine)
@nom = nom
@ville = ville
@cuisine = cuisine
@notes = []
end
def noter(note)
raise ArgumentError unless (1..5).include?(note)
@notes << note
end
def note_moyenne
return 0 if @notes.empty?
(@notes.sum.to_f / @notes.length).round(1)
end
def populaire?
note_moyenne >= 4.0 && @notes.length >= 5
end
def to_s
"#{@nom} (#{@ville}) - #{@cuisine} - #{note_moyenne}/5"
end
end
Section 13.2.1 : Héritage et modules Ruby
🎯 Objectif pédagogique
Comprendre l'héritage de classes et les modules Mixin en Ruby pour structurer du code réutilisable et bien organisé. Ces concepts sont fondamentaux pour comprendre Ruby on Rails.
L'héritage : spécialiser des classes
L'héritage permet de créer une classe enfant qui hérite de toutes les propriétés et méthodes d'une classe parent, tout en ajoutant ses spécificités. En Ruby, chaque classe ne peut hériter que d'une seule classe parent (héritage simple).
class Animal
attr_reader :nom, :age
def initialize(nom, age)
@nom = nom
@age = age
end
def parler
'...'
end
def presenter
"Je suis #{@nom}, j'ai #{@age} ans"
end
end
class Chien < Animal
def parler
'Woof!'
end
def fetch(objet)
"#{@nom} rapporte #{objet}"
end
end
class Chat < Animal
def parler
'Miaou!'
end
def ronronner
"#{@nom} ronronne..."
end
end
rex = Chien.new('Rex', 3)
puts rex.presenter # Hérité du parent
puts rex.parler # Surchargé
puts rex.fetch('balle') # Spécifique au Chien
Le mot-clé super
super appelle la méthode du même nom dans la classe parent :
class Vehicule
attr_reader :marque, :modele
def initialize(marque, modele)
@marque = marque
@modele = modele
@kilometrage = 0
end
def info
"#{@marque} #{@modele} - #{@kilometrage} km"
end
end
class VehiculeElectrique < Vehicule
def initialize(marque, modele, autonomie)
super(marque, modele)
@autonomie = autonomie
@batterie = 100
end
def info
"#{super} | Batterie: #{@batterie}% | Autonomie: #{@autonomie} km"
end
end
tesla = VehiculeElectrique.new('Tesla', 'Model 3', 560)
puts tesla.info
Les modules : Mixins pour la réutilisation
Ruby n'a pas d'héritage multiple, mais les modules (Mixins) permettent de partager du comportement entre classes non liées.
module Affichable
def afficher
puts to_s
end
end
module Validable
def valide?
erreurs.empty?
end
def erreurs
[]
end
end
class Produit
include Affichable
include Validable
attr_accessor :nom, :prix
def initialize(nom, prix)
@nom = nom
@prix = prix
end
def to_s
"#{@nom} - #{@prix} EUR"
end
def erreurs
errs = []
errs << 'Nom requis' if @nom.nil? || @nom.empty?
errs << 'Prix positif requis' if @prix.nil? || @prix <= 0
errs
end
end
p = Produit.new('MacBook Pro', 2399)
p.afficher # De Affichable
p.valide? # De Validable
Comparable et Enumerable
class Etudiant
include Comparable
attr_accessor :nom, :moyenne
def initialize(nom, moyenne)
@nom = nom
@moyenne = moyenne
end
def <=>(other)
@moyenne <=> other.moyenne
end
end
etudiants = [
Etudiant.new('Alice', 16.5),
Etudiant.new('Bob', 14.2),
Etudiant.new('Charlie', 18.0)
]
etudiants.sort.map(&:nom) # ['Bob', 'Alice', 'Charlie']
etudiants.max.nom # 'Charlie'
Section 13.2.2 : Design patterns fondamentaux
🎯 Objectif pédagogique
Découvrir les design patterns les plus courants et comprendre quand les appliquer. Ces patterns sont des solutions éprouvées à des problèmes récurrents en conception logicielle.
Qu'est-ce qu'un design pattern ?
Un design pattern est une solution réutilisable à un problème courant. Ce sont des schémas de pensée qui guident l'organisation du code, décrits dans le livre « Design Patterns » de la Gang of Four (GoF) en 1994.
Le pattern Iterator
Ruby l'intègre nativement grâce aux blocs et Enumerable :
class Playlist
include Enumerable
def initialize
@chansons = []
end
def ajouter(chanson)
@chansons << chanson
end
def each(&block)
@chansons.each(&block)
end
end
playlist = Playlist.new
playlist.ajouter('Bohemian Rhapsody')
playlist.ajouter('Imagine')
playlist.map(&:upcase)
playlist.count
Le pattern Strategy
Changer le comportement via un lambda ou bloc :
class Trieur
def initialize(strategie = nil)
@strategie = strategie || ->(a, b) { a <=> b }
end
def trier(collection)
collection.sort(&@strategie)
end
end
produits = [
{ nom: 'MacBook', prix: 2399 },
{ nom: 'iPad', prix: 899 },
{ nom: 'iPhone', prix: 1199 }
]
par_prix = ->(a, b) { a[:prix] <=> b[:prix] }
par_nom = ->(a, b) { a[:nom] <=> b[:nom] }
Trieur.new(par_prix).trier(produits)
Trieur.new(par_nom).trier(produits)
Le pattern Observer
Notifier des abonnés quand un événement se produit — Rails l'utilise avec les callbacks :
module Observable
def self.included(base)
base.instance_variable_set(:@observers, [])
base.extend(ClassMethods)
end
module ClassMethods
def add_observer(observer)
@observers << observer
end
def observers
@observers
end
end
def notify_observers(event, data = {})
self.class.observers.each do |obs|
obs.update(event, data) if obs.respond_to?(:update)
end
end
end
Le pattern Template Method
Définir le squelette d'un algorithme dans le parent, laisser les sous-classes redéfinir certaines étapes :
class DataExporter
def export(data)
header = generate_header
body = generate_body(data)
footer = generate_footer
"#{header}\n#{body}\n#{footer}"
end
private
def generate_header
raise NotImplementedError
end
def generate_body(data)
raise NotImplementedError
end
def generate_footer
'--- Fin ---'
end
end
class CSVExporter < DataExporter
def generate_header
'nom,email,age'
end
def generate_body(data)
data.map { |row| row.values.join(',') }.join("\n")
end
end
Section 13.2.3 : Architecture MVC
🎯 Objectif pédagogique
Comprendre le pattern MVC qui structure toute application Rails, et l'implémenter en Ruby pur avant de passer au framework.
MVC : séparer les responsabilités
- →Model : données et logique métier
- →View : présentation et affichage
- →Controller : orchestration entre Model et View
MVC en Ruby pur
# Model
class Recette
attr_accessor :nom, :description
attr_reader :id
@@toutes = []
@@prochain_id = 1
def initialize(attrs = {})
@id = @@prochain_id
@@prochain_id += 1
@nom = attrs[:nom]
@description = attrs[:description]
@@toutes << self
end
def self.all
@@toutes
end
def self.find(id)
@@toutes.find { |r| r.id == id }
end
def to_s
"#{@nom} - #{@description}"
end
end
# View
class RecetteView
def afficher_liste(recettes)
puts "\n--- Mes Recettes ---"
recettes.each_with_index do |r, i|
puts "#{i + 1}. #{r}"
end
end
def demander_nom
print 'Nom : '
gets.chomp
end
end
# Controller
class RecetteController
def initialize
@view = RecetteView.new
end
def liste
@view.afficher_liste(Recette.all)
end
def creer
nom = @view.demander_nom
Recette.new(nom: nom)
end
end
# Router
class Router
def initialize(controller)
@controller = controller
end
def run
loop do
puts "\n1. Lister 2. Ajouter 3. Quitter"
choix = gets.chomp.to_i
case choix
when 1 then @controller.liste
when 2 then @controller.creer
when 3 then break
end
end
end
end
Section 13.2.4 : Parsing et consommation d'API
🎯 Objectif pédagogique
Lire des fichiers structurés (CSV, JSON) et consommer des APIs REST en Ruby — compétences essentielles pour des applications connectées.
Parsing CSV
require 'csv'
CSV.foreach('clients.csv', headers: true) do |row|
puts "#{row['nom']} - #{row['email']}"
end
# Écrire un CSV
CSV.open('export.csv', 'w') do |csv|
csv << ['nom', 'email', 'age']
csv << ['Alice', 'alice@test.com', 28]
csv << ['Bob', 'bob@test.com', 32]
end
Parsing JSON
require 'json'
json_string = '{"nom": "Alice", "age": 28}'
data = JSON.parse(json_string)
data['nom']
# Avec clés symboliques
data = JSON.parse(json_string, symbolize_names: true)
data[:nom]
# Générer du JSON
utilisateur = { nom: 'Alice', age: 28 }
JSON.generate(utilisateur)
JSON.pretty_generate(utilisateur)
Consommer une API REST
require 'open-uri'
require 'json'
url = 'https://api.github.com/users/lewagon'
response = URI.open(url).read
user = JSON.parse(response)
puts user['name']
puts user['public_repos']
La gem rest-client
require 'rest-client'
require 'json'
# GET
response = RestClient.get('https://api.example.com/users')
users = JSON.parse(response.body)
# POST
response = RestClient.post(
'https://api.example.com/users',
{ nom: 'Alice', email: 'alice@test.com' }.to_json,
{ content_type: :json, accept: :json }
)
# Gestion d erreurs
begin
RestClient.get('https://api.example.com/unknown')
rescue RestClient::NotFound
puts '404 - Non trouvé'
rescue RestClient::ExceptionWithResponse => e
puts "Erreur: #{e.response.code}"
end
Exercice : CLI météo
require 'open-uri'
require 'json'
class MeteoService
BASE_URL = 'https://api.open-meteo.com/v1/forecast'
def temperature(lat, lon)
url = "#{BASE_URL}?latitude=#{lat}&longitude=#{lon}¤t_weather=true"
data = JSON.parse(URI.open(url).read)
data['current_weather']['temperature']
end
end
service = MeteoService.new
temp = service.temperature(48.8566, 2.3522)
puts "Paris : #{temp} C"
Section 13.2.5 : Livecodes et challenges
🎯 Objectif pédagogique
S'entraîner à résoudre des problèmes algorithmiques en Ruby dans un format livecode. Ces exercices développent la pensée logique et préparent aux entretiens techniques.
La méthodologie UPER
- →Understand : reformulez le problème, identifiez inputs/outputs/cas limites
- →Plan : pseudo-code en français
- →Execute : traduire en Ruby
- →Review : tester et optimiser
Challenge : Palindrome
def palindrome?(mot)
cleaned = mot.downcase.gsub(/[^a-z0-9]/, '')
cleaned == cleaned.reverse
end
puts palindrome?('kayak') # true
puts palindrome?('hello') # false
Challenge : Anagrammes
def anagrammes?(mot1, mot2)
normaliser = ->(m) { m.downcase.delete(' ').chars.sort }
normaliser.call(mot1) == normaliser.call(mot2)
end
puts anagrammes?('listen', 'silent') # true
Challenge : Chiffre de César
def caesar_cipher(text, shift)
text.chars.map do |char|
if char.match?(/[a-z]/)
((char.ord - 97 + shift) % 26 + 97).chr
elsif char.match?(/[A-Z]/)
((char.ord - 65 + shift) % 26 + 65).chr
else
char
end
end.join
end
puts caesar_cipher('Hello World', 3) # Khoor Zruog
Challenge : Mot le plus fréquent
def mot_frequent(texte)
mots = texte.downcase.scan(/\w+/)
frequences = mots.tally
frequences.max_by { |_mot, count| count }
end
mot, count = mot_frequent("le chat mange le poisson et le chat dort")
puts "#{mot} apparaît #{count} fois"
Section 13.3.1 : SQL fondamental et schema design
🎯 Objectif pédagogique
Maîtriser les fondamentaux de SQL pour créer, interroger et manipuler des bases de données relationnelles. Comprendre la conception de schémas avant de passer à Active Record.
Pourquoi SQL est incontournable
SQL (Structured Query Language) est le langage universel des bases de données relationnelles. Créé en 1970 par IBM, il a survécu à toutes les modes technologiques. Que vous utilisiez PostgreSQL, MySQL, SQLite ou BigQuery, SQL reste la compétence la plus demandée pour tout développeur.
Les bases : CREATE, INSERT, SELECT
-- Créer une table
CREATE TABLE utilisateurs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
age INTEGER,
ville VARCHAR(100) DEFAULT 'Paris',
date_inscription DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Insérer des données
INSERT INTO utilisateurs (nom, email, age, ville)
VALUES ('Alice Martin', 'alice@example.com', 28, 'Paris');
INSERT INTO utilisateurs (nom, email, age, ville)
VALUES ('Bob Dupont', 'bob@example.com', 32, 'Lyon');
INSERT INTO utilisateurs (nom, email, age, ville)
VALUES ('Charlie Petit', 'charlie@example.com', 25, 'Paris');
-- Lire des données
SELECT * FROM utilisateurs;
SELECT nom, email FROM utilisateurs;
SELECT nom, age FROM utilisateurs WHERE ville = 'Paris';
SELECT * FROM utilisateurs WHERE age >= 28 ORDER BY age DESC;
SELECT * FROM utilisateurs LIMIT 10 OFFSET 20; -- Pagination
WHERE : filtrer les données
-- Comparaisons
SELECT * FROM utilisateurs WHERE age > 25;
SELECT * FROM utilisateurs WHERE ville = 'Paris';
SELECT * FROM utilisateurs WHERE nom LIKE 'A%'; -- Commence par A
SELECT * FROM utilisateurs WHERE email LIKE '%@gmail.com';
-- Opérateurs logiques
SELECT * FROM utilisateurs WHERE age > 25 AND ville = 'Paris';
SELECT * FROM utilisateurs WHERE ville = 'Paris' OR ville = 'Lyon';
SELECT * FROM utilisateurs WHERE ville IN ('Paris', 'Lyon', 'Marseille');
SELECT * FROM utilisateurs WHERE age BETWEEN 25 AND 35;
SELECT * FROM utilisateurs WHERE email IS NOT NULL;
Agrégation : GROUP BY et fonctions
-- Fonctions d agrégation
SELECT COUNT(*) FROM utilisateurs; -- Nombre total
SELECT AVG(age) FROM utilisateurs; -- Âge moyen
SELECT MIN(age), MAX(age) FROM utilisateurs; -- Min et Max
SELECT SUM(age) FROM utilisateurs WHERE ville = 'Paris';
-- GROUP BY
SELECT ville, COUNT(*) as nb_utilisateurs
FROM utilisateurs
GROUP BY ville
ORDER BY nb_utilisateurs DESC;
-- HAVING (filtre sur les groupes)
SELECT ville, COUNT(*) as nb
FROM utilisateurs
GROUP BY ville
HAVING nb >= 2;
Schema design : relations entre tables
-- One-to-Many : un utilisateur a plusieurs articles
CREATE TABLE articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titre VARCHAR(255) NOT NULL,
contenu TEXT,
utilisateur_id INTEGER NOT NULL,
date_publication DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (utilisateur_id) REFERENCES utilisateurs(id)
);
-- Many-to-Many : articles et tags (via table de jonction)
CREATE TABLE tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE articles_tags (
article_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (article_id, tag_id),
FOREIGN KEY (article_id) REFERENCES articles(id),
FOREIGN KEY (tag_id) REFERENCES tags(id)
);
JOIN : combiner les tables
-- INNER JOIN : données qui matchent dans les deux tables
SELECT utilisateurs.nom, articles.titre
FROM articles
INNER JOIN utilisateurs ON articles.utilisateur_id = utilisateurs.id;
-- LEFT JOIN : tous les utilisateurs, même sans articles
SELECT utilisateurs.nom, COUNT(articles.id) as nb_articles
FROM utilisateurs
LEFT JOIN articles ON articles.utilisateur_id = utilisateurs.id
GROUP BY utilisateurs.id;
-- Requête complexe
SELECT
u.nom,
COUNT(a.id) as nb_articles,
AVG(LENGTH(a.contenu)) as longueur_moyenne
FROM utilisateurs u
LEFT JOIN articles a ON a.utilisateur_id = u.id
WHERE u.ville = 'Paris'
GROUP BY u.id
HAVING nb_articles > 0
ORDER BY nb_articles DESC
LIMIT 5;
Section 13.3.2 : Migrations et Active Record ORM
🎯 Objectif pédagogique
Comprendre Active Record, l'ORM de Rails qui transforme vos tables SQL en classes Ruby. Apprendre à créer et gérer des migrations pour faire évoluer votre schéma de base de données.
Qu'est-ce qu'un ORM ?
Un ORM (Object-Relational Mapping) fait le pont entre le monde objet (Ruby) et le monde relationnel (SQL). Au lieu d'écrire du SQL brut, vous manipulez des objets Ruby qui correspondent à des tables de la base de données.
# SANS ORM (SQL brut)
db.execute("SELECT * FROM users WHERE age > 25 ORDER BY name")
# AVEC Active Record (Ruby)
User.where('age > ?', 25).order(:name)
# Les deux produisent le même résultat SQL !
Les migrations : versionner votre base
Les migrations sont des fichiers Ruby qui décrivent les modifications de votre schéma de base. Elles permettent de versionner la structure de la DB comme vous versionnez votre code avec Git.
# Générer une migration
rails generate migration CreateUsers nom:string email:string age:integer
# Générer un model (crée le model ET la migration)
rails generate model User nom:string email:string age:integer
# db/migrate/20240315_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
t.string :nom, null: false
t.string :email, null: false
t.integer :age
t.string :ville, default: 'Paris'
t.timestamps # created_at et updated_at automatiques
end
add_index :users, :email, unique: true
end
end
# Exécuter les migrations
rails db:migrate
# Annuler la dernière migration
rails db:rollback
# Voir le statut
rails db:migrate:status
Ajouter/modifier des colonnes
# Ajouter une colonne
class AddBioToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :bio, :text
add_column :users, :avatar_url, :string
end
end
# Supprimer une colonne
class RemoveVilleFromUsers < ActiveRecord::Migration[7.1]
def change
remove_column :users, :ville, :string
end
end
# Renommer une colonne
class RenameNomToUsername < ActiveRecord::Migration[7.1]
def change
rename_column :users, :nom, :username
end
end
CRUD avec Active Record
# CREATE
user = User.new(nom: 'Alice', email: 'alice@test.com', age: 28)
user.save
# Raccourci
user = User.create(nom: 'Bob', email: 'bob@test.com', age: 32)
# READ
User.all # Tous les utilisateurs
User.find(1) # Par ID (lève une erreur si absent)
User.find_by(email: 'alice@test.com') # Par attribut (nil si absent)
User.where(ville: 'Paris') # Collection filtrée
User.where('age > ?', 25) # Avec condition SQL
User.order(age: :desc) # Tri
User.limit(10).offset(20) # Pagination
User.count # Nombre total
User.first # Premier
User.last # Dernier
# UPDATE
user = User.find(1)
user.update(age: 29)
# ou
user.nom = 'Alice Martin'
user.save
# DELETE
user.destroy
User.destroy_all # Tout supprimer
Requêtes avancées Active Record
# Chaîner les requêtes
User.where(ville: 'Paris')
.where('age > ?', 25)
.order(nom: :asc)
.limit(10)
# Agrégation
User.average(:age) # Âge moyen
User.maximum(:age) # Âge max
User.minimum(:age) # Âge min
User.count # Nombre total
User.group(:ville).count # Compter par ville
# Pluck (récupérer une seule colonne)
User.pluck(:email) # ['alice@test.com', 'bob@test.com']
# Exists?
User.exists?(email: 'alice@test.com') # true
Section 13.3.3 : Associations et validations
🎯 Objectif pédagogique
Maîtriser les associations Active Record (has_many, belongs_to, has_many through) et les validations pour garantir l'intégrité des données.
Les associations : modéliser les relations
Active Record traduit les relations SQL en méthodes Ruby élégantes. Les trois associations les plus courantes :
belongs_to et has_many (One-to-Many)
# Un article appartient à un utilisateur
# Un utilisateur a plusieurs articles
class User < ApplicationRecord
has_many :articles, dependent: :destroy
end
class Article < ApplicationRecord
belongs_to :user
end
# Utilisation
alice = User.find(1)
alice.articles # Tous les articles d Alice
alice.articles.count # Nombre d articles
alice.articles.create(titre: 'Mon premier article')
article = Article.find(1)
article.user # L auteur de l article
article.user.nom # Le nom de l auteur
has_many :through (Many-to-Many)
# Un article a plusieurs tags
# Un tag appartient à plusieurs articles
class Article < ApplicationRecord
has_many :article_tags, dependent: :destroy
has_many :tags, through: :article_tags
end
class Tag < ApplicationRecord
has_many :article_tags, dependent: :destroy
has_many :articles, through: :article_tags
end
class ArticleTag < ApplicationRecord
belongs_to :article
belongs_to :tag
end
# Utilisation
article = Article.find(1)
article.tags # Tous les tags
article.tags << Tag.find_by(nom: 'Ruby') # Ajouter un tag
tag = Tag.find_by(nom: 'Rails')
tag.articles # Tous les articles avec ce tag
has_one
class User < ApplicationRecord
has_one :profil, dependent: :destroy
end
class Profil < ApplicationRecord
belongs_to :user
end
user = User.find(1)
user.profil # Le profil unique
user.create_profil(bio: 'Développeuse Ruby')
Les validations : sécuriser les données
Les validations empêchent les données invalides d'être sauvegardées en base. Elles se déclarent dans le model.
class User < ApplicationRecord
validates :nom, presence: true, length: { minimum: 2, maximum: 50 }
validates :email, presence: true, uniqueness: true,
format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
validates :age, numericality: { greater_than: 0, less_than: 150 }, allow_nil: true
validates :role, inclusion: { in: %w[admin editor viewer] }
end
# Tester les validations
user = User.new(nom: '', email: 'invalid')
user.valid? # false
user.errors.full_messages
# ["Nom can't be blank", "Email is invalid"]
user.save # false (ne sauvegarde pas)
user.save! # ActiveRecord::RecordInvalid (lève une exception)
# Validations personnalisées
class Article < ApplicationRecord
validate :date_future
private
def date_future
if date_publication.present? && date_publication < Date.today
errors.add(:date_publication, 'doit être dans le futur')
end
end
end
Callbacks : hooks du cycle de vie
class User < ApplicationRecord
before_validation :normaliser_email
after_create :envoyer_email_bienvenue
before_destroy :archiver_donnees
private
def normaliser_email
self.email = email.downcase.strip if email.present?
end
def envoyer_email_bienvenue
UserMailer.welcome(self).deliver_later
end
def archiver_donnees
ArchiveService.archiver(self)
end
end
Section 13.3.4 : Requêtes avancées et optimisation
🎯 Objectif pédagogique
Maîtriser les requêtes SQL avancées (subqueries, window functions) et comprendre l'optimisation des performances avec Active Record (N+1, eager loading, indices).
Le problème N+1 : l'ennemi des performances
Le problème N+1 est le piège le plus courant en Active Record. Il se produit quand vous exécutez 1 requête pour récupérer N objets, puis N requêtes supplémentaires pour leurs associations.
# BAD : N+1 queries (1 + 10 = 11 requêtes)
articles = Article.limit(10)
articles.each do |article|
puts article.user.nom # Chaque appel fait une requête SQL !
end
# GOOD : Eager loading (2 requêtes seulement)
articles = Article.includes(:user).limit(10)
articles.each do |article|
puts article.user.nom # Déjà chargé en mémoire
end
Eager loading : includes, preload, eager_load
# includes : utilise preload ou eager_load selon le cas
Article.includes(:user, :tags).where(published: true)
# preload : 2+ requêtes séparées
Article.preload(:user).limit(10)
# SELECT * FROM articles LIMIT 10
# SELECT * FROM users WHERE id IN (1, 2, 3, ...)
# eager_load : LEFT OUTER JOIN (une seule requête)
Article.eager_load(:user).where(users: { ville: 'Paris' })
# SELECT articles.*, users.* FROM articles
# LEFT OUTER JOIN users ON users.id = articles.user_id
# WHERE users.ville = 'Paris'
Scopes : requêtes réutilisables
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
scope :by_city, ->(city) { joins(:user).where(users: { ville: city }) }
scope :popular, -> { where('views_count > ?', 100) }
# Equivalent à des méthodes de classe
def self.trending
published.recent.popular.limit(10)
end
end
# Chaînable
Article.published.recent.limit(5)
Article.by_city('Paris').published
Article.trending
Indices : accélérer les requêtes
Un index est comme l'index d'un livre — il permet à la base de trouver rapidement une ligne sans scanner toute la table.
class AddIndicesToArticles < ActiveRecord::Migration[7.1]
def change
add_index :articles, :user_id # Foreign key
add_index :articles, :published # Colonnes fréquemment filtrées
add_index :articles, :created_at # Colonnes triées
add_index :articles, [:user_id, :published] # Index composite
add_index :users, :email, unique: true # Index unique
end
end
Subqueries et SQL avancé
# Subquery : trouver les users qui ont au moins un article
User.where(id: Article.select(:user_id).distinct)
# SQL brut quand nécessaire
User.find_by_sql("SELECT users.*, COUNT(articles.id) as articles_count
FROM users
LEFT JOIN articles ON articles.user_id = users.id
GROUP BY users.id
HAVING COUNT(articles.id) > 5")
# select pour ajouter des colonnes calculées
User.select('users.*, COUNT(articles.id) as articles_count')
.joins(:articles)
.group('users.id')
.order('articles_count DESC')
Section 13.4.1 : HTML/CSS avancé et Bootstrap
🎯 Objectif pédagogique
Maîtriser HTML sémantique, CSS avancé (Flexbox, Grid) et Bootstrap pour construire des interfaces web responsive et professionnelles rapidement.
HTML sémantique
L'HTML sémantique utilise des balises qui décrivent le sens du contenu, pas juste son apparence. Cela améliore l'accessibilité, le SEO et la maintenabilité du code.
<!-- NON sémantique -->
<div class="header">
<div class="nav">...</div>
</div>
<div class="main">
<div class="article">...</div>
<div class="sidebar">...</div>
</div>
<div class="footer">...</div>
<!-- Sémantique -->
<header>
<nav>...</nav>
</header>
<main>
<article>...</article>
<aside>...</aside>
</main>
<footer>...</footer>
Flexbox : aligner en une dimension
Flexbox gère l'alignement sur un axe (horizontal OU vertical) :
.container {
display: flex;
justify-content: space-between; /* Axe principal */
align-items: center; /* Axe croisé */
gap: 16px; /* Espacement */
}
/* Direction */
.container { flex-direction: row; } /* Défaut : horizontal */
.container { flex-direction: column; } /* Vertical */
/* Wrap */
.container { flex-wrap: wrap; } /* Retour à la ligne */
/* Flex sur les enfants */
.item { flex: 1; } /* Prendre l espace disponible */
.item { flex: 0 0 200px; } /* Taille fixe 200px, pas de grow */
CSS Grid : layouts en deux dimensions
Grid gère les layouts en lignes ET colonnes simultanément :
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 3 colonnes égales */
grid-template-rows: auto;
gap: 24px;
}
/* Responsive */
.grid {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
/* Placement explicite */
.hero {
grid-column: 1 / -1; /* Toute la largeur */
}
.sidebar {
grid-row: 2 / 4; /* Span 2 lignes */
}
Bootstrap : prototyper rapidement
Bootstrap est le framework CSS le plus populaire. Il fournit un système de grille, des composants prêts à l'emploi et des utilitaires CSS qui permettent de construire une interface en quelques minutes.
<!-- Grille responsive Bootstrap -->
<div class="container">
<div class="row">
<div class="col-12 col-md-8">
<h1>Contenu principal</h1>
</div>
<div class="col-12 col-md-4">
<h2>Sidebar</h2>
</div>
</div>
</div>
<!-- Card Bootstrap -->
<div class="card" style="width: 18rem;">
<img src="image.jpg" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Titre</h5>
<p class="card-text">Description...</p>
<a href="#" class="btn btn-primary">Action</a>
</div>
</div>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="#">MonApp</a>
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" href="#">Accueil</a>
</li>
</ul>
</div>
</div>
</nav>
Section 13.4.2 : JavaScript ES6+ fondamentaux
🎯 Objectif pédagogique
Maîtriser les fondamentaux de JavaScript moderne (ES6+) : variables, fonctions fléchées, template literals, destructuring, modules, et les méthodes d'array qui sont le cœur du JS quotidien.
JavaScript : le langage du web
JavaScript est le seul langage natif des navigateurs web. Il s'exécute côté client (navigateur) et, depuis Node.js, côté serveur. C'est le langage le plus utilisé au monde selon le Stack Overflow Developer Survey depuis 11 années consécutives.
Variables : let, const, var
// const : valeur qui ne change pas (PRÉFÉRÉ)
const API_URL = 'https://api.example.com';
const MAX_ITEMS = 100;
// let : valeur qui peut changer
let compteur = 0;
compteur += 1;
// var : ancien mot-clé (ÉVITER)
// Portée fonction au lieu de bloc, source de bugs
Types de données
// Primitifs
const nom = 'Alice'; // String
const age = 28; // Number
const majeur = true; // Boolean
const rien = null; // Null
const indefini = undefined; // Undefined
// Objets
const personne = { nom: 'Alice', age: 28 };
const fruits = ['pomme', 'banane', 'cerise'];
// typeof
typeof nom // 'string'
typeof age // 'number'
typeof majeur // 'boolean'
typeof personne // 'object'
typeof fruits // 'object' (les arrays sont des objets)
Template literals
const prenom = 'Alice';
const age = 28;
// Ancien style (concaténation)
const msg1 = 'Bonjour, ' + prenom + ' ! Tu as ' + age + ' ans.';
// Template literal (ES6) — PRÉFÉRÉ
const msg2 = `Bonjour, ${prenom} ! Tu as ${age} ans.`;
// Multi-ligne possible
const html = `
<div class="card">
<h2>${prenom}</h2>
<p>Âge : ${age}</p>
</div>
`;
Arrow functions
// Fonction classique
function addition(a, b) {
return a + b;
}
// Arrow function (ES6)
const addition = (a, b) => a + b;
// Avec un seul paramètre, pas besoin de parenthèses
const double = n => n * 2;
// Avec corps multi-ligne, return explicite requis
const saluer = (nom) => {
const message = `Bonjour, ${nom} !`;
return message;
};
Destructuring
// Destructuring d objet
const personne = { nom: 'Alice', age: 28, ville: 'Paris' };
const { nom, age, ville } = personne;
console.log(nom); // 'Alice'
// Avec renommage
const { nom: prenom, age: annees } = personne;
// Destructuring d array
const couleurs = ['rouge', 'vert', 'bleu'];
const [premiere, deuxieme, troisieme] = couleurs;
// Spread operator
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5]
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 }
Méthodes d'array (le cœur de JS)
const nombres = [1, 2, 3, 4, 5];
// map : transformer chaque élément
nombres.map(n => n * 2); // [2, 4, 6, 8, 10]
// filter : garder les éléments qui passent un test
nombres.filter(n => n > 3); // [4, 5]
// reduce : réduire à une seule valeur
nombres.reduce((acc, n) => acc + n, 0); // 15
// find : premier élément qui passe le test
nombres.find(n => n > 3); // 4
// some / every
nombres.some(n => n > 4); // true
nombres.every(n => n > 0); // true
// forEach : exécuter sans retour
nombres.forEach(n => console.log(n));
// Chaînage
const resultat = nombres
.filter(n => n > 2)
.map(n => n * 10)
.reduce((acc, n) => acc + n, 0); // 120
Section 13.4.3 : DOM manipulation et AJAX
🎯 Objectif pédagogique
Comprendre le DOM (Document Object Model), manipuler dynamiquement les pages web avec JavaScript, et communiquer avec un serveur via AJAX (fetch API).
Le DOM : l'arbre HTML vivant
Le DOM est la représentation en mémoire de votre page HTML sous forme d'arbre d'objets. JavaScript peut lire et modifier cet arbre en temps réel, ce qui permet de créer des interfaces dynamiques sans recharger la page.
Sélectionner des éléments
// Par ID (un seul élément)
const header = document.getElementById('main-header');
// Par sélecteur CSS (premier match)
const btn = document.querySelector('.btn-primary');
const nav = document.querySelector('nav.main-nav');
// Par sélecteur CSS (tous les matchs)
const items = document.querySelectorAll('.list-item');
const links = document.querySelectorAll('a[href^="https"]');
// Itérer sur une NodeList
items.forEach(item => {
console.log(item.textContent);
});
Modifier le DOM
// Texte et HTML
element.textContent = 'Nouveau texte';
element.innerHTML = '<strong>HTML</strong> contenu';
// Classes CSS
element.classList.add('active');
element.classList.remove('hidden');
element.classList.toggle('open');
element.classList.contains('active'); // true/false
// Attributs
element.setAttribute('data-id', '42');
element.getAttribute('href');
element.removeAttribute('disabled');
// Styles inline (éviter si possible)
element.style.color = 'red';
element.style.display = 'none';
// Créer et insérer des éléments
const card = document.createElement('div');
card.classList.add('card');
card.innerHTML = `
<h3>${titre}</h3>
<p>${description}</p>
`;
document.querySelector('.container').appendChild(card);
// Supprimer
element.remove();
Les événements
// Click
const btn = document.querySelector('#submit-btn');
btn.addEventListener('click', (event) => {
event.preventDefault(); // Empêcher le comportement par défaut
console.log('Bouton cliqué !');
});
// Clavier
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeModal();
}
});
// Formulaire
const form = document.querySelector('#login-form');
form.addEventListener('submit', (event) => {
event.preventDefault();
const formData = new FormData(form);
const email = formData.get('email');
const password = formData.get('password');
// Envoyer au serveur...
});
// Input en temps réel
const search = document.querySelector('#search-input');
search.addEventListener('input', (event) => {
const query = event.target.value;
filterResults(query);
});
Event Delegation
Au lieu d'attacher un listener à chaque élément d'une liste, attachez-en un seul au parent :
// MAUVAIS : un listener par bouton
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', handleDelete);
});
// BON : event delegation
document.querySelector('.items-list').addEventListener('click', (event) => {
if (event.target.matches('.delete-btn')) {
const itemId = event.target.dataset.id;
deleteItem(itemId);
}
});
Fetch API : AJAX moderne
// GET request
const fetchUsers = async () => {
const response = await fetch('https://api.example.com/users');
const users = await response.json();
return users;
};
// POST request
const createUser = async (userData) => {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
const newUser = await response.json();
return newUser;
};
// Gestion d erreurs
const fetchData = async (url) => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Erreur fetch:', error);
}
};
Exemple complet : recherche dynamique
const searchInput = document.querySelector('#search');
const resultsList = document.querySelector('#results');
searchInput.addEventListener('input', async (event) => {
const query = event.target.value.trim();
if (query.length < 2) {
resultsList.innerHTML = '';
return;
}
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const results = await response.json();
resultsList.innerHTML = results
.map(r => `<li class="result-item">${r.nom}</li>`)
.join('');
});
Section 13.4.4 : Stimulus et animations
🎯 Objectif pédagogique
Maîtriser Stimulus, le framework JavaScript léger de Hotwire/Rails, et ajouter des animations CSS pour des interfaces fluides et réactives.
Stimulus : le JavaScript façon Rails
Stimulus est un framework JavaScript minimaliste créé par l'équipe Basecamp/Rails. Contrairement à React ou Vue qui prennent le contrôle de toute la page, Stimulus augmente le HTML existant avec du comportement JavaScript. C'est le compagnon idéal de Rails.
Les concepts Stimulus
Stimulus repose sur trois concepts : Controllers, Actions, et Targets.
<!-- HTML avec annotations Stimulus -->
<div data-controller="hello">
<input data-hello-target="name" type="text" placeholder="Votre nom">
<button data-action="click->hello#greet">Saluer</button>
<p data-hello-target="output"></p>
</div>
// controllers/hello_controller.js
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['name', 'output']
greet() {
const name = this.nameTarget.value
this.outputTarget.textContent = `Bonjour, ${name} !`
}
}
Controller de toggle (show/hide)
<div data-controller="toggle">
<button data-action="click->toggle#toggle">Menu</button>
<div data-toggle-target="content" class="hidden">
<ul>
<li>Option 1</li>
<li>Option 2</li>
</ul>
</div>
</div>
// controllers/toggle_controller.js
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['content']
toggle() {
this.contentTarget.classList.toggle('hidden')
}
}
Lifecycle callbacks
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
connect() {
// Appelé quand le controller est connecté au DOM
console.log('Controller connecté !')
}
disconnect() {
// Appelé quand l élément est retiré du DOM
console.log('Controller déconnecté')
}
}
Animations CSS
/* Transitions CSS */
.card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
/* Animations avec @keyframes */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* Loader spinner */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e2e8f0;
border-top-color: #7C3AED;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
Animations avec Stimulus
// controllers/animation_controller.js
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['item']
connect() {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-fade-in')
}
})
},
{ threshold: 0.1 }
)
this.itemTargets.forEach(item => {
this.observer.observe(item)
})
}
disconnect() {
this.observer.disconnect()
}
}
Section 13.4.5 : Projet frontend complet
🎯 Objectif pédagogique
Mettre en pratique HTML, CSS, JavaScript, Bootstrap et Stimulus en construisant un projet frontend complet — une application de gestion de tâches avec interface responsive et interactions dynamiques.
Le projet : TaskMaster
Vous allez construire une application de gestion de tâches avec les fonctionnalités suivantes :
- →Interface responsive (mobile-first)
- →Ajout, suppression, et marquage de tâches
- →Filtrage (toutes, actives, complétées)
- →Persistance locale (localStorage)
- →Animations fluides
Structure HTML
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskMaster</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container py-5" data-controller="tasks">
<header class="text-center mb-4">
<h1>TaskMaster</h1>
<p class="text-muted">Gérez vos tâches efficacement</p>
</header>
<div class="row justify-content-center">
<div class="col-12 col-md-8 col-lg-6">
<form data-action="submit->tasks#add" class="mb-4">
<div class="input-group">
<input type="text" class="form-control"
data-tasks-target="input"
placeholder="Nouvelle tâche...">
<button class="btn btn-primary" type="submit">Ajouter</button>
</div>
</form>
<div class="btn-group w-100 mb-3">
<button class="btn btn-outline-secondary active"
data-action="click->tasks#filterAll">Toutes</button>
<button class="btn btn-outline-secondary"
data-action="click->tasks#filterActive">Actives</button>
<button class="btn btn-outline-secondary"
data-action="click->tasks#filterCompleted">Complétées</button>
</div>
<ul class="list-group" data-tasks-target="list"></ul>
<div class="d-flex justify-content-between mt-3">
<span data-tasks-target="counter" class="text-muted">0 tâche(s)</span>
<button class="btn btn-sm btn-outline-danger"
data-action="click->tasks#clearCompleted">
Supprimer complétées
</button>
</div>
</div>
</div>
</div>
</body>
</html>
Stimulus Controller
// controllers/tasks_controller.js
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['input', 'list', 'counter']
connect() {
this.tasks = JSON.parse(localStorage.getItem('tasks') || '[]')
this.filter = 'all'
this.render()
}
add(event) {
event.preventDefault()
const text = this.inputTarget.value.trim()
if (!text) return
this.tasks.push({
id: Date.now(),
text,
completed: false,
createdAt: new Date().toISOString()
})
this.inputTarget.value = ''
this.save()
this.render()
}
toggle(event) {
const id = Number(event.currentTarget.dataset.id)
const task = this.tasks.find(t => t.id === id)
if (task) task.completed = !task.completed
this.save()
this.render()
}
delete(event) {
const id = Number(event.currentTarget.dataset.id)
this.tasks = this.tasks.filter(t => t.id !== id)
this.save()
this.render()
}
filterAll() { this.filter = 'all'; this.render() }
filterActive() { this.filter = 'active'; this.render() }
filterCompleted() { this.filter = 'completed'; this.render() }
clearCompleted() {
this.tasks = this.tasks.filter(t => !t.completed)
this.save()
this.render()
}
save() {
localStorage.setItem('tasks', JSON.stringify(this.tasks))
}
render() {
const filtered = this.tasks.filter(t => {
if (this.filter === 'active') return !t.completed
if (this.filter === 'completed') return t.completed
return true
})
this.listTarget.innerHTML = filtered.map(task => `
<li class="list-group-item d-flex align-items-center ${task.completed ? 'completed' : ''}">
<input type="checkbox" class="form-check-input me-3"
${task.completed ? 'checked' : ''}
data-id="${task.id}"
data-action="change->tasks#toggle">
<span class="flex-grow-1 ${task.completed ? 'text-decoration-line-through text-muted' : ''}">
${task.text}
</span>
<button class="btn btn-sm btn-outline-danger"
data-id="${task.id}"
data-action="click->tasks#delete">X</button>
</li>
`).join('')
const active = this.tasks.filter(t => !t.completed).length
this.counterTarget.textContent = `${active} tâche(s) restante(s)`
}
}
Styles CSS
body {
background: #f8f9fa;
font-family: 'Inter', sans-serif;
}
h1 {
background: linear-gradient(135deg, #7C3AED, #a78bfa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 800;
}
.list-group-item {
transition: all 0.3s ease;
border-left: 3px solid transparent;
}
.list-group-item:hover {
border-left-color: #7C3AED;
background: #faf5ff;
}
.list-group-item.completed {
opacity: 0.6;
}
/* Animation d apparition */
.list-group-item {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
Section 13.5.1 : Rails MVC — routing et controllers
🎯 Objectif pédagogique
Comprendre l'architecture MVC de Rails, maîtriser le système de routing RESTful et créer des controllers efficaces qui orchestrent la logique de votre application.
Le pattern MVC dans Rails
MVC (Model-View-Controller) est le patron architectural central de Rails. Chaque requête HTTP suit un chemin clair :
Le Router : le GPS de Rails
Le router traduit les URLs en actions de controllers. Rails suit la convention REST :
# config/routes.rb
Rails.application.routes.draw do
# Ressource RESTful complète (7 routes)
resources :articles
# Cela génère automatiquement :
# GET /articles => articles#index
# GET /articles/new => articles#new
# POST /articles => articles#create
# GET /articles/:id => articles#show
# GET /articles/:id/edit => articles#edit
# PATCH /articles/:id => articles#update
# DELETE /articles/:id => articles#destroy
# Ressources imbriquées
resources :articles do
resources :comments, only: [:create, :destroy]
end
# => POST /articles/:article_id/comments
# Route personnalisée
get '/about', to: 'pages#about'
# Root route
root 'pages#home'
end
Les Controllers
Un controller est une classe Ruby qui reçoit les requêtes et coordonne la réponse :
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
before_action :set_article, only: [:show, :edit, :update, :destroy]
def index
@articles = Article.all.order(created_at: :desc)
end
def show
# @article est défini par before_action
end
def new
@article = Article.new
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article, notice: 'Article créé avec succès.'
else
render :new, status: :unprocessable_entity
end
end
def edit
# @article est défini par before_action
end
def update
if @article.update(article_params)
redirect_to @article, notice: 'Article mis à jour.'
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@article.destroy
redirect_to articles_path, notice: 'Article supprimé.'
end
private
def set_article
@article = Article.find(params[:id])
end
def article_params
params.require(:article).permit(:title, :content, :published)
end
end
Strong Parameters
Rails utilise les Strong Parameters pour se protéger contre l'injection de paramètres (mass assignment) :
# DANGEREUX : accepte tout
@article = Article.new(params[:article])
# SÉCURISÉ : filtre les paramètres autorisés
@article = Article.new(article_params)
private
def article_params
params.require(:article).permit(:title, :content, :published)
# Seuls title, content et published sont acceptés
# Tout autre paramètre (is_admin, user_id...) est ignoré
end
Filtres (Callbacks)
class ApplicationController < ActionController::Base
# Exécuté avant chaque action
before_action :authenticate_user!
# Exécuté après
after_action :log_activity
# Conditionnel
before_action :set_article, only: [:show, :edit, :update, :destroy]
before_action :check_admin, except: [:index, :show]
end
Section 13.5.2 : Views, partials et helpers
🎯 Objectif pédagogique
Maîtriser le système de vues Rails (ERB), les partials pour la réutilisation, les helpers pour la logique de présentation, et les layouts pour la structure globale des pages.
ERB : Embedded Ruby
ERB est le moteur de templates par défaut de Rails. Il mélange HTML et Ruby :
<!-- app/views/articles/index.html.erb -->
<h1>Articles</h1>
<% @articles.each do |article| %>
<div class="card mb-3">
<div class="card-body">
<h2><%= article.title %></h2>
<p><%= truncate(article.content, length: 200) %></p>
<% if article.published? %>
<span class="badge bg-success">Publié</span>
<% else %>
<span class="badge bg-warning">Brouillon</span>
<% end %>
<p class="text-muted">
Publié le <%= article.created_at.strftime('%d/%m/%Y') %>
</p>
<%= link_to 'Lire', article_path(article), class: 'btn btn-primary' %>
</div>
</div>
<% end %>
Syntaxe ERB :
- →
<% code %>— exécute du Ruby (pas d'affichage) - →
<%= expression %>— exécute et affiche le résultat - →
<%# commentaire %>— commentaire (non affiché)
Layouts
Le layout est le squelette HTML qui entoure chaque vue :
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title><%= content_for?(:title) ? yield(:title) : 'MonApp' %></title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application' %>
<%= javascript_include_tag 'application', defer: true %>
</head>
<body>
<%= render 'shared/navbar' %>
<% if notice %>
<div class="alert alert-success alert-dismissible fade show">
<%= notice %>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<% end %>
<main class="container py-4">
<%= yield %>
</main>
<%= render 'shared/footer' %>
</body>
</html>
Partials : la réutilisation
Les partials sont des fragments de vue réutilisables. Leur nom commence par un underscore :
<!-- app/views/articles/_article.html.erb (partial) -->
<div class="card mb-3" id="<%= dom_id(article) %>">
<div class="card-body">
<h3><%= link_to article.title, article %></h3>
<p><%= truncate(article.content, length: 150) %></p>
<small class="text-muted"><%= time_ago_in_words(article.created_at) %></small>
</div>
</div>
<!-- Utilisation dans index.html.erb -->
<%= render @articles %>
<!-- Rails devine automatiquement qu il faut utiliser _article.html.erb -->
<!-- Ou explicitement -->
<%= render partial: 'article', collection: @articles %>
<!-- Partial avec variables locales -->
<%= render 'shared/sidebar', title: 'Navigation', links: @nav_links %>
Formulaires Rails
<!-- app/views/articles/_form.html.erb -->
<%= form_with(model: article, class: 'needs-validation') do |f| %>
<% if article.errors.any? %>
<div class="alert alert-danger">
<h4><%= pluralize(article.errors.count, 'erreur') %></h4>
<ul>
<% article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="mb-3">
<%= f.label :title, class: 'form-label' %>
<%= f.text_field :title, class: 'form-control' %>
</div>
<div class="mb-3">
<%= f.label :content, class: 'form-label' %>
<%= f.text_area :content, class: 'form-control', rows: 10 %>
</div>
<div class="mb-3 form-check">
<%= f.check_box :published, class: 'form-check-input' %>
<%= f.label :published, 'Publier', class: 'form-check-label' %>
</div>
<%= f.submit class: 'btn btn-primary' %>
<% end %>
Helpers
Les helpers contiennent la logique de présentation à réutiliser dans les vues :
# app/helpers/articles_helper.rb
module ArticlesHelper
def status_badge(article)
if article.published?
content_tag(:span, 'Publié', class: 'badge bg-success')
else
content_tag(:span, 'Brouillon', class: 'badge bg-warning')
end
end
def reading_time(article)
words = article.content.split.size
minutes = (words / 200.0).ceil
"#{minutes} min de lecture"
end
end
<!-- Utilisation dans la vue -->
<%= status_badge(@article) %>
<span><%= reading_time(@article) %></span>
Section 13.5.3 : Authentification avec Devise et autorisation Pundit
🎯 Objectif pédagogique
Implémenter un système d'authentification complet avec Devise (inscription, connexion, mot de passe oublié) et gérer les autorisations avec Pundit (qui peut faire quoi).
Devise : l'authentification en 10 minutes
Devise est LA gem d'authentification de Rails. Elle gère inscription, connexion, déconnexion, récupération de mot de passe, confirmation email, et bien plus.
# Installation
bundle add devise
rails generate devise:install
rails generate devise User
rails db:migrate
Cela crée automatiquement :
- →Le modèle
Useravec email et mot de passe chiffré - →Les routes de connexion/inscription
- →Les vues de formulaire
- →Les migrations de base de données
Configuration Devise
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:confirmable, :trackable
has_many :articles
has_one :profile
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :authenticate_user! # Requiert connexion partout
# Permettre des champs supplémentaires à l inscription
before_action :configure_permitted_parameters, if: :devise_controller?
private
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :avatar])
devise_parameter_sanitizer.permit(:account_update, keys: [:name, :avatar])
end
end
Helpers Devise dans les vues
<% if user_signed_in? %>
<span>Bonjour, <%= current_user.name %></span>
<%= link_to 'Mon profil', edit_user_registration_path %>
<%= button_to 'Déconnexion', destroy_user_session_path, method: :delete %>
<% else %>
<%= link_to 'Connexion', new_user_session_path %>
<%= link_to 'Inscription', new_user_registration_path %>
<% end %>
Pundit : l'autorisation
L'authentification répond à "Qui êtes-vous ?". L'autorisation répond à "Que pouvez-vous faire ?". Pundit gère cette seconde question.
bundle add pundit
rails generate pundit:install
# app/policies/article_policy.rb
class ArticlePolicy < ApplicationPolicy
def index?
true # Tout le monde peut voir la liste
end
def show?
true # Tout le monde peut voir un article
end
def create?
user.present? # Tout utilisateur connecté
end
def update?
user == record.author || user.admin?
end
def destroy?
user == record.author || user.admin?
end
class Scope < Scope
def resolve
if user&.admin?
scope.all
else
scope.where(published: true)
end
end
end
end
# Dans le controller
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
authorize @article # Vérifie ArticlePolicy#show?
end
def index
@articles = policy_scope(Article) # Filtre via Scope
end
end
Section 13.5.4 : Action Cable et temps réel
🎯 Objectif pédagogique
Intégrer le temps réel dans une application Rails avec Action Cable (WebSockets) pour créer des fonctionnalités comme le chat en direct, les notifications instantanées et les mises à jour live.
WebSockets vs HTTP classique
HTTP classique est un protocole requête-réponse : le client demande, le serveur répond, la connexion se ferme. WebSocket maintient une connexion permanente et bidirectionnelle entre client et serveur.
Action Cable : WebSockets intégré à Rails
Action Cable est la couche WebSocket de Rails. Il s'intègre nativement avec le reste du framework.
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room_id]}"
end
def unsubscribed
# Nettoyage si nécessaire
end
def receive(data)
# Quand le client envoie un message
message = Message.create!(
content: data['content'],
user: current_user,
room_id: params[:room_id]
)
ActionCable.server.broadcast(
"chat_#{params[:room_id]}",
{
content: message.content,
user: message.user.name,
created_at: message.created_at.strftime('%H:%M')
}
)
end
end
Côté JavaScript
// app/javascript/channels/chat_channel.js
import consumer from './consumer'
const chatChannel = consumer.subscriptions.create(
{ channel: 'ChatChannel', room_id: 1 },
{
connected() {
console.log('Connecté au chat !')
},
disconnected() {
console.log('Déconnecté du chat')
},
received(data) {
// Appelé quand le serveur broadcast un message
const messagesDiv = document.querySelector('#messages')
messagesDiv.insertAdjacentHTML('beforeend', `
<div class="message">
<strong>${data.user}</strong>
<span class="time">${data.created_at}</span>
<p>${data.content}</p>
</div>
`)
messagesDiv.scrollTop = messagesDiv.scrollHeight
},
// Envoyer un message au serveur
sendMessage(content) {
this.perform('receive', { content })
}
}
)
// Formulaire d envoi
document.querySelector('#message-form').addEventListener('submit', (e) => {
e.preventDefault()
const input = e.target.querySelector('input')
chatChannel.sendMessage(input.value)
input.value = ''
})
Broadcast depuis un Model
La manière la plus courante d'utiliser Action Cable est de broadcaster depuis un callback de modèle :
# app/models/message.rb
class Message < ApplicationRecord
belongs_to :user
belongs_to :room
after_create_commit :broadcast_message
private
def broadcast_message
ActionCable.server.broadcast(
"chat_#{room_id}",
{
content: content,
user: user.name,
html: ApplicationController.render(
partial: 'messages/message',
locals: { message: self }
)
}
)
end
end
Turbo Streams : le temps réel simplifié
Avec Hotwire/Turbo, le temps réel devient encore plus simple :
# app/models/message.rb
class Message < ApplicationRecord
broadcasts_to :room # UNE SEULE LIGNE !
end
<!-- app/views/rooms/show.html.erb -->
<%= turbo_stream_from @room %>
<div id="messages">
<%= render @room.messages %>
</div>
<%= turbo_frame_tag 'new_message' do %>
<%= render 'messages/form', message: Message.new(room: @room) %>
<% end %>
Turbo Streams vs Action Cable brut
Turbo Streams est une abstraction au-dessus d'Action Cable qui génère automatiquement le HTML côté serveur et le met à jour côté client. Pour la plupart des cas d'usage, Turbo Streams est le choix préféré car il élimine le JavaScript côté client.
Section 13.5.5 : Déploiement sur Heroku
🎯 Objectif pédagogique
Déployer une application Rails sur Heroku, configurer les variables d'environnement, la base de données PostgreSQL, et mettre en place un pipeline CI/CD basique.
Heroku : la plateforme de déploiement Rails
Heroku est une PaaS (Platform as a Service) qui simplifie le déploiement. Un simple git push déploie votre application. C'est la plateforme historique de la communauté Rails.
Préparation du projet
# Gemfile modifications pour production
group :production do
gem 'pg' # PostgreSQL en production
end
group :development, :test do
gem 'sqlite3' # SQLite en dev
end
# Procfile (à la racine du projet)
web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq
release: bundle exec rails db:migrate
Déploiement initial
# 1. Installer Heroku CLI
# 2. Se connecter
heroku login
# 3. Créer l application
heroku create mon-app-rails
# => https://mon-app-rails.herokuapp.com
# 4. Ajouter PostgreSQL
heroku addons:create heroku-postgresql:mini
# 5. Configurer les variables d'environnement
heroku config:set RAILS_MASTER_KEY=$(cat config/master.key)
heroku config:set RAILS_ENV=production
# 6. Déployer
git push heroku main
# 7. Exécuter les migrations
heroku run rails db:migrate
# 8. Seed si nécessaire
heroku run rails db:seed
Variables d'environnement
# Définir des variables
heroku config:set SECRET_KEY_BASE=abc123
heroku config:set SMTP_HOST=smtp.sendgrid.net
heroku config:set SMTP_USERNAME=apikey
# Voir les variables
heroku config
# Utiliser dans Rails
# config/environments/production.rb
config.action_mailer.smtp_settings = {
address: ENV['SMTP_HOST'],
user_name: ENV['SMTP_USERNAME'],
password: ENV['SMTP_PASSWORD']
}
Configuration PostgreSQL en production
# config/database.yml
production:
adapter: postgresql
encoding: unicode
url: <%= ENV['DATABASE_URL'] %>
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
Commandes Heroku utiles
# Logs en temps réel
heroku logs --tail
# Console Rails en production
heroku run rails console
# Bash dans le dyno
heroku run bash
# Redémarrer
heroku restart
# Échelle
heroku ps:scale web=2 worker=1
# Maintenance mode
heroku maintenance:on
heroku maintenance:off
# Backup de base de données
heroku pg:backups:capture
heroku pg:backups:download
Alternatives à Heroku
Section 13.6.1 : OpenAI API et prompt engineering
🎯 Objectif pédagogique
Intégrer l'API OpenAI dans une application Rails, comprendre les modèles GPT, maîtriser le prompt engineering et construire des fonctionnalités IA concrètes.
L'API OpenAI
L'API OpenAI donne accès aux modèles GPT-4, GPT-3.5 et aux embeddings. C'est l'interface programmatique qui permet à vos applications d'utiliser l'intelligence artificielle.
# Gemfile
gem 'ruby-openai'
# config/initializers/openai.rb
OpenAI.configure do |config|
config.access_token = ENV.fetch('OPENAI_API_KEY')
config.organization_id = ENV.fetch('OPENAI_ORG_ID', nil)
end
OPENAI_CLIENT = OpenAI::Client.new
Premier appel API
# app/services/ai_service.rb
class AiService
def self.chat(prompt, system_message: nil)
messages = []
messages << { role: 'system', content: system_message } if system_message
messages << { role: 'user', content: prompt }
response = OPENAI_CLIENT.chat(
parameters: {
model: 'gpt-4',
messages: messages,
temperature: 0.7,
max_tokens: 1000
}
)
response.dig('choices', 0, 'message', 'content')
end
end
# Utilisation
result = AiService.chat(
'Résume cet article en 3 points clés',
system_message: 'Tu es un assistant de rédaction concis et précis.'
)
Prompt Engineering : l'art du prompt
Le prompt engineering est la compétence de formuler des instructions claires pour obtenir le résultat désiré des LLMs.
# MAUVAIS prompt
AiService.chat('Écris quelque chose sur le marketing')
# BON prompt - structuré
AiService.chat(
'Rédige 5 titres d email marketing pour une vente flash de 48h ' \
'sur des chaussures de running. ' \
'Cible : hommes 25-40 ans, sportifs occasionnels. ' \
'Ton : urgent mais pas agressif. ' \
'Format : un titre par ligne, max 60 caractères.',
system_message: 'Tu es un expert en copywriting email avec 10 ans ' \
'd expérience en e-commerce sportif.'
)
Les techniques de prompt engineering
# 1. Few-shot prompting (exemples)
prompt = <<~PROMPT
Classifie le sentiment de ces avis client.
Exemples :
"Livraison rapide, produit top !" => POSITIF
"Trois semaines d attente, inadmissible" => NÉGATIF
"Le produit est correct, rien de spécial" => NEUTRE
Classifie :
"#{avis_client}"
PROMPT
# 2. Chain-of-thought (raisonnement étape par étape)
prompt = <<~PROMPT
Analyse ce business plan étape par étape :
1. Identifie le marché cible
2. Évalue la proposition de valeur
3. Analyse le modèle de revenus
4. Identifie les risques
5. Donne une note de 1 à 10
Business plan : #{business_plan}
Raisonne à voix haute avant de donner ta note finale.
PROMPT
# 3. Structured output (JSON)
prompt = <<~PROMPT
Extrais les informations de cette facture.
Retourne UNIQUEMENT un JSON valide avec cette structure :
{
"numero_facture": "string",
"date": "YYYY-MM-DD",
"montant_total": number,
"lignes": [{"description": "string", "montant": number}]
}
Facture : #{texte_facture}
PROMPT
result = AiService.chat(prompt)
data = JSON.parse(result)
Intégrer l'IA dans un Controller Rails
# app/controllers/ai_summaries_controller.rb
class AiSummariesController < ApplicationController
def create
@article = Article.find(params[:article_id])
authorize @article
summary = AiService.chat(
"Résume cet article en 3 bullet points de max 20 mots chacun :\n\n#{@article.content}",
system_message: 'Tu es un assistant de synthèse. Sois concis et précis.'
)
@article.update(ai_summary: summary)
redirect_to @article, notice: 'Résumé IA généré !'
end
end
Section 13.6.2 : RAG et LangChain
🎯 Objectif pédagogique
Implémenter un système RAG (Retrieval-Augmented Generation) qui permet à un LLM de répondre à des questions en se basant sur vos propres documents, en utilisant LangChain pour orchestrer le pipeline.
Le problème : les LLMs ne connaissent pas vos données
Les modèles GPT ont été entraînés sur des données publiques jusqu'à une date de coupure. Ils ne connaissent pas :
- →Vos documents internes
- →Votre base de connaissances
- →Vos dernières données
RAG résout ce problème en injectant du contexte pertinent dans le prompt.
Le pipeline RAG en 4 étapes
# Étape 1 : Charger et découper les documents
class DocumentProcessor
CHUNK_SIZE = 500
CHUNK_OVERLAP = 50
def self.chunk(text)
words = text.split
chunks = []
i = 0
while i < words.length
chunk = words[i, CHUNK_SIZE].join(' ')
chunks << chunk
i += CHUNK_SIZE - CHUNK_OVERLAP
end
chunks
end
end
# Étape 2 : Générer les embeddings
class EmbeddingService
def self.generate(text)
response = OPENAI_CLIENT.embeddings(
parameters: {
model: 'text-embedding-3-small',
input: text
}
)
response.dig('data', 0, 'embedding')
end
end
# Étape 3 : Stocker dans une base vectorielle
class VectorStore
def self.store(chunk, embedding, metadata = {})
Document.create!(
content: chunk,
embedding: embedding,
metadata: metadata
)
end
def self.search(query_embedding, limit: 5)
Document.nearest_neighbors(:embedding, query_embedding, distance: :cosine)
.first(limit)
end
end
# Étape 4 : Générer la réponse
class RagService
def self.answer(question)
# 1. Embed la question
query_embedding = EmbeddingService.generate(question)
# 2. Chercher les documents pertinents
relevant_docs = VectorStore.search(query_embedding, limit: 5)
context = relevant_docs.map(&:content).join("\n\n---\n\n")
# 3. Construire le prompt enrichi
prompt = <<~PROMPT
Contexte (documents internes) :
#{context}
Question : #{question}
Réponds en te basant UNIQUEMENT sur le contexte fourni.
Si le contexte ne contient pas la réponse, dis-le.
PROMPT
# 4. Appeler le LLM
AiService.chat(prompt, system_message: 'Tu es un assistant qui répond en se basant uniquement sur le contexte fourni.')
end
end
LangChain : orchestrer les pipelines IA
LangChain est un framework qui simplifie la construction de pipelines IA complexes. Bien que principalement Python, le concept s'applique en Ruby :
# Exemple LangChain en Python (concept)
from langchain.document_loaders import DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.llms import OpenAI
# 1. Charger les documents
loader = DirectoryLoader('./docs/', glob="**/*.md")
docs = loader.load()
# 2. Découper en chunks
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50
)
chunks = splitter.split_documents(docs)
# 3. Créer le vector store
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(chunks, embeddings)
# 4. Créer la chaîne RAG
qa_chain = RetrievalQA.from_chain_type(
llm=OpenAI(model_name="gpt-4"),
chain_type="stuff",
retriever=vectorstore.as_retriever(search_kwargs={"k": 5})
)
# 5. Poser des questions
answer = qa_chain.run("Quelle est la politique de remboursement ?")
Section 13.6.3 : Base de données vectorielle et embeddings
🎯 Objectif pédagogique
Comprendre les embeddings vectoriels, les bases de données vectorielles (pgvector, Pinecone), et implémenter une recherche sémantique performante dans une application Rails.
Qu'est-ce qu'un embedding ?
Un embedding est une représentation numérique d'un texte sous forme de vecteur (liste de nombres). Les textes sémantiquement proches ont des vecteurs proches dans l'espace vectoriel.
# "chat" et "félin" sont proches dans l'espace vectoriel
embedding_chat = EmbeddingService.generate("Le chat dort")
# => [0.023, -0.041, 0.089, ..., 0.012] (1536 dimensions)
embedding_felin = EmbeddingService.generate("Le félin sommeille")
# => [0.025, -0.039, 0.091, ..., 0.010] (très similaire !)
embedding_voiture = EmbeddingService.generate("La voiture roule")
# => [-0.067, 0.082, -0.015, ..., 0.044] (très différent)
Similarité cosinus
La similarité cosinus mesure l'angle entre deux vecteurs. Plus ils pointent dans la même direction, plus les textes sont similaires :
def cosine_similarity(vec_a, vec_b)
dot_product = vec_a.zip(vec_b).sum { |a, b| a * b }
magnitude_a = Math.sqrt(vec_a.sum { |a| a ** 2 })
magnitude_b = Math.sqrt(vec_b.sum { |b| b ** 2 })
dot_product / (magnitude_a * magnitude_b)
end
# "chat" vs "félin" => ~0.95 (très similaire)
# "chat" vs "voiture" => ~0.15 (pas similaire)
pgvector : les vecteurs dans PostgreSQL
pgvector est une extension PostgreSQL qui ajoute un type vector et des index de recherche vectorielle. C'est la solution la plus simple pour Rails.
# Migration Rails
class AddVectorToDocuments < ActiveRecord::Migration[7.0]
def up
enable_extension 'vector'
add_column :documents, :embedding, :vector, limit: 1536
add_index :documents, :embedding,
using: :ivfflat,
opclass: :vector_cosine_ops
end
end
# Gemfile
gem 'neighbor' # Gem Rails pour pgvector
# app/models/document.rb
class Document < ApplicationRecord
has_neighbors :embedding
# Recherche les documents les plus similaires
scope :search_similar, ->(query_embedding, limit: 5) {
nearest_neighbors(:embedding, query_embedding, distance: :cosine)
.first(limit)
}
end
Pipeline complet de recherche sémantique
# app/services/semantic_search.rb
class SemanticSearch
def self.index_document(document)
chunks = DocumentProcessor.chunk(document.content)
chunks.each_with_index do |chunk, index|
embedding = EmbeddingService.generate(chunk)
DocumentChunk.create!(
document: document,
content: chunk,
chunk_index: index,
embedding: embedding
)
end
end
def self.search(query, limit: 5)
query_embedding = EmbeddingService.generate(query)
DocumentChunk
.nearest_neighbors(:embedding, query_embedding, distance: :cosine)
.first(limit)
.map do |chunk|
{
content: chunk.content,
document: chunk.document,
similarity: chunk.neighbor_distance
}
end
end
end
# Utilisation
SemanticSearch.index_document(article)
results = SemanticSearch.search("Comment configurer l authentification ?")
Solutions de vector DB
Section 13.6.4 : AI-assisted coding et Cursor IDE
🎯 Objectif pédagogique
Maîtriser les outils d'IA pour le développement : GitHub Copilot, Cursor IDE, et les techniques pour maximiser la productivité avec l'assistance IA tout en gardant le contrôle du code.
L'ère du développement assisté par IA
L'IA transforme le métier de développeur. Les outils d'assistance IA ne remplacent pas le développeur — ils amplifient ses capacités. Les études montrent des gains de productivité de 30-55% sur certaines tâches.
GitHub Copilot
GitHub Copilot est l'outil d'autocomplétion IA le plus utilisé. Il s'intègre directement dans VS Code et suggère du code en temps réel.
# Tapez un commentaire descriptif, Copilot génère le code
# Valide un email avec regex
def valid_email?(email)
email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
# Convertit un prix en centimes
def price_in_cents(price_string)
(price_string.to_f * 100).round
end
# Génère un slug URL-friendly à partir d un titre
def slugify(title)
title.downcase.strip.gsub(/[^\w\s-]/, '').gsub(/[\s_]+/, '-')
end
Cursor IDE : l'IDE IA-natif
Cursor est un fork de VS Code entièrement repensé pour l'IA. Il va au-delà de l'autocomplétion avec des fonctionnalités de refactoring, débogage et génération de code à grande échelle.
Fonctionnalités clés de Cursor :
- →Cmd+K (Edit) : Sélectionnez du code et décrivez la modification
- →Cmd+L (Chat) : Chat avec le contexte de votre codebase
- →@ mentions : Référencez des fichiers, docs, ou URLs dans le chat
- →Composer : Modifiez plusieurs fichiers simultanément
- →Tab completion : Autocomplétion contextuelle avancée
Bonnes pratiques avec l'IA
# 1. Commentaires comme spécifications
# Service qui envoie un email de bienvenue
# - Vérifie que l'utilisateur a un email confirmé
# - Utilise le template 'welcome' avec le nom de l'utilisateur
# - Envoie de manière asynchrone via Sidekiq
# - Log le résultat dans le journal d'activité
class WelcomeEmailService
# L'IA génère le code basé sur cette spec...
end
# 2. Tests d abord (TDD + IA)
# Écrivez le test, l'IA génère l'implémentation
RSpec.describe WelcomeEmailService do
it 'sends welcome email to confirmed user' do
user = create(:user, email_confirmed: true)
expect { WelcomeEmailService.call(user) }
.to have_enqueued_job(ActionMailer::MailDeliveryJob)
end
it 'does not send to unconfirmed user' do
user = create(:user, email_confirmed: false)
expect { WelcomeEmailService.call(user) }
.not_to have_enqueued_job
end
end
Ce que l'IA fait bien vs mal
Règle d or de l IA assistée
Ne committez JAMAIS du code IA que vous ne comprenez pas à 100%. L'IA est un copilote, pas le pilote. Vous êtes responsable de chaque ligne de code dans votre application. Relisez, testez, et comprenez avant de merger.
Section 13.6.5 : Agents IA et orchestration
🎯 Objectif pédagogique
Comprendre les agents IA autonomes, l'orchestration multi-agents, et implémenter un agent capable d'utiliser des outils (function calling) pour accomplir des tâches complexes.
Qu'est-ce qu'un agent IA ?
Un agent IA est un système qui utilise un LLM pour raisonner, planifier, et agir de manière autonome. Contrairement à un simple chatbot qui répond, un agent peut utiliser des outils, consulter des bases de données, et exécuter des actions.
Function Calling (Tool Use)
Le function calling permet au LLM d'appeler des fonctions que vous définissez :
# Définir les outils disponibles
TOOLS = [
{
type: 'function',
function: {
name: 'search_products',
description: 'Recherche des produits dans le catalogue',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Termes de recherche' },
category: { type: 'string', description: 'Catégorie de produit' },
max_price: { type: 'number', description: 'Prix maximum' }
},
required: ['query']
}
}
},
{
type: 'function',
function: {
name: 'get_order_status',
description: 'Vérifie le statut d une commande',
parameters: {
type: 'object',
properties: {
order_id: { type: 'string', description: 'ID de la commande' }
},
required: ['order_id']
}
}
}
]
Implémentation d'un agent
# app/services/agent_service.rb
class AgentService
MAX_ITERATIONS = 10
def initialize(tools:)
@tools = tools
@messages = []
end
def run(user_message)
@messages << { role: 'user', content: user_message }
MAX_ITERATIONS.times do
response = OPENAI_CLIENT.chat(
parameters: {
model: 'gpt-4',
messages: @messages,
tools: @tools,
tool_choice: 'auto'
}
)
message = response.dig('choices', 0, 'message')
@messages << message
# Si le LLM veut appeler un outil
if message['tool_calls']
message['tool_calls'].each do |tool_call|
result = execute_tool(
tool_call['function']['name'],
JSON.parse(tool_call['function']['arguments'])
)
@messages << {
role: 'tool',
tool_call_id: tool_call['id'],
content: result.to_json
}
end
else
# Réponse finale
return message['content']
end
end
'Agent a atteint la limite d itérations.'
end
private
def execute_tool(name, args)
case name
when 'search_products'
Product.search(args['query'])
.where(category: args['category'])
.where('price <= ?', args['max_price'])
.limit(5)
.map { |p| { name: p.name, price: p.price } }
when 'get_order_status'
order = Order.find_by(id: args['order_id'])
order ? { status: order.status, eta: order.eta } : { error: 'Commande non trouvée' }
end
end
end
# Utilisation
agent = AgentService.new(tools: TOOLS)
response = agent.run(
"Je cherche des chaussures de running sous 100€, " \
"et aussi le statut de ma commande #12345"
)
Orchestration multi-agents
Pour des tâches complexes, plusieurs agents spécialisés collaborent :
# Agent de routage (dispatcher)
class DispatcherAgent
AGENTS = {
'sales' => SalesAgent,
'support' => SupportAgent,
'analytics' => AnalyticsAgent
}
def route(message)
# Le LLM détermine quel agent doit traiter
category = AiService.chat(
"Classifie ce message en une catégorie : sales, support, analytics.\n" \
"Message : #{message}\n" \
"Réponds uniquement par la catégorie.",
system_message: 'Tu es un routeur de messages.'
).strip.downcase
agent_class = AGENTS[category]
agent_class.new.handle(message)
end
end
Section 13.7.1 : Conception et architecture du projet final
🎯 Objectif pédagogique
Concevoir l'architecture complète d'une application web Rails intégrant de l'IA : user stories, modèle de données, wireframes, et choix technologiques argumentés.
Le projet final : votre vitrine professionnelle
Le projet final du bootcamp est une application full-stack Rails avec intégration IA que vous concevez et réalisez de A à Z. C'est votre démonstration de compétences pour les recruteurs et clients.
Exemples de projets :
- →Assistant juridique IA : RAG sur des textes de loi, chat avec un avocat virtuel
- →Plateforme de recrutement : analyse de CV par IA, matching candidat/offre
- →Dashboard analytics : visualisation de données avec résumés IA automatiques
- →E-commerce intelligent : recommandations produits, descriptions auto-générées
Phase 1 : User Stories
# Exemple de user stories pour un assistant juridique
En tant qu'utilisateur, je veux :
- [ ] M'inscrire et me connecter à l'application
- [ ] Poser une question juridique en langage naturel
- [ ] Recevoir une réponse sourcée (articles de loi cités)
- [ ] Consulter l'historique de mes conversations
- [ ] Sauvegarder des réponses en favoris
En tant qu'administrateur, je veux :
- [ ] Ajouter/supprimer des documents juridiques
- [ ] Voir les analytics d'utilisation
- [ ] Gérer les utilisateurs
- [ ] Modérer les contenus IA inappropriés
Phase 2 : Modèle de données
Phase 3 : Architecture technique
## Stack technique
- **Backend** : Ruby on Rails 7.1
- **Base de données** : PostgreSQL + pgvector
- **Frontend** : Hotwire (Turbo + Stimulus) + Bootstrap 5
- **IA** : OpenAI API (GPT-4 + text-embedding-3-small)
- **Background jobs** : Sidekiq + Redis
- **Déploiement** : Heroku (ou Render)
- **Tests** : RSpec + FactoryBot
## APIs externes
- OpenAI (chat + embeddings)
- SendGrid (emails)
- Sentry (monitoring erreurs)
Phase 4 : Wireframes
Avant de coder, dessinez vos écrans. Utilisez Figma, Excalidraw, ou même papier/crayon. L'objectif : valider le flux utilisateur avant d'écrire la première ligne de code.
Pages clés à wireframer :
- →Page d'accueil / Landing
- →Inscription / Connexion
- →Dashboard (interface principale)
- →Interface de chat IA
- →Page de résultats / historique
- →Panel administration
Section 13.7.2 : Développement full-stack du projet
🎯 Objectif pédagogique
Implémenter le projet final en suivant les bonnes pratiques Rails : développement itératif, TDD, code review, et gestion de projet avec Git flow.
Méthodologie de développement
Le développement suit une approche itérative avec des sprints courts :
## Sprint 1 (3 jours) : Fondations
- [ ] Initialiser le projet Rails
- [ ] Configurer la base de données (PostgreSQL + pgvector)
- [ ] Implémenter l'authentification (Devise)
- [ ] Créer le layout principal (Bootstrap)
- [ ] Déployer sur Heroku (CI/CD)
## Sprint 2 (3 jours) : Fonctionnalités core
- [ ] Modèles et migrations
- [ ] CRUD principal
- [ ] Interface utilisateur (Turbo + Stimulus)
- [ ] Tests RSpec
## Sprint 3 (3 jours) : Intégration IA
- [ ] Service OpenAI
- [ ] Pipeline RAG (embeddings + recherche)
- [ ] Interface de chat
- [ ] Gestion des erreurs et limites
## Sprint 4 (3 jours) : Finitions
- [ ] Administration (Pundit)
- [ ] Responsive design
- [ ] Performance (caching, N+1)
- [ ] Tests E2E
Git Flow pour le projet
# Branches principales
main # Production (toujours déployable)
develop # Développement (intégration)
# Branches de feature
git checkout -b feature/user-auth
# ... développer ...
git push origin feature/user-auth
# Créer une Pull Request vers develop
# Convention de commits
git commit -m "feat: add user authentication with Devise"
git commit -m "fix: resolve N+1 query in conversations#index"
git commit -m "test: add RSpec tests for AiService"
git commit -m "docs: update README with setup instructions"
Scaffold intelligent avec Rails
# Générer les modèles rapidement
rails generate model User name:string email:string role:string
rails generate model Conversation user:references title:string
rails generate model Message conversation:references role:string content:text tokens_used:integer
rails generate model Document title:string content:text category:string
rails generate model Chunk document:references content:text chunk_index:integer
# Générer un controller complet
rails generate controller Conversations index show create --skip-routes
# Migrations
rails db:migrate
TDD : tester en même temps que vous développez
# spec/services/ai_service_spec.rb
RSpec.describe AiService do
describe '.chat' do
it 'returns a response from OpenAI' do
VCR.use_cassette('openai_chat') do
result = AiService.chat('Dis bonjour')
expect(result).to be_a(String)
expect(result.length).to be > 0
end
end
it 'includes system message when provided' do
VCR.use_cassette('openai_chat_system') do
result = AiService.chat(
'Quel est ton rôle ?',
system_message: 'Tu es un assistant juridique.'
)
expect(result).to include('juridique').or include('droit')
end
end
end
end
# spec/models/conversation_spec.rb
RSpec.describe Conversation do
it { should belong_to(:user) }
it { should have_many(:messages).dependent(:destroy) }
it { should validate_presence_of(:title) }
describe '#last_message' do
it 'returns the most recent message' do
conversation = create(:conversation)
old_msg = create(:message, conversation: conversation, created_at: 1.hour.ago)
new_msg = create(:message, conversation: conversation, created_at: Time.current)
expect(conversation.last_message).to eq(new_msg)
end
end
end
Code Review checklist
## Avant chaque Pull Request, vérifiez :
### Fonctionnel
- [ ] La feature fonctionne comme prévu
- [ ] Les edge cases sont gérés
- [ ] Pas de régression sur les features existantes
### Code Quality
- [ ] DRY (pas de duplication)
- [ ] Methods < 10 lignes
- [ ] Nommage explicite (pas de variables x, y, temp)
- [ ] Strong Parameters utilisés
- [ ] N+1 queries éliminées
### Tests
- [ ] Tests unitaires pour les models/services
- [ ] Tests d intégration pour les controllers
- [ ] Tests passent en CI
### Sécurité
- [ ] Input sanitisé
- [ ] Autorisations Pundit en place
- [ ] Pas de secrets dans le code
Section 13.7.3 : Intégration IA avancée
🎯 Objectif pédagogique
Intégrer des fonctionnalités IA avancées dans le projet final : streaming de réponses, gestion de contexte conversationnel, monitoring des coûts, et gestion des erreurs IA.
Streaming des réponses IA
Au lieu d'attendre la réponse complète (5-10 secondes), le streaming affiche les tokens au fur et à mesure, comme ChatGPT :
# app/services/streaming_ai_service.rb
class StreamingAiService
def self.stream_chat(messages, &block)
OPENAI_CLIENT.chat(
parameters: {
model: 'gpt-4',
messages: messages,
stream: proc { |chunk, _bytesize|
delta = chunk.dig('choices', 0, 'delta', 'content')
block.call(delta) if delta
}
}
)
end
end
# Avec Action Cable pour le streaming temps réel
# app/channels/chat_stream_channel.rb
class ChatStreamChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_stream_#{params[:conversation_id]}"
end
def ask(data)
conversation = Conversation.find(params[:conversation_id])
messages = conversation.messages.map { |m| { role: m.role, content: m.content } }
messages << { role: 'user', content: data['content'] }
# Sauvegarder le message utilisateur
conversation.messages.create!(role: 'user', content: data['content'])
# Streamer la réponse
full_response = ''
StreamingAiService.stream_chat(messages) do |token|
full_response += token
ActionCable.server.broadcast(
"chat_stream_#{conversation.id}",
{ token: token, type: 'stream' }
)
end
# Sauvegarder la réponse complète
conversation.messages.create!(role: 'assistant', content: full_response)
ActionCable.server.broadcast(
"chat_stream_#{conversation.id}",
{ type: 'complete' }
)
end
end
Gestion du contexte conversationnel
# app/services/conversation_manager.rb
class ConversationManager
MAX_CONTEXT_TOKENS = 4000
def self.build_messages(conversation)
messages = []
# System message
messages << {
role: 'system',
content: system_prompt(conversation)
}
# Contexte RAG si pertinent
if conversation.uses_rag?
context = RagService.get_context(conversation.last_user_message)
messages << {
role: 'system',
content: "Contexte documentaire :\n#{context}"
}
end
# Historique (limité pour respecter la fenêtre de contexte)
recent_messages = conversation.messages
.order(created_at: :desc)
.limit(20)
.reverse
recent_messages.each do |msg|
messages << { role: msg.role, content: msg.content }
end
# Tronquer si nécessaire
truncate_to_token_limit(messages)
end
private
def self.truncate_to_token_limit(messages)
# Garder le system message et les derniers messages
while estimate_tokens(messages) > MAX_CONTEXT_TOKENS && messages.length > 2
messages.delete_at(1) # Supprimer les plus anciens (garder system)
end
messages
end
def self.estimate_tokens(messages)
messages.sum { |m| m[:content].length / 4 } # Approximation
end
end
Monitoring des coûts
# app/models/ai_usage.rb
class AiUsage < ApplicationRecord
belongs_to :user
# Tracker chaque appel API
def self.track(user:, model:, input_tokens:, output_tokens:)
costs = {
'gpt-4' => { input: 0.03, output: 0.06 }, # par 1K tokens
'gpt-3.5-turbo' => { input: 0.0015, output: 0.002 },
'text-embedding-3-small' => { input: 0.00002, output: 0 }
}
model_cost = costs[model]
total_cost = (input_tokens / 1000.0 * model_cost[:input]) +
(output_tokens / 1000.0 * model_cost[:output])
create!(
user: user,
model: model,
input_tokens: input_tokens,
output_tokens: output_tokens,
cost_usd: total_cost
)
end
# Limites par utilisateur
def self.monthly_cost(user)
where(user: user)
.where('created_at >= ?', Time.current.beginning_of_month)
.sum(:cost_usd)
end
end
Gestion des erreurs IA
# app/services/resilient_ai_service.rb
class ResilientAiService
MAX_RETRIES = 3
RETRY_DELAY = 1 # seconde
def self.chat(prompt, **options)
retries = 0
begin
result = AiService.chat(prompt, **options)
raise 'Réponse vide' if result.blank?
result
rescue Faraday::TooManyRequestsError => e
# Rate limiting — attendre et réessayer
retries += 1
if retries <= MAX_RETRIES
sleep(RETRY_DELAY * retries)
retry
end
fallback_response('Service temporairement surchargé.')
rescue Faraday::ServerError => e
# Erreur serveur OpenAI
Rails.logger.error("OpenAI server error: #{e.message}")
fallback_response('Le service IA est temporairement indisponible.')
rescue JSON::ParserError => e
Rails.logger.error("Invalid JSON from OpenAI: #{e.message}")
fallback_response('Erreur de format dans la réponse IA.')
end
end
private
def self.fallback_response(message)
"Désolé, #{message} Veuillez réessayer dans quelques instants."
end
end
Coûts IA en production
Surveillez vos coûts OpenAI dès le début. Un utilisateur qui abuse du chat peut facilement générer 10$/jour en tokens GPT-4. Implémentez des limites quotidiennes/mensuelles par utilisateur et utilisez GPT-3.5-turbo pour les tâches simples (classification, extraction) et GPT-4 uniquement quand la qualité est critique.
Section 13.7.4 : Tests, pitch et démo finale
🎯 Objectif pédagogique
Finaliser le projet avec des tests complets, préparer un pitch professionnel de 10 minutes, et réaliser une démo live convaincante pour les recruteurs ou clients.
Stratégie de tests
# spec/system/conversations_spec.rb (test E2E)
RSpec.describe 'Conversations', type: :system do
let(:user) { create(:user) }
before { sign_in user }
it 'allows user to start a new conversation' do
visit conversations_path
click_on 'Nouvelle conversation'
fill_in 'Votre question', with: 'Quelles sont les règles du RGPD ?'
click_on 'Envoyer'
expect(page).to have_content('Quelles sont les règles du RGPD ?')
expect(page).to have_css('.ai-response', wait: 15)
end
it 'saves conversation history' do
conversation = create(:conversation, user: user, title: 'RGPD')
create(:message, conversation: conversation, role: 'user', content: 'Question test')
create(:message, conversation: conversation, role: 'assistant', content: 'Réponse test')
visit conversation_path(conversation)
expect(page).to have_content('Question test')
expect(page).to have_content('Réponse test')
end
end
# spec/services/rag_service_spec.rb
RSpec.describe RagService do
describe '.answer' do
let!(:document) do
create(:document, content: 'Le RGPD impose une base légale pour tout traitement de données personnelles.')
end
before { SemanticSearch.index_document(document) }
it 'answers based on indexed documents' do
VCR.use_cassette('rag_answer') do
answer = RagService.answer('Que dit le RGPD sur les données personnelles ?')
expect(answer).to include('base légale').or include('données personnelles')
end
end
it 'indicates when no relevant context is found' do
VCR.use_cassette('rag_no_context') do
answer = RagService.answer('Comment faire une omelette ?')
expect(answer).to include('pas').or include('contexte')
end
end
end
end
Métriques de qualité
## Checklist de qualité projet final
### Code
- [ ] Test coverage > 80%
- [ ] Aucune erreur RuboCop
- [ ] Pas de N+1 queries (Bullet gem)
- [ ] Seeds fonctionnels pour démo
### Performance
- [ ] Page load < 2 secondes
- [ ] Réponse IA streaming < 500ms (premier token)
- [ ] Base de données avec index appropriés
### Sécurité
- [ ] Authentification (Devise)
- [ ] Autorisations (Pundit)
- [ ] Strong Parameters partout
- [ ] HTTPS forcé
- [ ] Variables d environnement pour les secrets
### Déploiement
- [ ] CI/CD fonctionnel
- [ ] Heroku (ou alternative) configuré
- [ ] Domain custom (optionnel)
- [ ] Monitoring (Sentry)
Préparer votre pitch
Le pitch est une présentation de 10 minutes qui raconte l'histoire de votre projet :
## Structure du pitch (10 minutes)
### 1. Le problème (2 min)
- Quel problème résolvez-vous ?
- Pour qui ? (persona)
- Pourquoi c est important ? (chiffres)
### 2. La solution (2 min)
- Votre application en une phrase
- Les 3 features clés
- Pourquoi l IA est nécessaire (pas juste cool)
### 3. Démo live (4 min)
- Scénario concret (pas un tour de features)
- Montrer le happy path complet
- Montrer une feature IA impressionnante
### 4. Architecture technique (1 min)
- Diagramme simple
- Technologies clés et pourquoi
- Un défi technique et comment vous l avez résolu
### 5. Conclusion (1 min)
- Ce que vous avez appris
- Prochaines étapes
- Call to action
Préparer la démo
## Checklist démo
### Avant la démo
- [ ] Données de seed chargées et visuellement attrayantes
- [ ] Connexion internet stable (ou démo offline si possible)
- [ ] Un compte de démo pré-créé
- [ ] Screenshots en backup si la démo plante
- [ ] Navigateur en mode présentation (clean, pas d onglets)
### Pendant la démo
- [ ] Raconter une HISTOIRE, pas lister des features
"Marie est avocate. Elle reçoit 50 questions par jour..."
- [ ] Montrer le problème AVANT la solution
- [ ] Interagir avec l app en temps réel
- [ ] Montrer une réponse IA en streaming (effet wow)
- [ ] Garder le rythme (pas de temps mort)
### Si quelque chose plante
- Restez calme
- "Comme dans tout logiciel en production, des bugs arrivent !"
- Montrez le screenshot de backup
- Passez à la suite
Portfolio et GitHub
## README professionnel pour votre repo
# 🤖 LegalAI — Assistant Juridique Intelligent
> Application web Rails permettant aux professionnels du droit
> de consulter une base juridique via un chat IA (RAG + GPT-4).
## Features
- Chat IA avec réponses sourcées (articles de loi)
- Base documentaire avec recherche sémantique
- Historique de conversations
- Dashboard d analytics
## Tech Stack
- Ruby on Rails 7.1
- PostgreSQL + pgvector
- OpenAI API (GPT-4 + embeddings)
- Hotwire (Turbo + Stimulus)
- Bootstrap 5
## Screenshots
[Screenshots de l app ici]
## Demo
🔗 [Live Demo](https://legalai.herokuapp.com)
## Installation
[Instructions de setup local]