ZEP-23 : Elaboration de l'API des MPs

a marqué ce sujet comme résolu.

Je vous propose cette seconde révision :

Cartouche
ZEP 23
Titre Elaboration de l'API des MPs
Révision 2
Date de création 15 Janvier 2015
Dernière révision 22 Janvier 2015
Type Feature
Statut Rédaction

Contexte

Cette ZEP ci-présente est une ZEP fille de la ZEP-02 qui concerne l'élaboration d'une API au sens large, c'est-à-dire qu'elle traite toutes les questions générales comme l'authentification, les codes HTTPs, les erreurs, la sécurité, etc.

Suite à la ZEP 17 sur l'élaboration d'une API pour les membres, cette ZEP 23 voit le jour pour la rédaction de l'API des MPs.

API des MPs

Objectif

Permettre aux clients de récupérer, ajouter, modifier ou supprimer les MPs d'un membre donné et d'effectuer une première association entre deux modules puisque des MPs sont détenus par des membres.

Représentations

Sujet d'un MP
Exemple d'objet
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
    "pk": 654321,
    "title": "Titre du MP",
    "subtitle": "Sous-titre du MP",
    "pubdate": "1977-04-22T06:00:00Z",
    "author": {
        "id": 42,
        "username": "Clem",
        "email": "clem@zds.com"
    },
    "participants": [
        {
            "id": 42,
            "username": "Clem"
        },
        {
            "id": 5,
            "username": "Andr0"
        }
    ],
    "last_message": 123456
}
Colonnes
Colonne Type Description
pk long Identifiant du sujet
title String Titre du MP
subtitle String Sous-titre du MP
pubdate Date Date de publication du MP
author User Auteur du MP
participants Array User Liste des participants du MP
last_message Message Dernier message du MP
Message d'un MP
Exemple d'objet
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "pk": 123456,
    "privatetopic": 654321,
    "text": "# Contenu du message",
    "text_html": "<h1>Contenu du message</h1>",
    "pubdate": "1977-04-22T06:00:00Z",
    "update": "1977-04-22T06:00:00Z",
    "position_in_topic": 1,
    "author": {
        "pk": 42,
        "username": "Clem"
    }
}
Colonnes
Colonne Type Description
pk long Identifiant du message
privatetopic long Identifiant du sujet
text String Contenu du message en Markdown
text_html String Contenu du message en HTML
pubdate Date Date de publication du message
update Date Date de dernière édition du message
position_in_topic entier Position du message dans le sujet

Lister les MPs d'un membre

Fonctionnalité : Renvoie une liste paginée pour lister les MPs d'un membre connecté par 10.

URL : http://api.zestedesavoir.com/mps/

Méthode : GET

Headers :

  • Authorization: Bearer < access_token >

Paramètres :

  • page : Affiche les MPs de la page voulue.
  • filter :
    • lu : Filtre tous les messages lus.
    • non-lu : Filtre tous les messages non lus.
    • date : Récupère les messages à partir de cette date.
  • sort :
    • creation : Tri la liste par date de création.
    • modification : Tri la liste par date de modification.
    • last-message : Tri la liste par date du dernier message.
    • titre : Tri par ordre alphabétique.
  • search :
    • titre : Recherche les conversations privées par leurs noms.

Codes HTTP :

  • 200 : La réponse est renvoyée sans problème.
  • 401 : Problème avec l'authentification.
  • 403 : Pas les permissions nécessaires.
  • 404 : Le MP n'existe pas.

Permissions nécessaires :

  • Être authentifié
  • Être auteur ou dans les participants des MPs

Afficher un MP donné

Fonctionnalité : Renvoie les informations concernant un MP donné. Le message est alors considéré comme lu par le système pour l'utilisateur qui fait la requête.

URL : http://api.zestedesavoir.com/mps/{id}

Méthode : GET

Headers :

  • Authorization: Bearer < access_token >

Paramètres :

  • id : Identifiant du message privé

Codes HTTP :

  • 200 : La réponse est renvoyée sans problème.
  • 401 : Problème avec l'authentification.
  • 403 : Pas les permissions nécessaires.
  • 404 : Le MP n'existe pas.

