URL: https://linuxfr.org/news/developper-une-interface-web-avec-le-toolkit-atlas-2-2 Title: Développer une interface web avec le toolkit Atlas (2/2) Authors: Claude SIMON orfenor, Pierre Jarillon, Benoît Sibaud et Ysabeau Date: 2020-12-21T17:19:44+01:00 License: CC By-SA Tags: web, spa, python et atlas_toolkit Score: 6 Le *toolkit* *Atlas* permet de programmer des interfaces d’applications web monopages ([SPA](https://en.wikipedia.org/wiki/Single-page_application)) sans qu’il ne soit nécessaire de savoir programmer en *JavaScript* et sans imposer d’architecture logicielle. De plus, toute application développée avec le *toolkit* *Atlas* est, dès son lancement, instantanément et automatiquement accessible d’Internet. Le *toolkit* *Atlas* s’apparente à ces bibliothèques qui, en s’appuyant sur GTK, Qt, wxWidgets…, ont pour but de faciliter le développement d’interfaces graphiques. La différence est que le *toolkit* *Atlas*, lui, s’appuie sur les technologies web (HTML/CSS). Le *toolkit* *Atlas* est disponible pour *Java*, *Node.js*, *Perl*, *Python* et *Ruby*. Ce document porte sur le développement, avec la version *Python* du *toolkit* *Atlas*, d’une application dont voici un aperçu : ![Apparence de l’application faisant l’objet du tutoriel 'Contacts'](https://q37.info/s/39dr4tcr.png) ---- [Première partie](https://linuxfr.org/news/developper-une-interface-web-avec-le-toolkit-atlas-1-2) [Homepage](https://atlastk.org) [Sur GitHub](https://github.com/epeios-q37/atlas-python) [Sur Repl.it](https://repl.it/@AtlasTK/atlas-python) [API](https://atlastk.org/api/fr) ---- # Précédemment dans *Développer une interface web avec le toolkit Atlas* Sur les recommandations de l’équipe de modération, ce document a été découpé en deux dépêches, dont voici la seconde. La [précédente dépêche](./developper-une-interface-web-avec-le-toolkit-atlas-1-2) présentait le fichier *HTML* principal, celui des métadonnées, ainsi que les principales fonctions relatives à l’affichage. Cette seconde dépêche va porter sur la gestion des évènements dédiés à l’édition. # Désactivation des champs + bouton *New* (`part4.py`) > * Code source : [lien sur GitHub](https://github.com/epeios-q37/atlas-python/blob/master/tutorials/Contacts/part4.py) ; > * exécution : > * sur [*Repl.it*](https://repl.it/@AtlasTK/atlas-python#tutorials/Contacts/part4.py) : bouton *Run*, `n4` + *entrée*, clic sur URL, > * en local : `python3 atlas-python/tutorials/Contacts/part4.py` On remarquera que le contenu des champs dans lesquels s’affichent les détails sont modifiables, ce qui n’est pas le comportement voulu dans ce contexte. On va donc écrire le code permettant de désactiver ces champs. ## Champs à désactiver Pour cela, on va d’abord créer une liste contenant les identifiants, définis dans le fichier `Main.html`, des différents champs à désactiver : ```python FIELDS = [ "Name", "Address", "Phone", "Note" ] ``` ## Gestion générale des éléments interactifs On va créer une fonction qui va gérer l’état de ces champs, et qui sera complétée ultérieurement pour gérer d’autres éléments : ```python def update_outfit(dom): dom.disable_elements(FIELDS) ``` Cette fonction fait appel à la méthode `disable_elements(…)`, dont le rôle est de désactiver les éléments dont les identifiants sont passés en paramètres. On va également utiliser cette fonction pour faire apparaître le bouton *New*, qui permet de saisir un nouveau contact. Pour cela, on a affecté la classe `Display` à ce bouton (voir le fichier `Main.html`). Comme l’élément `style` d’identifiant `HideDisplay` (voir le fichier `Head.html`) définit la règle qui cache les éléments de classe `Display`, on va le désactiver en utilisant la méthode `disable_element(…)`. La fonction `update_outfit(…)` se présente alors de la manière suivante : ```python def update_outfit(dom): dom.disable_elements(FIELDS) dom.disable_element("HideDisplay") ``` On pourrait également ajouter l’identifiant `HideDisplay` à la liste passée à `disable_elements(…)`, pour économiser un appel de fonction. ## Mise en œuvre On va appeler cette fonction à chaque action de l’utilisateur, ce qui peut sembler ne pas être approprié vu son contenu actuel, mais prendra sens avec la version finale de cette fonction, que l’on découvrira par la suite : ```python def ac_connect(dom): dom.inner("",open("Main.html").read()) display_contacts(dom) update_outfit(dom) def ac_select(dom,id): display_contact(int(id),dom) update_outfit(dom) ``` # Saisie d’un nouveau contact (`part5.py`) > * Code source : [lien sur GitHub](https://github.com/epeios-q37/atlas-python/blob/master/tutorials/Contacts/part5.py) ; > * exécution : > * sur [*Repl.it*](https://repl.it/@AtlasTK/atlas-python#tutorials/Contacts/part5.py) : bouton *Run*, `n5` + *entrée*, clic sur URL, > * en local : `python3 atlas-python/tutorials/Contacts/part5.py` On va maintenant gérer l’action affectée au bouton *New*. Pour cela, on va utiliser un objet qui va stocker le mode (*state* dans le code source) dans lequel est placé le logiciel, à savoir *édition* ou *affichage*. ## Les différents modes de l’application On va d’abord créer un *enum* relatifs à ces deux modes, à l’aide du module *enum*, que l’on va importer en modifiant l’instruction d’importation existante : ```python import atlastk, enum ``` Créons l'*enum* proprement dit : ```python class State(enum.Enum): DISPLAY = enum.auto() # Affichage EDIT = enum.auto() # Édition ``` ## Classe dédiée à chaque session On va maintenant créer une classe `Board` dans laquelle on va pouvoir stocker les différentes variables propres à chaque session : ```python class Board: def __init__(self): self.state = State.DISPLAY ``` Le constructeur de cette classe (`__init__(…)`) va stocker le mode initial de l’application, à savoir `DISPLAY` (affichage), dans la variable membre `state`. Il faudra créer une instance de cette classe pour chaque nouvelle session. Ceci est réalisé automatiquement par le *toolkit* *Atlas* : il suffit de modifier l’appel à la fonction `launch(…)` en remplaçant le paramètre de valeur `None` par le constructeur de cette classe, ce qui donne : ```python atlastk.launch(CALLBACKS,Board,open("Head.html").read()) ``` Ce faisant, toutes les fonctions référencées dans `CALLBACKS`, qui, je le rappelle, contient les associations entre fonctions et actions, vont recevoir l’instance de l’objet `Board` correspondant à la session à l’origine de l’appel. Il faut donc modifier le prototype de ces fonctions : ```python def ac_connect(board,dom): … def ac_select(board,dom,id): … ``` Notez l’ajout du paramètre `board`. ## Adaptation de la gestion des contrôles interactifs On va passer ce paramètre à la fonction `update_outfit(…)`, pour qu’on puisse y tenir compte du mode dans lequel se trouve l’application et agir en conséquence, ce qui donne : ```python def update_outfit(board,dom): if board.state == State.DISPLAY: dom.disable_elements(FIELDS) dom.disable_element("HideDisplay") elif board.state == State.EDIT: dom.enable_elements(FIELDS) dom.enable_elements("HideDisplay") ``` On y utilise les méthodes `enable_element[s](…)`, qui sont les pendants des méthodes `disable_element[s](…)`. ## Autres adaptations Il faut, bien entendu, également modifier les appels à `update_outfit(…)` en conséquence ; on va également, par précaution, mettre à jour, dans l’instance `board`, le mode de l’application pour être sûr qu’il correspond à l’action lancée : ```python def ac_connect(board,dom): … board.state = State.DISPLAY update_outfit(board,dom) def ac_select(board,dom,id): … board.state = State.DISPLAY update_outfit(board,dom) ``` On va également modifier la fonction `display_contact(…)`, pour pouvoir l’utiliser afin de vider le contenu des champs. Pour cela on va créer un dictionnaire correspondant à un contact vide : ```python EMPTY_CONTACT = { "Name": "", "Address": "", "Phone": "", "Note": "" } ``` qui va être utilisé de la manière suivante dans la fonction `display_contact(…)` : ```python def display_contact(contactId,dom): dom.set_values(EMPTY_CONTACT if contactId == None else contacts[contactId]) ``` On notera que donner la valeur `None` au paramètre `contactId` entraînera dorénavant le vidage des champs. ## Activation de la saisie Ne reste plus qu’à définir la fonction qui sera appelée lors d’un clic sur le bouton *New* : ```python def ac_new(board,dom): board.state = State.EDIT display_contact(None,dom) update_outfit(board,dom) dom.focus("Name") ``` Cette fonction réalise successivement les opérations suivantes : - stockage dans l’instance de l’objet `board` du nouveau mode du logiciel, à savoir `EDIT` (édition) ; - vidage des champs de saisie ; - mise à jour de l’apparence de l’interface ; - affectation du focus (méthode `focus(…)`) au premier champ éditable (d’identifiant `Name`, qui correspond au champ contenant le nom affecté au contact), de manière à ce que l’utilisateur puisse procéder immédiatement à la saisie du nouveau contact. N’oublions pas d’associer cette fonction à l’action idoine : ```python CALLBACKS = { … "New": ac_new } ``` # Boutons de saisie (`part6.py`) > * Code source : [lien sur GitHub](https://github.com/epeios-q37/atlas-python/blob/master/tutorials/Contacts/part6.py) ; > * exécution : > * sur [*Repl.it*](https://repl.it/@AtlasTK/atlas-python#tutorials/Contacts/part6.py) : bouton *Run*, `n6` + *entrée*, clic sur URL, > * en local : `python3 atlas-python/tutorials/Contacts/part6.py` On peut maintenant saisir un nouveau contact, mais il manque les boutons pour valider ou annuler cette saisie. ## Adaptation de la gestion des contrôles interactifs Pour afficher les boutons *Submit* et *Cancel*, on va désactiver l’élément `style` d’identifiant `HideEdition` (voir le fichier `Head.html`). Cet élément définit une règle permettant de cacher les éléments auxquels on a affecté la classe `Edition`, comme c’est le cas de l’élément `div` contenant les deux boutons *Submit* et *Cancel* (voir le fichier `Main.html`). Désactiver cet élément `style` pour faire apparaître les boutons d’éditions ne suffit pas ; il faut également l’activer pour cacher ces boutons lorsque requis. On va, pour cela, modifier la fonction `update_outfit(…)` afin d’obtenir cela : ```python def update_outfit(board,dom): if board.state == State.DISPLAY: dom.disable_elements(FIELDS) dom.disable_element("HideDisplay") dom.enable_element("HideEdition") elif board.state == State.EDIT: dom.enable_elements(FIELDS) dom.enable_element("HideDisplay") dom.disable_element("HideEdition") ``` ## Confirmation/annulation d’une saisie Maintenant que les boutons sont affichés, on va créer les fonctions associées. Pour le bouton *Cancel*, on va demander confirmation de l’annulation et, en fonction de la réponse, ne rien faire, ou repasser en mode d’affichage après avoir vidé les champs de saisie : ```python def ac_cancel(board,dom): if dom.confirm("Are you sure?"): display_contact(None,dom) board.state = State.DISPLAY update_outfit(board,dom) ``` La méthode `confirm(…)` ouvre une boîte de dialogue affichant la chaîne de caractères passée en paramètre. Elle retourne `True` lorsque l’on clique sur le bouton *OK* (ou ce qui en tient lieu), ou `False` si on clique sur le bouton *Cancel* (ou ce qui en tient lieu), tout en fermant ladite boîte de dialogue. Pour le bouton `Submit`, il s’agit de récupérer les valeurs des champs de saisie, de stocker lesdites valeurs dans ce qui tient lieu de base de donnée, à savoir la variable `contacts`, de rafraîchir la liste des contacts, et de rebasculer en mode saisie, tout cela sous condition que le champ `Name` contienne une valeur : ```python def ac_submit(board,dom): idsAndValues = dom.get_values(FIELDS) if not idsAndValues['Name'].strip(): dom.alert("The name field can not be empty!") else: board.state = State.DISPLAY contacts.append(idsAndValues) display_contact(None,dom) display_contacts(dom) update_outfit(board,dom) ``` La méthode `get_values(…)` prend une liste de chaînes de caractères correspondants à des identifiants d’éléments, et retourne un dictionnaire avec, pour clefs, ces identifiants, et, pour valeurs, le contenu de ces éléments. Comme les identifiants sont identiques aux clefs d’un contact, ou peut stocker le dictionnaire obtenu tel quel. La méthode `alert(…)` affiche simplement une boîte de dialogue contenant, comme message, la chaîne passée en paramètre, avec un bouton *OK* (ou équivalent) permettant de la fermer. On termine en mettant à jour `CALLBACKS` pour affecter ces nouvelles fonctions aux actions adéquates : ```python CALLBACKS = { … "Cancel": ac_cancel, "Submit": ac_submit } ``` # Les autres boutons (`part7.py`) > * Code source : [lien sur GitHub](https://github.com/epeios-q37/atlas-python/blob/master/tutorials/Contacts/part7.py) ; > * exécution : > * sur [*Repl.it*](https://repl.it/@AtlasTK/atlas-python#tutorials/Contacts/part7.py) : bouton *Run*, `n7` + *entrée*, clic sur URL, > * en local : `python3 atlas-python/tutorials/Contacts/part7.py` Il nous reste deux boutons à gérer : le bouton d’édition (*Edit*) et le bouton de suppression (*Delete*). ## Adaptation de la classe `Board` Avant toute chose, nous allons modifier la classe `Board` pour lui ajouter une variable (`contactId`) stockant l’index, dans la liste, du contact sélectionné. Cette variable est mise à `None` lorsqu’aucun contact n’est sélectionné : ```python class Board: def __init__(self): self.state = State.DISPLAY self.contactId = None ``` Nous allons également modifier `ac_select(…)` pour gérer cette nouvelle variable : ```python def ac_select(board,dom,id): board.contactId = int(id) display_contact(board.contactId,dom) … ``` ## Adaptation de la gestion des contrôles interactifs La variable ajoutée à la classe `Board` va également nous servir pour l’affichage des boutons manquants. La classe `DisplayAndSelect` est affectée à ces boutons (voir le fichier `Main.html`), dont la règle *CSS* pour cacher les éléments de cette classe est définie dans l’élément `style` d’identifiant `HideDisplayAndSelect` (voir le fichier `Head.html`). On obtient donc cela : ```python def update_outfit(board,dom): if board.state == State.DISPLAY: … if board.contactId == None: dom.enable_element("HideDisplayAndSelect") else: dom.disable_element("HideDisplayAndSelect") elif board.state == State.EDIT: … dom.enable_elements(("HideDisplay","HideDisplayAndSelect")) … ``` ## Modification d’un contact Passons à la fonction qui sera associée au bouton *Edit*. Elle reprendra en grande partie le contenu de la fonction `ac_new(…)` (on pourrait d’ailleurs en factoriser une partie) : ```python def ac_edit(board,dom): board.state = State.EDIT display_contact(board.contactId,dom) update_outfit(board,dom) dom.focus("Name") ``` Il faut aussi modifier la fonction `ac_submit(…)`, pour tenir compte de son exécution dans le cadre de la modification d’un contact : ```python def ac_submit(board,dom): … else: board.state = State.DISPLAY if board.contactId == None: contacts.append(idsAndValues) else: contacts[board.contactId] = idsAndValues display_contact(board.contactId,dom) display_contacts(dom) … ``` Et également la fonction `ac_cancel(…)` pour la même raison : ```python if dom.confirm("Are you sure?"): display_contact(board.contactId,dom) board.state = State.DISPLAY update_outfit(board,dom) ``` Et mettons à jour `CALLBACKS` : ```python CALLBACKS { … "Edit": ac_edit } ``` ## Suppression d’un contact Implémentons maintenant la fonction qui sera associée au bouton *Delete*, qui ne présente rien de particulier, au regard de ce qui a été abordé dans les précédentes sections : ```python def ac_delete(board,dom): contacts.pop(board.contactId) board.contactId = None; display_contact(None,dom) display_contacts(dom) update_outfit(board,dom) ``` Et mettons à jour `CALLBACKS` : ```python CALLBACKS { … "Delete": ac_delete } ``` # Bonus (`part8.py`) > * Code source : [lien sur GitHub](https://github.com/epeios-q37/atlas-python/blob/master/tutorials/Contacts/part8.py) ; > * exécution : > * sur [*Repl.it*](https://repl.it/@AtlasTK/atlas-python#tutorials/Contacts/part8.py) : bouton *Run*, `n8` + *entrée*, clic sur URL, > * en local : `python3 atlas-python/tutorials/Contacts/part8.py` Comme vous avez pu le constater, la variable `contacts` est globale. Cela a pour conséquence qu’elle est commune à toutes les sessions. Cependant, une modification apportée à cette variable par une session n’est pas immédiatement visible dans toutes les autres sessions. L’objet de cette section est d’apporter les modifications au code pour remédier à cela. On va se limiter à rafraîchir, dès qu’une modification y est apportée, l’affichage de la liste des contacts dans l’ensemble des sessions. Pour commencer, on va créer une fonction qui va rafraîchir la liste des contacts : ```python def ac_refresh(board,dom): display_contacts(dom) ``` Elle présente des similitudes, concernant les paramètres qu’elle reçoit, avec les fonctions associées à des actions (`ac_edit(…)`, `ac_submit(…)`…). Cela n’a rien d’étonnant, car on va effectivement l’associer à une action : ```python CALLBACKS = { … "Refresh": ac_refresh } ``` Et maintenant, on va remplacer, dans les fonctions qui modifient la liste des contacts, à savoir `ac_submit(…)` et `ac_delete(…)`, chaque appel à la fonction `display_contacts(dom)` par un appel à `atlastk.broadcast_action("Refresh")`. ```python def ac_submit(board,dom): … display_contact(board.contactId,dom) atlastk.broadcast_action("Refresh") update_outfit(board,dom) def ac_delete(board,dom): … display_contact(None,dom) atlastk.broadcast_action("Refresh") update_outfit(board,dom) ``` `atlastk.broadcast_action(…)` lance l’action dont le libellé est passé en paramètre dans toutes les sessions, ce qui, en l’occurrence, va provoquer l’appel à la fonction `display_contacts(…)`, et ainsi la liste des contacts sera rafraîchie dans toutes les sessions. Le fait que la variable `contacts` soit globale, et donc modifiable par toutes les sessions, nécessiterait d’écrire du code supplémentaire, notamment pour en contrôler l’accès. De par l’absence de ce code, il est facile de mettre cette application en défaut. Néanmoins, ce code ne concernant pas directement le *toolkit* *Atlas*, il sort du cadre de ce document, et ne sera donc pas abordé ici. # *Vers l’infini, et au-delà !* Dans le dépôt *GitHub*, et donc également présents sur *Repl.it*, on trouvera, en plus des fichiers sources correspondant aux différentes sections de ce document, un certain nombre d’exemples permettant d’explorer différents aspects du *toolkit* *Atlas*. En outre, comme déjà évoqué, le *toolkit* *Atlas* est disponible pour d’autres langages que *Python*. Bien que seule la version *Python* soit vraiment utilisée, j’envisage de développer d’autres versions du *toolkit* *Atlas*. Histoire de faire un peu de veille technologique, ça sera probablement une version *Rust* et/ou *Go*. Dans l’intervalle, de nouvelles fonctionnalités seront rendues disponibles, ainsi que, peut-être, de nouveaux documents comme celui-ci, ou encore de nouvelles bibliothèques s’appuyant sur le *toolkit* *Atlas*, à l’instar des bibliothèques [*EduTK*](https://github.com/epeios-q37/edutk-python) (création d’exercices de programmation d’un nouveau genre) ([dépêche](https://linuxfr.org/news/apprentissage-de-la-programmation-dans-les-lycees-snt-nsi-la-creation-d-exercices)), [*term2web*](https://github.com/epeios-q37/term2web-python) (redirection de l’entrée et de la sortie standard dans un navigateur web) ([journal](https://linuxfr.org/users/epeios/journaux/term2web-un-terminal-sur-le-web-python)) ou encore [*tortoise*](https://github.com/epeios-q37/tortoise-python) (la tortue du Logo dans un navigateur web) ([journal](https://linuxfr.org/users/epeios/journaux/la-tortue-passe-au-web))…