Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ release du même nom.

## [Non publié] — 1.2.0

- #44 — Import/analyse des CV : au chargement d'un CV, si une IA est configurée,
une case (cochée par défaut, avec un avertissement) propose de l'**analyser**.
L'IA extrait alors les informations principales (profil, expériences,
formations, compétences, langues, infos diverses), enregistrées **par CV** et
consultables depuis la fiche du CV (clic dans la liste). L'analyse est remise à
zéro à chaque (ré)analyse, relançable à la demande depuis la fiche.
- #43 — Sites d'emploi : suppression de la gestion des **identifiants et mots de
passe**. Seuls le nom, l'URL et le logo d'un site sont désormais conservés ;
les champs `username`/`password` (et leur stockage chiffré) sont retirés du
Expand Down
21 changes: 20 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ docker compose up -d --build # → http://127.0.0.1:53487/
`JobSite` (nom, URL, `is_builtin`, `logo_url` — plus d'identifiants depuis
l'issue #43), `Candidature` (cœur
du suivi, étapes de progression + `motif_cloture` = clôture), `StatusHistory`,
`Reminder`, `Interview`, `Contact`, `ApiToken`, `CV`, `AIConfig` (singleton de
`Reminder`, `Interview`, `Contact`, `ApiToken`, `CV` (avec analyse IA des
informations principales — champs `analysis`/`analyzed_at`/… , issue #44),
`AIConfig` (singleton de
config du coaching IA, clé Gemini chiffrée — issue #33). Énumérations
`TextChoices` : `Source`, `Canal`, `Statut`, `MotifCloture` (certaines avec icône
emoji dans le libellé pour les menus).
Expand All @@ -75,6 +77,23 @@ emoji dans le libellé pour les menus).
catégorie IA, issue #34). Endpoints POST AJAX `api/coaching/` (bilan) et
`api/candidatures/<pk>/relance/` (mail de relance) ; UI = modal partagé
`#ai-modal` dans `base.html` (spinner + rendu Markdown).
- Analyse de CV (issue #44) : `coaching.analyze_cv(cv)` demande à l'IA un JSON
structuré (profil, expériences, formations, compétences, langues, coordonnées/
références — adresse, téléphone, email, permis —, loisirs, infos diverses),
normalisé et stocké dans `CV.analysis`. CV joint pour Gemini, texte brut pour
les autres fournisseurs (formats texte seulement). Déclenchée au chargement via
la case « Analyser » (vue `cv_create`) ou à la demande (`cv_analyze`) ;
résultat affiché sur la fiche `cv_detail`. Le profil donne aussi une
`localisation`, et chaque expérience/formation un `lieu` + un `lien` (URL du
site, validée http(s) via `_as_url`). La fiche affiche les expériences et
formations en timeline et **cartographie les lieux** (`_cv_localisations`) avec
**OpenStreetMap/Leaflet** (géocodage **Nominatim**, marqueur emoji par type,
popup société) — aucune clé API requise.
- Exports de CV (issue #44) : `tracking/cv_export.py` convertit `CV.analysis` en
**JSON Resume**, **Europass** (SkillsPassport) et **HR-Open Standards**
(`EXPORTERS`/`EXPORT_LABELS`, stdlib) ; vues `cv_export` (téléchargement JSON) et
`cv_print` (template `cv_print.html` autonome, **PDF via impression navigateur**).
Boutons sur la fiche `cv_detail`.
- Quotas IA (issue #36) : `ai.generate` renvoie un `GenerationResult` (texte +
tokens) ; `coaching._run` journalise chaque appel dans `AIUsage`. `AIConfig`
porte une limite mensuelle de tokens par fournisseur (0 = illimitée) ; la
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,14 @@ données.
| `/` (`/candidatures/`) | Liste des candidatures (créer / modifier / consulter) |
| `/sites/` | Sites d'emploi : ajout, modification, suppression, logo (#366) |
| `/stats/` | Statistiques (#367 — premiers KPI) |
| `/cv/` | CV : chargement et suppression (#368) |
| `/cv/` | CV : chargement, analyse IA des informations principales et suppression (#368, #44) |
| `/aide/` | **Options** : apparence (thème), jetons de l'extension Chrome et configuration du coaching IA (#33) |

## Prochaines itérations

- #367 : KPI complets (taux de réponse par source, délais moyens, graphes)
- #368 : reformatage du CV (technique/pro) + import LinkedIn
- #365 : intégration API France Travail, rappels actifs et notifications

> L'analyse IA du CV (extraction des informations principales) est disponible
> depuis l'issue #44.
323 changes: 323 additions & 0 deletions templates/tracking/cv_detail.html

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions templates/tracking/cv_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ <h1>{{ title }}</h1>
{% csrf_token %}
{{ form.as_p }}
<p class="muted">Formats acceptés : PDF, DOC, DOCX, ODT, RTF, TXT.</p>
{% if ai_config.is_configured %}
<div style="margin:1rem 0; padding:0.8rem 1rem; border:1px solid var(--border); border-left:4px solid var(--accent); border-radius:8px;">
<p style="margin:0 0 0.5rem;">⚠️ Ce CV sera analysé par l'IA configurée
(<strong>{{ ai_config.get_provider_display }}</strong>) pour en extraire
automatiquement les informations principales (expériences, formations…).</p>
<label style="display:flex; gap:0.5rem; align-items:center; margin:0;">
<input type="checkbox" name="analyser" checked>
Analyser ce CV avec l'IA
</label>
</div>
{% endif %}
<div style="margin-top:1rem;">
<button type="submit" class="btn">Charger</button>
<a class="btn secondary" href="{% url 'tracking:cv_list' %}">Annuler</a>
Expand Down
13 changes: 7 additions & 6 deletions templates/tracking/cv_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@
<h1 style="margin:0;">Mes CV</h1>
<a class="btn" href="{% url 'tracking:cv_create' %}">+ Charger un CV</a>
</div>
<p class="muted">Le reformatage en formats techniques/professionnels et l'import LinkedIn
sont prévus pour une prochaine itération.</p>

<div class="card">
{% if cvs %}
<table>
<thead><tr><th>Libellé</th><th>Fichier</th><th>Ajouté le</th><th>Actions</th></tr></thead>
<thead><tr><th>Libellé</th><th>Fichier</th><th>Analyse IA</th><th>Ajouté le</th><th>Actions</th></tr></thead>
<tbody>
{% for cv in cvs %}
<tr>
<td>{{ cv.label }}</td>
<td><a href="{% url 'tracking:cv_detail' cv.pk %}">{{ cv.label }}</a></td>
<td><a href="{{ cv.file.url }}" target="_blank" rel="noopener">Télécharger</a></td>
<td>{% if cv.is_analyzed %}<span class="badge">✓ analysé</span>{% elif cv.analysis_error %}<span class="muted" title="{{ cv.analysis_error }}">⚠️ échec</span>{% else %}<span class="muted">—</span>{% endif %}</td>
<td>{{ cv.uploaded_at|date:"d/m/Y H:i" }}</td>
<td class="row-actions"><a href="{% url 'tracking:cv_delete' cv.pk %}">Supprimer</a></td>
<td class="row-actions">
<a href="{% url 'tracking:cv_detail' cv.pk %}">Détails</a>
<a href="{% url 'tracking:cv_delete' cv.pk %}">Supprimer</a>
</td>
</tr>
{% endfor %}
</tbody>
Expand Down
136 changes: 136 additions & 0 deletions templates/tracking/cv_print.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<!doctype html>
{% with a=cv.analysis %}
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ cv.label }} — CV</title>
<style>
:root { --ink:#0b132b; --soft:#5a6b85; --accent:#3a506b; --line:#d7dee8; }
* { box-sizing:border-box; }
body { margin:0; background:#eceff4; color:var(--ink);
font:14px/1.5 "Segoe UI", Arial, sans-serif; }
.sheet { max-width:780px; margin:1.5rem auto; background:#fff;
padding:2.4rem 2.6rem; box-shadow:0 8px 30px rgba(16,24,40,0.18); }
.toolbar { max-width:780px; margin:1rem auto 0; display:flex; gap:0.5rem;
justify-content:flex-end; }
.toolbar button, .toolbar a { font:inherit; cursor:pointer; text-decoration:none;
padding:0.5rem 1rem; border-radius:8px; border:1px solid var(--accent);
background:var(--accent); color:#fff; }
.toolbar a.ghost { background:#fff; color:var(--accent); }

header.cv { border-bottom:3px solid var(--accent); padding-bottom:1rem; margin-bottom:1.4rem; }
header.cv h1 { margin:0; font-size:1.9rem; letter-spacing:0.2px; }
header.cv .role { color:var(--accent); font-size:1.1rem; font-weight:600; margin-top:0.2rem; }
.contact { margin-top:0.7rem; color:var(--soft); font-size:0.9rem;
display:flex; flex-wrap:wrap; gap:0.3rem 1rem; }

section.block { margin:1.3rem 0; }
section.block > h2 { font-size:1rem; text-transform:uppercase; letter-spacing:0.08em;
color:var(--accent); border-bottom:1px solid var(--line);
padding-bottom:0.3rem; margin:0 0 0.7rem; }
.entry { margin-bottom:0.9rem; page-break-inside:avoid; }
.entry .top { display:flex; justify-content:space-between; gap:1rem; }
.entry .title { font-weight:700; }
.entry .org { color:var(--accent); }
.entry .period { color:var(--soft); font-size:0.85rem; white-space:nowrap; }
.entry .place { color:var(--soft); font-size:0.85rem; }
.entry .desc { margin-top:0.2rem; }
.tags { display:flex; flex-wrap:wrap; gap:0.4rem; }
.tag { background:#eef2f8; color:var(--accent); border:1px solid var(--line);
border-radius:999px; padding:0.18rem 0.7rem; font-size:0.85rem; }
.muted { color:var(--soft); }
a { color:var(--accent); }

@media print {
body { background:#fff; }
.sheet { box-shadow:none; margin:0; max-width:none; padding:0 0.4cm; }
.toolbar { display:none; }
@page { margin:1.4cm; }
}
</style>
</head>
<body>
<div class="toolbar">
<button type="button" onclick="window.print()">🖨️ Imprimer / Enregistrer en PDF</button>
<a class="ghost" href="{% url 'tracking:cv_detail' cv.pk %}">← Retour à la fiche</a>
</div>

<div class="sheet">
<header class="cv">
<h1>{{ cv.label }}</h1>
{% if a.titre_profil %}<div class="role">{{ a.titre_profil }}</div>{% endif %}
<div class="contact">
{% with c=a.coordonnees %}
{% if c.adresse %}<span>🏠 {{ c.adresse }}</span>{% endif %}
{% if a.localisation %}<span>📍 {{ a.localisation }}</span>{% endif %}
{% if c.telephone %}<span>📞 {{ c.telephone }}</span>{% endif %}
{% if c.email %}<span>✉️ {{ c.email }}</span>{% endif %}
{% if c.permis %}<span>🚗 {{ c.permis }}</span>{% endif %}
{% endwith %}
</div>
</header>

{% if a.experiences %}
<section class="block">
<h2>Expériences professionnelles</h2>
{% for exp in a.experiences %}
<div class="entry">
<div class="top">
<span class="title">{{ exp.poste }}{% if exp.entreprise %} <span class="org">· {{ exp.entreprise }}</span>{% endif %}</span>
{% if exp.periode %}<span class="period">{{ exp.periode }}</span>{% endif %}
</div>
{% if exp.lieu %}<div class="place">📍 {{ exp.lieu }}{% if exp.lien %} · <a href="{{ exp.lien }}">{{ exp.lien }}</a>{% endif %}</div>
{% elif exp.lien %}<div class="place"><a href="{{ exp.lien }}">{{ exp.lien }}</a></div>{% endif %}
{% if exp.description %}<div class="desc">{{ exp.description }}</div>{% endif %}
</div>
{% endfor %}
</section>
{% endif %}

{% if a.formations %}
<section class="block">
<h2>Formations</h2>
{% for f in a.formations %}
<div class="entry">
<div class="top">
<span class="title">{{ f.intitule }}{% if f.etablissement %} <span class="org">· {{ f.etablissement }}</span>{% endif %}</span>
{% if f.periode %}<span class="period">{{ f.periode }}</span>{% endif %}
</div>
{% if f.lieu %}<div class="place">📍 {{ f.lieu }}</div>{% endif %}
</div>
{% endfor %}
</section>
{% endif %}

{% if a.competences %}
<section class="block">
<h2>Compétences</h2>
<div class="tags">{% for c in a.competences %}<span class="tag">{{ c }}</span>{% endfor %}</div>
</section>
{% endif %}

{% if a.langues %}
<section class="block">
<h2>Langues</h2>
<div class="tags">{% for l in a.langues %}<span class="tag">{{ l }}</span>{% endfor %}</div>
</section>
{% endif %}

{% if a.loisirs %}
<section class="block">
<h2>Loisirs</h2>
<div class="tags">{% for h in a.loisirs %}<span class="tag">{{ h }}</span>{% endfor %}</div>
</section>
{% endif %}

{% if a.infos %}
<section class="block">
<h2>Informations diverses</h2>
<p class="muted" style="margin:0;">{{ a.infos|linebreaksbr }}</p>
</section>
{% endif %}
</div>
</body>
</html>
{% endwith %}
Loading
Loading