Permissions nécessaires :

  • Être authentifié
  • Être auteur ou dans les participants des MPs

Création d'un nouveau MP

Fonctionnalité : Création d'un nouveau MP avec plusieurs participants

URL : http://api.zestedesavoir.com/mps/

Méthode : POST

Headers :

  • Authorization: Bearer < access_token >

Paramètres :

  • participants : Liste des membres participants à la conversation
  • title : Titre du MP
  • subtitle : Sous-titre du MP (facultatif)
  • text : Contenu du MP.

Codes HTTP :

  • 200 : La réponse est renvoyée sans problème.
  • 400 : Dès qu'il y a un problème avec les paramètres de la requête.
  • 401 : Problème avec l'authentification.
  • 403 : Pas les permissions nécessaires.
  • 404 : Si un participant n'existe pas.

Permissions nécessaires :

  • Être authentifié

Edition d'un MP

Fonctionnalité : Ajoute un ou plusieurs participants dans un MP donné.

URL : http://api.zestedesavoir.com/mps/{id}/

Méthode : PUT

Headers :

  • Authorization: Bearer < access_token >

Paramètres :

  • id : Identifiant de la conversation
  • participants : Liste des membres à ajouter dans la conversation.

Codes HTTP :

  • 200 : La réponse est renvoyée sans problème.
  • 400 : Dès qu'il y a un problème avec les paramètres de la requête.
  • 401 : Problème avec l'authentification.
  • 403 : Pas les permissions nécessaires.
  • 404 : Si un participant n'existe pas.

Permissions nécessaires :

  • Être authentifié
  • Être auteur ou dans les participants des MPs

Quitter une liste de MPs

Fonctionnalité : Se retirer d'une liste de conversations.

URL : http://api.zestedesavoir.com/mps/

Méthode : DELETE

Headers :

  • Authorization: Bearer < access_token >

Paramètres :

  • pks : La liste des identifiants des conversations à quitter séparé par une virgule.

Codes HTTP :

  • 200 : La réponse est renvoyée sans problème.
  • 401 : Problème avec l'authentification.
  • 403 : Pas les permissions nécessaires.
  • 404 : L'un des MPs n'existe pas.

Permissions nécessaires :

  • Être authentifié
  • Être auteur ou dans les participants des MPs

Quitter un MP

Fonctionnalité : Se retirer d'une conversation.

URL : http://api.zestedesavoir.com/mps/{id}/

Méthode : DELETE

Headers :

  • Authorization: Bearer < access_token >

Paramètres :

  • id : Identifiant de la conversation

Codes HTTP :

  • 200 : La réponse est renvoyée sans problème.
  • 400 : Dès qu'il y a un problème avec les paramètres de la requête.
  • 401 : Problème avec l'authentification.
  • 403 : Pas les permissions nécessaires.
  • 404 : Le MP n'existe pas.

Permissions nécessaires :

  • Être authentifié
  • Être auteur ou dans les participants des MPs

Lister les messages d'un MP donné

Fonctionnalité : Renvoie une liste paginée pour lister les messages d'un MP par 10.

URL : http://api.zestedesavoir.com/mps/{id}/messages/

Méthode : GET

Headers :

  • Authorization: Bearer < access_token >

Paramètres :

  • id : Identifiant d'une conversation.
  • page : Affiche les MPs de la page voulue.

Codes HTTP :

  • 200 : La réponse est renvoyée sans problème.
  • 401 : Problème avec l'authentification.
  • 403 : Pas les permissions nécessaires.
  • 404 : Le MP n'existe pas ou la page n'existe pas.

Permissions nécessaires :

  • Être authentifié
  • Être auteur ou dans les participants des MPs

Afficher le message d'un MP donné

Fonctionnalité : Renvoie les informations concernant un MP donné.

URL : http://api.zestedesavoir.com/mps/{id}/messages/{id-mes}/

Méthode : GET

Headers :

  • Authorization: Bearer < access_token >

Paramètres :

  • id : Identifiant d'une conversation
  • id-mes : Identifiant du message privé

Codes HTTP :

  • 200 : La réponse est renvoyée sans problème.
  • 401 : Problème avec l'authentification.
  • 403 : Pas les permissions nécessaires.
  • 404 : Le MP n'existe pas ou le message n'existe pas.

