Notre stage chez Toobib

Notre stage chez Toobib

Une plateforme pour une santé connectée, éthique et accessible à tous

Nous avons débuté notre stage chez Toobib avec un objectif clair en tête : contribuer au développement d'une plateforme web qui cherche à améliorer l'accès aux soins de santé, tout en respectant les principes fondamentaux de l'éthique et de la protection des données personnelles.

Des données de santé au service de la santé

Au cœur de la philosophie de Toobib se trouve la volonté de placer les données de santé au service des patients et des professionnels de santé. Toobib s'engage à déployer une utilisation des données de santé éthique, transparente et respectueuse du droit à la protection des données personnelles, en particulier celles couvertes par le secret médical et avons eu l'objectif de véhiculer cette volonté au travers de notre travail.

Nous sommes fiers d'avoir contribué à ce projet porteur d'avenir et avons hâte de voir l'impact positif que Toobib aura sur la vie des patients et des professionnels de santé.

Nos missions au cœur de Toobib : une expérience enrichissante

Nous, Alexis et Alin, étudiants à l'école ENI, avons eu la chance de vivre une expérience professionnelle enrichissante lors de notre stage chez Toobib depuis avril 2024. Au cours de ces quelques mois, nous avons contribué à différents projets clés qui ont permis de faire évoluer la plateforme et d'améliorer l'expérience utilisateur.

L'implémentation d'un thème personnalisé pour l'authentification via KeyCloak

KeyCloak : Un pilier de la sécurité

KeyCloak est un serveur d'authentification et de gestion des accès Open Source qui joue un rôle crucial dans la sécurisation de la plateforme Toobib. Nous avons eu pour mission d'implémenter un thème personnalisé pour l'authentification via KeyCloak, en veillant à une intégration harmonieuse avec l'identité visuelle de Toobib.

Capture d'écran de l'interface d'authentification KeyCloak personnalisée pour Toobib

Au-delà de l'aspect esthétique, nous avons dû apporter des modifications spécifiques au processus d'authentification afin d'adapter les informations nécessaires à l'inscription d'un praticien de santé sur le site Toobib. Cela a impliqué l'intégration de champs d'inscription spécifiques et la mise en place d'un flux d'authentification fluide et intuitif pour les praticiens.

Pour mener à bien cette mission, nous avons mis à profit nos compétences en HTML, CSS, JavaScript et Java. Nous avons également utilisé FreemarkerTemplate, un moteur de template Open Source, pour personnaliser l'interface d'authentification et l'API KeyCloak pour gérer les interactions avec le serveur d'authentification via le panneau d'administration.

Création d'un champ obligatoire lors de l'inscription d'un utilisateur sur Toobib (Identifiant RPPS) Capture d'écran de l'interface d'inscription KeyCloak personnalisée pour Toobib

Extrait de code de l'implantation de ce champ de saisie :

            <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('rpps',properties.kcFormGroupErrorClass!)}">
                <div class="${properties.kcLabelWrapperClass!}">
                    <label for="rppsId" class="${properties.kcLabelClass!}">${msg("rppsId")}</label>
                </div>
                <div class="${properties.kcInputWrapperClass!}">
                    <input
                            type="text"
                            id="rppsId"
                            class="${properties.kcInputClass!}"
                            name="rppsId"
                            value="${(register.formData['user.attributes.rpps']!'')}"
                            minlength="11"
                            maxlength="11"

                            aria-invalid="<#if messagesPerField.existsError('rppsId')>true</#if>"
                    />

                        <span hidden id="input-error-rppsId" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
                                    </span>
                </div>
            </div>

Extrait de code du script JavaScript, rendant le champ implanté plus haut obligatoire à la soumission du formulaire d'inscription

