Section 11.3.15 : Job Tracker — Rappels et notifications
🎯 Objectif pédagogique
Ajouter un système de rappels et notifications au Job Tracker : relances automatiques, alertes deadline, et résumé quotidien par email. Vous serez capable d'intégrer des notifications dans une application Python.
Ne jamais oublier une relance
Marc a 15 candidatures en cours. Certaines attendent depuis 10 jours — il faut relancer. Un entretien est dans 2 jours — il faut préparer. Sans système de rappels, des opportunités passent à la trappe.
Système de rappels
# notifications.py — Système de rappels et alertes
import sqlite3
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime, timedelta
from database import get_connection
import os
class NotificationManager:
def __init__(self):
self.rules = [
\{
"name": "Relance candidature",
"condition": self._needs_follow_up,
"message": self._follow_up_message,
"priority": "medium"
\},
\{
"name": "Entretien imminent",
"condition": self._interview_soon,
"message": self._interview_message,
"priority": "high"
\},
\{
"name": "Candidature stale",
"condition": self._is_stale,
"message": self._stale_message,
"priority": "low"
\}
]
def _needs_follow_up(self, app):
"""Candidature > 7 jours sans mise à jour, statut 'applied'."""
if app['status'] != 'applied':
return False
last = datetime.fromisoformat(app['last_update'] or app['created_at'])
return (datetime.now() - last).days >= 7
def _interview_soon(self, app):
"""Entretien dans les 48h."""
conn = get_connection()
events = conn.execute(
"""SELECT * FROM events
WHERE application_id = ? AND event_type = 'interview_scheduled'
ORDER BY event_date DESC LIMIT 1""",
(app['id'],)
).fetchone()
conn.close()
if not events:
return False
interview_date = datetime.fromisoformat(events['event_date'])
hours_until = (interview_date - datetime.now()).total_seconds() / 3600
return 0 < hours_until <= 48
def _is_stale(self, app):
"""Candidature > 30 jours sans mise à jour."""
if app['status'] in ('offer', 'rejected'):
return False
last = datetime.fromisoformat(app['last_update'] or app['created_at'])
return (datetime.now() - last).days >= 30
def _follow_up_message(self, app):
days = (datetime.now() - datetime.fromisoformat(
app['last_update'] or app['created_at']
)).days
return f"📧 Relancer \{app['company']\} — \{app['position']\} (\{days\} jours sans réponse)"
def _interview_message(self, app):
return f"🎯 ENTRETIEN IMMINENT — \{app['company']\} — Préparez-vous !"
def _stale_message(self, app):
return f"⚠️ \{app['company']\} — Aucune mise à jour depuis 30+ jours. Archiver ?"
def check_all(self):
"""Vérifier toutes les règles pour toutes les candidatures actives."""
conn = get_connection()
apps = conn.execute(
"SELECT * FROM applications WHERE archived = 0"
).fetchall()
conn.close()
notifications = []
for app in apps:
app_dict = dict(app)
for rule in self.rules:
if rule["condition"](app_dict):
notifications.append(\{
"app": app_dict,
"rule": rule["name"],
"message": rule["message"](app_dict),
"priority": rule["priority"]
\})
# Trier par priorité
priority_order = \{"high": 0, "medium": 1, "low": 2\}
notifications.sort(key=lambda n: priority_order[n["priority"]])
return notifications
def send_daily_digest(notifications, recipient_email):
"""Envoyer un email récapitulatif quotidien."""
if not notifications:
return
# Construire le contenu
high = [n for n in notifications if n['priority'] == 'high']
medium = [n for n in notifications if n['priority'] == 'medium']
low = [n for n in notifications if n['priority'] == 'low']
body = "🎯 JOB TRACKER — Rapport quotidien\n"
body += f"Date : \{datetime.now().strftime('%d/%m/%Y')\}\n"
body += "=" * 50 + "\n\n"
if high:
body += "🔴 URGENT :\n"
for n in high:
body += f" → \{n['message']\}\n"
body += "\n"
if medium:
body += "🟡 À FAIRE :\n"
for n in medium:
body += f" → \{n['message']\}\n"
body += "\n"
if low:
body += "🔵 INFO :\n"
for n in low:
body += f" → \{n['message']\}\n"
# Envoyer par email (SMTP)
smtp_server = os.environ.get("SMTP_SERVER", "smtp.gmail.com")
smtp_port = int(os.environ.get("SMTP_PORT", "587"))
smtp_user = os.environ.get("SMTP_USER")
smtp_pass = os.environ.get("SMTP_PASS")
if not smtp_user:
print("⚠️ SMTP non configuré. Notifications affichées :")
print(body)
return
msg = MIMEMultipart()
msg['Subject'] = f"Job Tracker — \{len(notifications)\} actions (\{datetime.now().strftime('%d/%m')\})"
msg['From'] = smtp_user
msg['To'] = recipient_email
msg.attach(MIMEText(body, 'plain', 'utf-8'))
with smtplib.SMTP(smtp_server, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
print(f"✅ Digest envoyé à \{recipient_email\}")
Intégration dans Streamlit
# Dans app.py — Ajouter la page Notifications
from notifications import NotificationManager
def render_notifications():
"""Page des notifications et rappels."""
st.title("🔔 Notifications & Rappels")
manager = NotificationManager()
notifications = manager.check_all()
if not notifications:
st.success("✅ Aucune action requise — tout est à jour !")
return
st.warning(f"⚠️ \{len(notifications)\} actions requises")
for notif in notifications:
priority_colors = \{"high": "🔴", "medium": "🟡", "low": "🔵"\}
icon = priority_colors[notif['priority']]
with st.container(border=True):
col1, col2 = st.columns([4, 1])
with col1:
st.markdown(f"\{icon\} **\{notif['message']\}**")
st.caption(f"Règle : \{notif['rule']\}")
with col2:
if notif['priority'] == 'medium':
if st.button("✅ Relancé", key=f"done_\{notif['app']['id']\}"):
update_status(notif['app']['id'], 'screening')
st.rerun()
Automatiser avec un script cron / scheduler
# daily_check.py — Script lancé quotidiennement
from notifications import NotificationManager, send_daily_digest
def main():
manager = NotificationManager()
notifications = manager.check_all()
print(f"[\{__import__('datetime').datetime.now()\}] \{len(notifications)\} notifications")
# Afficher dans le terminal
for n in notifications:
print(f" [\{n['priority'].upper()\}] \{n['message']\}")
# Envoyer par email
send_daily_digest(notifications, "marc@email.com")
if __name__ == "__main__":
main()
# Lancer quotidiennement avec cron (Linux/Mac) :
# crontab -e
# 0 8 * * * cd /path/to/job-tracker && python daily_check.py
# Ou avec Task Scheduler (Windows) :
# schtasks /create /sc daily /tn "JobTracker" /tr "python daily_check.py" /st 08:00
Alternative : Make + n'importe quel trigger
Au lieu d'un script cron, Marc peut aussi utiliser un scénario Make : Schedule (tous les jours à 8h) → HTTP Request (vers une API Flask locale ou Cloud Function) → Email. L'avantage : pas besoin d'un serveur toujours allumé.
🏋️ Exercice pratique (25 minutes)
- →Ajoutez
notifications.pyà votre Job Tracker - →Créez 3 candidatures test : une vieille (>7 jours), une récente, une avec entretien
- →Exécutez
python daily_check.pyet vérifiez les notifications - →Intégrez la page Notifications dans l'app Streamlit
Pourquoi utiliser un système de règles (rules) plutôt qu'un simple 'if' pour les notifications ?
Quelle est la différence entre un script cron et un scheduler Make pour les rappels ?