<repo_name>gbarboteau/OFFSubstitutes<file_sep>/graphicstate.py
"""Handles the logic and actions of the program
when launched with a graphic interface.
"""
import sys
import tkinter as tk
from tkinter import font  as tkfont

import database
from util import is_integer
from statemachine import States


class GraphicStateMachine(tk.Frame):
    """Create an instance of a state machine
    handling states in terminal mode.
    """
    def __init__(self, my_auth, master=None):
        """Initialize the instance.
        The strings are what we use to check
        which categories and products we
        are dealing with.
        """
        super().__init__(master)
        self.state = None
        self.currentCategory = ""
        self.currentProduct = ""
        self.currentSubstitute = ""
        self.currentAssociation = ""
        self.state = States.LaunchScreen
        self.master = master
        self.pack()
        self.dt = database.Database(my_auth)
        self.title_font = tkfont.Font(family='Helvetica', size=18, weight="bold", slant="italic")
        self.normal_font = tkfont.Font(family='Helvetica', size=12, weight="bold")
        self.container = tk.Frame(self, width=720, height=400)
        self.container.pack(side="top", fill="both", expand=True)
        self.frame = NewScreen(parent=self.container, controller=self)
        self.frame.grid(row=0, column=0, sticky="nsew")
        self.frame.tkraise()
        self.widgetList = []
        self.state_launchscreen()

    def change_state(self, state_name):
        """Is called after (almost) every button click.
        Destroy current widget, change the current game
        state, and launch a new state.
        """
        self.state = state_name
        for widget in self.widgetList:
            widget.destroy()
        self.widgetList = []
        if self.state == States.LaunchScreen:
            self.state_launchscreen()
        if self.state == States.SearchForCategory:
            self.state_searchforcategory()
        if self.state == States.SearchForAliment:
            self.state_searchforaliment()
        if self.state == States.SearchForSubstitute:
            self.state_searchforsubstitute()
        if self.state == States.OnFoundSubstitute:
            self.state_onfoundsubstitute()
        if self.state == States.SaveAssociation:
            self.state_saveassociation()
        if self.state == States.LookAtSubstitutes:
            self.state_lookatsubstitutes()
        if self.state == States.LookAtOneSubstitute:
            self.state_lookatonesubstitute()
        if self.state == States.DeleteAssociation:
            self.state_deleteassociation()
        if self.state == States.Bye:
            self.state_bye()

    def state_launchscreen(self):
        """The menu screen. Appears when the
        program is launched or when you get back
        to it.
        """
        search_for_category_button = tk.Button(self, text="Chercher un substitut",
                            command=lambda: self.change_state(States.SearchForCategory))
        search_for_aliment_button = tk.Button(self, text="Montrer les substituts déjà enregistrés",
                            command=lambda: self.change_state(States.LookAtSubstitutes))
        bye_button = tk.Button(self, text="Quitter l'application",
                            command= self.master.destroy)
        search_for_category_button.pack()
        search_for_aliment_button.pack()
        bye_button.pack()
        self.widgetList.extend([search_for_category_button, search_for_aliment_button, bye_button])

    def state_searchforcategory(self):
        """Show the screen allowing the user
        to choose a category of food.
        """
        for category in range(0, len(self.dt.all_categories)):
            category_button = tk.Button(self, text=self.dt.all_categories[category],
                            command=lambda category=category : self.change_currentcategory(category, States.SearchForAliment))
            category_button.pack()
            self.widgetList.append(category_button)
        previous_screen_button = tk.Button(self, text="Retourner à l'écran précédent",
                            command=lambda: self.change_state(States.LaunchScreen))
        bye_button = tk.Button(self, text="Quitter l'application",
                            command= self.master.destroy)
        previous_screen_button.pack()
        bye_button.pack()
        self.widgetList.extend([previous_screen_button, bye_button])

    def change_currentcategory(self, category, state_name):
        """Change the current category."""
        self.currentCategory = self.dt.all_categories[category]
        self.change_state(state_name)

    def state_searchforaliment(self):
        """Show the screen allowing the user
        to choose a food from a category.
        """
        self.dt.get_all_aliments_from_category(self.currentCategory)
        for aliment in range(0, len(self.dt.all_aliments_from_category)):
            aliment_button = tk.Button(self, text=self.dt.all_aliments_from_category[aliment],
                            command=lambda aliment=aliment: self.change_currentproduct(aliment, States.SearchForSubstitute))
            aliment_button.pack()
            self.widgetList.append(aliment_button)
        previous_screen_button = tk.Button(self, text="Retourner à l'écran précédent",
                            command=lambda: self.change_state(States.SearchForCategory))
        bye_button = tk.Button(self, text="Quitter l'application",
                            command= self.master.destroy)
        previous_screen_button.pack()
        bye_button.pack()
        self.widgetList.extend([previous_screen_button, bye_button])

    def change_currentproduct(self, product, state_name):
        """Change the current product."""
        self.currentProduct = self.dt.all_aliments_from_category[product]
        self.change_state(state_name)

    def state_searchforsubstitute(self):
        """Show the screen allowing the user
        to choose a substitute for a specific
        food.
        """
        self.dt.get_aliment(self.currentProduct)
        self.dt.get_all_substitutes(self.currentProduct, self.currentCategory, self.dt.this_aliment[4])

        for substitute in range(0, len(self.dt.all_substitutes)):
            substitute_button = tk.Button(self, text=self.dt.all_substitutes[substitute],
                            command=lambda substitute=substitute: self.change_currentsubstitute(substitute, States.OnFoundSubstitute))
            substitute_button.pack()
            self.widgetList.append(substitute_button)
        previous_screen_button = tk.Button(self, text="Retourner à l'écran précédent",
                            command=lambda: self.change_state(States.SearchForAliment))
        bye_button = tk.Button(self, text="Quitter l'application",
                            command= self.master.destroy)
        previous_screen_button.pack()
        bye_button.pack()
        self.widgetList.extend([previous_screen_button, bye_button])

    def change_currentsubstitute(self, substitute, state_name):
        """Change the current substitute."""
        self.currentSubstitute = self.dt.all_substitutes[substitute]
        self.change_state(state_name)

    def state_onfoundsubstitute(self):
        """Show the screen allowing the user
        to examine a substitute for a specific
        food, then save it.
        """
        self.dt.get_this_substitute(self.currentSubstitute, self.currentProduct)
        label_text = (self.dt.this_substitute_stringed + "\n")
        label = tk.Label(self, text=label_text, font=self.normal_font)
        label.pack(side="top", fill="x", pady=10)
        save_association_button = tk.Button(self, text="Sauvegarder l'association",
                            command=lambda: self.change_state(States.SaveAssociation))
        previous_screen_button = tk.Button(self, text="Retourner à l'écran précédent",
                            command=lambda: self.change_state(States.SearchForSubstitute))
        bye_button = tk.Button(self, text="Quitter l'application",
                            command= self.master.destroy)
        save_association_button.pack()
        previous_screen_button.pack()
        bye_button.pack()
        self.widgetList.extend([label, save_association_button, previous_screen_button, bye_button])

    def state_saveassociation(self):
        """Show the screen indicating a substitute
        for a specific food has been saved.
        """
        self.dt.save_association(self.currentSubstitute, self.currentProduct)
        label_text = ("Association sauvegardée, merci beaucoup !")
        label = tk.Label(self, text=label_text, font=self.normal_font)
        label.pack(side="top", fill="x", pady=10)

        launch_screen_button = tk.Button(self, text="Retourner au menu principal",
                            command=lambda: self.change_state(States.LaunchScreen))
        previous_screen_button = tk.Button(self, text="Retourner à l'écran des substituts",
                            command=lambda: self.change_state(States.SearchForSubstitute))
        bye_button = tk.Button(self, text="Quitter l'application",
                            command= self.master.destroy)
        launch_screen_button.pack()
        previous_screen_button.pack()
        bye_button.pack()
        self.widgetList.extend([label, launch_screen_button, previous_screen_button, bye_button])

    def state_lookatsubstitutes(self):
        """Show the screen listing every
        combination of food/substitute
        previously registered.
        """
        self.dt.update_my_associations()
        for association in range(0, len(self.dt.all_associations)):
            association_button = tk.Button(self, text=self.dt.all_associations_list_stringed[association],
                            command=lambda association=association : self.change_currentassociation(association, States.LookAtOneSubstitute))
            association_button.pack()
            self.widgetList.append(association_button)
        previous_screen_button = tk.Button(self, text="Retourner à l'écran précédent",
                            command=lambda: self.change_state(States.LaunchScreen))
        bye_button = tk.Button(self, text="Quitter l'application",
                            command= self.master.destroy)
        previous_screen_button.pack()
        bye_button.pack()
        self.widgetList.extend([previous_screen_button, bye_button])

    def change_currentassociation(self, association, state_name):
        """Change the current association."""
        self.currentAssociation = self.dt.all_associations[association]
        self.change_state(state_name)

    def state_lookatonesubstitute(self):
        """Show the screen of a specific
        food/substitue combination, and allow
        the user to delete it.
        """
        alim1 = self.dt.get_product_by_id(self.currentAssociation[1])
        alim2 = self.dt.get_product_by_id(self.currentAssociation[2])
        alim1_stringed = self.dt.get_any_aliment_dict_to_string(alim1)
        alim2_stringed = self.dt.get_any_aliment_dict_to_string(alim2)
        label_1 = tk.Label(self, text=alim1_stringed, font=self.normal_font)
        label_1.pack(side="top", fill="x", pady=10)
        label_2 = tk.Label(self, text=alim2_stringed, font=self.normal_font)
        label_2.pack(side="top", fill="x", pady=10)
        delete_association_button = tk.Button(self, text="Supprimer l'association",
                            command=lambda: self.change_state(States.DeleteAssociation))
        previous_screen_button = tk.Button(self, text="Retourner à l'écran des substituts",
                            command=lambda: self.change_state(States.LookAtSubstitutes))
        bye_button = tk.Button(self, text="Quitter l'application",
                            command= self.master.destroy)
        delete_association_button.pack()
        previous_screen_button.pack()
        bye_button.pack()
        self.widgetList.extend([label_1, label_2, delete_association_button, previous_screen_button, bye_button])

    def state_deleteassociation(self):
        """Confirm the suppression of a former
        food/substitute association.
        """
        self.dt.delete_association(self.currentAssociation[0])
        label_text = ("Association supprimée, merci beaucoup !")
        label = tk.Label(self, text=label_text, font=self.normal_font)
        label.pack(side="top", fill="x", pady=10)
        launch_screen_button = tk.Button(self, text="Retourner au menu principal",
                            command=lambda: self.change_state(States.LaunchScreen))
        previous_screen_button = tk.Button(self, text="Retourner à l'écran des substituts",
                            command=lambda: self.change_state(States.LookAtSubstitutes))
        bye_button = tk.Button(self, text="Quitter l'application",
                            command= self.master.destroy)
        launch_screen_button.pack()
        previous_screen_button.pack()
        bye_button.pack()
        self.widgetList.extend([label, launch_screen_button, previous_screen_button, bye_button])

    def state_bye(self):
        """Quit the program"""
        self.master.destroy()


