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
2 changes: 1 addition & 1 deletion .github/workflows/sonarqube.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:

- name: Lancer les tests avec couverture
env:
# Clé Fernet éphémère : les tests chiffrent des mots de passe de sites.
# Clé Fernet éphémère : les tests chiffrent des clés API IA.
CANDITRACK_FERNET_KEY: ${{ secrets.SONAR_FERNET_KEY }}
run: |
# Génère une clé Fernet jetable si aucun secret n'est fourni.
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ release du même nom.

## [Non publié] — 1.2.0

- #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
modèle, du formulaire et de la liste.
- #41 — Ajout d'un skill Claude Code `web-development`
(`.claude/skills/web-development/`) qui formalise les principes de conception
de CandiTrack (thème clair/sombre, connexion aux IA, présentation UI,
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ docker compose up -d --build # → http://127.0.0.1:53487/

## Modèles (`tracking/models.py`)

`JobSite` (mot de passe chiffré, `is_builtin`, `logo_url`), `Candidature` (cœur
`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
config du coaching IA, clé Gemini chiffrée — issue #33). Énumérations
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,8 @@ chacun renseigne **sa propre clé API**.
**Options → IA**, à côté des infos de quota du tier gratuit).
2. Dans **Options → IA**, choisir le **fournisseur** et coller la clé (et,
facultativement, le modèle dans le menu déroulant). Chaque fournisseur garde
sa propre clé, son modèle et sa limite, stockés **chiffrés** en base (Fernet,
comme les mots de passe des sites) ; on bascule de l'un à l'autre sans
ressaisie.
sa propre clé, son modèle et sa limite, stockés **chiffrés** en base
(Fernet) ; on bascule de l'un à l'autre sans ressaisie.
3. Depuis la liste des candidatures, **« ✨ Coaching IA »** ouvre une fenêtre
modale : à partir du dernier CV chargé (analysé par Gemini) et des statistiques
(volume, motifs de refus, délais…), l'IA propose un positionnement et des
Expand Down
7 changes: 2 additions & 5 deletions templates/tracking/site_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
<h1 style="margin:0;">Sites d'emploi</h1>
<a class="btn" href="{% url 'tracking:site_create' %}">+ Ajouter un site</a>
</div>
<p class="muted">Les identifiants sont stockés avec le mot de passe <strong>chiffré au repos</strong>.
Le logo (favicon du site) est récupéré automatiquement à l'enregistrement.</p>
<p class="muted">Le logo (favicon du site) est récupéré automatiquement à l'enregistrement.</p>

<div class="card">
{% if sites %}
<table>
<thead><tr><th>Logo</th><th>Nom</th><th>URL</th><th>Identifiant</th><th>Mot de passe</th><th>Actions</th></tr></thead>
<thead><tr><th>Logo</th><th>Nom</th><th>URL</th><th>Actions</th></tr></thead>
<tbody>
{% for s in sites %}
<tr{% if not s.actif %} class="closed-row"{% endif %}>
Expand All @@ -20,8 +19,6 @@ <h1 style="margin:0;">Sites d'emploi</h1>
{% if s.is_builtin %} <span class="badge">défaut</span>{% endif %}
{% if not s.actif %} <span class="badge">désactivé</span>{% endif %}</td>
<td>{% if s.url %}<a href="{{ s.url }}" target="_blank" rel="noopener">{{ s.url }}</a>{% else %}—{% endif %}</td>
<td>{{ s.username|default:"—" }}</td>
<td>{% if s.password %}<span class="muted">••••••</span>{% else %}<span class="muted">—</span>{% endif %}</td>
<td class="row-actions" style="white-space:nowrap;">
<a href="{% url 'tracking:site_update' s.pk %}">Modifier</a>
<form method="post" action="{% url 'tracking:site_toggle_active' s.pk %}" style="display:inline;">
Expand Down
21 changes: 3 additions & 18 deletions tracking/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,38 +69,23 @@ def save(self, commit=True):
class JobSiteForm(forms.ModelForm):
"""Manual create/edit form for a job site (issue #366).

- The password is never pre-filled; leaving it blank on edit keeps the
stored (encrypted) value.
- When ``auto_logo`` is checked the logo is (re)fetched from the URL.
Les identifiants/mots de passe ne sont plus gérés (issue #43). Le logo est
récupéré depuis le favicon du site quand l'utilisateur n'en saisit pas.
"""

password = forms.CharField(
label="Mot de passe",
required=False,
widget=forms.PasswordInput(render_value=False),
help_text="Laisser vide pour conserver le mot de passe actuel.",
)
class Meta:
model = JobSite
fields = ["name", "url", "username", "password", "logo_url"]
fields = ["name", "url", "logo_url"]
widgets = {
"url": forms.URLInput(attrs={"placeholder": "https://www.exemple.fr/"}),
"logo_url": forms.URLInput(
attrs={"placeholder": "Laisser vide pour utiliser le favicon du site"}
),
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Capture the current (decrypted) password so a blank submission keeps it.
self._original_password = self.instance.password if self.instance.pk else ""

def save(self, commit=True):
instance = super().save(commit=False)

if not self.cleaned_data.get("password"):
instance.password = self._original_password

# Logo par défaut : le favicon du site (issue #27). On ne l'impose que si
# l'utilisateur n'a pas saisi de logo manuel.
if not instance.logo_url and instance.url:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 6.0.1 on 2026-06-14 12:44

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('tracking', '0017_aiconfig_anthropic_api_key_aiconfig_anthropic_model_and_more'),
]

operations = [
migrations.RemoveField(
model_name='jobsite',
name='password',
),
migrations.RemoveField(
model_name='jobsite',
name='username',
),
]
8 changes: 3 additions & 5 deletions tracking/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The schema anticipates the four board issues:
- #365 master plan: Candidature, StatusHistory, Reminder, Interview, Contact
- #366 job sites: JobSite (with an encrypted password)
- #366 job sites: JobSite (name, URL, logo)
- #367 statistics: built on top of Candidature aggregates
- #368 CV upload: CV
"""
Expand All @@ -23,14 +23,12 @@
class JobSite(models.Model):
"""A job board where applications are submitted (issue #366).

Credentials are optional; when a password is provided it is stored
encrypted at rest via :class:`EncryptedCharField`.
Les identifiants/mots de passe ne sont plus stockés (issue #43) : on ne
conserve que l'identité du site (nom, URL, logo) et son état.
"""

name = models.CharField("nom", max_length=100, unique=True)
url = models.URLField("URL", blank=True)
username = models.CharField("identifiant", max_length=200, blank=True)
password = EncryptedCharField("mot de passe", blank=True, default="")
logo_url = models.URLField("URL du logo", blank=True)
is_builtin = models.BooleanField("site par défaut", default=False)
# Un site désactivé reste en base mais n'est plus proposé pour de nouvelles
Expand Down
Loading