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 : ![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))...

AltStyle ă«ă‚ˆăŁăŠć€‰æ›ă•ă‚ŒăŸăƒšăƒŒă‚ž (->ă‚ȘăƒȘă‚žăƒŠăƒ«) /