class NewScreen(tk.Frame):
    """Create a new frame."""
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        label = tk.Label(self, text="This is the start page", font=controller.title_font)
        label.pack(side="top", fill="x", pady=10)
<file_sep>/DatabaseFiller/create_database.sql
SET NAMES utf8mb4;

DROP TABLE IF EXISTS Swap;
DROP TABLE IF EXISTS Aliment;
DROP TABLE IF EXISTS Category;

CREATE TABLE Category(
    id int unsigned NOT NULL AUTO_INCREMENT,
    category_name varchar(100) NOT NULL,
    category_url varchar(200) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY category_name (category_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE Aliment (
    id int unsigned NOT NULL AUTO_INCREMENT,
    product_name varchar(100) NOT NULL,
    product_description text,
    barcode varchar(20) NOT NULL,
    nutritional_score char(1) NOT NULL,
    stores varchar(200),
    product_category varchar(100) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY product_name (product_name),
    CONSTRAINT fk_product_category FOREIGN KEY (product_category) REFERENCES Category(category_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE Swap(
    id int unsigned NOT NULL AUTO_INCREMENT,
    aliment_id int unsigned NOT NULL,
    substitute_id int unsigned NOT NULL,
    PRIMARY KEY (id),
    CONSTRAINT fk_aliment_id FOREIGN KEY (aliment_id) REFERENCES Aliment(id),
    CONSTRAINT fk_substitute_id FOREIGN KEY (substitute_id) REFERENCES Aliment(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO Category(category_name, category_url) VALUES
("Compotes", "https://fr.openfoodfacts.org/categorie/compotes"),
("Bonbons de chocolat", "https://fr.openfoodfacts.org/categorie/bonbons-de-chocolat"),
("Sodas", "https://fr.openfoodfacts.org/categorie/sodas"),
("Barres", "https://fr.openfoodfacts.org/categorie/barres"),
("Yaourts aux fruits", "https://fr.openfoodfacts.org/categorie/yaourts-aux-fruits"),
("Produits de la mer", "https://fr.openfoodfacts.org/categorie/produits-de-la-mer"),
("Produits a la viande", "https://fr.openfoodfacts.org/categorie/produits-a-la-viande"),
("Produits deshydrates", "https://fr.openfoodfacts.org/categorie/produits-deshydrates"),
("Jambons", "https://fr.openfoodfacts.org/categorie/jambons"),
("Cereales pour petit-dejeuner", "https://fr.openfoodfacts.org/categorie/cereales-pour-petit-dejeuner");<file_sep>/README.md
Open Food Facts Substitutes

*1 - Create the database*

In MySQL, create database name "openfoodfacts". Create a user for this database (give them the username and the password you want).
Then, launch the DatabaseFiller/create_database.sql script.

CREATE DATABASE openfoodfacts;  
USE openfoodfacts;  
SOURCE Documents/OFFSubstitutes/DatabaseFiller/create_database.sql;  


*2 - Fill the database*

Launch the python script DatabaseFiller/databasefiller.py. Add the username of the MySQL account you'll use (password will be asked later).

python DatabaseFiller/databasefiller.py user


*3 - Use the software*

Launch the main.py script. You need to add the argument -t or -g (to use the program with a text or a graphic interface), and the username of the MySQL account you'll use (password will be asked later).

python main.py -t user
<file_sep>/program.py
"""The program providing users a list of aliments
and their possible substitutes. Launched through
main.py.
"""
import sys
import tkinter as tk

import terminalstate
import graphicstate
from statemachine import States


class Program:
    """Parent class of ProgramTerminal.
    Not used for now as ProgramGraphic isn't
    implemented yet.
    """
    def __init__(self, my_auth):
        """The constructor is common to ProgramTerminal
        and ProgramGraphic.
        """
        # self.dt = database.Database(my_auth)
        self.state = "LaunchScreen"
        self.sm = None


class ProgramGraphic(Program):
    """Allow to launch and use the program 
    with a graphic interface.
    """
    def __init__(self, my_auth):
        """Creates an instance of the ProgramTerminal
        and its state machine.
        """
        super().__init__(my_auth)
        root = tk.Tk()
        self.sm = graphicstate.GraphicStateMachine(my_auth, master=root)

    def launch(self):
        """Launch the tkinter instance.
        Logic is handled in graphicstate.py
        """
        self.sm.mainloop()
        

class ProgramTerminal(Program):
    """Allow to launch and use the program 
    in a terminal window.
    """
    def __init__(self, my_auth):
        """Creates an instance of the ProgramTerminal
        and its state machine.
        """
        super().__init__(my_auth)
        self.sm = terminalstate.TerminalStateMachine(my_auth)

    def launch(self):
        """Launch the program in a terminal window.
        Checks for inputs and current state, and changes
        what's is displayed according to these.
        """
        print("\n")
        user_input = ""
        while 1:
            if self.sm.state == States.LaunchScreen:
                self.sm.state_launchscreen()
            if self.sm.state == States.SearchForCategory:
                self.sm.state_searchforcategory()
            if self.sm.state == States.SearchForAliment:
                self.sm.state_searchforaliment()
            if self.sm.state == States.SearchForSubstitute:
                self.sm.state_searchforsubstitute()
            if self.sm.state == States.OnFoundSubstitute:
                self.sm.state_onfoundsubstitute()
            if self.sm.state == States.SaveAssociation:
                self.sm.state_saveassociation()
            if self.sm.state == States.LookAtSubstitutes:
                self.sm.state_lookatsubstitutes()
            if self.sm.state == States.LookAtOneSubstitute:
                self.sm.state_lookatonesubstitute()
            if self.sm.state == States.DeleteAssociation:
                self.sm.state_deleteassociation()
            if self.sm.state == States.Bye:
                self.sm.state_bye()
<file_sep>/terminalstate.py
"""Handle the different state of the program
when it's launched in terminal mode.
"""
import sys

import database
from util import is_integer
from statemachine import States


class TerminalStateMachine:
    """Create an instance of a state machine
    handling states in terminal mode.
    """
    def __init__(self, my_auth):
        """Initialize the instance.
        The strings are what is shown in terminal,
        we update them as we change states.
        """
        self.dt = database.Database(my_auth)
        self.state = ""
        self.currentCategory = ""
        self.currentProduct = ""
        self.currentSubstitute = ""
        self.currentAssociation = ""
        self.state = States.LaunchScreen

    def state_launchscreen(self):
        """The menu screen. Appears when the
        program is launched or when you get back
        to it.
        """
        print("Bienvenue sur OpenFoodSubstitute ! Cherchez-vous un aliment à substituer, ou un substitut déjà enregistré ?\n")
        user_input = input("Appuyez sur:\n1: Chercher un substitut\n2: Un aliment déjà enregistré\n3 : Quitter le programme\nVous pouvez quitter le programme à tout moment en entrant Q\n")
        if user_input.lower() == "1":
            self.state = States.SearchForCategory
        elif user_input.lower() == "2":
            self.state = States.LookAtSubstitutes
        if user_input.lower() == "3" or user_input.lower() == "q":
            self.state = States.Bye

    def state_searchforcategory(self):
        """Show the screen allowing the user
        to choose a category of food.
        """
        print("Voici les substituts mis à votre disposition:\n")
        print (self.dt.all_categories_stringed)
        user_input = input("Entrez le chiffre correspondant à la catégorie désirée, R pour retourner à l'écran précédent, ou Q pour quitter le programme\n")
        if is_integer(user_input):
            if 1 <= int(user_input) <= len(self.dt.all_categories) +1:
                self.currentCategory = self.dt.all_categories[int(user_input) - 1]
                self.state = States.SearchForAliment
            else:
                print("Nombre invalide ! Réessayez\n")
        else:
            if user_input.lower() == "q":
                self.state = States.Bye
            elif user_input.lower() == "r":
                self.state = States.LaunchScreen
            else:
                print("Commande invalide, réessayez!\n")

    def state_searchforaliment(self):
        """Show the screen allowing the user
        to choose a food from a category.
        """
        self.dt.get_all_aliments_from_category(self.currentCategory)
        print("Bienvenue dans la catégorie " + self.currentCategory + "\n")
        print(self.dt.all_aliments_from_category_stringed)
        user_input = input("Entrez le chiffre correspondant au produit désiré, R pour retourner à l'écran précédent, ou Q pour quitter le programme\n")
        if is_integer(user_input):
            if 1 <= int(user_input) <= len(self.dt.all_aliments_from_category) +1:
                self.currentProduct = self.dt.all_aliments_from_category[int(user_input) - 1]
                self.state = States.SearchForSubstitute
            else:
                print("Nombre invalide ! Réessayez\n")
        else:
            if user_input.lower() == "q":
                self.state = States.Bye
            elif user_input.lower() == "r":
                self.state = States.SearchForCategory
            else:
                print("Commande invalide, réessayez!\n")

    def state_searchforsubstitute(self):
        """Show the screen allowing the user
        to choose a substitute for a specific
        food.
        """
        self.dt.get_aliment(self.currentProduct)
        print(self.dt.this_aliment_stringed + "\n\n")
        self.dt.get_all_substitutes(self.currentProduct, self.currentCategory, self.dt.this_aliment[4])
        print(self.dt.all_substitutes_stringed)
        user_input = input("Entrez le chiffre correspondant au substitut désiré, R pour retourner à l'écran précédent, ou Q pour quitter le programme\n")
        if is_integer(user_input):
            if 1 <= int(user_input) <= len(self.dt.all_substitutes) +1:
                self.currentSubstitute = self.dt.all_substitutes[int(user_input) - 1]
                self.state = States.OnFoundSubstitute
            else:
                print("Nombre invalide ! Réessayez\n")
        else:
            if user_input.lower() == "q":
                self.state = States.Bye
            elif user_input.lower() == "r":
                self.state = States.SearchForAliment
            else:
                print("Commande invalide, réessayez!\n")

    def state_onfoundsubstitute(self):
        """Show the screen allowing the user
        to examine a substitute for a specific
        food, then save it.
        """
        print("C'est gagné ! Aliment : " + self.currentSubstitute)
        self.dt.get_this_substitute(self.currentSubstitute, self.currentProduct)
        print(self.dt.this_substitute_stringed + "\n")
        user_input = input("Entrez S pour sauvegarder cette association, R pour retourner à l'écran précédent, ou Q pour quitter le programme\n")
        if user_input.lower() == "q":
            self.state = States.Bye
        elif user_input.lower() == "r":
            self.state = States.SearchForSubstitute
        elif user_input.lower() == "s":
            self.state = States.SaveAssociation
        else:
            print("Commande invalide, réessayez!\n")

    def state_saveassociation(self):
        """Show the screen indicating a substitute
        for a specific food has been saved.
        """
        self.dt.save_association(self.currentSubstitute, self.currentProduct)
        print("\nAssociation sauvegardée ! Vous pouvez la consulter depuis le menu principal.\n")
        user_input = input("Entrez M pour retourner au menu principal, R pour retourner à l'écran des substituts disponibles, ou Q pour quitter le programme\n")
        if user_input.lower() == "q":
            self.state = States.Bye
        elif user_input.lower() == "r":
            self.state = States.SearchForSubstitute
        elif user_input.lower() == "m":
            self.state = States.LaunchScreen
        else:
            print("Commande invalide, réessayez!\n")

    def state_lookatsubstitutes(self):
        """Show the screen listing every
        combination of food/substitute
        previously registered.
        """
        self.dt.update_my_associations()
        print(self.dt.all_associations_stringed)
        user_input = input("Entrez le chiffre correspondant au substitut désiré, R pour retourner à l'écran précédent, ou Q pour quitter le programme\n")
        if is_integer(user_input):
            if 1 <= int(user_input) <= len(self.dt.all_associations) +1:
                self.currentAssociation = self.dt.all_associations[int(user_input) - 1]
                self.state = States.LookAtOneSubstitute
            else:
                print("Nombre invalide ! Réessayez\n")
        else:
            if user_input.lower() == "q":
                self.state = States.Bye
            elif user_input.lower() == "r":
                self.state = States.LaunchScreen
            else:
                print("Commande invalide, réessayez!\n")

    def state_lookatonesubstitute(self):
        """Show the screen of a specific
        food/substitue combination, and allow
        the user to delete it.
        """
        alim1 = self.dt.get_product_by_id(self.currentAssociation[1])
        alim2 = self.dt.get_product_by_id(self.currentAssociation[2])
        alim1_stringed = self.dt.get_any_aliment_dict_to_string(alim1)
        alim2_stringed = self.dt.get_any_aliment_dict_to_string(alim2)
        print("L'aliment à substituer est:\n")
        print(alim1_stringed)
        print("Nous vous proposons à la place :\n")
        print(alim2_stringed)
        user_input = input("Entrez R pour retourner à l'écran précédent, S pour supprimer l'association, ou Q pour quitter le programme\n")
        if user_input.lower() == "q":
            self.state = States.Bye
        elif user_input.lower() == "r":
            self.state = States.LookAtSubstitutes
        elif user_input.lower() == "s":
            self.state = States.DeleteAssociation
        else:
            print("Commande invalide, réessayez!\n")

    def state_deleteassociation(self):
        """Confirm the suppression of a former
        food/substitute association.
        """
        self.dt.delete_association(self.currentAssociation[0])
        print("Voilà qui est fait !\n")
        user_input = input("Entrez R pour retourner à l'écran des substituts, M pour le menu principal, ou Q pour quitter le programme\n")
        if user_input.lower() == "q":
            self.state = States.Bye
        elif user_input.lower() == "r":
            self.state = States.LookAtSubstitutes
        elif user_input.lower() == "m":
            self.state = States.LaunchScreen
        else:
            print("Commande invalide, réessayez!\n")

    def state_bye(self):
        """Quit the program"""
        sys.exit()
<file_sep>/database.py
"""Handles every call to our database."""
import mysql.connector

from util import list_to_num_string


class Database:
    """A class handling interactions with our
    database. Read data and write in the
    'swap' table.
    """
    def __init__(self, my_auth):
        """Create an instance of database."""
        self.user = my_auth.user
        self.password = my_auth.<PASSWORD>
        self.all_categories = self.find_categories()
        self.all_categories_stringed = list_to_num_string(self.all_categories)
        self.all_aliments_from_category = []
        self.all_aliments_from_category_stringed = ""
        self.this_aliment = None
        self.this_aliment_stringed = ""
        self.all_substitutes = []
        self.all_substitutes_stringed = ""
        self.this_substitute = None
        self.this_substitute_stringed = ""
        self.all_associations = self.find_associations()
        self.all_associations_stringed = ""
        self.all_associations_list_stringed = []
        self.find_associations_string()

    def find_categories(self):
        """Find every category in the database."""
        my_categories = []
        my_connection = mysql.connector.connect(user=self.user, password=self.password, database='openfoodfacts')
        cursor = my_connection.cursor()
        query = ("SELECT category_name FROM category ORDER BY id")
        cursor.execute(query)
        for category_name in cursor:
            my_categories.append(category_name[0])
        cursor.close()
        my_connection.close()
        return my_categories

    def get_all_aliments_from_category(self, category):
        """Find every aliment from a cateegory."""
        self.all_aliments_from_category = []
        my_connection = mysql.connector.connect(user=self.user, password=self.<PASSWORD>, database='openfoodfacts')
        cursor = my_connection.cursor()
        query = ("SELECT product_name FROM Aliment WHERE product_category = %s ORDER BY id") % ("\'" + category + "\'")
        cursor.execute(query)
        for product_name in cursor:
            self.all_aliments_from_category.append(product_name[0])
        cursor.close()
        my_connection.close()
        self.all_aliments_from_category_stringed = list_to_num_string(self.all_aliments_from_category)

    def get_aliment(self, aliment):
        """Get a specific product informations."""
        self.this_aliment = ""
        my_connection = mysql.connector.connect(user=self.user, password=self.<PASSWORD>, database='openfoodfacts')
        cursor = my_connection.cursor()
        query = ("SELECT * FROM Aliment WHERE product_name = %s") % ("\'" + aliment + "\'")
        cursor.execute(query)
        for product_name in cursor:
            self.this_aliment = product_name
        cursor.close()
        my_connection.close()
        self.this_aliment_stringed = self.get_any_aliment_dict_to_string(self.this_aliment)

    def get_any_aliment_dict_to_string(self, aliment_dict):
        """Take a dictionnary containing informations for
        a specific food, then turn it into a formatted 
        string for readability purposes.
        """
        string_to_return = "Produit: {}\nCatégorie: {}\nDescription: {}\nUrl: https://fr.openfoodfacts.org/produit/{}\nNutri-score: {}\nMagasins: {}\n".format(aliment_dict[1],
            aliment_dict[6], aliment_dict[2], aliment_dict[3], aliment_dict[4], aliment_dict[5])
        return string_to_return

    def get_product_by_id(self, id_aliment):
        """Get the informations for a specific
        product by consulting the OFF database.
        """
        product_by_id = ""
        my_connection = mysql.connector.connect(user=self.user, password=self.password, database='openfoodfacts')
        cursor = my_connection.cursor()
        query = ("SELECT * FROM Aliment WHERE id = %s") % ("\'" + str(id_aliment) + "\'")
        cursor.execute(query)
        for category_name in cursor:
            product_by_id = category_name
        cursor.close()
        my_connection.close()
        return product_by_id

    def get_all_substitutes(self, aliment, category, nutriscore):
        """Find every substitutes for a given product."""
        self.all_substitutes = []
        my_connection = mysql.connector.connect(user=self.user, password=self.<PASSWORD>, database='openfoodfacts')
        cursor = my_connection.cursor()
        query = ("SELECT product_name FROM Aliment WHERE product_category = %s AND STRCMP(nutritional_score, %s) < 0 ORDER BY id") % ("\'" + category + "\'", "\'" + nutriscore + "\'")
        cursor.execute(query)
        for product_name in cursor:
            self.all_substitutes.append(product_name[0])
        cursor.close()
        my_connection.close()
        self.all_substitutes_stringed = list_to_num_string(self.all_substitutes)

    def get_this_substitute(self, aliment, alimentSubstitued):
        """Get a specific substitute informations."""
        self.this_substitute = None
        my_connection = mysql.connector.connect(user=self.user, password=self.password, database='openfoodfacts')
        cursor = my_connection.cursor()
        query = ("SELECT * FROM Aliment WHERE product_name = %s") % ("\'" + aliment + "\'")
        cursor.execute(query)
        for product_name in cursor:
            self.this_substitute = product_name
        cursor.close()
        my_connection.close()
        self.this_substitute_stringed = self.get_any_aliment_dict_to_string(self.this_substitute)

    def save_association(self, aliment, alimentSubstitued):
        """Write an entry in the swap table."""
        my_connection = mysql.connector.connect(user=self.user, password=self.password, database='openfoodfacts')
        cursor = my_connection.cursor()
        add_swap = ("INSERT INTO Swap "
               "(aliment_id, substitute_id) "
               "VALUES (%s, %s)")
        data_swap = (self.this_aliment[0], self.this_substitute[0])
        cursor.execute(add_swap, data_swap)
        my_connection.commit()
        cursor.close()
        my_connection.close()

    def find_associations(self):
        """Find every food/product association in the 
        swap table.
        """
        my_associations = []
        my_connection = mysql.connector.connect(user=self.user, password=self.password, database='openfoodfacts')
        cursor = my_connection.cursor()
        query = ("SELECT * FROM Swap ORDER BY id")
        cursor.execute(query)
        for category_name in cursor:
            my_associations.append(category_name)
        cursor.close()
        my_connection.close()
        return my_associations

    def find_associations_string(self):
        """Return a string of find_associations()"""
        self.all_associations_stringed = ""
        self.all_associations_list_stringed = []
        for i in range(1, len(self.all_associations) +1):
            aliment_string = self.get_product_by_id(self.all_associations[i-1][1])[1]
            substitude_string = self.get_product_by_id(self.all_associations[i-1][2])[1]
            self.all_associations_stringed = self.all_associations_stringed + str(i) + ") " + aliment_string + " / " + substitude_string + "\n"
            self.all_associations_list_stringed.append(aliment_string + " / " + substitude_string + "\n")

    def update_my_associations(self):
        """Update the associations list after creating
        or deleting a food association.
        """
        self.all_associations = self.find_associations()
        self.find_associations_string()

    def delete_association(self, id_row_to_delete):
        """Delete an association between two aliments."""
        my_connection = mysql.connector.connect(user=self.user, password=self.password, database='openfoodfacts')
        cursor = my_connection.cursor()
        query = ("DELETE FROM Swap WHERE id = %s") % ("\'" + str(id_row_to_delete) + "\'")
        cursor.execute(query)
        my_connection.commit()
        cursor.close()
        my_connection.close()
<file_sep>/util.py
"""Some useful functions abstract enough
to be used into several scripts.
"""
def is_integer(string_to_check):
    """Check if a command entered on the command
    line is a number or not.
    """
    try:
        int(string_to_check)
        return True
    except ValueError:
        return False

def list_to_num_string(my_list):
    """Turns a list into a numbered list."""
    string_to_return = ""
    for i in range(1, len(my_list) +1):
        string_to_return = string_to_return + str(i) + ") " + my_list[i - 1] + "\n"
    return string_to_return
<file_sep>/main.py
"""Launches the substitute finder program.
Let's you enter a username and a password
who match an existing MySQL account.
"""
import argparse
import getpass
import mysql.connector

from program import ProgramGraphic, ProgramTerminal
from auth import Auth


def main():
    """Checks for a matching username/password
    combination, then launches the program.
    The user needs to enter his username as an
    argument, then the password afterwards.
    """
    parser = argparse.ArgumentParser()
    my_auth = Auth()
    parser.add_argument("user", help="is the user for the database")
    parser.add_argument("-t", "--terminal", help="Launches the program in terminal mode", action="store_true")
    parser.add_argument("-g", "--graphic", help="Launches the program in graphic mode", action="store_true")
    args = parser.parse_args()
    my_auth.user = args.user
    my_auth.password = <PASSWORD>()
    try:
        my_connection = mysql.connector.connect(user=my_auth.user, password=my_auth.<PASSWORD>, database='openfoodfacts')
        my_connection.close()
        if args.terminal:
            pr = ProgramTerminal(my_auth)
        elif args.graphic:
            pr = ProgramGraphic(my_auth)
        else:
            pr = ProgramGraphic(my_auth)
        pr.launch()
    except mysql.connector.errors.ProgrammingError:
        print("This user/password combination doesn't exist! Try another combination.")

main();
<file_sep>/DatabaseFiller/databasefiller.py
"""The program filling the openfoodfacts database
with entries for its different categories.
"""
import argparse
import getpass
import mysql.connector

import datacollecter
import filler
import auth


def main():
    """Checks for a matching username/password
    combination, then fill the openfoodfacts database.
    """
    parser = argparse.ArgumentParser()
    my_auth = auth.Auth()
    parser.add_argument("user", help="is the user for the database")
    args = parser.parse_args()
    my_auth.user = args.user
    my_auth.password = <PASSWORD>()
    try:
        my_connection = mysql.connector.connect(user=my_auth.user, password=my_auth.<PASSWORD>, database='openfoodfacts')
        my_connection.close()
        dt = datacollecter.DataCollecter(my_auth)
        my_data = dt.my_data
        fil = filler.Filler(my_data, my_auth)
        fil.put_it_in_tables()
    except mysql.connector.errors.ProgrammingError:
        print("This user/password combination doesn't exist! Try another combination.")

main();
<file_sep>/DatabaseFiller/util.py
"""Some useful functions abstract enough
to be used into several scripts.
"""
def does_exist_in_dict(my_key, my_dict):
    """Check if a key exists in a given dict."""
    if my_key in my_dict:
        return my_dict[my_key]
    else:
        return "NONE"
<file_sep>/DatabaseFiller/datacollecter.py
"""Collects the data from the OpenFoodFacts API.
"""
import requests
import json
import mysql.connector

import util


class DataCollecter:
    """Creates instances of CategoryScrapper to 
    collect datas from food categories.
    """
    def __init__(self, my_auth):
        """Creates an instance of DataCollecter, 
        which then creates instances of 
        CategoryScrapper, and put returned
        informations in a list.
        """
        self.user = my_auth.user
        self.password = <PASSWORD>
        my_connection = mysql.connector.connect(user=my_auth.user, password=<PASSWORD>, database='openfoodfacts')
        cursor = my_connection.cursor()
        self.categories = []
        query = ("SELECT category_name FROM category ")
        cursor.execute(query)
        for category_name in cursor:
            self.categories.append(category_name[0])
        cursor.close()
        my_connection.close()
        self.category_scrapper = []
        for i in self.categories:
            self.category_scrapper.append(CategoryScrapper(i))
        self.data = self.category_scrapper
        self.my_data = []
        for i in self.data:
            for j in i.cleanedData:
                if j.cleanedData:
                    self.my_data.append(j.cleanedData)


class CategoryScrapper:
    """Create instances of ProductScrapper which
    collects informations of every product in
    the category.
    """
    def __init__(self, my_category):
        """Create an instance of CategoryScrapper"""
        self.fullRequest = requests.get("https://fr.openfoodfacts.org/categorie/" + my_category + ".json")
        self.readableRequest = json.loads(self.fullRequest.text)
        self.cleanedData = self.clean_data(self.readableRequest, my_category)

    def clean_data(self, my_dict, my_category):
        """Create a list of readable data
        by creating a ProductScrapper instance
        for every product in the category.
        """
        my_ids = []
        all_ids = len(my_dict['products'])
        for i in range(all_ids):
            my_ids.append(my_dict['products'][i]['_id'])
        my_products = []
        for i in my_ids:
            my_products.append(ProductScrapper(i, my_category))
        return my_products


class ProductScrapper:
    """Collect informations about a given
    product.
    """
    def __init__(self, barcode, my_category):
        """Create an instance of ProductScrapper"""
        self.fullRequest = requests.get("https://world.openfoodfacts.org/api/v0/product/" + barcode + ".json")
        self.readableRequest = json.loads(self.fullRequest.text)
        self.cleanedData = self.clean_data(self.readableRequest, my_category)

    def clean_data(self, my_dict, my_category):
        """Create a list of readable data
        for a given product.
        """
        my_data = {}
        all_the_data = my_dict['product']
        my_data['product_name'] = util.does_exist_in_dict('product_name_fr', all_the_data)
        my_data['product_description'] = util.does_exist_in_dict('ingredients_text', all_the_data)
        my_data['barcode'] = util.does_exist_in_dict('id', all_the_data)
        my_data['nutritional_score'] = util.does_exist_in_dict('nutrition_grade_fr', all_the_data)
        my_data['stores'] = util.does_exist_in_dict('stores', all_the_data)
        my_data['product_category'] = my_category
        if my_data['product_description'] == "":
            my_data['product_description'] = "Pas de description disponible"
        if my_data['stores'] == "":
            my_data['stores'] = "Pas de magasins disponibles"
        if "NONE" in my_data.values() or "" in my_data.values():
            return None
        else:
            return my_data
<file_sep>/DatabaseFiller/filler.py
"""Fill the database with the data collected by
datacollecter.py
"""
import mysql.connector


class Filler:
    """Adds a set of data in the openfoodfacts
    database. Needs to be authentificated.
    """
    def __init__(self, my_data, my_auth):
        """Create an instance of Filler"""
        self.user = my_auth.user
        self.password = <PASSWORD>
        self.my_data = my_data

    def put_it_in_tables(self):
        """Put all the data given at the instance creation
        into the database.
        """
        my_connection = mysql.connector.connect(user=self.user, password=self.password, database='openfoodfacts')
        cursor = my_connection.cursor(buffered=True)
        for i in self.my_data:
            prod_name = i['product_name']
            try:
                add_aliment = ("INSERT INTO aliment "
                       "(product_name, product_description, barcode, nutritional_score, stores, product_category) "
                       "VALUES (%s, %s, %s, %s, %s, %s)")
                data_aliment = (i['product_name'].replace("'", "''"), i['product_description'].replace("'", "''"), i['barcode'].replace("'", "''"), i['nutritional_score'].replace("'", "''"), i['stores'].replace("'", "''"), i['product_category'].replace("'", "''"))
                cursor.execute(add_aliment, data_aliment)
            except mysql.connector.IntegrityError:
                pass 
        my_connection.commit()
        cursor.close()
        my_connection.close()
        print("ok c'est fait")
<file_sep>/statemachine.py
"""A state machine with every different state
of the program. 
"""
from enum import Enum


class States(Enum):
    """An enumeration of every possible state."""
    LaunchScreen = "LaunchScreen"
    SearchForCategory = "SearchForCategory"
    SearchForAliment = "SearchForAliment"
    SearchForSubstitute = "SearchForSubstitute"
    OnFoundSubstitute = "OnFoundSubstitute"
    SaveAssociation = "SaveAssociation"
    LookAtSubstitutes = "LookAtSubstitutes"
    LookAtOneSubstitute = "LookAtOneSubstitute"
    DeleteAssociation = "DeleteAssociation"
    Bye = "Bye"
<file_sep>/auth.py
"""Handles the user authentification
User's username and password need to match
a MySQL's already created profile
(see the README for more info)
"""
class Auth:
    """A token identifying the current user"""
    def __init__(self):
        """The user is identified with its username 
        and password (must match the database)
        """
        self.user = ""
        self.password = ""