document.addEventListener('DOMContentLoaded', function() {
    var form = document.getElementById('kc-register-form');
    var inputRppsId = document.getElementById('rppsId');
    var selectedLanguage = document.getElementById('kc-current-locale-link');

    // Je récupère la valeur de mon message d'erreur dans mon formulaire (register.ftl)
    var errorMessage = document.getElementById('input-error-rppsId');

    form.addEventListener('submit', function(event) {
        var inputRppsIdValue = inputRppsId.value.trim();
        var  regex = /^[0-9]+$/;

        if (!inputRppsIdValue || inputRppsIdValue.length !== 11 || !regex.test(inputRppsIdValue)) {

                // Vérification sur le language utilisé par l'utilisateur (valeur de l'attribut outerText de la variable prenant pour valeur l'id "kc-current-locale-link)
                // afin d'adapter le message d'erreur utilisé
                if(selectedLanguage.outerText === "Français"){
                    errorMessage.textContent = 'Veuillez renseigner un identifiant RPPS valide de 11 chiffres.';
                }else if(selectedLanguage.outerText === "English"){
                    errorMessage.textContent = 'Please specify a valid RPPS Identifier of 11 digits.';
                }else{
                    errorMessage.textContent = 'Please specify a valid RPPS Identifier of 11 digits.';
                }

                // Ici j'utilise le comportement d'affichage des messages d'erreur de Keycloak par défaut en mettant la valeur de l'attribut "aria-invalid" à true
                inputRppsId.setAttribute('aria-invalid', 'true');

                // Je mets l'attribut "hidden" de mon message d'erreur à faux,
                // car je ne peux pas utiliser les conditions par défaut de Keycloak,
                // je dois donc faire une logique ici, qui fait simplement en sorte qu'un élément HTML apparaisse si l'on parvient à cet endroit du code
                errorMessage.hidden = false;

                // J'empêche la soumission du formulaire en cas d'erreur
                event.preventDefault();
        }else{

            errorMessage.hidden = true;
            inputRppsId.setAttribute('aria-invalid', 'false');

        }
    });
});

Réalisation des espaces personnels et des pages du site

Espaces personnels : Une interface utilisateur personnalisée et fluide

Nous avons également contribué à la création des interfaces utilisateur pour les différents types d'utilisateurs de Toobib, en veillant à une ergonomie intuitive et à une cohérence visuelle avec l'identité de Toobib. Nous avons notamment conçu les pages de profil, la page de recherche de praticien de santé, les pages de présentation des outils ainsi que la modification de diverses pages déjà présentes sur Toobib, en respectant au mieux, la structure de la maquette fournie.

Création de la page "Mon profil" Capture d'écran de l'interface "Mon profil" pour Toobib

Création de la page "Modifier mon profil" Capture d'écran de l'interface "Modifier mon profil" pour Toobib

Création de la page "Nos outils" Capture d'écran de l'interface "Nos outils" pour Toobib

Au-delà de l'aspect visuel, nous avons dû intégrer les fonctionnalités attendues dans les espaces personnels et les pages du site. Par exemple, nous avons mis en place un système permettant aux praticiens de santé de modifier leur profil en un seul clic, en récupérant automatiquement les informations disponibles les concernant via l'API FHIR.

Extrait de code de la fonctionnalité du bouton FHIR, permettant le remplissage des champs en un clic

  const handleFhirClick = async () => {
    const practitioner = await getPractitionerAndPractitionerRole(10100965150);
    if (practitioner.error) {
      console.log(practitioner);
      setErrorNotif(true);
      return;
    }
    setErrorNotif(false);
    let professionCode;
    practitioner.entry[1].resource.code[0].coding.forEach((code) => {
      if (parseInt(code.code)) {
        professionCode = code.code;
      }
    });
    const profession = await getProfessionString(professionCode);

    const orgId = extractOrgIdFromFhirPractitionerBundle(practitioner);

    let fhirData = {};

    if (orgId) {
      fhirData = await getFormatedOrganizationData(orgId);
      formik.setFieldValue("Cabinet.1.Lieu", fhirData.Lieu);
      formik.setFieldValue("Cabinet.1.CodePostal", fhirData.CodePostal);
      formik.setFieldValue("Cabinet.1.Adresse", fhirData.Adresse);
      formik.setFieldValue(
        "Cabinet.1.PhoneLieuConsultation",
        fhirData.PhoneLieuConsultation,
      );
      formik.setFieldValue("Cabinet.1.Ville", fhirData.Ville);
    }
    fhirData = { ...fhirData, profession: profession };
    formik.setFieldValue("PractionerRole", fhirData.profession);
  };

