1. Gardez l’exemple simple
Dans une vraie API, vous pourriez aussi créer plusieurs notes d’un coup, les modifier ou les supprimer. Ce n’est pas le sujet aujourd’hui. Nous gardons un seul cas, très concret : créer une note avec un titre et un contenu.
La lecture et la création utilisent le même chemin /training/api/notes. GET lit les notes ; POST en crée une. Cette petite limite nous permet de suivre clairement les différentes pièces qui communiquent : cURL, la route Odoo, le contrôleur, l’ORM et PostgreSQL.
Méthode : POST
Chemin : /training/api/notes
Authentification : en-tête Authorization: Bearer ... obligatoire
Type du corps : application/json
Corps : {"title":"Un titre","body":"Un contenu"}
Réussite : 201 Created
Réponse : {"note":{"id":entier,"title":chaîne,"body":chaîne}}
Corps refusé : 400 Bad Request
Sans Bearer : 401 UnauthorizedLe contrôleur accepte seulement title et body. Il refuse les données incomplètes ou incorrectes avant de demander une écriture à l’ORM. Nous n’ajoutons ni création multiple, ni modification, ni suppression : ce seront d’autres besoins, dans une autre API.
2. Reliez ce contrat au modèle du jour 2
Vous ne créez pas un nouveau type de données. Le POST utilise les deux champs déjà définis dans le modèle training.note :
title = fields.Char(string="Titre", required=True)
body = fields.Text(string="Contenu")Le titre reste obligatoire. Le contenu est un véritable champ Text du modèle ; le client doit envoyer une chaîne, mais cette chaîne peut être explicitement vide avec "body": "". Le contrôleur transmet cette valeur à create() et la réponse utilise note.body or "" pour conserver une chaîne JSON lorsque le champ est vide.
3. Remplacez le contrôleur par sa version complète
Ouvrez controllers/notes_api.py et remplacez tout son contenu par le fichier suivant. La route GET de la section 3 garde son chemin, son authentification, sa recherche ORM, son ordre et son contrat de réponse. Le garde Bearer est ajouté aux deux méthodes, puis une seconde route traite seulement le POST.
from odoo import http
from odoo.http import request
def _require_bearer_header():
authorization = request.httprequest.headers.get("Authorization", "")
if authorization.startswith("Bearer "):
return None
return request.make_json_response(
{
"error": {
"code": "bearer_required",
"message": "Envoyez une clé d’API dans l’en-tête Authorization: Bearer.",
}
},
status=401,
)
def _invalid_payload_response():
return request.make_json_response(
{
"error": {
"code": "invalid_payload",
"message": "Envoyez un objet JSON avec title (texte non vide) et body (texte, éventuellement vide).",
}
},
status=400,
)
class TrainingNotesApiController(http.Controller):
@http.route(
"/training/api/notes",
type="http",
auth="bearer",
methods=["GET"],
)
def list_notes(self):
bearer_error = _require_bearer_header()
if bearer_error is not None:
return bearer_error
notes = request.env["training.note"].search_read(
fields=["id", "title", "body"],
order="id desc",
limit=20,
)
for note in notes:
note["body"] = note["body"] or ""
return request.make_json_response({
"notes": notes,
"count": len(notes),
})
@http.route(
"/training/api/notes",
type="http",
auth="bearer",
methods=["POST"],
csrf=False,
)
def create_note(self):
bearer_error = _require_bearer_header()
if bearer_error is not None:
return bearer_error
try:
payload = request.get_json_data()
except ValueError:
return _invalid_payload_response()
if not isinstance(payload, dict) or set(payload) != {"title", "body"}:
return _invalid_payload_response()
title = payload["title"]
body = payload["body"]
if not isinstance(title, str) or not title.strip():
return _invalid_payload_response()
if not isinstance(body, str):
return _invalid_payload_response()
note = request.env["training.note"].create({
"title": title.strip(),
"body": body,
})
return request.make_json_response(
{
"note": {
"id": note.id,
"title": note.title,
"body": note.body or "",
}
},
status=201,
)request.get_json_data() lit le corps JSON. Le contrôleur vérifie ensuite sa forme et ses types avant toute écriture. Seul le titre est nettoyé avec strip() ; le contenu est conservé tel que le client l’a envoyé.
request.env["training.note"].create(...) utilise l’ORM avec l’utilisateur associé à la clé. Odoo applique donc le droit de création et les éventuelles règles sur les enregistrements. Le dictionnaire donné à create() contient uniquement title et body : le client ne choisit aucun identifiant d’utilisateur et le contrôleur n’élève pas ses privilèges.
4. Distinguez la clé Bearer de la session du navigateur
Dans Odoo, auth="bearer" valide une clé reçue dans l’en-tête Authorization et retrouve l’utilisateur qui la possède. Le contrôleur ne stocke pas la clé et ne la compare jamais lui-même. Une valeur Bearer invalide est refusée par Odoo avant l’écriture.
Ce mode peut aussi s’appuyer sur une session Odoo déjà authentifiée lorsqu’aucun en-tête Bearer n’est fourni et que les contrôles prévus pour cet usage interactif sont satisfaits. Ce repli est utile à Odoo, mais il ne correspond pas au contrat de cette API. La fonction _require_bearer_header() exige donc explicitement un en-tête qui commence par Bearer avant que GET lise les notes ou que POST en crée une. Si une session seule atteint la fonction, elle reçoit une réponse JSON 401.
Cette fonction vérifie seulement la présence et le début de l’en-tête. Odoo reste responsable de vérifier la clé, son expiration et son propriétaire. N’ajoutez jamais une comparaison manuelle du secret dans le contrôleur.
5. Comprenez le choix de csrf=False
Une route POST de type http applique normalement la protection CSRF des formulaires de session. Ici, le client envoie du JSON et s’authentifie avec un secret placé explicitement dans l’en-tête Bearer ; il ne possède pas le jeton CSRF d’un formulaire Odoo. csrf=False désactive donc cette vérification pour cette route POST précise.
Cette désactivation ne transforme pas la route en écriture ouverte. Le décorateur exige toujours auth="bearer", et le garde refuse qu’un cookie de session du navigateur suffise à lui seul. L’entrée reste bornée à un objet title/body, puis l’ORM applique les droits de l’utilisateur authentifié.
6. Rechargez le code Python
Action qui redémarre Odoo : redémarrez votre processus Odoo local, puis attendez que les journaux indiquent que le serveur est prêt. Vous avez modifié un fichier Python ; actualiser le navigateur ne recharge pas le contrôleur.
Vous n’avez modifié ni modèle, ni XML, ni manifeste. Une mise à niveau du module n’est donc pas nécessaire. À ce stade, aucun nouvel enregistrement n’a encore été créé : la base changera seulement lors du POST valide de l’étape suivante.
Point de contrôle — relisez la surface d’écriture
Repérez une seule route POST, le chemin exact /training/api/notes, les seules clés title et body, le statut 201 et l’appel ORM sur training.note. Aucun nom de modèle, champ supplémentaire ou utilisateur ne vient du client.