Concevoir les modèles Django d’une application d’entretien IA
Concevez des modèles Django clairs pour une application d’entretien IA : relations entre entretien, questions et réponses, prêtes à évoluer.
Long Nguyen
Développeur fullstack · Ingénieur IA · Chercheur
Ce qu’un entretien produit réellement
Avant d’écrire le moindre champ, il est utile de concevoir les modèles Django d’une application d’entretien IA à partir de ce qu’est réellement un entretien, plutôt que de choisir la solution la plus rapide à coder. Dans la première partie, nous avons choisi la stack et planifié les fonctionnalités. Nous allons maintenant donner à ce plan une véritable structure de données.
Réfléchissez à ce qu’une session d’entretien génère :
- un entretien associé à un intitulé de poste et à un CV, qui suit un cycle de vie allant de l’état en attente à l’état terminé ;
- un ensemble de questions, chacune associée à un thème, à un niveau de difficulté et à une position dans l’entretien ;
- une réponse du candidat et une note associées à chaque question ;
- un rapport final qui résume l’ensemble de la session.
Commencez par modéliser le domaine et les champs s’imposeront presque d’eux-mêmes. Si vous passez directement aux champs, vous devrez remanier votre schéma plus tard — et c’est le type de changement qui coûte le plus cher une fois que les données réelles se sont accumulées.
Les modèles principaux
Le flux d’entretien repose sur trois modèles : Interview, InterviewQuestion et InterviewReport. L’entretien est la session parente ; les questions lui appartiennent et le rapport la résume. Un petit modèle complémentaire, DeviceToken, permet à l’application de reconnaître un appareil déjà utilisé sans imposer de connexion.
import uuid
from django.db import models
class DeviceToken(TimeInfo):
token_hash = models.CharField(max_length=64, unique=True, db_index=True)
class Meta:
db_table = "device_tokens"
class Interview(TimeInfo):
INTERVIEW_STATUS = [
(1, "Pending"),
(2, "Processing Resume"),
(3, "Preparing Interview"),
(4, "In Progress"),
(5, "Started"),
(6, "Generating Report"),
(7, "Completed"),
(8, "Failed"),
]
job_title = models.CharField(max_length=255, null=True, blank=True)
interview_uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
cv_file = models.FileField(upload_to="storage/files/", null=True, blank=True)
cv_text = models.TextField(null=True, blank=True)
status = models.SmallIntegerField(default=1, choices=INTERVIEW_STATUS)
candidate_name = models.CharField(max_length=100, null=True, blank=True)
candidate_email = models.EmailField(null=True, blank=True)
device = models.ForeignKey(
DeviceToken, on_delete=models.SET_NULL, null=True, blank=True
)
client_ip = models.GenericIPAddressField(
null=True, blank=True, db_index=True
) # for per-IP daily rate limit
class Meta:
db_table = "interviews"
def __str__(self):
return str(self.interview_uuid)
class InterviewQuestion(TimeInfo):
QUESTION_LEVEL_CHOICES = (
("easy", "Easy"),
("medium", "Medium"),
("hard", "Hard"),
)
interview = models.ForeignKey(Interview, on_delete=models.CASCADE)
topic = models.CharField(max_length=255, null=True, blank=True)
position = models.IntegerField(default=1)
question = models.TextField(null=True, blank=True)
sample_answer = models.TextField(null=True, blank=True) # suggested model answer
user_answer = models.TextField(null=True, blank=True)
user_score = models.SmallIntegerField(null=True, blank=True)
reason_score = models.TextField(null=True, blank=True)
level = models.CharField(
max_length=10, null=True, blank=True, choices=QUESTION_LEVEL_CHOICES
)
record = models.FileField(
upload_to="storage/audio/", null=True, blank=True
) # voice answer recording
class Meta:
db_table = "interview_questions"
def __str__(self):
return f"{self.question}"
class InterviewReport(TimeInfo):
interview = models.OneToOneField(Interview, on_delete=models.CASCADE)
content = models.TextField(null=True, blank=True)
is_passed = models.BooleanField(default=False)
class Meta:
db_table = "interview_reports"
Quelques choix méritent d’être soulignés. Le champ status utilise un cycle de vie à choix entiers afin que l’étape exacte de l’entretien — du traitement du CV à la génération du rapport — soit toujours explicite et jamais laissée à l’interprétation. Chaque question possède son propre topic, son level et sa position : l’entretien peut ainsi être reconstitué dans le bon ordre, avec une difficulté progressive, tout en conservant côte à côte un sample_answer (une réponse solide suggérée) et la user_answer du candidat. Le champ record stocke l’enregistrement audio d’une réponse orale, ce qui permet ensuite de proposer un véritable entraînement vocal. Enfin, interview_uuid attribue à chaque session un identifiant public non séquentiel : les URL ne révèlent donc pas le nombre d’entretiens existants et ne permettent pas de deviner le suivant.
Remarquez également que device et client_ip sont rattachés à l’entretien. L’application est volontairement conçue sans connexion : elle s’appuie donc sur un token d’appareil haché et sur l’adresse IP du client pour appliquer des limites quotidiennes équitables par appareil et par IP. Les utilisateurs peuvent s’entraîner sérieusement, tandis que l’utilisation abusive des endpoints d’IA reste limitée.
Relations et choix de on_delete
Les relations ont une véritable portée fonctionnelle. Elles méritent donc une décision réfléchie plutôt qu’un réglage copié par défaut :
| Relation | Type de champ | Signification | on_delete |
|---|---|---|---|
| Interview → InterviewQuestion | ForeignKey | Un entretien possède plusieurs questions | CASCADE |
| Interview → InterviewReport | OneToOneField | Chaque entretien possède exactement un rapport | CASCADE |
| Interview → DeviceToken | ForeignKey | Un entretien est associé à l’appareil qui l’a démarré | SET_NULL |
on_delete=CASCADE est le bon choix pour les questions et le rapport : il s’agit de données liées à la session, donc la suppression d’un entretien doit entraîner leur suppression. Laisser des lignes orphelines serait un bug, pas une fonctionnalité. Utiliser OneToOneField pour le rapport fait également respecter cette règle au niveau de la base de données : un entretien ne peut jamais se retrouver accidentellement associé à deux rapports. La relation avec l’appareil utilise plutôt SET_NULL, car un appareil et un entretien ont des cycles de vie indépendants : supprimer un token d’appareil ne doit pas supprimer les entretiens déjà réalisés.
Exécuter vos premières migrations
Les modèles ne sont que du Python tant que vous ne les avez pas transformés en tables de base de données. Django procède en deux étapes : la première génère le fichier de migration, la seconde l’applique :
python manage.py makemigrations
python manage.py migrate
makemigrations lit vos modèles et écrit une migration qui décrit la modification ; migrate l’exécute sur la base de données. Il est utile d’ouvrir au moins une fois le fichier de migration généré pour voir le SQL que Django s’apprête à exécuter. Comprendre ce qui se passe en coulisses évite que les migrations ressemblent à de la magie lorsqu’un problème survient.
Un avertissement pratique : SQLite est pratique pour le développement local, mais il n’applique pas toutes les contraintes comme PostgreSQL — la longueur des champs et certaines validations se comportent différemment. Si vous prévoyez un déploiement sur PostgreSQL, développez assez tôt avec cette base afin de ne pas découvrir ces différences en production.
C’est le cœur du modèle de données de l’entretien. L’application complète ajoute plusieurs couches : un système de tâches en arrière-plan pour exécuter le traitement en dehors du cycle de la requête, la suppression logique des anciens entretiens, ainsi que les éléments nécessaires pour rendre les réponses vocales et la protection contre les abus adaptées à la production. Ces composants, ainsi que le reste du projet, sont réunis dans le kit de démarrage. Sinon, la troisième partie est consacrée à l’importation du CV et à l’extraction de texte propre depuis des CV PDF avec pdfplumber.
FAQ
Questions fréquentes
Pourquoi séparer Question et Answer en deux modèles ?
Cette séparation permet à chaque question de posséder ses propres métadonnées et à chaque réponse sa propre note et ses propres commentaires. Les regrouper obligerait à utiliser des champs pouvant être nuls et compliquerait les requêtes par question à mesure que l’application évolue.
SQLite ou PostgreSQL pour ce projet ?
SQLite convient au développement local, mais son comportement diffère de celui de PostgreSQL en matière de validations et de contraintes. Si vous prévoyez un déploiement, développer dès le début avec PostgreSQL permet d’éviter les mauvaises surprises.
Que fait on_delete=CASCADE ici ?
La suppression d’un entretien supprime automatiquement ses questions et ses réponses, ce qui évite de laisser des lignes orphelines. Pour une application organisée autour de sessions, c’est généralement le comportement recherché.