Pour réaliser ces missions, nous avons utilisé un ensemble de technologies web modernes, notamment HTML, CSS, Tailwind, JavaScript et Next.js. Nous avons également utilisé React pour développer des interfaces utilisateur dynamiques et réactives.

Création d'un système de recherche de praticiens de santé performant

Création de la page de recherche d'un professionnel de santé Capture d'écran de l'interface de recherche d'un professionnel de santé pour Toobib

Toobib : Une source de données fiable

L'un des défis majeurs que nous avons dû relever a été la création d'un système de recherche de praticiens de santé performant et fiable. Nous avons dû intégrer les données des praticiens de santé provenant de deux sources distinctes : l'annuaire de santé et l'API FHIR. Cela a nécessité une analyse minutieuse des données et une mise en place de processus de récupération de données rigoureux.

L'exploitation de l'API FHIR nous a permis d'obtenir des informations détaillées et standardisées sur les praticiens de santé, garantissant ainsi une source d'information fiable et cohérente pour les utilisateurs de la plateforme.

La performance du système de recherche était un enjeu crucial. Nous avons mis en place des techniques d'optimisation des requêtes pour minimiser les délais de recherche et offrir aux utilisateurs une expérience fluide et réactive.

Pour répondre aux besoins spécifiques des utilisateurs, nous avons ajouté des filtres et des critères de recherche avancés. Cela permet aux utilisateurs de trouver facilement les praticiens correspondant à leurs besoins, que ce soit en fonction de la profession recherchée, de la localisation du praticien ou encore de la distance par rapport à l'utilisateur et le praticien dont il a besoin.

Pour mener à bien ce projet, nous avons créé une base de données PostgreSQL afin de stocker les données des praticiens de santé de manière structurée et efficace. Nous avons ensuite exécuté une API en Python pour peupler cette base de données à partir des sources d'information disponibles (Annuaire de santé).

Schéma de la base de données peuplée par l'annuaire de santé, à laquelle nous avons ajouté la table coordonnées Capture d'écran du schéma de base de données de l'API de recherche

Schéma de la vue matérialisée, créée à partir des données contenues dans la base de données Capture d'écran du schéma de base de données de la vue matérialisée de l'API de recherche

Requête SQL nous ayant permis la création de cette vue matérialisée, au sein du script Python

            recreate_materialized_view = text("""
            CREATE MATERIALIZED VIEW external.recherche AS
            SELECT 
                p.identification_nationale_pp AS inpp,
                p.nom,
                COALESCE(external.multi_replace(lower(unaccent(p.nom)), '{" ": "", "-": ""}'::jsonb), ''::text) AS nom_normalise,
                p.prenom,
                COALESCE(external.multi_replace(lower(unaccent(p.prenom)), '{" ": "", "-": ""}'::jsonb), ''::text) AS prenom_normalise,
                p.libelle_profession AS profession,
                COALESCE(external.multi_replace(lower(unaccent(p.libelle_profession)), '{" ": "", "-": ""}'::jsonb), ''::text) AS profession_normalise,
                p.libelle_savoirfaire AS savoir_faire,
                COALESCE(external.multi_replace(lower(unaccent(p.libelle_savoirfaire)), '{" ": "", "-": ""}'::jsonb), ''::text) AS savoir_faire_normalise,
                p.libelle_commune AS commune,
                COALESCE(external.multi_replace(lower(unaccent(p.libelle_commune)), '{" ": "", "-": ""}'::jsonb), ''::text) AS commune_normalise,
                c.latitude,
                c.longitude
            FROM 
                external.ps_libreacces_personne_activite p
            left JOIN 
                external.coordonnees c ON p.coordonnees_id = c.coordonnees_id;
            """)

