clementBE commited on
Commit
81f4ba1
·
verified ·
1 Parent(s): 8567859

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +512 -125
app.py CHANGED
@@ -6,12 +6,12 @@ from typing import List, Union, Dict, Any, Tuple
6
  from datetime import datetime
7
 
8
  # =================================================================
9
- # CONFIGURATION AND LANGUAGE DEFINITIONS
10
  # =================================================================
11
 
12
- # CRITICAL FIX: The actual count based on component definitions is 105.
13
- EXPECTED_DATA_COUNT = 105
14
- EXPECTED_COUNT = 105
15
 
16
  # --- Shared Choices (French Only) ---
17
  FREQ_FR = ["Jamais", "Rarement", "Parfois", "Souvent"]
@@ -31,13 +31,19 @@ DOMAIN_CHOICES_FR = [
31
  "Ancrage local (bar associatif, aides aux personnes agées, bibliothèque associative…)",
32
  "autre - préciser"
33
  ]
 
34
  ENGAGE3_CHOICES_FR = [
35
- "écologie", "lutte contre la pauvreté", "action contre les discriminations",
36
- "vie politique locale ou nationale", "action syndicale", "causes internationales", "autre / précisez"
 
 
 
 
 
37
  ]
38
  SOCIAL_MEDIA_CHOICES = [
39
- "Instagram", "YouTube", "TikTok", "Facebook", "WhatsApp", "BlueSky", "Signal",
40
- "Telegram", "Mastodon", "Discord", "Twitter", "Reddit", "messagerie de jeu en ligne",
41
  "autre : préciser", "je ne sais pas"
42
  ]