Permissions nécessaires :

  • Être authentifié
  • Être auteur ou dans les participants des MPs

Ajouter un nouveau message dans un MP

Fonctionnalité : Ajoute un message dans un MP de l'utilisateur.

URL : http://api.zestedesavoir.com/mps/{id}/messages/

Méthode : POST

Headers :

  • Authorization: Bearer < access_token >

Paramètres :

  • id : Identifiant d'une conversation
  • id-mes :
  • text : Contenu du message

Codes HTTP :

  • 200 : La réponse est renvoyée sans problème.
  • 400 : Dès qu'il y a un problème avec les paramètres de la requête.
  • 401 : Problème avec l'authentification.
  • 403 : Pas les permissions nécessaires.
  • 404 : Le MP n'existe pas.

Permissions nécessaires :

  • Être authentifié
  • Être auteur ou dans les participants des MPs

Editer un message dans un MP

Fonctionnalité : Editer le dernier message d'un MP si c'est celui de l'utilisateur.

URL : http://api.zestedesavoir.com/mps/{id}/messages/{id-mes}/

Méthode : PUT

Headers :

  • Authorization: Bearer < access_token >

Paramètres :

  • id : Identifiant d'une conversation
  • id-mes : Identifiant du message privé
  • text : Nouveau contenu du message

Codes HTTP :

  • 200 : La réponse est renvoyée sans problème.
  • 400 : Dès qu'il y a un problème avec les paramètres de la requête.
  • 401 : Problème avec l'authentification.
  • 403 : Pas les permissions nécessaires.
  • 404 : Le MP n'existe pas.

Permissions nécessaires :

  • Être authentifié
  • Être auteur ou dans les participants des MPs
+3 -0

Je viens de lire tout ça par curiosité et j'aurais juste quelques petites remarques sur des détails :