Personnalisation de la recherche : Une expérience utilisateur sur mesure

Soucieux de la pérennité du système, nous avons conçu un script Python flexible permettant d'adapter la base de données et d'intégrer facilement de nouveaux filtres de recherche, comme par exemple le filtrage par distance. Ce script a nécessité l'utilisation de la librairie SQLAlchemy, un outil puissant pour l'interaction avec les bases de données relationnelles.

Création d'un filtre "Autour de moi", permettant à l'utilisateur de trier les résultats de sa recherche en fonction de la distance des praticiens de santé Capture d'écran de l'interface de recherche d'un professionnel de santé pour Toobib

Création de la table "coordonnees" dans la base de données, ainsi que des contraintes nécessaires à l'implantation d'un tel filtre, en utilisant ce script Python

from sqlalchemy import create_engine, text
from sqlalchemy.exc import OperationalError

def main():
    engine = create_engine("postgresql+psycopg2://userdb:pwd@localhost:5432/db")

    try:
        with engine.connect() as connection:
            print("Connected to DB")
            print("-" * 40)
            
            # Drop the table coordonnees in cascade
            drop_table_query = text("DROP TABLE IF EXISTS external.coordonnees CASCADE;")
            connection.execute(drop_table_query)
            connection.commit()
            print("Dropped table coordonnees")
            
            # Drop the column coordonnees_id from the table ps_libreacces_personne_activite
            drop_column_query = text("ALTER TABLE external.ps_libreacces_personne_activite DROP COLUMN IF EXISTS coordonnees_id;")
            connection.execute(drop_column_query)
            connection.commit()
            print("Dropped column coordonnees_id from ps_libreacces_personne_activite")
            
            # Recreate the table coordonnees
            create_table_query = text("""
            CREATE TABLE external.coordonnees (
                coordonnees_id SERIAL PRIMARY KEY,
                latitude DOUBLE PRECISION,
                longitude DOUBLE PRECISION,
                code_postal INTEGER,
                libelle_commune VARCHAR,
                label VARCHAR,
                code_insee VARCHAR,
                rue VARCHAR,
                house_nr VARCHAR,
                quality BOOLEAN,
                geom GEOGRAPHY(Point, 4326)                                                      
            );
            """)
            connection.execute(create_table_query)
            connection.commit()
            print("Recreated table coordonnees with columns coordonnees_id, latitude, and longitude")

            # Recreate the column coordonnees_id in the table ps_libreacces_personne_activite
            add_column_query = text("ALTER TABLE external.ps_libreacces_personne_activite ADD COLUMN coordonnees_id INTEGER;")
            connection.execute(add_column_query)
            connection.commit()
            print("Added column coordonnees_id to ps_libreacces_personne_activite")
            
            # Add the foreign key constraint
            add_foreign_key_query = text("""
            ALTER TABLE external.ps_libreacces_personne_activite
            ADD CONSTRAINT fk_coordonnees_id
            FOREIGN KEY (coordonnees_id) REFERENCES external.coordonnees(coordonnees_id);
            """)
            connection.execute(add_foreign_key_query)
            connection.commit()
            print("Added foreign key constraint fk_coordonnees_id")

            drop_existing_materialized_view = text("""
            DROP MATERIALIZED VIEW IF EXISTS external.recherche;
            """)
            connection.execute(drop_existing_materialized_view)
            connection.commit()

            print("Existing materialized view dropped successfully")

            recreate_materialized_view = text("""
            CREATE MATERIALIZED VIEW external.recherche AS
            SELECT 
                p.identification_nationale_pp AS inpp,
                p.nom,
                COALESCE(external.multi_replace(lower(unaccent(p.nom)), '{" ": "", "-": ""}'::jsonb), ''::text) AS nom_normalise,
                p.prenom,
                COALESCE(external.multi_replace(lower(unaccent(p.prenom)), '{" ": "", "-": ""}'::jsonb), ''::text) AS prenom_normalise,
                p.libelle_profession AS profession,
                COALESCE(external.multi_replace(lower(unaccent(p.libelle_profession)), '{" ": "", "-": ""}'::jsonb), ''::text) AS profession_normalise,
                p.libelle_savoirfaire AS savoir_faire,
                COALESCE(external.multi_replace(lower(unaccent(p.libelle_savoirfaire)), '{" ": "", "-": ""}'::jsonb), ''::text) AS savoir_faire_normalise,
                p.libelle_commune AS commune,
                COALESCE(external.multi_replace(lower(unaccent(p.libelle_commune)), '{" ": "", "-": ""}'::jsonb), ''::text) AS commune_normalise,
                c.latitude,
                c.longitude
            FROM 
                external.ps_libreacces_personne_activite p
            left JOIN 
                external.coordonnees c ON p.coordonnees_id = c.coordonnees_id;
            """)

            print("Materialized view created successfully")

            print("All operations completed successfully")

    except OperationalError as e:
        print(f"Not connected: {e}")

