Tous les guides
IntermédiaireÀ votre rythmeGuide gratuitSection 53/95

Projet Job Tracker — Architecture et Kanban Board

Pratiquez les fondations du développement : programmation, web, API, données et usages de l’IA. Des sections et exercices pour construire vos premiers projets.

💡

Contenu gratuit, accessible sans compte. Les outils et modèles évoluent : consultez le dossier actuel pour vérifier leurs versions. Modèles et sources

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)

  1. →Créez le dossier job-tracker/ avec les fichiers database.py, kanban.py, app.py
  2. →Installez les dépendances : pip install streamlit
  3. →Lancez streamlit run app.py
  4. →Ajoutez 5 candidatures fictives et déplacez-les dans le Kanban
  5. →Vérifiez que la base SQLite est créée dans data/jobs.db
Vérification rapide

Pourquoi utiliser SQLite plutôt que PostgreSQL pour le Job Tracker ?

Vérification rapide

Quel est l'avantage d'utiliser Streamlit pour l'interface du Job Tracker ?

Apprendre l’IA, du premier prompt aux agents

15 guides gratuitsQuiz, schémas et ateliersSans compte

Votre progression

Cette progression est déclarative et conservée dans votre navigateur. L’attestation finale ne constitue pas un diplôme ni une évaluation indépendante.

Voir ma progression et mon attestation

Apprendre l’IA, du premier prompt aux agents

15 guides gratuitsQuiz, schémas et ateliersSans compte

Un cas pratique IA dans votre boîte mail

Méthodes, ressources et nouveaux exercices. Gratuit, avec désinscription à tout moment.