Pourquoi "pk" et pas "id", comme pour les membres ? (point déjà abordé par quelqu'un)

Dans "author", pourquoi retourner l'adresse mail ? En quoi cette info est pertinente ?

Par extension, dans "author" et dans "participants", pourquoi fournir le pseudo ? L'id est suffisant vu qu'on peut récupérer toutes les autres données via l'API membres.

Par rapport au "Sujet d'un mp", je vois pas tellement l'intérêt du "last_message" mais soit. Par contre peut-être qu'un champ "nb_messages" ou "total_messages" serait pertinent ?

Le champ "text_html" m'a un peu bloqué, je suis d'accord avec ce qui a été dit précédemment, un paramètre pour le format de récupération du texte serait pas mal.

Dernier point, le "mps" dans l'URL me bloque pour deux raisons :

  • "mp" signifie "message privé" et l'équivalent anglais est "pm" pour "private message". Vu que toute l'API est en anglais je vois pas pourquoi utiliser "mp".
  • "mps" et "pms" c'est moche, les s ça colle vraiment pas avec les acronymes. Pourquoi pas "private_messages" ? Qu'est-ce qui empêche la verbosité ici ?

J'ai rien dit pour ce dernier point, j'avais pas réalisé que les URL de ZdS étaient en /mp/ :-°

C'est tout ce qui me vient en première lecture.

+0 -0

Bonjour Thiht,

D'abord merci pour tes remarques, je n'ai pas beaucoup de temps ce matin mais je vais quand même te faire des réponses rapides :

Pourquoi "pk" et pas "id", comme pour les membres ? (point déjà abordé par quelqu'un)

Erreur de ma part. C'est pk partout. Je corrigerais ça.

Dans "author", pourquoi retourner l'adresse mail ? En quoi cette info est pertinente ?

Elle ne l'est pas. Elle sera retirée.

Par extension, dans "author" et dans "participants", pourquoi fournir le pseudo ? L'id est suffisant vu qu'on peut récupérer toutes les autres données via l'API membres.

Avec l'API des membres, nous pouvons trouver ou rechercher un membre suivant son identifiant ou son pseudonyme. Je pense que cela reste important de le renvoyer (et cela ne coute vraiment rien).

Par rapport au "Sujet d'un mp", je vois pas tellement l'intérêt du "last_message" mais soit. Par contre peut-être qu'un champ "nb_messages" ou "total_messages" serait pertinent ?

Le nombre de messages dans un sujet sera retourné dans la liste paginée de la liste des messages pour un sujet donné.

Le champ "text_html" m'a un peu bloqué, je suis d'accord avec ce qui a été dit précédemment, un paramètre pour le format de récupération du texte serait pas mal.

En gros, nous ferons la requête avec un paramètre qui indique si nous voulons la version Markdown ou HTML. Je vais y réfléchir mais en y repensant, j'aime plutôt l'idée.

J'ai rien dit pour ce dernier point, j'avais pas réalisé que les URL de ZdS étaient en /mp/ :-°

Les URLs actuelles du site ZdS sont horribles, nous sommes bien d'accord.

Avec l'API des membres, nous pouvons trouver ou rechercher un membre suivant son identifiant ou son pseudonyme. Je pense que cela reste important de le renvoyer (et cela ne coute vraiment rien).

C'est une vaste question…

Certaines APIs proposent de tout marshaller quand on demande une ressource (ce que fait Github je crois, tu charges une liste d'issues, pour chaque auteur tu récupères toutes les infos, pas seulement l'id).

D'autres ne renvoient que l'id.

Et c'est vachement délicat de savoir quelle est la meilleure solution…

Si tu considères le problème du point de vue d'une requête, tu as tendance à favoriser la première. "Une requête me donne toutes les infos dont j'ai besoin, point".

Si tu considères le problème du point de vue de données qui évoluent dans le temps (e.g. un message édité plusieurs fois) et du point de vue global d'un client (qui fait donc plein de requêtes sur des ressources différentes) t'as tendance à plutôt favoriser la seconde en te disant : "Attention, je ne t'ai marshallé que l'id de la ressource car il vaudrait mieux que tu la tiennes à jour de ton côté, cette donné change souvent, méfies-toi".

Quand on est son propre client, la solution 1 va bien, c'est du best-effort : je te renvoie un snapshot de la ressource à un instant t, si tu n'as besoin que de l'id débrouille-toi ça ne m'intéresse pas.

Si je devais redesigner une nouvelle API aujourd'hui, je pense que je laisserais le choix au client, en lui fournissant un queryParam (genre marshallChildren=false) qui est considéré comme vrai par défaut.

Comme cela un client est libre d'optimiser son trafic réseau et gratter par ci par là. Attention là on parle des membres donc c'est pas grand chose, mais marshaller tous les enfants d'une ressource ça devient vite indémerdable une fois que l'API prend de l'ampleur.

Autre truc très important à garder à l'esprit : la récursion infinie… Ex : Je récupère un membre, j'ai la liste de ses messages dedans (c'est fictif hein), qui chacun ont un champ auteur, qui marshall le membre, avec la liste de ses messages… Vous avez pigé le problème… Donc on se retrouve avec du marshalling conditionnel "le champ auteur je le mets pas dans le cas où c'est une liste de messages" et c'est un bordel innommable.

Donc gaffe, la question n'est pas si anodine qu'il y paraît au premier abord. Et c'est le bon moment d'y réfléchir puisqu'on commence à servir deux ressources différentes par API.

Il est important de prendre une décision et s'y tenir, pour que les clients ne soient pas perdus.

+3 -0

Merci Javier de m'avoir fait prendre conscience que les clients devront adopter une stratégie pour mettre à jour leurs données si notre API renvoie trop d'informations. Nous renvoyons déjà que l'identifiant du dernier message pour une conversation privée (ce qui est une excellente chose vu les informations que tu soulèves) et il est peut-être intéressant d'en faire de même pour les utilisateurs puisque un pseudo est sujet à des changements (sans doute avec un taux faible mais il est quand même possible de le modifier).

J'accepte de voter pour l'idée de ne renvoyer que des identifiants pour toutes les ressources externes à la ressource concernée par une API (ici les conversations privée et les messages).

Note : ça complique la vie du client aussi. Puisque, dans certains cas, ils vont charger une conversation qui fait référence à une personne qu'il ne connaît pas, donc avant d'afficher la conversation il va devoir faire une requête pour charger le participant manquant, etc. Cas qui ne se produit jamais si tu donnes toutes les infos dans une requête.

Donc c'est assez compliqué, parce que si on prend un truc simple comme une messagerie privée, pourquoi aller se taper la liste des membres ? Ou pourquoi faire N requêtes individuelles par conversation pour aller chercher les participants.

C'est pour ça que la solution 1 est parfois intéressante. En disant "tiens, toi t'es un client assez simple, je te donne ce dont tu as besoin, si tu ne veux pas tout, dis le moi".

Bref… C'est compliqué.

+0 -0

Pourquoi pas, en première approche, faire un mix raisonable. La proposition actuel, comme pour l'API membre, de renvoyer l'id + l'info la plus commune (le pseudo) a l'avantage de ne pas couter grand chose et de fournir les informations principales.

Faut etre honnête, quand on va manipuler les MP, on va presque obligatoirement aller chercher tous les pseudos des membres qui sont liés. Autant les avatars ou signature pas forcément, autant les pseudo c'est presque le minimum.

Et en v2, il sera toujours temps de rajouter un paramètre pour récupérer moins d'information (et optimiser les requettes pour ceux qui n'ont pas besoin des pseudo) ou pour récupérer plus d'informations.

Merci pour ces réponses Andr0.

C'est vrai que la donnée du pseudo est importante et sa présence peut être justifiée étant donné que l'API membres permet de récupérer les données d'un membre soit par son id, soit par son pseudo.

J'aime aussi bien l'idée de laisser au client le choix de récupérer ou non toutes les informations via un paramètre, ça me parait un bon compromis d'un point de vue utilisateur.

http://stackoverflow.com/questions/22331676/the-restful-way-to-include-or-not-include-children-of-a-resource

Tiens, l'une des remarques (dans la deuxième solution) est intéressante : c'est l'inverse de ce que je suggérais, à savoir : par défaut on donne l'id de la ressource et un lien pour la charger individuellement (ça c'est vachement bien j'y avais pas pensé !) et un paramètre permet de spécifier qu'on souhaite disposer des noeuds enfants expand=membre, expand=all, expand=false etc.

C'est pas vilain comme solution.

A noter aussi la première remarque, qui pousse un peu à l'adoption de la solution 1 (je m'en doutais en fait) : faire des requêtes HTTP c'est coûteux (plutôt qu'avoir une payload plus grosse la première requête) évidemment.