if __name__ == "__main__":
    main()

Peuplement des colonnes au sein de la table "coordonnees" grâce à ce script Python et l'API BAN

from sqlalchemy import create_engine, text
from sqlalchemy.exc import OperationalError
import requests
import json
from tqdm import tqdm
import logging

# Configure logging to display information about the process
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def main():
    # Create a connection to the PostgreSQL database
    engine = create_engine("postgresql+psycopg2://userdb:pwd@localhost:5432/db")

    # SQL query to select distinct finess records along with associated address data

    #Sous-requête :
    #La clause WITH introduit une CTE nommée ranked_data, qui est une sous-requête temporaire que l'on peut utiliser dans la requête principale.
    #Sélectionne les colonnes finess, numero_voie, indice_repetition_voie, libelle_type_de_voie, libelle_voie, code_postal, libelle_commune, coordonnees_id depuis la table external.ps_libreacces_personne_activite.
    #Utilise la fonction de fenêtre ROW_NUMBER() pour attribuer un numéro de rang à chaque ligne dans chaque partition définie par finess.
    #Partitionne les lignes par la colonne finess, c'est-à-dire que ROW_NUMBER() redémarre à 1 pour chaque valeur distincte de finess.
    #Indique que les lignes dans chaque partition sont ordonnées par finess. Dans ce cas, l'ordre par finess ne change pas grand-chose, mais il est nécessaire pour la syntaxe de ROW_NUMBER().

    #Requête principale : 
    #Sélectionne les mêmes colonnes que dans la CTE depuis ranked_data
    #Filtre les résultats pour ne conserver que les lignes où row_num vaut 1. Cela signifie que pour chaque valeur distincte de finess, seule la première ligne (déterminée par ROW_NUMBER()) est conservée.

    query_select_all_finness = text("""
                                    
        WITH ranked_data AS (
            SELECT finess, 
                numero_voie, 
                indice_repetition_voie,
                libelle_type_de_voie, 
                libelle_voie,
                code_postal, 
                libelle_commune, 
                coordonnees_id,
                ROW_NUMBER() OVER (PARTITION BY finess ORDER BY finess) AS row_num
            FROM external.ps_libreacces_personne_activite
            WHERE finess IS NOT NULL
        )
        SELECT finess, 
            numero_voie, 
            indice_repetition_voie,
            libelle_type_de_voie, 
            libelle_voie,
            code_postal, 
            libelle_commune, 
            coordonnees_id
        FROM ranked_data
        WHERE row_num = 1;
    """)

    try:
        # Establish the connection to the database
        with engine.connect() as connection:
            logging.info("Connected to DB")
            logging.info("-" * 40)

            # Execute the query and fetch all results
            result = connection.execute(query_select_all_finness)
            rows = result.fetchall()
            total_results = len(rows)  # Get the total number of results

            # Initialize the progress bar with the total number of results
            progress_bar = tqdm(total=total_results)

            # Loop through each row in the results
            for row in rows:
                progress_bar.update(1)  # Update the progress bar

                # Skip the row if coordonnees_id is already present
                if row[7] is not None:
                    continue

                # Extract relevant data from the row
                finess, numero_voie, indice_repetition_voie, libelle_type_de_voie, libelle_voie, code_postal, libelle_commune = row[:7]

                # Construct the full address from the extracted data
                full_address = (
                    f"{str(numero_voie or '')} "
                    f"{str(indice_repetition_voie or '')} "
                    f"{str(libelle_type_de_voie or '')} "
                    f"{str(libelle_voie or '')} "
                    f"{str(code_postal or '')} "
                    f"{str(libelle_commune or '')}"
                ).strip().replace('&', '')

                logging.info(f"Trying to get coordinates for FINESS nr: {finess}")
                logging.info(f"Searching for the address: {full_address}")

                # Get coordinates from the BAN API using the full address
                ban_api_response = get_from_ban_api(full_address)

                if ban_api_response is not None:
                    try:
                        # Parse the JSON response from the API
                        response_data = json.loads(ban_api_response)

                        if response_data["features"]:
                            # Extract coordinates from the response
                            coordinates = response_data["features"][0]["geometry"]["coordinates"]
                            lat, long = coordinates[1], coordinates[0]

                            # Extract additional data from the API response
                            properties = response_data["features"][0]["properties"]
                            code_postal_api = properties.get("postcode")
                            libelle_commune_api = properties.get("city")
                            label = properties.get("label")
                            code_insee = properties.get("citycode")
                            rue_api = properties.get("street")
                            house_nr = properties.get("housenumber")
                            quality = len(response_data["features"]) == 1  # Check if there is only one feature

                            logging.info(f"Inserting data: lat-{lat}, long-{long}, "
                                         f"code_postal_api-{code_postal_api}, "
                                         f"libelle_commune_api-{libelle_commune_api}, "
                                         f"label-{label}, code_insee-{code_insee}, "
                                         f"rue_api-{rue_api}, house_nr-{house_nr}")

                            # Begin a new transaction for database operations
                            with engine.begin() as transaction:
                                # Insert the coordinates and additional data into the coordonnees table
                                insert_query_coordonnees = text("""
                                    INSERT INTO external.coordonnees (latitude, longitude, code_postal, libelle_commune, label, code_insee, rue, house_nr, quality)
                                    VALUES (:lat, :long, :code_postal_api, :libelle_commune_api, :label, :code_insee, :rue_api, :house_nr, :quality)
                                    RETURNING coordonnees_id;
                                """)
                                insert_result = transaction.execute(insert_query_coordonnees, {
                                    'lat': lat, 'long': long,
                                    'code_postal_api': code_postal_api, 'libelle_commune_api': libelle_commune_api,
                                    'label': label, 'code_insee': code_insee,
                                    'rue_api': rue_api, 'house_nr': house_nr,
                                    'quality': quality
                                })

                                # Get the generated coordonnees_id from the insert result
                                generated_id_coordonnees = insert_result.fetchone()[0]
                                logging.info(f"Inserted Coordinates, ID: {generated_id_coordonnees}")

                                # Update the ps_libreacces_personne_activite table with the new coordonnees_id
                                update_query_ps_libreacces_personne_activite = text("""
                                    UPDATE external.ps_libreacces_personne_activite
                                    SET coordonnees_id = :generated_id_coordonnees
                                    WHERE finess = :finess
                                """)
                                update_result = transaction.execute(update_query_ps_libreacces_personne_activite, {
                                    "generated_id_coordonnees": generated_id_coordonnees, "finess": finess
                                })

                                # Check if the update was successful
                                if update_result.rowcount > 0:
                                    logging.info(f"Update successful, updated: {update_result.rowcount}")
                                else:
                                    logging.warning(f"Update failed for FINESS nr: {finess}")
                        else:
                            logging.warning("No coordinates found in the response.")
                    except json.JSONDecodeError:
                        logging.error("Error decoding the JSON response.")
                else:
                    logging.error("Error in API request or no response received.")

                logging.info("-" * 40)

            progress_bar.close()

    except OperationalError as e:
        logging.error(f"Not connected: {e}")

