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, Ysabeau đ§¶, Pierre Jarillon et BenoĂźt Sibaud Date: 2020ćčŽ12æ21æ„T17:19:44+01:00 License: CC By-SA Tags: web, spa, python et atlas_toolkit Score: 15 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 :  ---- [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))...