Et pour le coup Kje si je suis plutôt partisan de l'approche "on commence simple, on verra plus tard" là je suis pas d'accord. C'est une question à envisager maintenant qu'on a deux ressources servies par API, parce que c'est un vrai problème de design et de fond. C'est un peu comme dire "la pagination on verra quand on aura plusieurs pages", non… Là on attaque un choix fort de conception. Peut-être qu'on ne fera pas le bon, tant pis, mais faut bien y réfléchir et pas le repousser à une future version, ça ne me paraît pas sain.

+0 -0

Bin je ne dis pas qu'il ne faut pas y refléchir, je dis simplement qu'entre la solution "on ne fournit que l'id" et la solution "on fournit tout", il y a la solution intermediaire "on fournit par defaut les infos qui nous semble les plus pertinente pour le maximum d'utilisateur".

Je propose donc simplement une 3eme possibilité. Au lieu de ne fournir que l'id, qui va obliger le client a refaire de couteuses requêtes pour obtenir quelques infos indispensable, et au lieu de fournir au client la bio de l'auteur, dont il se fiche probablement, on peut envisager que par defaut on fournisse quelques informations (a minima le pseudo) et je propose qu'a terme on ai un paramètre pour retomber sur les 2 autres propositions (requête minimal ou complète) .

Bref je ne veux pas laisser la question a plus tard, je propose qu'on réfléchisse a cette solution intermediaire qui peut correspondre facilement à la majorité des utilisateurs/clients.

On ne pourra jamais contenter tout le monde. Mais pour les autres, ils pourront toujours faire une requete supplémentaire. A titre personnel j'ajouterai bien le pseudo et l'url vers l'avatar qui me semblent les infos qui vont être le plus souvent utilisés. Mais justement, pour moi on peut discuter de ça : quels infos peuvent être petinente. Apres comme tu l'as dit, la solution choisi ne sera probablement pas parfaite. Mais bon il faut faire des choix. Et il me semblait juste que fournir le pseudo a minima était un choix raisonnable.