def get_from_ban_api(param):
    # Construct the API URL with the query parameter
    # url = f"https://ban.api.interhop.org/search?q={param}"
    url = f"https://api-adresse.data.gouv.fr/search?q={param}"
    try:
        # Make the GET request to the API
        response = requests.get(url)
        response.raise_for_status()  # Raise an exception for HTTP errors
        return response.text  # Return the response text
    except requests.RequestException as e:
        logging.error(f"Error making API request: {e}")
        return None  # Return None if there was an error

if __name__ == "__main__":
    main()

Peuplement de la colonne "geom" de la table "coordonnees" grâce aux données contenues dans les colonnes "latitude" et "longitude" avec l'exécution de ce script Python

from sqlalchemy import create_engine, text
from sqlalchemy.exc import OperationalError

def main():
    engine = create_engine("postgresql+psycopg2://userdb:pwd@localhost:5432/db")

    try:
        with engine.connect() as connection:
            print("Connected to DB")
            print("-" * 40)

            # Populate geom column
            populate_geom_column_query = text("""
            UPDATE external.coordonnees  SET geom = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326);                                            
            """)
            connection.execute(populate_geom_column_query)
            connection.commit()
            print("Geom column populated")

    except OperationalError as e:
        print(f"Not connected: {e}")

if __name__ == "__main__":
    main()

Méthode utilisée dans notre API Flask, afin d'effectuer nos requêtes avec SQLAlchemy lors de la recherche d'un praticien par un utilisateur de Toobib

    @classmethod
    def get_practicioner_by_data(cls, form_data):
        if(form_data.get('rangeAround')):
            distance = form_data.get('rangeAround').strip()
        if (form_data.get('position[latitude]') and form_data.get('position[longitude]')):
            lat = form_data.get('position[latitude]')
            long = form_data.get('position[longitude]')
        if(form_data.get('ville')):
            ville = form_data.get('ville').strip()
            ville = ville.lower()
            
        if(form_data.get('profession')):
            profession = form_data.get('profession').strip()
            profession = profession.lower()
            
        query = cls.query

        if form_data.get('toobib'):
            name = form_data.get('toobib').strip()
            name = name.split(' ')
            if len(name) == 2:
                nom, prenom = name
                query = query.filter(and_(
                    cls.nom.ilike(nom),
                    cls.prenom.ilike(prenom),
                ))
            elif len(name) == 1:
                nom = name[0]
                query = query.filter(or_(
                    cls.nom.ilike(nom),
                    cls.prenom.ilike(nom),
                ))

        if form_data.get('ville'):
            query = query.filter(or_(
                cls.commune.ilike(ville),
            ))

        if form_data.get('profession'):
            query = query.filter(or_(
                cls.profession.ilike(profession),
            ))
        
        if (form_data.get('rangeAround') and form_data.get('position[latitude]') and form_data.get('position[longitude]')):
            search_point = ST_MakePoint(lat, long).cast(Geography)
            query = query.filter(or_(
                ST_DWithin(
                ST_MakePoint(
                    db.cast(cls.latitude, db.Float),
                    db.cast(cls.longitude, db.Float)
                ).cast(Geography),
                search_point,
                int(distance) * 1000
            )
            ))

        return [obj.json() for obj in query.all()]

Conclusion : Une contribution stimulante et formatrice

Notre stage chez Toobib a été une expérience riche en apprentissages et en défis stimulants. Nous avons pu mettre en pratique nos connaissances techniques et développer de nouvelles compétences dans un environnement innovant et porteur de sens. Notre contribution a permis de faire évoluer la plateforme Toobib et d'améliorer l'expérience utilisateur pour les patients et les praticiens de santé, le tout, en cherchant à porter les convictions et les missions que mènent les personnes qui nous ont encadrés lors de ce stage, que nous tenions à remercier.