survey_test / app.py
clementBE's picture
Update app.py
821b606 verified
import gradio as gr
import pandas as pd
import os
import time
from typing import List, Union, Dict, Any, Tuple
from datetime import datetime
# =================================================================
# CONFIGURATION ET DÉFINITIONS LINGUISTIQUES
# =================================================================
# CRITICAL FIX: Base count was 91. Adding SPACE9_CHOICE and SPACE9_SCALE brings it to 93.
EXPECTED_DATA_COUNT = 93
EXPECTED_COUNT = 93
# --- Shared Choices (French Only) ---
FREQ_FR = ["Jamais", "Rarement", "Parfois", "Souvent"]
THEMES_ACTU_FR = ["politique", "environnement", "économie/finance", "santé", "social/société", "situation internationale/géopolitique", "science", "modes de vie (lifestyle)", "vie culturelle", "autre/précisez"]
ACTIVITY_CHOICES = ["Lecture (de livres, articles)", "Cinéma/Séries", "Musique/Concerts", "Théâtre/Spectacles", "Expositions/Musées", "Sport", "Voyages", "Jeux vidéo", "Bricolage/Jardinage", "Réseaux sociaux"]
PLATFORM_CHOICES = ["Instagram", "TikTok", "YouTube", "Twitter/X", "Facebook", "Snapchat", "Twitch", "Reddit", "LinkedIn", "Autre"]
PURPOSE_CHOICES = ["Actualité/Info", "Loisirs/Divertissement", "Éducation/Apprentissage", "Engagement/Politique", "Social/Communication", "Professionnel", "Autre (précisez)"]
INSEE_CHOICES = ["Agriculteur", "Ouvrier", "Employé", "Profession intermédiaire", "Artisan, commerçant, chef d'entreprise", "Cadre, profession intellectuelle supérieure", "Retraité", "Demandeur d'emploi", "Étudiant"]
INCOME_CHOICES = ["Tu vis confortablement", "Tu t’en sors", "Tu trouves la vie difficile", "Tu ne t’en s’en sors vraiment pas"]
DOMAIN_CHOICES_FR = [
"écologie / action environnementale",
"inclusion et lutte contre les discriminations / Accueil de populations vulnérables",
"démocratie radicale",
"Création et experimentation artistiques",
"Éducation et transmission",
"Économie alternative",
"Ancrage local (bar associatif, aides aux personnes agées, bibliothèque associative…)",
"autre - préciser"
]
ENGAGE3_CHOICES_FR = [
"écologie",
"lutte contre la pauvreté",
"action contre les discriminations",
"vie politique locale ou nationale",
"action syndicale",
"causes internationales",
"autre / précisez"
]
SOCIAL_MEDIA_CHOICES = [
"Instagram", "YouTube", "TikTok", "Facebook", "WhatsApp", "BlueSky", "Signal",
"Telegram", "Mastodon", "Discord", "Twitter", "Reddit", "messagerie de jeu en ligne",
"autre : préciser", "je ne sais pas"
]
ANXIETY_MANIFESTATION_CHOICES = [
"J’ai tendance à l’ignorer", "Je cherche à agir par moi-même", "Je lis des livres et je me documente sur le sujet",
"J’ai un journal intime", "Je discute avec mes amis / parents", "Je pose des questions à mes enseignants",
"Je rejoins une association ou je cherche une activité collective", "Je consulte un spécialiste de santé",
"Je fais des recherches sur internet", "J’interroge / je confie à un chatbot",
"Je rejoins une communauté spécialisée sur les réseaux sociaux"
]
INFO4_CHOICES = ["Curieux", "Indifférent·e", "Légèrement agacé·e", "Inquiet·ète", "En colère", "Déstabilisé·e", "J'ignore son opinion"]
INFO5_CHOICES = [
"Je le lis/regarde avec attention.",
"Je le ferme ou l’ignore.",
"Je cherche des arguments pour le réfuter.",
"Je le partage pour en discuter avec d’autres.",
"autre réaction"
]
ENGAGE1_CHOICES = [
"non",
"en tant que sympathisant",
"comme donateur-rice",
"en tant que bénévole",
"comme participant·e/spectateur-rice",
"comme organisateur-rice/responsable"
]
ENGAGE2_CHOICES = [
"une association",
"un collectif",
"une ONG",
"un parti politique",
"un syndicat étudiant",
"une assemblée ou une instance représentative (collectivité, communes…)",
"autre (préciser)",
"aucune de ces formes"
]
DEMO_INSCRIPTION_CHOICES = ["L1", "L2", "L3", "M1", "M2", "D", "DU", "autre niveau d’étude/diplôme"]
DEMO_DISCIPLINE_CHOICES = [
"Cinéma", "Communication", "Langues", "LEA", "Lettres", "Médiation", "Musique",
"Sciences du langage", "Théâtre", "Traduction", "Autres"
]
# --- Language Definitions (French Only) ---
LANG_FR = {
"TITLE": "Questionnaire : Pratiques culturelles et Engagement Citoyen",
"INTRO_TEXT": "Bonjour, je suis étudiant.e en **master de communication** et j’effectue une enquête sur les **pratiques culturelles** et les **formes d’engagements citoyen** dans le cadre de mon stage.",
"ENQUETEUR_LABEL": "Identifiant de l'enquêteur",
"TAB_1_TITLE": "1. Contact & Lieux Fréquentés", "TAB_2_TITLE": "2. Engagement Citoyen",
"TAB_3_TITLE": "3. Consommation d’actualités", "TAB_4_TITLE": "4. Pratiques culturelles et usages numériques",
"TAB_5_TITLE": "5. Profil Démographique", "TAB_6_TITLE": "6. Questions ouvertes",
"TAB_7_TITLE": "7. Soumission et Téléchargement",
"APPROACH_LABEL": "APPROACH - Accepterais-tu de répondre à un questionnaire ? (10 min)",
"REFUSAL_REASON_LABEL": "ANSWER_NO1 (si APPROACH=non) - Pourquoi ?",
"CONTACT_LATER_LABEL": "ANSWER_NO2 (si APPROACH=non) - On peut prendre rendez-vous plus tard ?",
"SPACE1_LABEL": "SPACE1 - Fréquentes-tu des lieux culturels (au sens large, y compris alternatifs, non institutionnels) ?",
"SPACE2A_TITLE": "Derniers lieux culturels fréquentés (3 max. - si Rarement/Parfois/Souvent)",
"SPACE2B_LABEL": "SPACE2b (si “jamais”) - Pourquoi ne fréquentes-tu jamais de lieux culturels ? (plusieurs réponses possibles)",
"SPACE3_TITLE": "SPACE4 - Pourrais-tu qualifier ces lieux ?",
"SPACE3_QUAL_1": "Lieu 1 : Alternatif/Underground (1) à Institutionnel/Mainstream (10)",
"SPACE3_QUAL_2": "Lieu 2 : Alternatif (1) à Institutionnel (10)",
"SPACE3_QUAL_3": "Lieu 3 : Alternatif (1) à Institutionnel (10)",
"SPACE4_LABEL": "SPACE4_bis - Est-ce que tu suis régulièrement une communauté alternative en ligne (médias sociaux, newsletter, groupe dédié...)",
"SPACE5_LABEL": "SPACE5 - Peux-tu indiquer son nom ou son url ? (si SPACE4_bis=oui)",
"SPACE6_LABEL": "SPACE6 - As-tu connaissance de pratiques d’engagement citoyen dans ces espaces physiques et numériques ?",
"SPACE7_TITLE": "SPACE7 (si SPACE6=oui) - Cite ces pratiques (3 maximum)",
"SPACE_DOMAIN_LABEL": "Peux-tu caractériser le domaine dans lequel s’inscrit principalement cette pratique ?",
# NOUVELLES ÉTIQUETTES DE QUESTION SPACE9
"SPACE9_CHOICE_LABEL": "SPACE9 - Laquelle de ces 3 pratiques te semble la plus stimulante ? (1, 2 ou 3)",
"SPACE9_SCALE_LABEL": "SPACE9_BIS - Intensité de la stimulation (1 'pas du tout' à 10 'extrêmement')",
"SPACE11_LABEL": "SPACE11 - Sais-tu si elle s’appuie sur une communauté en ligne et l’usage d’un média social en ligne ?",
"SPACE2B_OPTIONS": ["Pas le temps", "Trop cher", "Je ne sais pas où aller", "Pas intéressé", "Autre"],
"ENGAGE1_LABEL": "ENGAGE1 - Toi-même, participes-tu à des pratiques d’engagement citoyen ?",
"ENGAGEMENT_ORGANISATION_LABEL": "ENGAGE2 - Dans quelle(s) organisation(s) es-tu engagé·e ?",
"ENGAGE2_AUTRE_DETAIL_LABEL": "ENGAGE2 - Précisez 'autre'",
"ENGAGEMENT_DOMAINE_LABEL": "ENGAGE3 - Dans quel(s) domaine(s) es-tu engagé·e ?",
"ENGAGE3_AUTRE_DETAIL_LABEL": "ENGAGE3 - Précisez 'autre'", # NOUVELLE ÉTIQUETTE
"INFO_ACTIVITES_LABEL": "ENGAGE4 - Participes-tu ou as-tu participé à une ou plusieurs des activités suivantes ?",
"INFO_RESEAUX_SOCIAUX_LABEL": "ENGAGE5 - Utilises-tu les réseaux sociaux pour t’engager dans des causes ?",
"INFO_FREQUENCE_ACTU_LABEL": "INFO1 - À quelle fréquence te tiens-tu informé·e de l’actualité ?",
"INFO2_LABEL": "INFO2 - Quels sont le ou les thèmes d’actualité que tu as suivis avec le plus d’intérêt ?",
"INFO3_LABEL": "INFO3 - Coche **jusqu’à 3 thèmes** qui te semblent poser des enjeux publics majeurs :",
"OPPOSITE_FEELING_BASE_LABEL": "INFO4 - Quand un·e ami·e exprime une opinion opposée à la tienne sur l’un des thèmes qui te semblent représenter des enjeux majeurs, comment te sens-tu ?",
"INFO_CONTRADICT_OPINION_LABEL": "INFO5 - Quand tu tombes sur une vidéo ou un article de presse qui contredit tes croyances, quelle est ta première réaction ?",
"INFO4_LABEL": "INFO6 - Parmi ces thèmes, lequel ou lesquels génèrent chez toi un sentiment d’anxiété ?",
"INFO5_LABEL": "INFO7 - Comment se manifeste ce sentiment d’anxiété ?",
"INFO6_TITLE": "INFO8 - Éléments les plus problématiques pour toi (Échelle 1 'sans importance' à 10 'essentiel')",
"INFO6_ITEMS": ["Ton logement", "Politique/Gouvernement", "Études/Avenir professionnel", "Crise climatique/Environnement", "Inégalités sociales", "Ta vie sentimentale/familiale", "Ta sécurité personnelle", "Estime de tes proches", "Ta liberté individuelle"],
"PRAT_CULT1_LABEL": "PRAT_CULT1 - Combien de sorties culturelles environ par mois :",
"PRAT_CULT_PRACTICES_TITLE": "PRAT_CULT2 - Parmi les pratiques suivantes, lesquelles effectues-tu régulièrement ?",
"PRAT_NATURE_LABEL": "PRAT_CULT3 - Pratiques-tu une activité liée à la nature (randonnée, observation, bénévolat environnemental...) ?",
"PLATFORM_TITLE": "DIGITAL1 - Quelles plateformes utilises-tu le plus ? (Heures/jour approx.)",
"PURPOSE_TITLE": "DIGITAL2 - Pour quel(s) usage(s) emploies-tu ces plateformes ?",
"DEMO_GENDER_LABEL": "DEMO_GENDER - Ton genre :",
"DEMO_AGE_LABEL": "DEMO_AGE - Ton âge (années) :",
"DEMO_LOCATION_COMMUNE_LABEL": "DEMO_LOCATION - Commune de résidence habituelle :",
"DEMO_LOCATION_ARROND_LABEL": "Arrondissement (si Paris) :",
"DEMO_PARENTS_LOCATION_LABEL": "DEMO_PARENTS - Est-ce le lieu d’habitation de tes parents ?",
"DEMO_INSCRIPTION_LABEL": "DEMO_INSCRIPTION : Tu es inscrit.e. à titre principal en quelle année d’études ?",
"DEMO_DISCIPLINE_LABEL": "DEMO_DISCIPLINE : Dans quelle discipline/filière s’inscrivent principalement tes études ?",
"DEMO_JOB_LABEL": "DEMO_JOB - Exerces-tu une activité professionnelle en parallèle ?",
"DEMO_INCOME_LABEL": "DEMO_INCOME - Opinion sur votre revenu actuel :",
"DEMO_SOCIALCAPITAL1_PARENT1_LABEL": "DEMO_SOCIALCAPITAL1 - Activité principale Parent 1 (ou responsable légal) :",
"DEMO_SOCIALCAPITAL1_PARENT2_LABEL": "Activite principale Parent 2 :",
"DEMO_SOCIALCAPITAL2_LABEL": "DEMO_SOCIALCAPITAL2 : Sur combien de personnes (hors famille) peux-tu compter en cas de coup dur ?",
"OPEN_NON_INSTITUTIONNEL_LABEL": "OEQ1 - Qu’est-ce qu’un lieu culturel non institutionnel selon toi ?",
"OPEN_ALTERNATIVES_LABEL": "OEQ2 - Comment définirais-tu le terme “alternatif” dans ce cas ?",
"OPEN_MOTIVATIONS_LABEL": "OEQ3 - Si tu es engagé.e, quelles en sont les motivations principales ?",
"SUBMIT_BUTTON": "Soumettre le Questionnaire",
"RESET_BUTTON": "Recommencer",
}
LANG = LANG_FR
# =================================================================
# COMPONENT DEFINITIONS
# =================================================================
# === Global ===
enqueteur_id = gr.Textbox(label=LANG["ENQUETEUR_LABEL"], placeholder="Entrez votre identifiant")
# === Submission/Output Components ===
submit_btn = gr.Button(LANG["SUBMIT_BUTTON"], variant="primary")
reset_btn = gr.Button(LANG["RESET_BUTTON"])
output_status = gr.Markdown("---")
output_message = gr.File(label="Télécharger les Données", file_types=['.csv'], visible=False, elem_id="output_message")
# === TAB 1 Components (23 + 2 = 25 total) ===
approach_answer = gr.Radio(label=LANG["APPROACH_LABEL"], choices=["Oui", "Non"])
refusal_reason = gr.CheckboxGroup(label=LANG["REFUSAL_REASON_LABEL"], choices=["Pas le temps", "Cela ne m’intéresse pas", "Autre"], visible=False)
refusal_reason_other = gr.Textbox(label="Précisez 'Autre'", visible=False)
contact_later = gr.Radio(label=LANG["CONTACT_LATER_LABEL"], choices=["Oui", "Non"], visible=False)
space1 = gr.Radio(label=LANG["SPACE1_LABEL"], choices=FREQ_FR)
space2a_1 = gr.Textbox(label="Lieu 1", placeholder="Nom du dernier lieu fréquenté...", visible=False)
space2a_2 = gr.Textbox(label="Lieu 2", visible=False)
space2a_3 = gr.Textbox(label="Lieu 3", visible=False)
space2b = gr.CheckboxGroup(label=LANG["SPACE2B_LABEL"], choices=LANG["SPACE2B_OPTIONS"], visible=False)
space3_1 = gr.Slider(label=LANG["SPACE3_QUAL_1"], minimum=1, maximum=10, step=1, visible=False)
space3_2 = gr.Slider(label=LANG["SPACE3_QUAL_2"], minimum=1, maximum=10, step=1, visible=False)
space3_3 = gr.Slider(label=LANG["SPACE3_QUAL_3"], minimum=1, maximum=10, step=1, visible=False)
space4 = gr.Radio(label=LANG["SPACE4_LABEL"], choices=["Oui", "Non"])
space5 = gr.Textbox(label=LANG["SPACE5_LABEL"], visible=False)
space6 = gr.Radio(label=LANG["SPACE6_LABEL"], choices=["Oui", "Non", "Ne sait pas"])
space7_1 = gr.Textbox(label="Pratique 1", placeholder="Décrivez la pratique d'engagement 1", visible=False)
space7_2 = gr.Textbox(label="Pratique 2", placeholder="Décrivez la pratique d'engagement 2", visible=False)
space7_3 = gr.Textbox(label="Pratique 3", placeholder="Décrivez la pratique d'engagement 3", visible=False)
# Domaine components (CheckboxGroup)
space_domain_1 = gr.CheckboxGroup(label=f"Domaines pour Pratique 1", choices=DOMAIN_CHOICES_FR, visible=False)
space_domain_2 = gr.CheckboxGroup(label=f"Domaines pour Pratique 2", choices=DOMAIN_CHOICES_FR, visible=False)
space_domain_3 = gr.CheckboxGroup(label=f"Domaines pour Pratique 3", choices=DOMAIN_CHOICES_FR, visible=False)
# NOUVEAUX COMPOSANTS: SPACE9_CHOICE et SPACE9_SCALE
space9_choice = gr.Radio(label=LANG["SPACE9_CHOICE_LABEL"], choices=["1", "2", "3"], visible=False)
space9_scale = gr.Slider(label=LANG["SPACE9_SCALE_LABEL"], minimum=1, maximum=10, step=1, visible=False)
space11 = gr.CheckboxGroup(label=LANG["SPACE11_LABEL"], choices=SOCIAL_MEDIA_CHOICES)
# === TAB 2 Components ===
engage1_role = gr.CheckboxGroup(label=LANG["ENGAGE1_LABEL"], choices=ENGAGE1_CHOICES)
engage2_type = gr.CheckboxGroup(label=LANG["ENGAGEMENT_ORGANISATION_LABEL"], choices=ENGAGE2_CHOICES)
engage2_autre_detail = gr.Textbox(label=LANG["ENGAGE2_AUTRE_DETAIL_LABEL"], visible=False)
# ENGAGE3 : Changement de Textbox à CheckboxGroup + Textbox pour 'autre'
engagement_domaine = gr.CheckboxGroup(label=LANG["ENGAGEMENT_DOMAINE_LABEL"], choices=ENGAGE3_CHOICES_FR)
engage3_autre_detail = gr.Textbox(label=LANG["ENGAGE3_AUTRE_DETAIL_LABEL"], visible=False) # Champ de précision pour 'autre'
info_activites = gr.CheckboxGroup(label=LANG["INFO_ACTIVITES_LABEL"], choices=["Participer à une manifestation/grève", "Contacter un élu", "Signer une pétition en ligne", "Boycotter un produit/marque", "Faire un don à une association", "Autre"], interactive=True)
info_reseaux_sociaux = gr.Radio(label=LANG["INFO_RESEAUX_SOCIAUX_LABEL"], choices=["Oui", "Non", "Parfois"])
# === TAB 3 Components ===
info_frequence_actu = gr.Radio(label=LANG["INFO_FREQUENCE_ACTU_LABEL"], choices=["Plusieurs fois par jour", "Une fois par jour", "Quelques fois par semaine", "Rarement", "Jamais"])
info2 = gr.CheckboxGroup(label=LANG["INFO2_LABEL"], choices=THEMES_ACTU_FR)
info3 = gr.CheckboxGroup(label=LANG["INFO3_LABEL"], choices=THEMES_ACTU_FR)
info_opposite_feeling = gr.Radio(label=LANG["OPPOSITE_FEELING_BASE_LABEL"], choices=INFO4_CHOICES)
info_contradict_opinion = gr.Radio(label=LANG["INFO_CONTRADICT_OPINION_LABEL"], choices=INFO5_CHOICES)
info4 = gr.CheckboxGroup(label=LANG["INFO4_LABEL"], choices=THEMES_ACTU_FR)
info5 = gr.CheckboxGroup(label=LANG["INFO5_LABEL"], choices=ANXIETY_MANIFESTATION_CHOICES)
# Info 8 Sliders (9 components)
info6_sliders = {}
for item in LANG["INFO6_ITEMS"]:
clean_id = item.split('/')[0].lower().replace(' ', '_').replace('é', 'e').replace('è', 'e')
info6_sliders[clean_id] = gr.Slider(label=item, minimum=1, maximum=10, step=1)
info6_slider_components = list(info6_sliders.values())
info6_logement, info6_politique, info6_etudes, info6_climat, info6_sociales, info6_sentimentale, info6_securite, info6_estime, info6_liberte = info6_slider_components
# === TAB 4 Components ===
prat_cult1 = gr.Number(label=LANG["PRAT_CULT1_LABEL"], minimum=0, maximum=100, step=1)
# Cultural Frequency Radios (10 components)
prat_cult_freq_components = [
gr.Radio(label=f"{activity}", choices=FREQ_FR) for activity in ACTIVITY_CHOICES
]
(prat_cult_lecture, prat_cult_cinema, prat_cult_musique, prat_cult_theatre, prat_cult_expositions,
prat_cult_sport, prat_cult_voyages, prat_cult_jeux_video, prat_cult_bricolage, prat_cult_reseaux) = prat_cult_freq_components
prat_nature = gr.Radio(label=LANG["PRAT_NATURE_LABEL"], choices=["Oui", "Non", "Parfois"])
# Platform Usage Numbers (10 components)
platform_components = [
gr.Number(label=f"Heures/jour pour {platform}", placeholder="0.5, 1, 2...", minimum=0, step=0.5) for platform in PLATFORM_CHOICES
]
(plat_instagram, plat_tiktok, plat_youtube, plat_twitter, plat_facebook,
plat_snapchat, plat_twitch, plat_reddit, plat_linkedin, plat_autre) = platform_components
# Purpose Frequency Radios (7 components)
purpose_components = [
gr.Radio(label=f"Fréquence pour: {purpose}", choices=FREQ_FR) for purpose in PURPOSE_CHOICES[:-1]
]
purpose_autre_detail = gr.Textbox(label="Précisez l'usage 'Autre'", visible=True)
purpose_components.append(gr.Radio(label=f"Fréquence pour: {PURPOSE_CHOICES[-1]}", choices=FREQ_FR))
(purpose_actu, purpose_loisirs, purpose_education, purpose_engagement,
purpose_social, purpose_professionnel, purpose_autre_freq) = purpose_components
# === TAB 5 Components ===
demo_gender = gr.Radio(label=LANG["DEMO_GENDER_LABEL"], choices=["Homme", "Femme", "Non-binaire", "Préfère ne pas dire"])
demo_age = gr.Number(label=LANG["DEMO_AGE_LABEL"], minimum=18, maximum=100, step=1, placeholder="ex: 25")
demo_location_commune = gr.Textbox(label=LANG["DEMO_LOCATION_COMMUNE_LABEL"], placeholder="Nom de la commune")
demo_location_arrond = gr.Textbox(label=LANG["DEMO_LOCATION_ARROND_LABEL"], placeholder="ex: 75005")
demo_parents_location = gr.Radio(label=LANG["DEMO_PARENTS_LOCATION_LABEL"], choices=["Oui", "Non"])
demo_inscription = gr.CheckboxGroup(label=LANG["DEMO_INSCRIPTION_LABEL"], choices=DEMO_INSCRIPTION_CHOICES)
demo_discipline = gr.CheckboxGroup(label=LANG["DEMO_DISCIPLINE_LABEL"], choices=DEMO_DISCIPLINE_CHOICES)
demo_job = gr.Radio(label=LANG["DEMO_JOB_LABEL"], choices=["Oui", "Non"])
demo_income = gr.Radio(label=LANG["DEMO_INCOME_LABEL"], choices=INCOME_CHOICES)
demo_socialcapital1_parent1 = gr.Radio(label=LANG["DEMO_SOCIALCAPITAL1_PARENT1_LABEL"], choices=INSEE_CHOICES)
demo_socialcapital1_parent2 = gr.Radio(label=LANG["DEMO_SOCIALCAPITAL1_PARENT2_LABEL"], choices=INSEE_CHOICES)
demo_socialcapital2 = gr.Number(label=LANG["DEMO_SOCIALCAPITAL2_LABEL"], minimum=0, step=1)
# === TAB 6 Components ===
open_non_institutionnel = gr.Textbox(label=LANG["OPEN_NON_INSTITUTIONNEL_LABEL"], lines=3)
open_alternatives = gr.Textbox(label=LANG["OPEN_ALTERNATIVES_LABEL"], placeholder="5 mots maximum")
open_motivations = gr.Textbox(label=LANG["OPEN_MOTIVATIONS_LABEL"], lines=3)
# Helper function to generate robust column names
def get_column_names(data_components: list) -> list:
names = []
for i, component in enumerate(data_components):
label_attr = getattr(component, 'label', None)
if label_attr:
name = label_attr
# Nettoyage et simplification du nom de colonne
clean_name = name.split('-')[0].strip().replace(' ', '_').replace('.', '').replace(':', '')
# Gestion des noms spécifiques
if "Domaines pour Pratique 1" in name:
clean_name = "P1_DOMAINES"
elif "Domaines pour Pratique 2" in name:
clean_name = "P2_DOMAINES"
elif "Domaines pour Pratique 3" in name:
clean_name = "P3_DOMAINES"
elif "Alternatif/Underground" in name:
clean_name = "SPACE4_QUAL_1"
elif "Lieu 2 : Alternatif" in name:
clean_name = "SPACE4_QUAL_2"
elif "Lieu 3 : Alternatif" in name:
clean_name = "SPACE4_QUAL_3"
elif clean_name == "ENGAGE2_Précisez_'autre'":
clean_name = "ENGAGE2_Autre_Precision"
# NOUVEAU NOM DE COLONNE POUR ENGAGE3_AUTRE
elif clean_name == "ENGAGE3_Précisez_'autre'":
clean_name = "ENGAGE3_Autre_Precision"
# NOUVEAU NOM DE COLONNE POUR SPACE9_CHOICE
elif clean_name == "SPACE9":
clean_name = "SPACE9_CHOICE"
# NOUVEAU NOM DE COLONNE POUR SPACE9_SCALE
elif clean_name == "SPACE9_BIS":
clean_name = "SPACE9_SCALE"
names.append(clean_name)
else:
names.append(f"Component_{i+1}")
return names
# =================================================================
# GRADIO UI & LOGIC - UTILITY FUNCTIONS
# =================================================================
def update_visibility_approach(approach):
updates = {}
is_non = approach == "Non"
updates[refusal_reason] = gr.update(visible=is_non)
updates[contact_later] = gr.update(visible=is_non)
if not is_non:
updates[refusal_reason_other] = gr.update(visible=False)
return updates[refusal_reason], updates[contact_later]
def update_visibility_refusal(reasons):
if reasons and "Autre" in reasons:
return gr.update(visible=True)
return gr.update(visible=False)
def update_visibility_space1(frequency):
is_frequent = frequency in ["Rarement", "Parfois", "Souvent"]
is_never = frequency == "Jamais"
return {
space2a_1: gr.update(visible=is_frequent),
space2a_2: gr.update(visible=is_frequent),
space2a_3: gr.update(visible=is_frequent),
space3_1: gr.update(visible=is_frequent),
space3_2: gr.update(visible=is_frequent),
space3_3: gr.update(visible=is_frequent),
space2b: gr.update(visible=is_never),
}
def update_visibility_space4(follow_community):
return gr.update(visible=follow_community == "Oui")
def update_visibility_space6(knows_practices):
is_yes = knows_practices == "Oui"
outputs = {
space7_1: gr.update(visible=is_yes),
space7_2: gr.update(visible=is_yes),
space7_3: gr.update(visible=is_yes),
space9_choice: gr.update(visible=is_yes), # NOUVEAU
space9_scale: gr.update(visible=is_yes) # NOUVEAU
}
# If not "Oui", hide both the practice description and its domains
if not is_yes:
outputs[space_domain_1] = gr.update(visible=False)
outputs[space_domain_2] = gr.update(visible=False)
outputs[space_domain_3] = gr.update(visible=False)
# If "Oui", the practice description (space7_X) is visible, but the domains (space_domain_X)
# must be hidden by default until a practice is actually typed in (handled by space7_X.change)
else:
outputs[space_domain_1] = gr.update(visible=False)
outputs[space_domain_2] = gr.update(visible=False)
outputs[space_domain_3] = gr.update(visible=False)
return outputs
def update_domain_visibility(practice_text):
is_cited = bool(practice_text.strip())
# Note: CheckboxGroup (or Radio) is now visible if the corresponding practice text is present
return gr.update(visible=is_cited)
def update_visibility_engage2_autre(engage2_choices: List[str]):
return gr.update(visible="autre (préciser)" in engage2_choices)
# NOUVELLE FONCTION de visibilité pour ENGAGE3
def update_visibility_engage3_autre(engage3_choices: List[str]):
return gr.update(visible="autre / précisez" in engage3_choices)
def process_survey(*data_values: Any) -> Tuple[str, gr.File]:
"""
Processes survey data, creates a temporary CSV file, and returns the path
and a success message for download.
"""
all_inputs_list = list(data_values)
if len(all_inputs_list) != EXPECTED_COUNT:
error_msg = f"Erreur critique: Le nombre d'entrées reçues est incorrect ({len(all_inputs_list)} au lieu de {EXPECTED_COUNT}). Veuillez contacter le développeur."
return error_msg, gr.update(visible=False, value=None)
language = "FR"
try:
column_names = get_column_names(DATA_INPUT_COMPONENTS)
column_names.insert(0, "DEMO_LANGUAGE")
data_row = [language] + all_inputs_list
df = pd.DataFrame([data_row], columns=column_names)
# Nommage du fichier: Enquêteur ID + Date
enqueteur_id_val = all_inputs_list[0]
current_date_str = datetime.now().strftime("%Y-%m-%d")
safe_enqueteur_id = str(enqueteur_id_val).strip()
safe_enqueteur_id = "".join(c if c.isalnum() else '_' for c in safe_enqueteur_id)
if not safe_enqueteur_id:
safe_enqueteur_id = "NO_ID"
temp_filename = f"/tmp/{safe_enqueteur_id}_{current_date_str}.csv"
os.makedirs(os.path.dirname(temp_filename), exist_ok=True)
df.to_csv(temp_filename, index=False, encoding='utf-8')
success_msg = f"✨ Succès ! Le questionnaire a été soumis. Cliquez sur le lien ci-dessous pour **Télécharger le Fichier CSV** nommé `{safe_enqueteur_id}_{current_date_str}.csv`."
return success_msg, gr.update(value=temp_filename, visible=True)
except Exception as e:
error_msg = f"Erreur lors de la génération du CSV: {str(e)}"
return error_msg, gr.update(visible=False, value=None)
# =================================================================
# DATA COMPONENT FINAL ASSEMBLY (93 components)
# =================================================================
DATA_INPUT_COMPONENTS = [
# Global (1)
enqueteur_id,
# TAB 1 - Contact & Lieux (25)
approach_answer, refusal_reason, refusal_reason_other, contact_later,
space1, space2a_1, space2a_2, space2a_3, space2b,
space3_1, space3_2, space3_3,
space4, space5,
space6,
space7_1, space_domain_1,
space7_2, space_domain_2,
space7_3, space_domain_3,
space9_choice, # NOUVEAU COMPOSANT
space9_scale, # NOUVEAU COMPOSANT
space11,
# TAB 2 - Engagement (7 composants)
engage1_role, engage2_type, engage2_autre_detail,
engagement_domaine, engage3_autre_detail,
info_activites, info_reseaux_sociaux,
# TAB 3 - Actualité (16)
info_frequence_actu, info2, info3, info_opposite_feeling, info_contradict_opinion,
info4, info5,
info6_logement, info6_politique, info6_etudes, info6_climat, info6_sociales,
info6_sentimentale, info6_securite, info6_estime, info6_liberte,
# TAB 4 - Cultural & Digital (30)
prat_cult1,
prat_cult_lecture, prat_cult_cinema, prat_cult_musique, prat_cult_theatre, prat_cult_expositions,
prat_cult_sport, prat_cult_voyages, prat_cult_jeux_video, prat_cult_bricolage, prat_cult_reseaux,
prat_nature,
plat_instagram, plat_tiktok, plat_youtube, plat_twitter, plat_facebook,
plat_snapchat, plat_twitch, plat_reddit, plat_linkedin, plat_autre,
purpose_autre_detail,
purpose_actu, purpose_loisirs, purpose_education, purpose_engagement,
purpose_social, purpose_professionnel, purpose_autre_freq,
# TAB 5 - Demographics (12)
demo_gender, demo_age, demo_location_commune, demo_location_arrond, demo_parents_location,
demo_inscription, demo_discipline, demo_job, demo_income,
demo_socialcapital1_parent1, demo_socialcapital1_parent2, demo_socialcapital2,
# TAB 6 - Open Questions (3)
open_non_institutionnel, open_alternatives, open_motivations,
]
if len(DATA_INPUT_COMPONENTS) != EXPECTED_DATA_COUNT:
raise RuntimeError(f"Internal component count mismatch. Expected {EXPECTED_DATA_COUNT}, got {len(DATA_INPUT_COMPONENTS)}. Please verify the DATA_INPUT_COMPONENTS list definition.")
SUBMIT_INPUT_COMPONENTS = DATA_INPUT_COMPONENTS
# =================================================================
# GRADIO UI SETUP (Utilisation des Accordions pour le F2F)
# =================================================================
with gr.Blocks(title=LANG["TITLE"], css=".gradio-container { max-width: 1200px; }") as demo:
gr.Markdown(f"## {LANG['TITLE']}")
gr.Markdown(LANG["INTRO_TEXT"])
with gr.Row():
enqueteur_id.render()
# --- Bloc Accordions ---
# =================================================================
# ACCORDION 1: Contact & Lieux Fréquentés (Ouvert par défaut)
# =================================================================
with gr.Accordion(LANG["TAB_1_TITLE"], open=True):
gr.Markdown("### Phase d'approche")
with gr.Row():
approach_answer.render()
with gr.Column() as refusal_block:
refusal_reason.render()
refusal_reason_other.render()
contact_later.render()
approach_answer.change(
update_visibility_approach,
inputs=[approach_answer],
outputs=[refusal_reason, contact_later]
)
refusal_reason.change(update_visibility_refusal, inputs=[refusal_reason], outputs=[refusal_reason_other])
gr.Markdown("### Lieux et Communautés")
space1.render()
gr.Markdown(f"#### {LANG['SPACE2A_TITLE']}")
with gr.Row() as space2_row:
space2a_1.render()
space2a_2.render()
space2a_3.render()
gr.Markdown(f"#### {LANG['SPACE3_TITLE']}")
with gr.Row() as space3_row:
space3_1.render()
space3_2.render()
space3_3.render()
space2b.render()
space1.change(update_visibility_space1, inputs=[space1], outputs=[space2a_1, space2a_2, space2a_3, space3_1, space3_2, space3_3, space2b])
gr.Markdown("### Usages alternatifs et Engagement")
space4.render()
space5.render()
space4.change(update_visibility_space4, inputs=[space4], outputs=[space5])
space6.render()
gr.Markdown(f"#### {LANG['SPACE7_TITLE']} (3 max.) et leurs Domaines (Choix multiples)")
# Pratique 1 et son Domaine (Mis côte-à-côte)
with gr.Row():
with gr.Column(scale=1): # Moins de place pour le champ de texte
space7_1.render()
with gr.Column(scale=2): # Plus de place pour les cases à cocher
space_domain_1.render()
# Pratique 2 et son Domaine (Mis côte-à-côte)
with gr.Row():
with gr.Column(scale=1):
space7_2.render()
with gr.Column(scale=2):
space_domain_2.render()
# Pratique 3 et son Domaine (Mis côte-à-côte)
with gr.Row():
with gr.Column(scale=1):
space7_3.render()
with gr.Column(scale=2):
space_domain_3.render()
# NOUVELLE SECTION SPACE9
gr.Markdown("#### Stimulation de la pratique choisie")
space9_choice.render()
space9_scale.render()
gr.Markdown(f"#### {LANG['SPACE11_LABEL']}")
space11.render()
# Logique de visibilité pour SPACE7, les domaines et SPACE9
space6.change(
fn=update_visibility_space6,
inputs=[space6],
outputs=[space7_1, space7_2, space7_3, space_domain_1, space_domain_2, space_domain_3, space9_choice, space9_scale]
)
# Les domaines ne sont visibles que si une pratique est citée
space7_1.change(fn=update_domain_visibility, inputs=[space7_1], outputs=[space_domain_1])
space7_2.change(fn=update_domain_visibility, inputs=[space7_2], outputs=[space_domain_2])
space7_3.change(fn=update_domain_visibility, inputs=[space7_3], outputs=[space_domain_3])
# =================================================================
# ACCORDION 2: Engagement Citoyen
# =================================================================
with gr.Accordion(LANG["TAB_2_TITLE"], open=False):
engage1_role.render()
engage2_type.render()
engage2_autre_detail.render()
# ENGAGE3 - Domaine de l'engagement (CheckboxGroup)
engagement_domaine.render()
engage3_autre_detail.render() # Champ de précision pour 'autre'
info_activites.render()
info_reseaux_sociaux.render()
engage2_type.change(
fn=update_visibility_engage2_autre,
inputs=[engage2_type],
outputs=[engage2_autre_detail]
)
# NOUVELLE LOGIQUE pour ENGAGE3 'autre'
engagement_domaine.change(
fn=update_visibility_engage3_autre,
inputs=[engagement_domaine],
outputs=[engage3_autre_detail]
)
# =================================================================
# ACCORDION 3: Consommation d’actualités
# =================================================================
with gr.Accordion(LANG["TAB_3_TITLE"], open=False):
info_frequence_actu.render()
info2.render()
info3.render()
info_opposite_feeling.render()
info_contradict_opinion.render()
gr.Markdown("---")
info4.render()
info5.render()
gr.Markdown(f"### {LANG['INFO6_TITLE']}")
with gr.Column():
info6_logement.render()
info6_politique.render()
info6_etudes.render()
info6_climat.render()
info6_sociales.render()
info6_sentimentale.render()
info6_securite.render()
info6_estime.render()
info6_liberte.render()
# =================================================================
# ACCORDION 4: Pratiques culturelles et usages numériques
# =================================================================
with gr.Accordion(LANG["TAB_4_TITLE"], open=False):
prat_cult1.render()
gr.Markdown(f"### {LANG['PRAT_CULT_PRACTICES_TITLE']}")
with gr.Row():
with gr.Column():
prat_cult_lecture.render()
prat_cult_cinema.render()
prat_cult_musique.render()
prat_cult_theatre.render()
prat_cult_expositions.render()
with gr.Column():
prat_cult_sport.render()
prat_cult_voyages.render()
prat_cult_jeux_video.render()
prat_cult_bricolage.render()
prat_cult_reseaux.render()
prat_nature.render()
gr.Markdown(f"### {LANG['PLATFORM_TITLE']}")
with gr.Row():
with gr.Column():
plat_instagram.render()
plat_tiktok.render()
plat_youtube.render()
plat_twitter.render()
plat_facebook.render()
with gr.Column():
plat_snapchat.render()
plat_twitch.render()
plat_reddit.render()
plat_linkedin.render()
plat_autre.render()
gr.Markdown(f"### {LANG['PURPOSE_TITLE']}")
with gr.Row():
with gr.Column():
purpose_actu.render()
purpose_loisirs.render()
purpose_education.render()
purpose_engagement.render()
with gr.Column():
purpose_social.render()
purpose_professionnel.render()
purpose_autre_freq.render()
purpose_autre_detail.render()
# =================================================================
# ACCORDION 5: Profil Démographique
# =================================================================
with gr.Accordion(LANG["TAB_5_TITLE"], open=False):
demo_gender.render()
demo_age.render()
with gr.Row():
demo_location_commune.render()
demo_location_arrond.render()
demo_parents_location.render()
with gr.Row():
demo_inscription.render()
demo_discipline.render()
with gr.Row():
demo_job.render()
demo_income.render()
gr.Markdown("### Capital Social")
with gr.Row():
demo_socialcapital1_parent1.render()
demo_socialcapital1_parent2.render()
demo_socialcapital2.render()
# =================================================================
# ACCORDION 6: Questions ouvertes
# =================================================================
with gr.Accordion(LANG["TAB_6_TITLE"], open=False):
open_non_institutionnel.render()
open_alternatives.render()
open_motivations.render()
# =================================================================
# Soumission et Téléchargement
# =================================================================
with gr.Accordion(LANG["TAB_7_TITLE"], open=False):
gr.Markdown("### Finalisation du Questionnaire")
gr.Markdown("Vérifiez que toutes vos réponses sont complètes avant de soumettre. Une fois soumis, le fichier CSV (nommé avec l'identifiant de l'enquêteur et la date) apparaîtra ci-dessous pour téléchargement.")
with gr.Row():
submit_btn.render()
reset_btn.render()
output_status.render()
output_message.render()
# Submit action
submit_btn.click(
fn=process_survey,
inputs=SUBMIT_INPUT_COMPONENTS,
outputs=[output_status, output_message]
)
# Reset action
reset_btn.click(
fn=lambda: ([None] * EXPECTED_DATA_COUNT) + ["---", gr.update(value=None, visible=False)],
inputs=[],
outputs=DATA_INPUT_COMPONENTS + [output_status, output_message],
js="() => { document.getElementById('output_message').innerHTML = '---'; }"
)
if __name__ == "__main__":
import sys
# Assurez-vous que le compte final est correct avant le lancement
sys.modules[__name__].EXPECTED_DATA_COUNT = len(DATA_INPUT_COMPONENTS)
sys.modules[__name__].EXPECTED_COUNT = len(DATA_INPUT_COMPONENTS)
demo.launch()