L'avatar est en effet un aspect important de l'utilisateur, ça permet d'identifier d'un coup d'œil ceux avec lesquels on est le plus familiers. Mais il ne faut pas oublier que même dans un navigateur, ces avatars sont mis en cache : à mon sens, cela devrait faire partie des infos « supplémentaires » que l'on ne va chercher qu'une fois de temps en temps.

+0 -0

J'ai l'impression que la question ne se pose que pour les membres. Je ne vois pas à première vue d'autres requêtes fréquentes qui nécessiterait d'aller boucler sur une autre ressource pour avoir toutes les infos qu'on veut afficher. Ca concerne par contre tous les types de discussion (commentaires d'un article/tuto, discussion dans un forum/message privé).

On peut sans doute imaginer d'autres cas possibles, mais bien plus marginaux il me semble.

Du coup, j'aurais tendance a privilégier un mix de ce qui a été proposé, pour ces cas-là uniquement :

  1. on renvoie d'office un lien vers la ressource complète (valable aussi quand il ne s'agit pas d'une ressource membre d'ailleurs, ça me semble une bonne pratique)
  2. on propose un paramètre expand-members qui peut prendre 3 valeurs :
  • false : on ne renvoie que l'id et l'url de la ressource correspondante
  • full : on renvoie toutes les infos du profil + url de la ressource
  • basic : on renvoie l'id, l'url de la ressource et un set de champs "souvent affichés". A nous de définir ce set de champs indispensable (perso, j'imagine le pseudo et l'avatar, eventuellement la signature, et les permissions (banni, lecture seule, staff, …))

C'est sans doute pas tout à fait standard mais ça me semble un bon compromis.

+2 -0

Je n'ai vraiment pas le temps aujourd'hui mais après lecture en diagonale de vos derniers messages, je viens lancer une idée pour la faire évoluer ici :

Pourquoi ne pas renvoyer toujours que les identifiants pour toutes les ressources non concernées par cette partie de l'API et proposer un paramètre expand "générique" qui va "ouvrir" (renvoyer les données supplémentaires) pour les attributs qui figurent dans le paramètre de la requête ?

Exemple :

api.zestedesavoir.com/mps/654321/ :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
    "pk": 654321,
    "title": "Titre du MP",
    "subtitle": "Sous-titre du MP",
    "pubdate": "1977-04-22T06:00:00Z",
    "author": 42,
    "participants": [
        {
            "id": 42
        },
        {
            "id": 5
        }
    ],
    "last_message": 123456
}

api.zestedesavoir.com/mps/654321/?expand=participants :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
    "pk": 654321,
    "title": "Titre du MP",
    "subtitle": "Sous-titre du MP",
    "pubdate": "1977-04-22T06:00:00Z",
    "author": 42,
    "participants": [
        {
            "id": 42,
            "username": "Clem",
            "show_email": true,
            "email": "clem@zestedesavoir.com",
            "is_active": true,
            "site": "www.zestedesavoir.com",
            "avatar_url": "http://i.imgur.com/k6R1plZ.png",
            "biography": "Signature.",
            "sign": "",
            "email_for_answer": true,
            "last_visit": "1977-04-22T06:00:00Z",
            "date_joined": "1977-04-22T06:00:00Z"
        },
        {
            "id": 43,
            "username": "Zozor",
            "show_email": true,
            "email": "zozor@openclassrooms.com",
            "is_active": true,
            "site": "www.zestedesavoir.com",
            "avatar_url": "http://i.imgur.com/k6R1plZ.png",
            "biography": "Signature.",
            "sign": "",
            "email_for_answer": true,
            "last_visit": "1977-04-22T06:00:00Z",
            "date_joined": "1977-04-22T06:00:00Z"
        }
    ],
    "last_message": 123456
}
+0 -0

Oui donc on revient a la proposition de Javier. Je proposais juste que par defaut on est une version intermediaire avec juste les infos importantes (id, pseudo et avatar) et qu'on puisse avoir la version minimal ou complete avec un paramètre. Mais en soit je m'en fout un peu.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte