import tkinter.messagebox as messagebox from tkinter import IntVar, Checkbutton, LabelFrame, Scrollbar, Listbox, Entry, Button, Label, Frame, Tk, Toplevel from re import sub from users import Utilisateurs # import de mon fichier pour gérer la base de donnée class GesMag: """Programme de Gestion d'une caise de magasin.""" def __init__(self) -> None: """Instancie quelques variables pour plus de clareté.""" self.parent = Tk() # fenêtre affiché à l'utilisateur self.parent.resizable(False, False) # empêche la fenêtre d'être redimensionnée self.f = Frame(self.parent) # `Frame` affiché à l'écran def demarrer(self) -> None: """Lance le programme GesMag.""" self.font = ("Comfortaa", 14) # police par défaut Utilisateurs().creationTable() # on créer la base de donnée si elle n'existe pas déjà self._interfaceConnexion() # on créer la variable `self.f` qui est la frame a affiché self.f.grid() # on affiche la frame self.parent.mainloop() # on affiche la fenêtre def motDePasseCorrect(self, motDPasse: str) -> tuple: """Détermine si un mot de passe suit la politique du programme ou non.""" if len(motDPasse) == 0: # si le champs est vide return (False, "Mot de passe incorrect.") if len(motDPasse) < 8: # si le mot de passe est plus petit que 8 caractères return (False, "Un mot de passe doit faire 8 caractères minimum.") """ - Pour le regex, la fonction `sub` élimine tout ce qui est donné en fonction du pattern renseigné, alors si la fonction `sub` renvoie pas exactement la même chaîne de charactère alors c'est qu'il y avait un charactère interdit. - J'utilises pas `match` parce que je suis plus à l'aise avec `sub`. """ if not sub(r"[A-Z]", '', motDPasse) != motDPasse: return (False, "Un mot de passe doit au moins contenir une lettre majuscule.") if not sub(r"[a-z]", '', motDPasse) != motDPasse: return (False, "Un mot de passe doit au moins contenir une lettre minuscule.") if not sub(r" *?[^\w\s]+", '', motDPasse) != motDPasse: return (False, "Un mot de passe doit au moins contenir un caractère spécial.") return (True,) # si aucun des tests précédents n'est valide, alors le mot de passe est valide def utilisateurCorrect(self, utilisateur: str) -> tuple: """Détermine si un nom d'utilisateur suit la politique du programme ou non.""" """ Pour le nom d'utilisateur on vérifie si le champs n'est pas vide et si il y a bien que des lettres et des chiffres. """ if len(utilisateur) == 0: return (False, "Utilisateur incorrect.") if sub(r" *?[^\w\s]+", '', utilisateur) != utilisateur: return (False, "Un nom d'utilisateur ne doit pas contenir de caractère spécial.") return (True,) def nomCorrect(self, nom: str) -> bool: """Détermine si un nom suit la politique du programme ou non.""" if len(nom) == 0: return False if sub(r" *?[^\w\s]+", '', nom) != nom: # pas de caractères spéciaux dans un nom return False return True def prenomCorrect(self, prenom: str) -> bool: """Détermine si un prénom suit la politique du programme ou non.""" if len(prenom) == 0: return False if sub(r" *?[^\w\s]+", '', prenom) != prenom: # pas de caractères spéciaux dans un prénom return False return True def naissanceCorrect(self, naissance: str) -> bool: """Détermine si une date de naissance suit la politique du programme ou non.""" if len(naissance) == 0: return False # lien pour mieux comprendre ce qui se passe : https://www.debuggex.com/r/hSD-6BfSqDD1It5Z if sub(r"[0-9]{4}\/(0[1-9]|1[0-2])\/(0[1-9]|[1-2][0-9]|3[0-1])", '', naissance) != '': return False return True def adresseCorrect(self, adresse: str) -> bool: """Détermine si une adresse suit la politique du programme ou non.""" if len(adresse) == 0: return False return True def postalCorrect(self, code: str) -> bool: """Détermine si un code postal suit la politique du programme ou non.""" if len(code) == 0: return False if sub(r"\d{5}", '', code) != '': return False return True def connexion(self, utilisateur: str, motDePasse: str): """Gère la connexion aux différentes interfaces de l'application.""" """ Vérification nom d'utilisateur / mot de passe correctement entré avec leurs fonctions respectives. """ pseudoOk = self.utilisateurCorrect(utilisateur) if not pseudoOk[0]: messagebox.showerror("Erreur", pseudoOk[1]) return mdpOk = self.motDePasseCorrect(motDePasse) if not mdpOk[0]: messagebox.showerror("Erreur", mdpOk[1]) return # Redirection vers la bonne interface utilisateurBaseDeDonnee = Utilisateurs().verificationIdentifiants(utilisateur, motDePasse) if utilisateurBaseDeDonnee[0] > 0: if utilisateurBaseDeDonnee[1] == 0: # si le métier est "Manager" self._interfaceManager(utilisateurBaseDeDonnee[0]) elif utilisateurBaseDeDonnee[1] == 1: # si le métier est "Caissier" self._interfaceCaissier(utilisateurBaseDeDonnee[0]) else: messagebox.showerror("Erreur", "Une erreur est survenue : métier inconnue.") else: messagebox.showerror("Erreur", "Utilisateur ou mot de passe incorrect.") def dimensionsFenetre(self, fenetre, nouveauX: int, nouveauY: int): """Permet de changer les dimensions de la fenêtre parent et la place au centre de l'écran.""" largeur = fenetre.winfo_screenwidth() hauteur = fenetre.winfo_screenheight() x = (largeur // 2) - (nouveauX // 2) y = (hauteur // 2) - (nouveauY // 2) fenetre.geometry(f"{nouveauX}x{nouveauY}+{x}+{y}") def _interfaceConnexion(self): """Affiche l'interface de connexion.""" # Paramètres de la fenêtre self.dimensionsFenetre(self.parent, 400, 600) self.parent.title("Fenêtre de connexion") # Suppresssion de la dernière Frame self.f.destroy() # Instanciation d'une nouvelle Frame, on va donc ajouter tout nos widgets à cet Frame self.f = Frame(self.parent) self.f.grid() # Affichage des labels et boutons tentativeDeConnexion = lambda _ = None: self.connexion(utilisateur.get(), motDpasse.get()) # lambda pour envoyer les informations entrés dans le formulaire ecart = 80 # écart pour avoir un affichage centré Label(self.f).grid(row=0, pady=50) # utilisé pour du padding (meilleur affichage) Label(self.f, text="Utilisateur", font=self.font).grid(column=0, row=1, columnspan=2, padx=ecart - 20, pady=20, sticky='w') utilisateur = Entry(self.f, font=self.font, width=18) utilisateur.grid(column=1, row=2, columnspan=2, padx=ecart) Label(self.f, text="Mot de passe", font=self.font).grid(column=0, row=3, columnspan=2, padx=ecart - 20, pady=20, sticky='w') motDpasse = Entry(self.f, font=self.font, show='⁎', width=18) motDpasse.grid(column=1, row=4, columnspan=2, padx=ecart) motDpasse.bind("", tentativeDeConnexion) def __afficherMDP(self): """Permet de gérer l'affichage du mot de passe dans le champs sur la page de connexion.""" if self.mdpVisible == False: # si mot de passe caché, alors on l'affiche self.mdpVisible = True motDpasse.config(show='') bouttonAffichageMDP.config(font=("Arial", 10, "overstrike")) else: # inversement self.mdpVisible = False motDpasse.config(show='⁎') bouttonAffichageMDP.config(font=("Arial", 10)) bouttonAffichageMDP = Button(self.f, text='👁', command=lambda: __afficherMDP(self)) bouttonAffichageMDP.grid(column=2, row=4, columnspan=2) self.mdpVisible = False bouton = Button(self.f, text="Se connecter", font=self.font, command=tentativeDeConnexion) bouton.grid(column=0, row=5, columnspan=3, padx=ecart, pady=20) bouton.bind("", tentativeDeConnexion) Button(self.f, text="Quitter", font=self.font, command=quit).grid(column=0, row=6, columnspan=4, pady=20) self._interfaceCaissier(1) # DEBUG: affiche directement l'interface du Caissier def _interfaceCaissier(self, id: int): """Affiche l'interface du caissier.""" caissier = Utilisateurs().recuperationUtilisateur(id=id) self.parent.title(f"Caissier {caissier['nom']} {caissier['prenom']}") self.dimensionsFenetre(self.parent, 1280, 720) # Suppresssion de la dernière Frame self.f.destroy() # Instanciation d'une nouvelle Frame, on va donc ajouter tout nos widgets à cet Frame self.f = Frame(self.parent) self.f.grid() Label(self.f, text="Interface Caissier", font=(self.font[0], 20)).grid(column=0, row=0) # titre de l'interface stock = LabelFrame(self.f, text="Stock") # partie affichage du stock stock.grid(column=0, row=1) Label(stock, text="TODO").grid() ticket = LabelFrame(self.f, text="Ticket de caisse") # partie affichage du ticket de caisse ticket.grid(column=1, row=1) Label(ticket, text="TODO").grid() # Boutton pour passer en mode manager si la personne est un manager if caissier["metier"] == 0: Button(self.f, text="Passer en mode Manager", font=self.font, command=lambda: self._interfaceManager(id)).grid() def _interfaceManager(self, id: int): """Affiche l'interface du manager.""" manager = Utilisateurs().recuperationUtilisateur(id=id) # Dans le cas où un utilisateur réussi à trouvé cette interface alors qu'il n'a pas le droit, il sera bloqué if manager["metier"] != 0: messagebox.showerror("Erreur", "Vous ne pouvez pas accéder à cette interface.") return self.parent.title(f"Manager {manager['nom']} {manager['prenom']}") self.dimensionsFenetre(self.parent, 580, 310) # Suppresssion de la dernière Frame self.f.destroy() # Instanciation d'une nouvelle Frame, on va donc ajouter tout nos widgets à cet Frame self.f = Frame(self.parent) self.f.grid() Label(self.f, text="Interface Manager", font=(self.font[0], 20)).grid(column=0, row=0) Button(self.f, text="Se déconnecter", font=self.font, command=self._interfaceConnexion).grid(column=1, row=0, padx=50) Label(self.f).grid(row = 1, pady=10) # séparateur def __ajouterUtilisateur(metier: int): """Permet de créer un nouvel utilisateur, manager (`metier = 0`) et caissier (`metier = 1`).""" """ L'enfant (`TopLevel`) dépend de la `Frame` et non du parent (`Tk`) pour éviter de resté ouverte meme lorsque le manager se déconnecte. """ enfant = Toplevel(self.f) enfant.title(f"Ajouter un {'manager' if metier == 0 else 'caissier'}") def ___verification(): """Vérifie si les champs renseignées sont valides.""" """ La variable `ok` sert à savoir si la vérification est passée si elle vaut `True` alors tout est bon, Par contre si elle vaut `False` alors il y a eu une erreur. Les valeurs `Entry` qui ne sont pas passés seront dans la liste `mauvaisChamps`. """ ok = True mauvaisChamps = [] # vérification pour le nom d'utilisateur if self.utilisateurCorrect(pseudo.get())[0] == False or Utilisateurs().utilisateurExistant(pseudo.get()) == True: ok = False mauvaisChamps.append(pseudo) # vérification pour le mot de passe if self.motDePasseCorrect(passe.get())[0] == False: ok = False mauvaisChamps.append(passe) # vérification pour le nom if self.nomCorrect(nom.get()) == False: ok = False mauvaisChamps.append(nom) # vérification pour le prénom if self.prenomCorrect(prenom.get()) == False: ok = False mauvaisChamps.append(prenom) # vérification pour la date de naissance if self.naissanceCorrect(naissance.get()) == False: ok = False mauvaisChamps.append(naissance) # vérification pour l'adresse if self.adresseCorrect(adresse.get()) == False: ok = False mauvaisChamps.append(adresse) # vérification pour le code postal if self.postalCorrect(postal.get()) == False: ok = False mauvaisChamps.append(postal) if ok == False: """ Tous les champs qui n'ont pas réunies les conditions nécéssaires sont mis en orange pendant 3 secondes pour bien comprendre quelles champs sont à modifié. La fonction lambda `remettreCouleur` permet de remettre la couleur initial après les 3 secondes. """ remettreCouleur = lambda widget, ancienneCouleur: widget.configure(bg=ancienneCouleur) for champs in mauvaisChamps: couleur = champs["background"] # couleur d'avant changement champs.configure(bg="orange") # on change la couleur du champs en orange # dans 3 secondes on fait : `remettreCouleur(champs, couleur)` champs.after(3000, remettreCouleur, champs, couleur) else: # Tous les tests sont passés, on peut ajouter l'utilisateur à la base de donnée Utilisateurs().ajoutUtilisateur( pseudo.get(), passe.get().strip(), metier, nom.get(), prenom.get(), naissance.get(), adresse.get(), int(postal.get()), # pas besoin de gérer d'erreur lors du cast car on a vérifié avant que c'était bien une suite de chiffre ) __ajouterUtilisateursListe(listeUtilisateurs) # met à jour la liste # Champs de saisie # Nom d'utilisateurs Label(enfant, text="Nom d'utilisateur :").grid(column=0, row=0, sticky='e') Label(enfant, text="Pas de caractères spéciaux", font=("Arial", 10, "italic")).grid(column=2, row=0, sticky='w') pseudo = Entry(enfant) pseudo.grid(column=1, row=0, sticky='w') # Mot de passe Label(enfant, text="Mot de passe :").grid(column=0, row=1, sticky='e') Label(enfant, text="1 majuscule, miniscule et caractère spécial minimum", font=("Arial", 10, "italic")).grid(column=2, row=1, sticky='w') passe = Entry(enfant) passe.grid(column=1, row=1, sticky='w') # Nom Label(enfant, text="Nom :").grid(column=0, row=2, sticky='e') Label(enfant, text="Pas de caractères spéciaux", font=("Arial", 10, "italic")).grid(column=2, row=2, sticky='w') nom = Entry(enfant) nom.grid(column=1, row=2, sticky='w') # Prénom Label(enfant, text="Prénom :").grid(column=0, row=3, sticky='e') Label(enfant, text="Pas de caractères spéciaux", font=("Arial", 10, "italic")).grid(column=2, row=3, sticky='w') prenom = Entry(enfant) prenom.grid(column=1, row=3, sticky='w') # Date de naissance Label(enfant, text="Date de naissance :").grid(column=0, row=4, sticky='e') Label(enfant, text="Format : AAAA/MM/JJ", font=("Arial", 10, "italic")).grid(column=2, row=4, sticky='w') naissance = Entry(enfant) naissance.grid(column=1, row=4, sticky='w') # Adresse Label(enfant, text="Adresse").grid(column=0, row=5, sticky='e') adresse = Entry(enfant) adresse.grid(column=1, row=5, sticky='w') # Code postal Label(enfant, text="Code postal :").grid(column=0, row=6, sticky='e') Label(enfant, text="5 chiffres", font=("Arial", 10, "italic")).grid(column=2, row=6, sticky='w') postal = Entry(enfant) postal.grid(column=1, row=6, sticky='w') def ___viderChamps(): """Vide tout les champs de leur contenu""" # On récupère toutes les `Entry` de la fenêtre et on change leur contenu for champ in [widget for type, widget in enfant.children.items() if "entry" in type]: champ.delete(0, "end") champ.update() # Boutons Button(enfant, text="Valider", command=___verification).grid(column=0, row=8, columnspan=3, sticky='w') Button(enfant, text="Vider les champs", command=___viderChamps).grid(column=0, row=8, columnspan=3) Button(enfant, text="Quitter", command=enfant.destroy).grid(column=0, row=8, columnspan=3, sticky='e') def __retirerUtilisateur(metier: int): """Permet de supprimer un utilisateur existant, manager (`metier = 0`) et caissier (`metier = 1`).""" enfant = Toplevel(self.f) # cf. l'explication dans `__ajouterUtilisateur` enfant.title(f"Retirer un {'manager' if metier == 0 else 'caissier'}") # Liste des utilisateurs Label(enfant, text=f"Liste des {'manager' if metier == 0 else 'caissier'}", font=self.font).grid(column=0, row=0) # titre # On définit une barre pour pouvoir scroller dans la liste scroll_retirer = Scrollbar(enfant, orient="vertical") scroll_retirer.grid(column=1, row=1, sticky="nse") # On définit notre liste et on la lie à notre `Scrollbar` listeUtilisateurs_retirer = Listbox(enfant, width=25, height=4, yscrollcommand=scroll_retirer.set) scroll_retirer["command"] = listeUtilisateurs_retirer.yview # scroll à la verticale dans notre liste # On ajoute nos utilisateurs à notre liste __ajouterUtilisateursListe(listeUtilisateurs_retirer, metier) listeUtilisateurs_retirer.grid(column=0, row=1) # On affiche l'utilisateur quand on double-clique dessus def ___suppressionUtilisateur(): """Supprime l'utilisateur actuellement sélectionné dans la liste""" element = listeUtilisateurs_retirer.curselection() if len(element) == 0: # si aucun élément n'est selectionné messagebox.showwarning("Attention", "Aucun utilisateur n'a été selectionné.") else: utilisateur = listeUtilisateurs_retirer.get(listeUtilisateurs_retirer.curselection()[0]).split('(')[0][:-1] reponse = messagebox.askyesno("Confirmation", f"Voulez vous supprimer {utilisateur} ?") if reponse == True: Utilisateurs().suppressionUtilisateurs(utilisateur) __ajouterUtilisateursListe(listeUtilisateurs_retirer) # met à jour la liste dans la fenêtre de suppression __ajouterUtilisateursListe(listeUtilisateurs) # met à jour la liste dans l'interface principale messagebox.showinfo("Information", f"Utilisateur {utilisateur} supprimé.") # Boutons Button(enfant, text="Supprimer", command=___suppressionUtilisateur).grid(column=0, row=8, columnspan=3, sticky='w') Button(enfant, text="Quitter", command=enfant.destroy).grid(column=0, row=8, columnspan=3, sticky='e') def __afficherInformationsUtilisateur(_): """Permet d'afficher les informations d'un utilisateur""" element = listeUtilisateurs.curselection() if len(element) == 0: # si aucun élément n'est selectionné return """ On split le champs car dans la liste on affiche le métier entre parenthèses et on doit donner que le nom d'utilisateur à la fonction `recuperationUtilisateur`, aussi on retire le dernièr charactère avec [:-1] car c'est un espace. """ utilisateur = Utilisateurs().recuperationUtilisateur(pseudo=listeUtilisateurs.get(element[0]).split('(')[0][:-1]) enfant = Toplevel(self.f) # cf. l'explication dans `__ajouterUtilisateur` enfant.title(f"{utilisateur['nom']} {utilisateur['prenom']}") # Informations sur l'utilisateur frameInfos = LabelFrame(enfant, text="Informations utilisateur", font=self.font) frameInfos.grid(column=0, row=0, sticky='n', padx=5) utilisateur["metier"] = "Manager" if utilisateur["metier"] == 0 else "Caissier" del utilisateur["passe"] # le manager ne doit pas connaître le mot de passe de l'utilisateur for idx, cle in enumerate(utilisateur): Label(frameInfos, text=f"{cle.capitalize()} :").grid(column=0, row=idx + 1, sticky='e') Label(frameInfos, text=utilisateur[cle]).grid(column=1, row=idx + 1, sticky='w') frameSuivi = LabelFrame(enfant, text="Suivi des ventes", font=self.font) frameSuivi.grid(column=1, row=0, sticky='n', padx=5) Label(frameSuivi, text="Aucun résultat récemment enrengistré").grid() Button(enfant, text="Quitter", command=enfant.destroy).grid(column=0, row=1, columnspan=2) Button(self.f, text="Ajouter un caissier", font=self.font, command=lambda: __ajouterUtilisateur(1)).grid(column=0, row=2) Button(self.f, text="Retirer un caissier", font=self.font, command=lambda: __retirerUtilisateur(1)).grid(column=1, row=2) Label(self.f).grid(row = 3, pady=10) # séparateur # Liste des utilisateurs managerVerif = IntVar(self.f) # filtre pour afficher ou non les managers dans la liste caissierVerif = IntVar(self.f) # filtre pour afficher ou non les caissiers ou non dans la liste caissierVerif.set(1) # par défaut on affiche que les caissiers def __ajouterUtilisateursListe(liste: Listbox, force: int = None): """ Ajoute des utilisateurs à la liste du Manager. -> metier = 0 : manager uniquement -> metier = 1 : caissier uniquement -> metier = 2 : manager et caissier """ liste.delete(0, "end") # vidé la liste des utilisateurs if force: # si `force` n'est pas `None`, alors on force l'utilisation d'un métier metier = force else: # sinon on fait une vérification normale en fonction des filtres de l'interface manager if managerVerif.get() == 1: if caissierVerif.get() == 1: metier = None # on affiche les 2 else: metier = 0 # on affiche seulement les managers else: metier = 1 # on affiche les caissiers if caissierVerif.get() == 0: # rien est coché, on revient à la configuration par défaut (caissiers uniquement) metier = 1 caissierVerif.set(1) if metier == None: # on ajoute tous les utilisateurs for idx, utilisateur in enumerate(Utilisateurs().listUtilisateurs()): liste.insert(idx, f"{utilisateur[0]} ({'manager' if utilisateur[1] == 0 else 'caissier'})") elif metier == 0: # on ajoute que les managers for idx, utilisateur in enumerate(Utilisateurs().listUtilisateurs()): if utilisateur[1] == metier: liste.insert(idx, f"{utilisateur[0]} ({'manager' if utilisateur[1] == 0 else 'caissier'})") elif metier == 1: # on ajoute que les caissiers for idx, utilisateur in enumerate(Utilisateurs().listUtilisateurs()): if utilisateur[1] == metier: liste.insert(idx, f"{utilisateur[0]} ({'manager' if utilisateur[1] == 0 else 'caissier'})") else: # ce cas est là au cas où mais n'est pas sensé être appellé raise NameError("Métier inconnu.") # Label d'information Label(self.f, text=""" Double-cliquez sur un utilisateur de la liste pour obtenir des informations supplémentaire à son sujet. """, justify="right").grid(column=1, row=4, rowspan=2, sticky="e") Label(self.f, text="Liste des utilisateurs", font=self.font).grid(column=0, row=4) # titre # On définit une barre pour pouvoir scroller dans la liste scroll = Scrollbar(self.f, orient="vertical") scroll.grid(column=0, row=5, sticky="nse") # On définit notre liste et on la lie à notre `Scrollbar` listeUtilisateurs = Listbox(self.f, width=25, height=4, yscrollcommand=scroll.set) scroll["command"] = listeUtilisateurs.yview # scroll à la verticale dans notre liste # On ajoute nos utilisateurs à notre liste __ajouterUtilisateursListe(listeUtilisateurs) listeUtilisateurs.grid(column=0, row=5) listeUtilisateurs.bind('', __afficherInformationsUtilisateur) # on affiche l'utilisateur quand on double-clique dessus # Filtre pour la liste Label(self.f, text="Filtre", font=self.font).grid(column=1, row=4, sticky='w', padx=10) # titre filtres = Frame(self.f) # Morceau qui va contenir nos checkbutton filtres.grid(column=1, row=4, rowspan=2, sticky='w') Checkbutton(filtres, text="Manager", variable=managerVerif, command=lambda: __ajouterUtilisateursListe(listeUtilisateurs)).grid(sticky='w') Checkbutton(filtres, text="Caissier", variable=caissierVerif, command=lambda: __ajouterUtilisateursListe(listeUtilisateurs)).grid(sticky='w') Button(self.f, text="Passer en mode caissier", font=self.font, command=lambda: self._interfaceCaissier(id)).grid(column=0, row=6, columnspan=3, pady=10) if __name__ == "__main__": """Application "GesMag" pour le module de Programmation d'interfaces (2021-2022)""" print("-- Compte par défaut --\nNom d'utilisateur: admin\nMot de passe: P@ssword") GesMag().demarrer()