43
  ANXIETY_MANIFESTATION_CHOICES = [
@@ -47,35 +53,52 @@ ANXIETY_MANIFESTATION_CHOICES = [
47
  "Je fais des recherches sur internet", "J’interroge / je confie à un chatbot",
48
  "Je rejoins une communauté spécialisée sur les réseaux sociaux"
49
  ]
 
50
  INFO4_CHOICES = ["Curieux", "Indifférent·e", "Légèrement agacé·e", "Inquiet·ète", "En colère", "Déstabilisé·e"]
51
  INFO5_CHOICES = [
52
- "Je le lis/regarde avec attention.", "Je le ferme ou l’ignore.", "Je cherche des arguments pour le réfuter.",
53
- "Je le partage pour en discuter avec dautres.", "autre réaction"
 
 
 
54
  ]
55
- ENGAGE1_CHOICES = ["non", "en tant que sympathisant", "comme donateur-rice", "en tant que bénévole",
56
- "comme participant·e/spectateur-rice", "comme organisateur-rice/responsable"
 
 
 
 
 
 
57
  ]
 
58
  ENGAGE2_CHOICES = [
59
- "une association", "un collectif", "une ONG", "un parti politique", "un syndicat étudiant",
60
- "une assemblée ou une instance représentative (collectivité, communes…)", "autre (préciser)", "aucune de ces formes"
 
 
 
 
 
 
61
  ]
 
62
  DEMO_INSCRIPTION_CHOICES = ["L1", "L2", "L3", "M1", "M2", "D", "DU", "autre niveau d’étude/diplôme"]
63
  DEMO_DISCIPLINE_CHOICES = [
64
- "Cinéma", "Communication", "Langues", "LEA", "Lettres", "Médiation", "Musique",
65
  "Sciences du langage", "Théâtre", "Traduction", "Autres"
66
  ]
67
- INFO_ACTIVITES_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"]
68
 
69
  # --- Language Definitions (French Only) ---
70
- # (omitted for brevity, assume LANG_FR is fully defined)
71
- LANG = {
72
  "TITLE": "Questionnaire : Pratiques culturelles et Engagement Citoyen",
73
  "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.",
74
- "ENQUETEUR_LABEL": "Identifiant de l'enquêteur",
75
  "TAB_1_TITLE": "1. Contact & Lieux Fréquentés", "TAB_2_TITLE": "2. Engagement Citoyen",
76
  "TAB_3_TITLE": "3. Consommation d’actualités", "TAB_4_TITLE": "4. Pratiques culturelles et usages numériques",
77
  "TAB_5_TITLE": "5. Profil Démographique", "TAB_6_TITLE": "6. Questions ouvertes",
78
- "TAB_7_TITLE": "7. Soumission et Téléchargement",
79
  "APPROACH_LABEL": "APPROACH - Accepterais-tu de répondre à un questionnaire ? (10 min)",
80
  "REFUSAL_REASON_LABEL": "ANSWER_NO1 (si APPROACH=non) - Pourquoi ?",
81
  "CONTACT_LATER_LABEL": "ANSWER_NO2 (si APPROACH=non) - On peut prendre rendez-vous plus tard ?",
@@ -95,9 +118,9 @@ LANG = {
95
  "SPACE2B_OPTIONS": ["Pas le temps", "Trop cher", "Je ne sais pas où aller", "Pas intéressé", "Autre"],
96
  "ENGAGE1_LABEL": "ENGAGE1 - Toi-même, participes-tu à des pratiques d’engagement citoyen ?",
97
  "ENGAGEMENT_ORGANISATION_LABEL": "ENGAGE2 - Dans quelle(s) organisation(s) es-tu engagé·e ?",
98
- "ENGAGE2_AUTRE_DETAIL_LABEL": "ENGAGE2 - Précisez 'autre' pour l'organisation",
99
  "ENGAGEMENT_DOMAINE_LABEL": "ENGAGE3 - Dans quel(s) domaine(s) es-tu engagé·e ?",
100
- "ENGAGE3_AUTRE_DETAIL_LABEL": "ENGAGE3 - Précisez 'autre' pour le domaine",
101
  "INFO_ACTIVITES_LABEL": "ENGAGE4 - Participes-tu ou as-tu participé à une ou plusieurs des activités suivantes ?",
102
  "INFO_RESEAUX_SOCIAUX_LABEL": "ENGAGE5 - Utilises-tu les réseaux sociaux pour t’engager dans des causes ?",
103
  "INFO_FREQUENCE_ACTU_LABEL": "INFO1 - À quelle fréquence te tiens-tu informé·e de l’actualité ?",
@@ -106,7 +129,7 @@ LANG = {
106
  "OPPOSITE_FEELING_BASE_LABEL": "INFO4 - Quand une personne exprime une opinion opposée à la tienne sur le thème qui te semble représenter des enjeux majeurs, comment te sens-tu ?",
107
  "INFO_CONTRADICT_OPINION_LABEL": "INFO5 - Quand tu tombes sur un article ou une vidéo qui contredit tes croyances, quelle est ta première réaction ?",
108
  "INFO4_LABEL": "INFO6 - Parmi ces thèmes, lequel ou lesquels génèrent chez toi un sentiment d’anxiété ?",
109
- "INFO5_LABEL": "INFO7 - Comment se manifeste ce sentiment d’anxiété ?",
110
  "INFO6_TITLE": "INFO8 - Éléments les plus problématiques pour toi (Échelle 1 'sans importance' à 10 'essentiel')",
111
  "INFO6_ITEMS": ["Logement", "Politique/Gouvernement", "Études/Avenir professionnel", "Crise climatique/Environnement", "Inégalités sociales", "Vie sentimentale/familiale", "Sécurité personnelle", "Estime de soi", "Liberté individuelle"],
112
  "PRAT_CULT1_LABEL": "PRAT_CULT1 - Combien de sorties culturelles environ par mois :",
@@ -133,23 +156,26 @@ LANG = {
133
  "RESET_BUTTON": "Recommencer",
134
  }
135
 
 
136
 
137
  # =================================================================
138
- # COMPONENT DEFINITIONS
139
  # =================================================================
140
 
141
  # === Global ===
142
  enqueteur_id = gr.Textbox(label=LANG["ENQUETEUR_LABEL"], placeholder="Entrez votre identifiant")
 
 
143
  submit_btn = gr.Button(LANG["SUBMIT_BUTTON"], variant="primary")
144
  reset_btn = gr.Button(LANG["RESET_BUTTON"])
145
  output_status = gr.Markdown("---")
146
  output_message = gr.File(label="Télécharger les Données", file_types=['.csv'], visible=False, elem_id="output_message")
147
 
148
 
149
- # === TAB 1 Components (24 total) ===
150
  approach_answer = gr.Radio(label=LANG["APPROACH_LABEL"], choices=["Oui", "Non"])
151
  refusal_reason = gr.CheckboxGroup(label=LANG["REFUSAL_REASON_LABEL"], choices=["Pas le temps", "Cela ne m’intéresse pas", "Autre"], visible=False)
152
- refusal_reason_other = gr.Textbox(label="Précisez 'Autre'", visible=False) # PRECISION 1
153
  contact_later = gr.Radio(label=LANG["CONTACT_LATER_LABEL"], choices=["Oui", "Non"], visible=False)
154
 
155
  space1 = gr.Radio(label=LANG["SPACE1_LABEL"], choices=FREQ_FR)
@@ -170,45 +196,33 @@ space7_1 = gr.Textbox(label="Pratique 1", placeholder="Décrivez la pratique d'e
170
  space7_2 = gr.Textbox(label="Pratique 2", placeholder="Décrivez la pratique d'engagement 2", visible=False)
171
  space7_3 = gr.Textbox(label="Pratique 3", placeholder="Décrivez la pratique d'engagement 3", visible=False)
172
 
 
173
  space_domain_1 = gr.CheckboxGroup(label=f"Domaines pour Pratique 1", choices=DOMAIN_CHOICES_FR, visible=False)
174
  space_domain_2 = gr.CheckboxGroup(label=f"Domaines pour Pratique 2", choices=DOMAIN_CHOICES_FR, visible=False)
175
  space_domain_3 = gr.CheckboxGroup(label=f"Domaines pour Pratique 3", choices=DOMAIN_CHOICES_FR, visible=False)
176
- space_domain_autre_detail = gr.Textbox(label="Précisez le domaine 'autre - préciser'", visible=False) # PRECISION 2
177
 
178
  space11 = gr.CheckboxGroup(label=LANG["SPACE11_LABEL"], choices=SOCIAL_MEDIA_CHOICES)
179
- space11_autre_detail = gr.Textbox(label="Précisez le média social 'autre : préciser'", visible=False) # PRECISION 3
180
-
181
 
182
- # === TAB 2 Components (8 total) ===
183
  engage1_role = gr.CheckboxGroup(label=LANG["ENGAGE1_LABEL"], choices=ENGAGE1_CHOICES)
184
  engage2_type = gr.CheckboxGroup(label=LANG["ENGAGEMENT_ORGANISATION_LABEL"], choices=ENGAGE2_CHOICES)
185
- engage2_autre_detail = gr.Textbox(label=LANG["ENGAGE2_AUTRE_DETAIL_LABEL"], visible=False) # PRECISION 4
186
 
 
187
  engagement_domaine = gr.CheckboxGroup(label=LANG["ENGAGEMENT_DOMAINE_LABEL"], choices=ENGAGE3_CHOICES_FR)
188
- engage3_autre_detail = gr.Textbox(label=LANG["ENGAGE3_AUTRE_DETAIL_LABEL"], visible=False) # PRECISION 5
189
-
190
- info_activites = gr.CheckboxGroup(label=LANG["INFO_ACTIVITES_LABEL"], choices=INFO_ACTIVITES_CHOICES, interactive=True)
191
- info_activites_autre_detail = gr.Textbox(label="Précisez l'activité 'Autre'", visible=False) # PRECISION 6
192
 
 
193
  info_reseaux_sociaux = gr.Radio(label=LANG["INFO_RESEAUX_SOCIAUX_LABEL"], choices=["Oui", "Non", "Parfois"])
194
 
195
-
196
- # === TAB 3 Components (25 total) ===
197
  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"])
198
-
199
  info2 = gr.CheckboxGroup(label=LANG["INFO2_LABEL"], choices=THEMES_ACTU_FR)
200
- info2_autre_detail = gr.Textbox(label="INFO2 - Précisez 'autre/précisez'", visible=False) # PRECISION 7
201
-
202
  info3 = gr.CheckboxGroup(label=LANG["INFO3_LABEL"], choices=THEMES_ACTU_FR)
203
- info3_autre_detail = gr.Textbox(label="INFO3 - Précisez 'autre/précisez'", visible=False) # PRECISION 8
204
-
205
  info_opposite_feeling = gr.Radio(label=LANG["OPPOSITE_FEELING_BASE_LABEL"], choices=INFO4_CHOICES)
206
-
207
  info_contradict_opinion = gr.Radio(label=LANG["INFO_CONTRADICT_OPINION_LABEL"], choices=INFO5_CHOICES)
208
- info_contradict_opinion_autre_detail = gr.Textbox(label="INFO5 - Précisez l''autre réaction'", visible=False) # PRECISION 9
209
-
210
  info4 = gr.CheckboxGroup(label=LANG["INFO4_LABEL"], choices=THEMES_ACTU_FR)
211
- info5 = gr.CheckboxGroup(label=LANG["INFO5_LABEL"], choices=ANXIETY_MANIFESTATION_CHOICES)
212
 
213
  # Info 8 Sliders (9 components)
214
  info6_sliders = {}
@@ -216,16 +230,17 @@ for item in LANG["INFO6_ITEMS"]:
216
  clean_id = item.split('/')[0].lower().replace(' ', '_').replace('é', 'e').replace('è', 'e')
217
  info6_sliders[clean_id] = gr.Slider(label=item, minimum=1, maximum=10, step=1)
218
  info6_slider_components = list(info6_sliders.values())
 
219
 
220
 
221
- # === TAB 4 Components (30 total) ===
222
  prat_cult1 = gr.Number(label=LANG["PRAT_CULT1_LABEL"], minimum=0, maximum=100, step=1)
223
 
224
  # Cultural Frequency Radios (10 components)
225
  prat_cult_freq_components = [
226
  gr.Radio(label=f"{activity}", choices=FREQ_FR) for activity in ACTIVITY_CHOICES
227
  ]
228
- (prat_cult_lecture, prat_cult_cinema, prat_cult_musique, prat_cult_theatre, prat_cult_expositions,
229
  prat_cult_sport, prat_cult_voyages, prat_cult_jeux_video, prat_cult_bricolage, prat_cult_reseaux) = prat_cult_freq_components
230
 
231
  prat_nature = gr.Radio(label=LANG["PRAT_NATURE_LABEL"], choices=["Oui", "Non", "Parfois"])
@@ -234,130 +249,502 @@ prat_nature = gr.Radio(label=LANG["PRAT_NATURE_LABEL"], choices=["Oui", "Non", "
234
  platform_components = [
235
  gr.Number(label=f"Heures/jour pour {platform}", placeholder="0.5, 1, 2...", minimum=0, step=0.5) for platform in PLATFORM_CHOICES
236
  ]
237
- (plat_instagram, plat_tiktok, plat_youtube, plat_twitter, plat_facebook,
238
  plat_snapchat, plat_twitch, plat_reddit, plat_linkedin, plat_autre) = platform_components
239
 
240
- # Purpose Components (1 Textbox + 7 Radios = 8 total)
241
- purpose_actu = gr.Radio(label="Fréquence pour: Actualité/Info", choices=FREQ_FR)
242
- purpose_loisirs = gr.Radio(label="Fréquence pour: Loisirs/Divertissement", choices=FREQ_FR)
243
- purpose_education = gr.Radio(label="Fréquence pour: Éducation/Apprentissage", choices=FREQ_FR)
244
- purpose_engagement = gr.Radio(label="Fréquence pour: Engagement/Politique", choices=FREQ_FR)
245
- purpose_social = gr.Radio(label="Fréquence pour: Social/Communication", choices=FREQ_FR)
246
- purpose_professionnel = gr.Radio(label="Fréquence pour: Professionnel", choices=FREQ_FR)
247
- purpose_autre_freq = gr.Radio(label="Fréquence pour: Autre (précisez)", choices=FREQ_FR)
248
 
249
- purpose_autre_detail = gr.Textbox(label="Précisez l'usage 'Autre'", visible=True) # PRECISION 10
 
250
 
251
 
252
- # === TAB 5 Components (14 total) ===
253
  demo_gender = gr.Radio(label=LANG["DEMO_GENDER_LABEL"], choices=["Homme", "Femme", "Non-binaire", "Préfère ne pas dire"])
254
  demo_age = gr.Number(label=LANG["DEMO_AGE_LABEL"], minimum=18, maximum=100, step=1, placeholder="ex: 25")
255
  demo_location_commune = gr.Textbox(label=LANG["DEMO_LOCATION_COMMUNE_LABEL"], placeholder="Nom de la commune")
256
  demo_location_arrond = gr.Textbox(label=LANG["DEMO_LOCATION_ARROND_LABEL"], placeholder="ex: 75005")
257
  demo_parents_location = gr.Radio(label=LANG["DEMO_PARENTS_LOCATION_LABEL"], choices=["Oui", "Non"])
258
-
259
  demo_inscription = gr.CheckboxGroup(label=LANG["DEMO_INSCRIPTION_LABEL"], choices=DEMO_INSCRIPTION_CHOICES)
260
- demo_inscription_autre_detail = gr.Textbox(label="Précisez 'autre niveau d’étude/diplôme'", visible=False) # PRECISION 11
261
-
262
  demo_discipline = gr.CheckboxGroup(label=LANG["DEMO_DISCIPLINE_LABEL"], choices=DEMO_DISCIPLINE_CHOICES)
263
- demo_discipline_autre_detail = gr.Textbox(label="Précisez 'Autres' disciplines", visible=False) # PRECISION 12 (TOTAL PRECISIONS: 12)
264
-
265
  demo_job = gr.Radio(label=LANG["DEMO_JOB_LABEL"], choices=["Oui", "Non"])
266
  demo_income = gr.Radio(label=LANG["DEMO_INCOME_LABEL"], choices=INCOME_CHOICES)
267
  demo_socialcapital1_parent1 = gr.Radio(label=LANG["DEMO_SOCIALCAPITAL1_PARENT1_LABEL"], choices=INSEE_CHOICES)
268
  demo_socialcapital1_parent2 = gr.Radio(label=LANG["DEMO_SOCIALCAPITAL1_PARENT2_LABEL"], choices=INSEE_CHOICES)
269
  demo_socialcapital2 = gr.Number(label=LANG["DEMO_SOCIALCAPITAL2_LABEL"], minimum=0, step=1)
270
 
271
- # === TAB 6 Components (3 total) ===
272
  open_non_institutionnel = gr.Textbox(label=LANG["OPEN_NON_INSTITUTIONNEL_LABEL"], lines=3)
273
  open_alternatives = gr.Textbox(label=LANG["OPEN_ALTERNATIVES_LABEL"], placeholder="5 mots maximum")
274
  open_motivations = gr.Textbox(label=LANG["OPEN_MOTIVATIONS_LABEL"], lines=3)
275
 
276
 
277
- # [Utility functions are omitted for brevity, assuming they are included in your full file]
278
- # ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
  # =================================================================
281
- # DATA COMPONENT FINAL ASSEMBLY (105 components)
282
- # --- CRITICAL FIX APPLIED HERE: All 105 components are explicitly listed ---
283
  # =================================================================
284
  DATA_INPUT_COMPONENTS = [
285
  # Global (1)
286
- enqueteur_id,
287
-
288
- # TAB 1 - Contact & Lieux (24)
289
- approach_answer, refusal_reason, refusal_reason_other, contact_later,
290
  space1, space2a_1, space2a_2, space2a_3, space2b,
291
- space3_1, space3_2, space3_3,
292
  space4, space5,
293
- space6,
294
- space7_1, space_domain_1, space_domain_autre_detail,
295
  space7_2, space_domain_2,
296
  space7_3, space_domain_3,
297
- space11, space11_autre_detail,
298
-
299
- # TAB 2 - Engagement (8)
300
- engage1_role, engage2_type, engage2_autre_detail,
301
- engagement_domaine, engage3_autre_detail,
302
- info_activites, info_activites_autre_detail,
303
- info_reseaux_sociaux,
304
-
305
- # TAB 3 - Actualité (25)
306
- info_frequence_actu, info2, info2_autre_detail,
307
- info3, info3_autre_detail,
308
- info_opposite_feeling, info_contradict_opinion, info_contradict_opinion_autre_detail,
309
- info4, info5,
310
- # INFO6 SLIDERS (9 components: Explicit unpacking via * is safest here)
311
- *info6_slider_components,
312
-
313
  # TAB 4 - Cultural & Digital (30)
314
- prat_cult1,
315
- # PRAT_CULT FREQUENCY (10 components: Explicit names are safer)
316
- prat_cult_lecture, prat_cult_cinema, prat_cult_musique, prat_cult_theatre, prat_cult_expositions,
317
  prat_cult_sport, prat_cult_voyages, prat_cult_jeux_video, prat_cult_bricolage, prat_cult_reseaux,
318
  prat_nature,
319
- # PLATFORM USAGE (10 components)
320
- plat_instagram, plat_tiktok, plat_youtube, plat_twitter, plat_facebook,
321
  plat_snapchat, plat_twitch, plat_reddit, plat_linkedin, plat_autre,
322
- # PURPOSE (8 components)
323
  purpose_autre_detail,
324
- purpose_actu, purpose_loisirs, purpose_education, purpose_engagement,
325
- purpose_social, purpose_professionnel, purpose_autre_freq,
326
-
327
- # TAB 5 - Demographics (14)
328
  demo_gender, demo_age, demo_location_commune, demo_location_arrond, demo_parents_location,
329
- demo_inscription, demo_inscription_autre_detail,
330
- demo_discipline, demo_discipline_autre_detail,
331
- demo_job, demo_income,
332
  demo_socialcapital1_parent1, demo_socialcapital1_parent2, demo_socialcapital2,
333
-
334
  # TAB 6 - Open Questions (3)
335
  open_non_institutionnel, open_alternatives, open_motivations,
336
  ]
337
 
338
- # =================================================================
339
- # CRITICAL DEBUGGING CHECK
340
- # =================================================================
341
  if len(DATA_INPUT_COMPONENTS) != EXPECTED_DATA_COUNT:
342
- print(f"DEBUG: Application Startup Check Failed!")
343
- print(f"DEBUG: Found component count: {len(DATA_INPUT_COMPONENTS)}")
344
- print(f"DEBUG: Expected count: {EXPECTED_DATA_COUNT}")
345
-
346
- try:
347
- last_15_labels = [getattr(c, 'label', 'N/A') for c in DATA_INPUT_COMPONENTS[-15:]]
348
- print(f"DEBUG: Last 15 component labels found: {last_15_labels}")
349
- except Exception as e:
350
- print(f"DEBUG: Could not print last 15 labels: {e}")
351
-
352
- # This line number should now be around 640 in your full file
353
  raise RuntimeError(f"Internal component count mismatch. Expected {EXPECTED_DATA_COUNT}, got {len(DATA_INPUT_COMPONENTS)}. Please verify the DATA_INPUT_COMPONENTS list definition.")
354
 
355
  SUBMIT_INPUT_COMPONENTS = DATA_INPUT_COMPONENTS
356
 
357
- # [Gradio UI setup and handlers follow]
358
- # ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
 
360
  if __name__ == "__main__":
361
- # Ensure you are running this exact file, and if using Docker, that the image is rebuilt.
362
- # demo.launch(show_api=False)
363
- pass
 
 
 
6
  from datetime import datetime
7
 
8
  # =================================================================
9
+ # CONFIGURATION AND LANGUAGE DEFINITIONS
10
  # =================================================================
11
 
12
+ # Setting EXPECTED_DATA_COUNT based on the final verified structure: 91
13
+ EXPECTED_DATA_COUNT = 91
14
+ EXPECTED_COUNT = 91
15
 
16
  # --- Shared Choices (French Only) ---
17
  FREQ_FR = ["Jamais", "Rarement", "Parfois", "Souvent"]
 
31
  "Ancrage local (bar associatif, aides aux personnes agées, bibliothèque associative…)",
32
  "autre - préciser"
33
  ]
34
+ # LISTE POUR ENGAGE3
35
  ENGAGE3_CHOICES_FR = [
36
+ "écologie",
37
+ "lutte contre la pauvreté",
38
+ "action contre les discriminations",
39
+ "vie politique locale ou nationale",
40
+ "action syndicale",
41
+ "causes internationales",
42
+ "autre / précisez"
43
  ]
44
  SOCIAL_MEDIA_CHOICES = [
45
+ "Instagram", "YouTube", "TikTok", "Facebook", "WhatsApp", "BlueSky", "Signal",
46
+ "Telegram", "Mastodon", "Discord", "Twitter", "Reddit", "messagerie de jeu en ligne",
47
  "autre : préciser", "je ne sais pas"
48
  ]
49
  ANXIETY_MANIFESTATION_CHOICES = [
 
53
  "Je fais des recherches sur internet", "J’interroge / je confie à un chatbot",
54
  "Je rejoins une communauté spécialisée sur les réseaux sociaux"
55
  ]
56
+
57
  INFO4_CHOICES = ["Curieux", "Indifférent·e", "Légèrement agacé·e", "Inquiet·ète", "En colère", "Déstabilisé·e"]
58
  INFO5_CHOICES = [
59
+ "Je le lis/regarde avec attention.",
60
+ "Je le ferme ou lignore.",
61
+ "Je cherche des arguments pour le réfuter.",
62
+ "Je le partage pour en discuter avec d’autres.",
63
+ "autre réaction"
64
  ]
65
+
66
+ ENGAGE1_CHOICES = [
67
+ "non",
68
+ "en tant que sympathisant",
69
+ "comme donateur-rice",
70
+ "en tant que bénévole",
71
+ "comme participant·e/spectateur-rice",
72
+ "comme organisateur-rice/responsable"
73
  ]
74
+
75
  ENGAGE2_CHOICES = [
76
+ "une association",
77
+ "un collectif",
78
+ "une ONG",
79
+ "un parti politique",
80
+ "un syndicat étudiant",
81
+ "une assemblée ou une instance représentative (collectivité, communes…)",
82
+ "autre (préciser)",
83
+ "aucune de ces formes"
84
  ]
85
+
86
  DEMO_INSCRIPTION_CHOICES = ["L1", "L2", "L3", "M1", "M2", "D", "DU", "autre niveau d’étude/diplôme"]
87
  DEMO_DISCIPLINE_CHOICES = [
88
+ "Cinéma", "Communication", "Langues", "LEA", "Lettres", "Médiation", "Musique",
89
  "Sciences du langage", "Théâtre", "Traduction", "Autres"
90
  ]
91
+
92
 
93
  # --- Language Definitions (French Only) ---
94
+ LANG_FR = {
 
95
  "TITLE": "Questionnaire : Pratiques culturelles et Engagement Citoyen",
96
  "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.",
97
+ "ENQUETEUR_LABEL": "Identifiant de l'enquêteur",
98
  "TAB_1_TITLE": "1. Contact & Lieux Fréquentés", "TAB_2_TITLE": "2. Engagement Citoyen",
99
  "TAB_3_TITLE": "3. Consommation d’actualités", "TAB_4_TITLE": "4. Pratiques culturelles et usages numériques",
100
  "TAB_5_TITLE": "5. Profil Démographique", "TAB_6_TITLE": "6. Questions ouvertes",
101
+ "TAB_7_TITLE": "7. Soumission et Téléchargement",
102
  "APPROACH_LABEL": "APPROACH - Accepterais-tu de répondre à un questionnaire ? (10 min)",
103
  "REFUSAL_REASON_LABEL": "ANSWER_NO1 (si APPROACH=non) - Pourquoi ?",
104
  "CONTACT_LATER_LABEL": "ANSWER_NO2 (si APPROACH=non) - On peut prendre rendez-vous plus tard ?",
 
118
  "SPACE2B_OPTIONS": ["Pas le temps", "Trop cher", "Je ne sais pas où aller", "Pas intéressé", "Autre"],
119
  "ENGAGE1_LABEL": "ENGAGE1 - Toi-même, participes-tu à des pratiques d’engagement citoyen ?",
120
  "ENGAGEMENT_ORGANISATION_LABEL": "ENGAGE2 - Dans quelle(s) organisation(s) es-tu engagé·e ?",
121
+ "ENGAGE2_AUTRE_DETAIL_LABEL": "ENGAGE2 - Précisez 'autre'",
122
  "ENGAGEMENT_DOMAINE_LABEL": "ENGAGE3 - Dans quel(s) domaine(s) es-tu engagé·e ?",
123
+ "ENGAGE3_AUTRE_DETAIL_LABEL": "ENGAGE3 - Précisez 'autre'",
124
  "INFO_ACTIVITES_LABEL": "ENGAGE4 - Participes-tu ou as-tu participé à une ou plusieurs des activités suivantes ?",
125
  "INFO_RESEAUX_SOCIAUX_LABEL": "ENGAGE5 - Utilises-tu les réseaux sociaux pour t’engager dans des causes ?",
126
  "INFO_FREQUENCE_ACTU_LABEL": "INFO1 - À quelle fréquence te tiens-tu informé·e de l’actualité ?",
 
129
  "OPPOSITE_FEELING_BASE_LABEL": "INFO4 - Quand une personne exprime une opinion opposée à la tienne sur le thème qui te semble représenter des enjeux majeurs, comment te sens-tu ?",
130
  "INFO_CONTRADICT_OPINION_LABEL": "INFO5 - Quand tu tombes sur un article ou une vidéo qui contredit tes croyances, quelle est ta première réaction ?",
131
  "INFO4_LABEL": "INFO6 - Parmi ces thèmes, lequel ou lesquels génèrent chez toi un sentiment d’anxiété ?",
132
+ "INFO5_LABEL": "INFO7 - Comment se manifeste ce sentiment d’anxiété ?",
133
  "INFO6_TITLE": "INFO8 - Éléments les plus problématiques pour toi (Échelle 1 'sans importance' à 10 'essentiel')",
134
  "INFO6_ITEMS": ["Logement", "Politique/Gouvernement", "Études/Avenir professionnel", "Crise climatique/Environnement", "Inégalités sociales", "Vie sentimentale/familiale", "Sécurité personnelle", "Estime de soi", "Liberté individuelle"],
135
  "PRAT_CULT1_LABEL": "PRAT_CULT1 - Combien de sorties culturelles environ par mois :",
 
156
  "RESET_BUTTON": "Recommencer",
157
  }
158
 
159
+ LANG = LANG_FR
160
 
161
  # =================================================================
162
+ # COMPONENT DEFINITIONS
163
  # =================================================================
164
 
165
  # === Global ===
166
  enqueteur_id = gr.Textbox(label=LANG["ENQUETEUR_LABEL"], placeholder="Entrez votre identifiant")
167
+
168
+ # === Submission/Output Components ===
169
  submit_btn = gr.Button(LANG["SUBMIT_BUTTON"], variant="primary")
170
  reset_btn = gr.Button(LANG["RESET_BUTTON"])
171
  output_status = gr.Markdown("---")
172
  output_message = gr.File(label="Télécharger les Données", file_types=['.csv'], visible=False, elem_id="output_message")
173
 
174
 
175
+ # === TAB 1 Components ===
176
  approach_answer = gr.Radio(label=LANG["APPROACH_LABEL"], choices=["Oui", "Non"])
177
  refusal_reason = gr.CheckboxGroup(label=LANG["REFUSAL_REASON_LABEL"], choices=["Pas le temps", "Cela ne m’intéresse pas", "Autre"], visible=False)
178
+ refusal_reason_other = gr.Textbox(label="Précisez 'Autre'", visible=False)
179
  contact_later = gr.Radio(label=LANG["CONTACT_LATER_LABEL"], choices=["Oui", "Non"], visible=False)
180
 
181
  space1 = gr.Radio(label=LANG["SPACE1_LABEL"], choices=FREQ_FR)
 
196
  space7_2 = gr.Textbox(label="Pratique 2", placeholder="Décrivez la pratique d'engagement 2", visible=False)
197
  space7_3 = gr.Textbox(label="Pratique 3", placeholder="Décrivez la pratique d'engagement 3", visible=False)
198
 
199
+ # Domaine components (CheckboxGroup)
200
  space_domain_1 = gr.CheckboxGroup(label=f"Domaines pour Pratique 1", choices=DOMAIN_CHOICES_FR, visible=False)
201
  space_domain_2 = gr.CheckboxGroup(label=f"Domaines pour Pratique 2", choices=DOMAIN_CHOICES_FR, visible=False)
202
  space_domain_3 = gr.CheckboxGroup(label=f"Domaines pour Pratique 3", choices=DOMAIN_CHOICES_FR, visible=False)
 
203
 
204
  space11 = gr.CheckboxGroup(label=LANG["SPACE11_LABEL"], choices=SOCIAL_MEDIA_CHOICES)
 
 
205
 
206
+ # === TAB 2 Components ===
207
  engage1_role = gr.CheckboxGroup(label=LANG["ENGAGE1_LABEL"], choices=ENGAGE1_CHOICES)
208
  engage2_type = gr.CheckboxGroup(label=LANG["ENGAGEMENT_ORGANISATION_LABEL"], choices=ENGAGE2_CHOICES)
209
+ engage2_autre_detail = gr.Textbox(label=LANG["ENGAGE2_AUTRE_DETAIL_LABEL"], visible=False)
210
 
211
+ # ENGAGE3 : CheckboxGroup + Textbox pour 'autre'
212
  engagement_domaine = gr.CheckboxGroup(label=LANG["ENGAGEMENT_DOMAINE_LABEL"], choices=ENGAGE3_CHOICES_FR)
213
+ engage3_autre_detail = gr.Textbox(label=LANG["ENGAGE3_AUTRE_DETAIL_LABEL"], visible=False)
 
 
 
214
 
215
+ 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)
216
  info_reseaux_sociaux = gr.Radio(label=LANG["INFO_RESEAUX_SOCIAUX_LABEL"], choices=["Oui", "Non", "Parfois"])
217
 
218
+ # === TAB 3 Components ===
 
219
  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"])
 
220
  info2 = gr.CheckboxGroup(label=LANG["INFO2_LABEL"], choices=THEMES_ACTU_FR)
 
 
221
  info3 = gr.CheckboxGroup(label=LANG["INFO3_LABEL"], choices=THEMES_ACTU_FR)
 
 
222
  info_opposite_feeling = gr.Radio(label=LANG["OPPOSITE_FEELING_BASE_LABEL"], choices=INFO4_CHOICES)
 
223
  info_contradict_opinion = gr.Radio(label=LANG["INFO_CONTRADICT_OPINION_LABEL"], choices=INFO5_CHOICES)
 
 
224
  info4 = gr.CheckboxGroup(label=LANG["INFO4_LABEL"], choices=THEMES_ACTU_FR)
225
+ info5 = gr.CheckboxGroup(label=LANG["INFO5_LABEL"], choices=ANXIETY_MANIFESTATION_CHOICES)
226
 
227
  # Info 8 Sliders (9 components)
228
  info6_sliders = {}
 
230
  clean_id = item.split('/')[0].lower().replace(' ', '_').replace('é', 'e').replace('è', 'e')
231
  info6_sliders[clean_id] = gr.Slider(label=item, minimum=1, maximum=10, step=1)
232
  info6_slider_components = list(info6_sliders.values())
233
+ info6_logement, info6_politique, info6_etudes, info6_climat, info6_sociales, info6_sentimentale, info6_securite, info6_estime, info6_liberte = info6_slider_components
234
 
235
 
236
+ # === TAB 4 Components ===
237
  prat_cult1 = gr.Number(label=LANG["PRAT_CULT1_LABEL"], minimum=0, maximum=100, step=1)
238
 
239
  # Cultural Frequency Radios (10 components)
240
  prat_cult_freq_components = [
241
  gr.Radio(label=f"{activity}", choices=FREQ_FR) for activity in ACTIVITY_CHOICES
242
  ]
243
+ (prat_cult_lecture, prat_cult_cinema, prat_cult_musique, prat_cult_theatre, prat_cult_expositions,
244
  prat_cult_sport, prat_cult_voyages, prat_cult_jeux_video, prat_cult_bricolage, prat_cult_reseaux) = prat_cult_freq_components
245
 
246
  prat_nature = gr.Radio(label=LANG["PRAT_NATURE_LABEL"], choices=["Oui", "Non", "Parfois"])
 
249
  platform_components = [
250
  gr.Number(label=f"Heures/jour pour {platform}", placeholder="0.5, 1, 2...", minimum=0, step=0.5) for platform in PLATFORM_CHOICES
251
  ]
252
+ (plat_instagram, plat_tiktok, plat_youtube, plat_twitter, plat_facebook,
253
  plat_snapchat, plat_twitch, plat_reddit, plat_linkedin, plat_autre) = platform_components
254
 
255
+ # Purpose Frequency Radios (7 components)
256
+ purpose_components = [
257
+ gr.Radio(label=f"Fréquence pour: {purpose}", choices=FREQ_FR) for purpose in PURPOSE_CHOICES[:-1]
258
+ ]
259
+ purpose_autre_detail = gr.Textbox(label="Précisez l'usage 'Autre'", visible=True)
260
+ purpose_components.append(gr.Radio(label=f"Fréquence pour: {PURPOSE_CHOICES[-1]}", choices=FREQ_FR))
 
 
261
 
262
+ (purpose_actu, purpose_loisirs, purpose_education, purpose_engagement,
263
+ purpose_social, purpose_professionnel, purpose_autre_freq) = purpose_components
264
 
265
 
266
+ # === TAB 5 Components ===
267
  demo_gender = gr.Radio(label=LANG["DEMO_GENDER_LABEL"], choices=["Homme", "Femme", "Non-binaire", "Préfère ne pas dire"])
268
  demo_age = gr.Number(label=LANG["DEMO_AGE_LABEL"], minimum=18, maximum=100, step=1, placeholder="ex: 25")
269
  demo_location_commune = gr.Textbox(label=LANG["DEMO_LOCATION_COMMUNE_LABEL"], placeholder="Nom de la commune")
270
  demo_location_arrond = gr.Textbox(label=LANG["DEMO_LOCATION_ARROND_LABEL"], placeholder="ex: 75005")
271
  demo_parents_location = gr.Radio(label=LANG["DEMO_PARENTS_LOCATION_LABEL"], choices=["Oui", "Non"])
 
272
  demo_inscription = gr.CheckboxGroup(label=LANG["DEMO_INSCRIPTION_LABEL"], choices=DEMO_INSCRIPTION_CHOICES)
 
 
273
  demo_discipline = gr.CheckboxGroup(label=LANG["DEMO_DISCIPLINE_LABEL"], choices=DEMO_DISCIPLINE_CHOICES)
 
 
274
  demo_job = gr.Radio(label=LANG["DEMO_JOB_LABEL"], choices=["Oui", "Non"])
275
  demo_income = gr.Radio(label=LANG["DEMO_INCOME_LABEL"], choices=INCOME_CHOICES)
276
  demo_socialcapital1_parent1 = gr.Radio(label=LANG["DEMO_SOCIALCAPITAL1_PARENT1_LABEL"], choices=INSEE_CHOICES)
277
  demo_socialcapital1_parent2 = gr.Radio(label=LANG["DEMO_SOCIALCAPITAL1_PARENT2_LABEL"], choices=INSEE_CHOICES)
278
  demo_socialcapital2 = gr.Number(label=LANG["DEMO_SOCIALCAPITAL2_LABEL"], minimum=0, step=1)
279
 
280
+ # === TAB 6 Components ===
281
  open_non_institutionnel = gr.Textbox(label=LANG["OPEN_NON_INSTITUTIONNEL_LABEL"], lines=3)
282
  open_alternatives = gr.Textbox(label=LANG["OPEN_ALTERNATIVES_LABEL"], placeholder="5 mots maximum")
283
  open_motivations = gr.Textbox(label=LANG["OPEN_MOTIVATIONS_LABEL"], lines=3)
284
 
285
 
286
+ # Helper function to generate robust column names
287
+ def get_column_names(data_components: list) -> list:
288
+ names = []
289
+ for i, component in enumerate(data_components):
290
+ label_attr = getattr(component, 'label', None)
291
+
292
+ if label_attr:
293
+ name = label_attr
294
+ # Nettoyage et simplification du nom de colonne
295
+ clean_name = name.split('-')[0].strip().replace(' ', '_').replace('.', '').replace(':', '')
296
+
297
+ # Gestion des noms spécifiques
298
+ if "Domaines pour Pratique 1" in name:
299
+ clean_name = "P1_DOMAINES"
300
+ elif "Domaines pour Pratique 2" in name:
301
+ clean_name = "P2_DOMAINES"
302
+ elif "Domaines pour Pratique 3" in name:
303
+ clean_name = "P3_DOMAINES"
304
+ elif "Alternatif/Underground" in name:
305
+ clean_name = "SPACE4_QUAL_1"
306
+ elif "Lieu 2 : Alternatif" in name:
307
+ clean_name = "SPACE4_QUAL_2"
308
+ elif "Lieu 3 : Alternatif" in name:
309
+ clean_name = "SPACE4_QUAL_3"
310
+ elif clean_name == "ENGAGE2_Précisez_'autre'":
311
+ clean_name = "ENGAGE2_Autre_Precision"
312
+ # NOM DE COLONNE POUR ENGAGE3_AUTRE
313
+ elif clean_name == "ENGAGE3_Précisez_'autre'":
314
+ clean_name = "ENGAGE3_Autre_Precision"
315
+ elif clean_name == "ENGAGE3_Dans_quel(s)_domaine(s)_es-tu_engagé·e_":
316
+ clean_name = "ENGAGE3_DOMAINE"
317
+
318
+ names.append(clean_name)
319
+ else:
320
+ names.append(f"Component_{i+1}")
321
+
322
+ return names
323
+
324
+ # =================================================================
325
+ # GRADIO UI & LOGIC - UTILITY FUNCTIONS
326
+ # =================================================================
327
+
328
+ def update_visibility_approach(approach):
329
+ updates = {}
330
+ is_non = approach == "Non"
331
+ updates[refusal_reason] = gr.update(visible=is_non)
332
+ updates[contact_later] = gr.update(visible=is_non)
333
+
334
+ if not is_non:
335
+ updates[refusal_reason_other] = gr.update(visible=False)
336
+
337
+ return updates[refusal_reason], updates[contact_later]
338
+
339
+ def update_visibility_refusal(reasons):
340
+ if reasons and "Autre" in reasons:
341
+ return gr.update(visible=True)
342
+ return gr.update(visible=False)
343
+
344
+ def update_visibility_space1(frequency):
345
+ is_frequent = frequency in ["Rarement", "Parfois", "Souvent"]
346
+ is_never = frequency == "Jamais"
347
+ return {
348
+ space2a_1: gr.update(visible=is_frequent),
349
+ space2a_2: gr.update(visible=is_frequent),
350
+ space2a_3: gr.update(visible=is_frequent),
351
+ space3_1: gr.update(visible=is_frequent),
352
+ space3_2: gr.update(visible=is_frequent),
353
+ space3_3: gr.update(visible=is_frequent),
354
+ space2b: gr.update(visible=is_never),
355
+ }
356
+
357
+ def update_visibility_space4(follow_community):
358
+ return gr.update(visible=follow_community == "Oui")
359
+
360
+ def update_visibility_space6(knows_practices):
361
+ is_yes = knows_practices == "Oui"
362
+ outputs = {
363
+ space7_1: gr.update(visible=is_yes),
364
+ space7_2: gr.update(visible=is_yes),
365
+ space7_3: gr.update(visible=is_yes),
366
+ }
367
+ # If not "Oui", hide both the practice description and its domains
368
+ if not is_yes:
369
+ outputs[space_domain_1] = gr.update(visible=False)
370
+ outputs[space_domain_2] = gr.update(visible=False)
371
+ outputs[space_domain_3] = gr.update(visible=False)
372
+ # If "Oui", the practice description (space7_X) is visible, but the domains (space_domain_X)
373
+ # must be hidden by default until a practice is actually typed in (handled by space7_X.change)
374
+ else:
375
+ outputs[space_domain_1] = gr.update(visible=False)
376
+ outputs[space_domain_2] = gr.update(visible=False)
377
+ outputs[space_domain_3] = gr.update(visible=False)
378
+
379
+ return outputs
380
+
381
+ def update_domain_visibility(practice_text):
382
+ is_cited = bool(practice_text.strip())
383
+ # Note: CheckboxGroup (or Radio) is now visible if the corresponding practice text is present
384
+ return gr.update(visible=is_cited)
385
+
386
+ def update_visibility_engage2_autre(engage2_choices: List[str]):
387
+ return gr.update(visible="autre (préciser)" in engage2_choices)
388
+
389
+ # NOUVELLE FONCTION de visibilité pour ENGAGE3
390
+ def update_visibility_engage3_autre(engage3_choices: List[str]):
391
+ return gr.update(visible="autre / précisez" in engage3_choices)
392
+
393
+ def process_survey(*data_values: Any) -> Tuple[str, gr.File]:
394
+ """
395
+ Processes survey data, creates a temporary CSV file, and returns the path
396
+ and a success message for download.
397
+ """
398
+
399
+ all_inputs_list = list(data_values)
400
+
401
+ if len(all_inputs_list) != EXPECTED_COUNT:
402
+ 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."
403
+ return error_msg, gr.update(visible=False, value=None)
404
+
405
+ language = "FR"
406
+
407
+ try:
408
+ column_names = get_column_names(DATA_INPUT_COMPONENTS)
409
+ column_names.insert(0, "DEMO_LANGUAGE")
410
+
411
+ data_row = [language] + all_inputs_list
412
+
413
+ df = pd.DataFrame([data_row], columns=column_names)
414
+
415
+ # Nommage du fichier: Enquêteur ID + Date
416
+ enqueteur_id_val = all_inputs_list[0]
417
+ current_date_str = datetime.now().strftime("%Y-%m-%d")
418
+
419
+ safe_enqueteur_id = str(enqueteur_id_val).strip()
420
+ safe_enqueteur_id = "".join(c if c.isalnum() else '_' for c in safe_enqueteur_id)
421
+ if not safe_enqueteur_id:
422
+ safe_enqueteur_id = "NO_ID"
423
+
424
+ temp_filename = f"/tmp/{safe_enqueteur_id}_{current_date_str}.csv"
425
+ os.makedirs(os.path.dirname(temp_filename), exist_ok=True)
426
+ df.to_csv(temp_filename, index=False, encoding='utf-8')
427
+
428
+ 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`."
429
+
430
+ return success_msg, gr.update(value=temp_filename, visible=True)
431
+
432
+ except Exception as e:
433
+ error_msg = f"Erreur lors de la génération du CSV: {str(e)}"
434
+ return error_msg, gr.update(visible=False, value=None)
435
+
436
 
437
  # =================================================================
438
+ # DATA COMPONENT FINAL ASSEMBLY
 
439
  # =================================================================
440
  DATA_INPUT_COMPONENTS = [
441
  # Global (1)
442
+ enqueteur_id,
443
+ # TAB 1 - Contact & Lieux (22)
444
+ approach_answer, refusal_reason, refusal_reason_other, contact_later,
 
445
  space1, space2a_1, space2a_2, space2a_3, space2b,
446
+ space3_1, space3_2, space3_3,
447
  space4, space5,
448
+ space6,
449
+ space7_1, space_domain_1,
450
  space7_2, space_domain_2,
451
  space7_3, space_domain_3,
452
+ space11,
453
+ # TAB 2 - Engagement (7)
454
+ engage1_role, engage2_type, engage2_autre_detail,
455
+ engagement_domaine, engage3_autre_detail,
456
+ info_activites, info_reseaux_sociaux,
457
+ # TAB 3 - Actualité (16)
458
+ info_frequence_actu, info2, info3, info_opposite_feeling, info_contradict_opinion,
459
+ info4, info5,
460
+ info6_logement, info6_politique, info6_etudes, info6_climat, info6_sociales,
461
+ info6_sentimentale, info6_securite, info6_estime, info6_liberte,
 
 
 
 
 
 
462
  # TAB 4 - Cultural & Digital (30)
463
+ prat_cult1,
464
+ prat_cult_lecture, prat_cult_cinema, prat_cult_musique, prat_cult_theatre, prat_cult_expositions,
 
465
  prat_cult_sport, prat_cult_voyages, prat_cult_jeux_video, prat_cult_bricolage, prat_cult_reseaux,
466
  prat_nature,
467
+ plat_instagram, plat_tiktok, plat_youtube, plat_twitter, plat_facebook,
 
468
  plat_snapchat, plat_twitch, plat_reddit, plat_linkedin, plat_autre,
 
469
  purpose_autre_detail,
470
+ purpose_actu, purpose_loisirs, purpose_education, purpose_engagement,
471
+ purpose_social, purpose_professionnel, purpose_autre_freq,
472
+ # TAB 5 - Demographics (12)
 
473
  demo_gender, demo_age, demo_location_commune, demo_location_arrond, demo_parents_location,
474
+ demo_inscription, demo_discipline, demo_job, demo_income,
 
 
475
  demo_socialcapital1_parent1, demo_socialcapital1_parent2, demo_socialcapital2,
 
476
  # TAB 6 - Open Questions (3)
477
  open_non_institutionnel, open_alternatives, open_motivations,
478
  ]
479
 
 
 
 
480
  if len(DATA_INPUT_COMPONENTS) != EXPECTED_DATA_COUNT:
 
 
 
 
 
 
 
 
 
 
 
481
  raise RuntimeError(f"Internal component count mismatch. Expected {EXPECTED_DATA_COUNT}, got {len(DATA_INPUT_COMPONENTS)}. Please verify the DATA_INPUT_COMPONENTS list definition.")
482
 
483
  SUBMIT_INPUT_COMPONENTS = DATA_INPUT_COMPONENTS
484
 
485
+
486
+ # =================================================================
487
+ # GRADIO UI SETUP (Utilisation des Accordions pour le F2F)
488
+ # =================================================================
489
+
490
+ with gr.Blocks(title=LANG["TITLE"], css=".gradio-container { max-width: 1200px; }") as demo:
491
+
492
+ gr.Markdown(f"## {LANG['TITLE']}")
493
+ gr.Markdown(LANG["INTRO_TEXT"])
494
+
495
+ with gr.Row():
496
+ enqueteur_id.render()
497
+
498
+ # --- Bloc Accordions ---
499
+
500
+ # =================================================================
501
+ # ACCORDION 1: Contact & Lieux Fréquentés (Ouvert par défaut)
502
+ # =================================================================
503
+ with gr.Accordion(LANG["TAB_1_TITLE"], open=True):
504
+ gr.Markdown("### Phase d'approche")
505
+ with gr.Row():
506
+ approach_answer.render()
507
+ with gr.Column() as refusal_block:
508
+ refusal_reason.render()
509
+ refusal_reason_other.render()
510
+ contact_later.render()
511
+
512
+ approach_answer.change(
513
+ update_visibility_approach,
514
+ inputs=[approach_answer],
515
+ outputs=[refusal_reason, contact_later]
516
+ )
517
+ refusal_reason.change(update_visibility_refusal, inputs=[refusal_reason], outputs=[refusal_reason_other])
518
+
519
+ gr.Markdown("### Lieux et Communautés")
520
+ space1.render()
521
+ gr.Markdown(f"#### {LANG['SPACE2A_TITLE']}")
522
+ with gr.Row() as space2_row:
523
+ space2a_1.render()
524
+ space2a_2.render()
525
+ space2a_3.render()
526
+
527
+ gr.Markdown(f"#### {LANG['SPACE3_TITLE']}")
528
+ with gr.Row() as space3_row:
529
+ space3_1.render()
530
+ space3_2.render()
531
+ space3_3.render()
532
+
533
+ space2b.render()
534
+ space1.change(update_visibility_space1, inputs=[space1], outputs=[space2a_1, space2a_2, space2a_3, space3_1, space3_2, space3_3, space2b])
535
+
536
+ gr.Markdown("### Usages alternatifs et Engagement")
537
+ space4.render()
538
+ space5.render()
539
+ space4.change(update_visibility_space4, inputs=[space4], outputs=[space5])
540
+
541
+ space6.render()
542
+ gr.Markdown(f"#### {LANG['SPACE7_TITLE']} (3 max.) et leurs Domaines (Choix multiples)")
543
+
544
+ # Pratique 1 et son Domaine (Mis côte-à-côte)
545
+ with gr.Row():
546
+ with gr.Column(scale=1): # Moins de place pour le champ de texte
547
+ space7_1.render()
548
+ with gr.Column(scale=2): # Plus de place pour les cases à cocher
549
+ space_domain_1.render()
550
+
551
+ # Pratique 2 et son Domaine (Mis côte-à-côte)
552
+ with gr.Row():
553
+ with gr.Column(scale=1):
554
+ space7_2.render()
555
+ with gr.Column(scale=2):
556
+ space_domain_2.render()
557
+
558
+ # Pratique 3 et son Domaine (Mis côte-à-côte)
559
+ with gr.Row():
560
+ with gr.Column(scale=1):
561
+ space7_3.render()
562
+ with gr.Column(scale=2):
563
+ space_domain_3.render()
564
+
565
+ gr.Markdown(f"#### {LANG['SPACE11_LABEL']}")
566
+ space11.render()
567
+
568
+ # Logique de visibilité pour SPACE7 et les domaines
569
+ space6.change(
570
+ fn=update_visibility_space6,
571
+ inputs=[space6],
572
+ outputs=[space7_1, space7_2, space7_3, space_domain_1, space_domain_2, space_domain_3]
573
+ )
574
+
575
+ # Les domaines ne sont visibles que si une pratique est citée
576
+ space7_1.change(fn=update_domain_visibility, inputs=[space7_1], outputs=[space_domain_1])
577
+ space7_2.change(fn=update_domain_visibility, inputs=[space7_2], outputs=[space_domain_2])
578
+ space7_3.change(fn=update_domain_visibility, inputs=[space7_3], outputs=[space_domain_3])
579
+
580
+
581
+ # =================================================================
582
+ # ACCORDION 2: Engagement Citoyen
583
+ # =================================================================
584
+ with gr.Accordion(LANG["TAB_2_TITLE"], open=False):
585
+ engage1_role.render()
586
+ engage2_type.render()
587
+ engage2_autre_detail.render()
588
+
589
+ # ENGAGE3 - Domaine de l'engagement (CheckboxGroup)
590
+ # Utilisation de gr.Box pour mettre en évidence ce bloc de questions.
591
+ with gr.Box(label="Domaine(s) de l'engagement"):
592
+ engagement_domaine.render()
593
+ engage3_autre_detail.render() # Champ de précision pour 'autre'
594
+
595
+ info_activites.render()
596
+ info_reseaux_sociaux.render()
597
+
598
+ engage2_type.change(
599
+ fn=update_visibility_engage2_autre,
600
+ inputs=[engage2_type],
601
+ outputs=[engage2_autre_detail]
602
+ )
603
+
604
+ # LOGIQUE pour ENGAGE3 'autre'
605
+ engagement_domaine.change(
606
+ fn=update_visibility_engage3_autre,
607
+ inputs=[engagement_domaine],
608
+ outputs=[engage3_autre_detail]
609
+ )
610
+
611
+
612
+ # =================================================================
613
+ # ACCORDION 3: Consommation d’actualités
614
+ # =================================================================
615
+ with gr.Accordion(LANG["TAB_3_TITLE"], open=False):
616
+ info_frequence_actu.render()
617
+ info2.render()
618
+ info3.render()
619
+ info_opposite_feeling.render()
620
+ info_contradict_opinion.render()
621
+ gr.Markdown("---")
622
+ info4.render()
623
+ info5.render()
624
+
625
+ gr.Markdown(f"### {LANG['INFO6_TITLE']}")
626
+ with gr.Column():
627
+ info6_logement.render()
628
+ info6_politique.render()
629
+ info6_etudes.render()
630
+ info6_climat.render()
631
+ info6_sociales.render()
632
+ info6_sentimentale.render()
633
+ info6_securite.render()
634
+ info6_estime.render()
635
+ info6_liberte.render()
636
+
637
+ # =================================================================
638
+ # ACCORDION 4: Pratiques culturelles et usages numériques
639
+ # =================================================================
640
+ with gr.Accordion(LANG["TAB_4_TITLE"], open=False):
641
+ prat_cult1.render()
642
+ gr.Markdown(f"### {LANG['PRAT_CULT_PRACTICES_TITLE']}")
643
+ with gr.Row():
644
+ with gr.Column():
645
+ prat_cult_lecture.render()
646
+ prat_cult_cinema.render()
647
+ prat_cult_musique.render()
648
+ prat_cult_theatre.render()
649
+ prat_cult_expositions.render()
650
+ with gr.Column():
651
+ prat_cult_sport.render()
652
+ prat_cult_voyages.render()
653
+ prat_cult_jeux_video.render()
654
+ prat_cult_bricolage.render()
655
+ prat_cult_reseaux.render()
656
+ prat_nature.render()
657
+ gr.Markdown(f"### {LANG['PLATFORM_TITLE']}")
658
+ with gr.Row():
659
+ with gr.Column():
660
+ plat_instagram.render()
661
+ plat_tiktok.render()
662
+ plat_youtube.render()
663
+ plat_twitter.render()
664
+ plat_facebook.render()
665
+ with gr.Column():
666
+ plat_snapchat.render()
667
+ plat_twitch.render()
668
+ plat_reddit.render()
669
+ plat_linkedin.render()
670
+ plat_autre.render()
671
+ gr.Markdown(f"### {LANG['PURPOSE_TITLE']}")
672
+ with gr.Row():
673
+ with gr.Column():
674
+ purpose_actu.render()
675
+ purpose_loisirs.render()
676
+ purpose_education.render()
677
+ purpose_engagement.render()
678
+ with gr.Column():
679
+ purpose_social.render()
680
+ purpose_professionnel.render()
681
+ purpose_autre_freq.render()
682
+ purpose_autre_detail.render()
683
+
684
+
685
+ # =================================================================
686
+ # ACCORDION 5: Profil Démographique
687
+ # =================================================================
688
+ with gr.Accordion(LANG["TAB_5_TITLE"], open=False):
689
+ demo_gender.render()
690
+ demo_age.render()
691
+ with gr.Row():
692
+ demo_location_commune.render()
693
+ demo_location_arrond.render()
694
+ demo_parents_location.render()
695
+ with gr.Row():
696
+ demo_inscription.render()
697
+ demo_discipline.render()
698
+ with gr.Row():
699
+ demo_job.render()
700
+ demo_income.render()
701
+ gr.Markdown("### Capital Social")
702
+ with gr.Row():
703
+ demo_socialcapital1_parent1.render()
704
+ demo_socialcapital1_parent2.render()
705
+ demo_socialcapital2.render()
706
+
707
+ # =================================================================
708
+ # ACCORDION 6: Questions ouvertes
709
+ # =================================================================
710
+ with gr.Accordion(LANG["TAB_6_TITLE"], open=False):
711
+ open_non_institutionnel.render()
712
+ open_alternatives.render()
713
+ open_motivations.render()
714
+
715
+ # =================================================================
716
+ # Soumission et Téléchargement
717
+ # =================================================================
718
+ with gr.Accordion(LANG["TAB_7_TITLE"], open=False):
719
+ gr.Markdown("### Finalisation du Questionnaire")
720
+ 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.")
721
+
722
+ with gr.Row():
723
+ submit_btn.render()
724
+ reset_btn.render()
725
+
726
+ output_status.render()
727
+ output_message.render()
728
+
729
+
730
+ # Submit action
731
+ submit_btn.click(
732
+ fn=process_survey,
733
+ inputs=SUBMIT_INPUT_COMPONENTS,
734
+ outputs=[output_status, output_message]
735
+ )
736
+
737
+ # Reset action
738
+ reset_btn.click(
739
+ fn=lambda: ([None] * EXPECTED_DATA_COUNT) + ["---", gr.update(value=None, visible=False)],
740
+ inputs=[],
741
+ outputs=DATA_INPUT_COMPONENTS + [output_status, output_message],
742
+ js="() => { document.getElementById('output_message').innerHTML = '---'; }"
743
+ )
744
 
745
  if __name__ == "__main__":
746
+ import sys
747
+ sys.modules[__name__].EXPECTED_DATA_COUNT = len(DATA_INPUT_COMPONENTS)
748
+ sys.modules[__name__].EXPECTED_COUNT = len(DATA_INPUT_COMPONENTS)
749
+
750
+ demo.launch()