Section 11.3.14 : Projet Job Tracker — Architecture et Kanban Board
🎯 Objectif pédagogique
Concevoir et implémenter un Job Tracker personnel avec interface Kanban. C'est le début d'un projet fil rouge qui s'étend sur 4 sections — un outil réel que Marc utilisera tous les jours.
De la théorie au projet réel
Marc a appris les bases de données, Python, les APIs, l'automatisation. Il est temps de tout combiner dans un projet concret : un Job Tracker — son outil personnel pour gérer sa recherche d'emploi avec un tableau Kanban visuel.
Architecture du projet
job-tracker/
├── app.py # Application principale Flask/Streamlit
├── database.py # Couche base de données (SQLite)
├── models.py # Modèles de données
├── kanban.py # Logique du Kanban board
├── notifications.py # Rappels et alertes
├── dashboard.py # Statistiques et graphiques
├── importer.py # Import CSV/LinkedIn
├── requirements.txt # Dépendances
├── data/
│ └── jobs.db # Base de données SQLite
└── tests/
├── test_database.py
└── test_kanban.py
Couche base de données
# database.py — SQLite avec un ORM léger
import sqlite3
from datetime import datetime
from pathlib import Path
DB_PATH = Path(__file__).parent / "data" / "jobs.db"
def get_connection():
"""Retourne une connexion à la base de données."""
DB_PATH.parent.mkdir(exist_ok=True)
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn
def init_db():
"""Crée les tables si elles n'existent pas."""
conn = get_connection()
conn.executescript("""
CREATE TABLE IF NOT EXISTS applications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
company TEXT NOT NULL,
position TEXT NOT NULL,
status TEXT DEFAULT 'wishlist',
salary_min INTEGER,
salary_max INTEGER,
location TEXT,
remote TEXT DEFAULT 'unknown',
url TEXT,
contact_name TEXT,
contact_email TEXT,
notes TEXT,
applied_date TEXT,
last_update TEXT,
created_at TEXT DEFAULT (datetime('now')),
archived INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
application_id INTEGER NOT NULL,
event_type TEXT NOT NULL,
event_date TEXT NOT NULL,
description TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (application_id) REFERENCES applications(id)
);
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL
);
CREATE TABLE IF NOT EXISTS application_tags (
application_id INTEGER,
tag_id INTEGER,
PRIMARY KEY (application_id, tag_id),
FOREIGN KEY (application_id) REFERENCES applications(id),
FOREIGN KEY (tag_id) REFERENCES tags(id)
);
""")
conn.commit()
conn.close()
# CRUD Operations
def add_application(company, position, **kwargs):
"""Ajouter une nouvelle candidature."""
conn = get_connection()
fields = ['company', 'position'] + list(kwargs.keys())
values = [company, position] + list(kwargs.values())
placeholders = ', '.join(['?'] * len(values))
columns = ', '.join(fields)
cursor = conn.execute(
f"INSERT INTO applications (\{columns\}) VALUES (\{placeholders\})",
values
)
app_id = cursor.lastrowid
# Log l'événement
conn.execute(
"INSERT INTO events (application_id, event_type, event_date, description) VALUES (?, ?, ?, ?)",
(app_id, 'created', datetime.now().isoformat(), f"Candidature créée : \{position\} chez \{company\}")
)
conn.commit()
conn.close()
return app_id
def update_status(app_id, new_status):
"""Mettre à jour le statut (déplacement Kanban)."""
conn = get_connection()
conn.execute(
"UPDATE applications SET status = ?, last_update = ? WHERE id = ?",
(new_status, datetime.now().isoformat(), app_id)
)
conn.execute(
"INSERT INTO events (application_id, event_type, event_date, description) VALUES (?, ?, ?, ?)",
(app_id, 'status_change', datetime.now().isoformat(), f"Statut → \{new_status\}")
)
conn.commit()
conn.close()
def get_applications_by_status(status=None, archived=False):
"""Récupérer les candidatures, optionnellement filtrées par statut."""
conn = get_connection()
if status:
rows = conn.execute(
"SELECT * FROM applications WHERE status = ? AND archived = ? ORDER BY last_update DESC",
(status, int(archived))
).fetchall()
else:
rows = conn.execute(
"SELECT * FROM applications WHERE archived = ? ORDER BY last_update DESC",
(int(archived),)
).fetchall()
conn.close()
return [dict(row) for row in rows]
Interface Kanban avec Streamlit
# kanban.py — Tableau Kanban visuel
import streamlit as st
from database import get_applications_by_status, update_status, add_application
KANBAN_COLUMNS = [
("wishlist", "💭 Wishlist", "#9CA3AF"),
("applied", "📨 Applied", "#3B82F6"),
("screening", "📞 Screening", "#F59E0B"),
("interview", "🎯 Interview", "#8B5CF6"),
("offer", "🏆 Offer", "#10B981"),
("rejected", "❌ Rejected", "#EF4444"),
]
def render_kanban():
"""Afficher le tableau Kanban."""
st.title("🎯 Job Tracker — Kanban Board")
# Bouton d'ajout
with st.expander("➕ Nouvelle candidature"):
with st.form("new_app"):
company = st.text_input("Entreprise")
position = st.text_input("Poste")
url = st.text_input("URL de l'offre")
salary = st.text_input("Fourchette salariale")
location = st.text_input("Localisation")
if st.form_submit_button("Ajouter"):
add_application(company, position, url=url, location=location)
st.success(f"✅ \{position\} chez \{company\} ajouté !")
st.rerun()
# Colonnes Kanban
cols = st.columns(len(KANBAN_COLUMNS))
for col, (status, label, color) in zip(cols, KANBAN_COLUMNS):
with col:
apps = get_applications_by_status(status)
st.markdown(f"### \{label\}")
st.markdown(f"**\{len(apps)\}** candidatures")
st.divider()
for app in apps:
with st.container(border=True):
st.markdown(f"**\{app['company']\}**")
st.caption(app['position'])
if app.get('salary_min'):
st.caption(f"💰 \{app['salary_min']\}—\{app['salary_max']\}€")
if app.get('location'):
st.caption(f"📍 \{app['location']\}")
# Boutons de déplacement
new_status = st.selectbox(
"Déplacer →",
[s for s, _, _ in KANBAN_COLUMNS if s != status],
key=f"move_\{app['id']\}"
)
if st.button("Déplacer", key=f"btn_\{app['id']\}"):
update_status(app['id'], new_status)
st.rerun()
Point d'entrée
# app.py — Application principale
import streamlit as st
from database import init_db
from kanban import render_kanban
# Configuration
st.set_page_config(
page_title="Job Tracker",
page_icon="🎯",
layout="wide"
)
# Initialiser la base de données
init_db()
# Sidebar navigation
page = st.sidebar.radio(
"Navigation",
["🎯 Kanban", "📊 Dashboard", "⚙️ Import"]
)
if page == "🎯 Kanban":
render_kanban()
elif page == "📊 Dashboard":
st.info("Le dashboard sera construit dans la section 11.3.16")
elif page == "⚙️ Import":
st.info("L'import CSV sera construit dans la section 11.3.17")
Lancer l'application
streamlit run app.py — L'application s'ouvre dans votre navigateur. C'est une vraie application web, pas un script terminal. Vous pouvez l'utiliser sur votre téléphone si vous la déployez sur Streamlit Cloud (gratuit).
🏋️ Exercice pratique (30 minutes)
- →Créez le dossier
job-tracker/avec les fichiersdatabase.py,kanban.py,app.py - →Installez les dépendances :
pip install streamlit - →Lancez
streamlit run app.py - →Ajoutez 5 candidatures fictives et déplacez-les dans le Kanban
- →Vérifiez que la base SQLite est créée dans
data/jobs.db
Pourquoi utiliser SQLite plutôt que PostgreSQL pour le Job Tracker ?
Quel est l'avantage d'utiliser Streamlit pour l'interface du Job Tracker ?
Apprendre l’IA, du premier prompt aux agents