Extrapolation améliorée + possibilité de voir le point voulu
[perso/Denise/oms.git] / gestion_donnees.py
CommitLineData
5679dfd0
DL
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
d03279e7 4from configuration import CONFIG,DEFAUT
a680b2f7
DL
5from gestion_erreurs import debug, warning, erreur, initialise_erreurs
6from gestion_couleurs import rgb_vers_tuple, tuple_vers_rgb
7from gestion_unites import choix_unite
5679dfd0 8import datetime
be2bf515
DL
9import json
10import unidecode
915e90bb 11import copy
5679dfd0 12
61020126
DL
13### Les données "tournent" selon :
14### python -> json -> (export/import) -> formulaire HTML -> données POST -> python etc
5679dfd0
DL
15
16############ Fonctions de conversion
17
18def convertit_jours_vers_python(chaine,liste_err):
19 """ convertit une chaine de type 1a 3m 1s 10j en jours
be2bf515 20 Renvoie un nombre de jours en float
5679dfd0
DL
21 Si un des caractères n'est ni un nombre, ni une lettre "autorisée" ni une espace,
22 on affiche un warning et on ignore ce caractère
23 """
b5ac625b 24# debug("conversion de "+chaine+" vers un nb de jours",liste_err)
5679dfd0
DL
25 chainenombre = ""
26 agejours = 0.
27 for lettre in chaine:
28 if lettre.isdigit():
29 chainenombre += lettre
30 else:
31 if lettre == 'a' or lettre == 'A':
32 # On a trouvé l'année, on ajoute tout ce qui est trouvé jusque là
d03279e7 33 agejours += int(chainenombre)*CONFIG["jours_dans_annee"]
5679dfd0
DL
34 chainenombre = ""
35 elif lettre == 'm' or lettre == 'M':
36 # On a trouvé le mois
d03279e7 37 agejours += int(chainenombre)*CONFIG["jours_dans_mois"]
5679dfd0
DL
38 chainenombre = ""
39 elif lettre == 's' or lettre == 'S':
40 # la semaine
d03279e7 41 agejours += int(chainenombre)*CONFIG["jours_dans_semaine"]
5679dfd0
DL
42 chainenombre = ""
43 elif lettre == 'j' or lettre == 'J':
44 # On a trouvé le jour
45 agejours += int(chainenombre)
46 chainenombre = ""
47 elif lettre != ' ':
48 # autre caractère : bizarre ?
49 warning("convertit_jour_vers_python : caractère invalide : "+lettre,liste_err)
50 # à la fin s'il reste qqch on le garde dans les jours
51 if chainenombre != "":
be2bf515 52 agejours += int(chainenombre)
5679dfd0
DL
53 if agejours<0:
54 warning("L'âge est négatif !",liste_err)
55 agejours = 0
b5ac625b 56# debug("On a convertit ! Résultat : "+str(agejours),liste_err)
be2bf515 57 return agejours
5679dfd0 58
8b5845ff 59# python -> json
5679dfd0
DL
60def convertit_age_vers_texte(nombre):
61 """ convertit un nombre de jours en un truc plus lisible en mois, années, jours
62 et renvoie une chaîne sous la forme 3a2m1j par exemple"""
d03279e7
DL
63 annees = int(nombre / CONFIG["jours_dans_annee"])
64 restant = nombre - annees*CONFIG["jours_dans_annee"]
65 mois = int(restant/CONFIG["jours_dans_mois"])
66 jours= round(nombre - mois*CONFIG["jours_dans_mois"] - annees*CONFIG["jours_dans_annee"])
5679dfd0
DL
67
68 chaine = ""
69 if annees >0:
70 chaine += str(annees)+"a"
71 if mois >0:
72 chaine += str(mois)+"m"
73 if jours>0 or nombre ==0: # si c'est la naissance, faut beien écrire 0j quand même
74 chaine += str(jours)+"j"
75 return chaine
a680b2f7 76
5679dfd0 77##########################
be2bf515 78
8b5845ff
DL
79# fonction qui calcule "auto" le maxi du graphique en fonction du max
80def calcule_max_graphique(l_jours):
81 """ calcule l'age maxi sur le graphique"""
82 if l_jours == []:
83 return CONFIG["jours_defaut_donneesvides"]
84 else:
85 jour_maxi = max(l_jours)# pas la peine d'aller très au delà du jour max
c2fe511b 86 jour_maxi = int(jour_maxi* 1.2)+3 # on rajoute un peu
8b5845ff
DL
87 return jour_maxi
88
be2bf515
DL
89
90def simplifie_nom(chaine):
91 """ simplifie le nom chaine afin d'en faire une extension
92 pour le nom du fichier. Met tout en minuscules et vire les caractères spéciaux
93 et max 15 caractères"""
94 chaine2 = ""
95 for l in chaine:
96 if l.isalpha():
97 chaine2+=l
98 chaine2 = unidecode.unidecode(chaine2)
99 return chaine2[:15]
5679dfd0 100
8b5845ff
DL
101def convertit_donnee_vers_python(chaine,typedonnee,liste_err):
102 """ convertit une chaine vers un float qui est le type donnee voulu.
103 La virgule peut être . ou , et on vire d'éventuels espaces.
104 Taille invalide : on renvoie 0 avec un warning."""
5679dfd0
DL
105 chaine2 = chaine.replace(",",".")
106 chaine2 = chaine2.replace(" ","")
107
108 try:
8b5845ff 109 donnee = float(chaine2)
5679dfd0 110 except:
8b5845ff
DL
111 warning(typedonnee+" impossible à lire : "+chaine,liste_err)
112 donnee = 0
d9eaf2e2
DL
113
114 # Pour le poids, un cas particulier
115 if typedonnee == "poids" and donnee > CONFIG["poids_maxi_conversion"]:
116 donnee = donnee/1000 # conversion en grammes
8b5845ff 117 if not( 0<=donnee<CONFIG[typedonnee+"_maxi"]):
d9eaf2e2 118 warning(typedonnee+" incohérent(e) : "+str(donnee),liste_err)
8b5845ff
DL
119 donnee = 0
120 return donnee
5679dfd0 121
5679dfd0
DL
122
123#########################
124
8b5845ff 125# web -> python
5679dfd0
DL
126def convertit_date_vers_python(chaine,liste_err):
127 """ prend une chaine comme renvoyée par un champ de formulaire date
128 aaaa-mm-jj et en fait une date python
129 renvoie "" si ne marche pas"""
130 liste = chaine.split("-")
131 if len(liste) != 3:
132 warning("La date : "+chaine+" est invalide !",liste_err)
133 return ""
134 else:
30158504 135 debug("Conversion de la date "+chaine+". Découpage : "+str(liste),liste_err)
9cb3c31c
DL
136 try:
137 date = datetime.date(int(liste[0]),int(liste[1]),int(liste[2]))
138 except:
139 date = ""
30158504 140 warning("Impossible de lire la date "+chaine+". Format accepté : aaaa-mm-jj",liste_err)
9cb3c31c 141 return date
8b5845ff
DL
142
143# python -> json
5679dfd0
DL
144def convertit_date_vers_texte(date):
145 """ convertit une date python en format texte aaaa-mm-jj"""
146 if date == "":
147 return ""
148 else:
149 return (str(date.year)+"-"+str(date.month)+"-"+str(date.day))
150
151
152def delta_date(date1,datenaissance):
153 """ renvoie le nombre de jours (entier) entre date1 et datenaissance format "datetime"
154 datenaissance est supposée antérieure. Erreur sinon."""
155 d = date1 - datenaissance
156 jours = d.days
157 if jours<0:
a680b2f7 158 warning("La différence entre les dates est négative... :/")
5679dfd0
DL
159 return -1
160 return jours
161
162
8b5845ff
DL
163################### On regroupe tout ce qui gère les données en une fonction
164
165def web_vers_python(data,liste_err):
166 """ prend en argument le dictionnaire de requête et renvoie la config, et les
167 tableaux de donnée"""
168
169 # Régler la configuration
170 config = gere_configuration(data,liste_err)
171
172 # récupérer les données
173 listes_jours = {}
174 listes_donnees = {}
175 for typed in CONFIG["liste_typedonnees"]:
176 listes_jours[typed],listes_donnees[typed] = gere_donnees(data,config["naissance"],typed,liste_err)
177
178 # Si on a choisi la même échelle de données
179 if config["memechelle"] == "oui":
180 config["non_sauve"]["maxi"] = calcule_max_graphique([j for lj in listes_jours.values() for j in lj])
181 config["non_sauve"]["unite"] = choix_unite(config["non_sauve"]["maxi"])
182
183 return (config,listes_jours,listes_donnees)
184
185
186
a680b2f7 187########### Fonctions qui gèretn les données web vers python
5679dfd0 188
8b5845ff
DL
189def gere_checkbox(chaine):
190 """ prend en arg une chaine, et renvoie "oui" si c'est "on" (sortie de la checkbox)
191 et chaîne vide si n'importe quoi d'autre"""
192 if chaine == "on":
193 return "oui"
194 else:
195 return ""
5679dfd0
DL
196
197def gere_configuration(data,liste_err):
be2bf515 198 """ prend en argument le dictionnaire de requête (configuration imparfaite), et
5679dfd0
DL
199 construit le dictionnaire de configuration qui va bien.
200 Vérifie que chaque entrée est cohérente évidemment."""
a680b2f7 201 # Initialisation
685a5f75 202 configuration = {"non_sauve": {}}
5679dfd0
DL
203
204 # Pour le nom, osef qu'il soit vide
205 nom = data.get("nom","")
206 # Par contre s'il est trop long on le tronque
d03279e7 207 configuration["nom"] = nom[:CONFIG["longueur_max_nom_bebe"]]
5679dfd0
DL
208
209 sexe = data.get("sexe","")
a46e1269 210 if not (sexe in ["F","M","N"]):
5679dfd0 211 warning("Le sexe de l'enfant est invalide ! "+sexe,liste_err)
a46e1269 212 sexe = "N"
5679dfd0
DL
213 configuration["sexe"] = sexe
214
215 naissance = data.get("naissance","")
216 if naissance !="":
217 naissance = convertit_date_vers_python(naissance,liste_err)
218 configuration["naissance"] = naissance
219
b5ac625b
DL
220 prematurite = data.get("prematurite","")
221 j = convertit_jours_vers_python(prematurite,liste_err)
222 configuration["prematurite"] = convertit_age_vers_texte(j)
223
224 configuration["agecorrige"] = gere_checkbox(data.get("agecorrige",""))
225
5679dfd0
DL
226 # Type de courbe. Au pire on met P
227 tyc = data.get("typecourbe","")
228 if not (tyc in ["P","Z"]):
229 tyc = "P"
230 configuration["typecourbe"] = tyc
231
232 # unité
233 unite = data.get("unite","")
9e4c51c7 234 if not (unite in CONFIG["liste_unites"]):
5679dfd0
DL
235 unite = ""
236 #warning("L'unité "+unite+" n'est pas reconnue !",liste_err)
237 configuration["unite"] = unite
9e4c51c7 238
5679dfd0 239 # grille
8b5845ff
DL
240 configuration["grille"] = gere_checkbox(data.get("grille",""))
241
242 # tracer ou non les courbes vides
243 configuration["tracevide"] = gere_checkbox(data.get("tracevide",""))
5679dfd0 244
8b5845ff
DL
245 # Même échelle sur tous les graphiques
246 configuration["memechelle"] = gere_checkbox(data.get("memechelle",""))
c2fe511b 247
8b5845ff 248
5679dfd0
DL
249 # maxi. 0 signifie qu'on veut pas de maxi
250 maxi = data.get("maxi","")
251 if maxi == "":
252 configuration["maxi"] = 0
253 else:
54c71831 254 configuration["maxi"] = int(convertit_jours_vers_python(maxi,liste_err))
5679dfd0
DL
255
256 # dimensions du graphique
be2bf515 257 largeur = data.get("largeur","")
5679dfd0 258 if largeur == "":
915e90bb 259 largeur = DEFAUT["largeur_graphique"]
5679dfd0
DL
260 else:
261 try:
262 largeur = int(largeur)
263 except:
264 warning("La largeur "+largeur+"est invalide !",liste_err)
915e90bb 265 largeur = DEFAUT["largeur_graphique"]
d03279e7
DL
266 if largeur > CONFIG["largeur_graphique_max"]:
267 largeur = CONFIG["largeur_graphique_max"]
268 warning("Largeur du graphique trop grande !",liste_err)
269 elif largeur < CONFIG["largeur_graphique_min"]:
270 largeur = CONFIG["largeur_graphique_min"]
271 warning("Largeur du graphique trop petite !",liste_err)
5679dfd0
DL
272 configuration["largeur"] = largeur
273
be2bf515 274 hauteur = data.get("hauteur","")
5679dfd0 275 if hauteur == "":
915e90bb 276 hauteur = DEFAUT["hauteur_graphique"]
5679dfd0
DL
277 else:
278 try:
279 hauteur = int(hauteur)
280 except:
281 warning("La hauteur "+hauteur+"est invalide !",liste_err)
915e90bb 282 hauteur = DEFAUT["hauteur_graphique"]
d03279e7
DL
283 if hauteur > CONFIG["hauteur_graphique_max"]:
284 hauteur = CONFIG["hauteur_graphique_max"]
285 warning("Hauteur du graphique trop grande !",liste_err)
286 elif hauteur < CONFIG["hauteur_graphique_min"]:
287 hauteur = CONFIG["hauteur_graphique_min"]
288 warning("Hauteur du graphique trop petite !",liste_err)
5679dfd0
DL
289 configuration["hauteur"] = hauteur
290
291 # existence et position de la légende
8b5845ff 292 configuration["legende"] = gere_checkbox(data.get("legende",""))
5679dfd0
DL
293
294 positionlegende = data.get("positionlegende","")
295 if not(positionlegende in ['upper left','upper right','lower left','lower right']):
296 positionlegende = "upper left"
297 configuration["positionlegende"] = positionlegende
fd69b6b5 298
915e90bb
DL
299 configuration["couleurs"] = {}
300 # gérer les couleurs
915e90bb
DL
301 for clecouleur in DEFAUT["couleurs"]:
302 coul = rgb_vers_tuple(data.get("couleur_"+clecouleur,""),CONFIG["couleurs"][clecouleur],liste_err)
303 configuration["couleurs"][clecouleur] = coul
685a5f75 304
cf0d4c8c
DL
305
306 configuration["non_sauve"]["grilleamelio"] = gere_checkbox(data.get("grilleamelio",""))
307
308
309 #### La partie extrapolation n'a pas besoin d'être sauvée
310 configuration["non_sauve"]["prolongercourbes"] = gere_checkbox(data.get("prolongercourbes",""))
311
312 # Valeur par défaut : 1
313 debug(data.get("nbextradata", "aaargh"), liste_err)
314 nbextradata = data.get("nbextradata",1)
315 try:
316 nbextradata = int(nbextradata)
317 except:
318 warning("Le nombre de données sur lequel on extrapole est invalide : "+nbextradata, liste_err)
319 nbextradata = 1
320 configuration["non_sauve"]["nbextradata"] = nbextradata
321
322 if data.get("calculextradata_type","") in CONFIG["liste_typedonnees"]:
323 configuration["non_sauve"]["calculextradata_type"] = data.get("calculextradata_type","")
324 configuration["non_sauve"]["calculextradata_age"] = convertit_jours_vers_python(data.get("calculextradata_age","0j"),liste_err)
325 else:
326 configuration["non_sauve"]["calculextradata_type"] = ""
327 # On ne met rien dans l'âge, pas la peine
328
329 ctyped = data.get("calculextratemps_type","")
330 if ctyped in CONFIG["liste_typedonnees"]:
331 configuration["non_sauve"]["calculextratemps_type"] = ctyped
332 configuration["non_sauve"]["calculextratemps_val"] = convertit_donnee_vers_python(data.get("calculextratemps_val",""), ctyped, liste_err)
333 else:
334 configuration["non_sauve"]["calculextratemps_type"] = ""
fd69b6b5 335
3d7da80a
DL
336 # Tracer les calculs sur la grille
337 configuration["non_sauve"]["calculextradata_trace"] = gere_checkbox(data.get("calculextradata_trace"))
338 configuration["non_sauve"]["calculextratemps_trace"] = gere_checkbox(data.get("calculextratemps_trace"))
339
340
5679dfd0 341 return configuration
5679dfd0 342
cf0d4c8c
DL
343
344
a680b2f7 345## web vers python : données
8b5845ff
DL
346def gere_donnees(data,naissance,typedonnee,liste_err):
347 """ prend en argument le dictionnaire de requête, et la date de
348 naissance (éventuellement vide), et construit deux listes :
349 l_jours et l_data correspondantes.
350 Il faut donner en argument le type de données : voir
351 CONFIG["liste_typedonnees"]"""
352 if typedonnee not in CONFIG["liste_typedonnees"]:
353 warning("gere_donnees : le type de données : "+typedonnee+" est invalide !! Types acceptés : "+str(CONFIG["liste_typedonnees"]),liste_err)
354 return ([],[])
355
356 # On construit une liste de couples d'abord
5679dfd0
DL
357 liste_donnees = []
358
359 i = 0
8b5845ff
DL
360 # On va chercher si y'a des données à donnee_i
361 while typedonnee+"_"+str(i) in data.keys():
362 if data[typedonnee+"_"+str(i)] != "":
363 donnee = convertit_donnee_vers_python(data[typedonnee+"_"+str(i)],typedonnee,liste_err)
5679dfd0
DL
364 age = data.get("age_"+str(i),"")
365 if age !="":
366 age = convertit_jours_vers_python(age,liste_err)
8b5845ff 367 liste_donnees.append((age,donnee))
5679dfd0
DL
368 else:
369 date = data.get("date_"+str(i),"")
370 datep = convertit_date_vers_python(date,liste_err)
371 # on vérifie la date
372 if naissance == "":
373 warning("La date de naissance n'a pas été précisée. Du coup on ne peut pas calculer l'âge de l'enfant le "+date,liste_err)
374 elif datep != "": # la date est valide et on a une date de naissance
375 age = delta_date(datep,naissance)
8b5845ff 376 liste_donnees.append((age,donnee))
5679dfd0
DL
377 i+=1
378
379 # Trier la liste
380 liste_donnees.sort(key=lambda x : x[0])
381
382 # splitter la liste
383 l_jours = [x[0] for x in liste_donnees]
8b5845ff 384 l_donnee = [x[1] for x in liste_donnees]
5679dfd0 385
8b5845ff 386 return (l_jours,l_donnee)
be2bf515
DL
387
388
a680b2f7 389# python vers Json
8b5845ff 390#### export vers json
be2bf515 391
8b5845ff 392def donnees_vers_json(l_jours,l_poids,l_jourst,l_taille,config):
be2bf515 393 """ retourne le json à renvoyer"""
915e90bb 394 gros_dico = copy.deepcopy(config)
be2bf515 395 l_jours2 = [convertit_age_vers_texte(d) for d in l_jours]
8b5845ff 396 l_jourst2 = [convertit_age_vers_texte(d) for d in l_jourst]
be2bf515
DL
397 gros_dico["data_j"] = l_jours2
398 gros_dico["data_p"] = l_poids
8b5845ff
DL
399 gros_dico["data_jours_taille"] = l_jourst2
400 gros_dico["data_taille"] = l_taille
be2bf515
DL
401 # gérer la date de naissance
402 if gros_dico.get("naissance","") != "":
403 gros_dico["naissance"] = convertit_date_vers_texte(gros_dico["naissance"])
404 # gérer l'age maxi
405 gros_dico["maxi"] = convertit_age_vers_texte(gros_dico["maxi"])
fd69b6b5 406 # gérer les couleurs
915e90bb
DL
407 for clecouleur in DEFAUT["couleurs"]:
408 gros_dico["couleurs"][clecouleur] = tuple_vers_rgb(gros_dico["couleurs"][clecouleur])
fd69b6b5 409
8b5845ff
DL
410 # Enlever ce qui ne se sauvegarde pas si y'a
411 if "non_sauve" in gros_dico:
412 del gros_dico["non_sauve"]
5679dfd0 413
be2bf515 414 return json.dumps(gros_dico, indent=2,ensure_ascii=False )
5679dfd0 415
8b5845ff
DL
416def fusionne_donnees(listes_jours,listes_donnees):
417 """ prend en argument deux dicos de listes. Chaque liste de jours est associée à une liste
418 de données (par la même clé de type de données). Il faut les fusionner pour avoir une liste de dictionnaires, de type
419 ("age":truc, "donnee1":truc, "donnee2":truc, ...) triée par ordre de jours. Si jamais une des données est vide,
420 le champ du dictionnaire n'est pas rempli"""
a680b2f7 421
8b5845ff
DL
422 def fini(lj):
423 """ teste si les listes sont toutes vides """
424 for l in lj.values():
425 if l!=[]:
426 return False
427 return True
428
429 def mini(lj):
430 """ renvoie la clé de la liste où il y a le min """
431 cle_mini = CONFIG["liste_typedonnees"][0]
432 for (cle,liste) in lj.items():
433 if lj[cle_mini]== []:
434 cle_mini = cle
435 elif lj[cle] != []:
b5ac625b 436 if convertit_jours_vers_python(lj[cle][0],initialise_erreurs())<convertit_jours_vers_python(lj[cle_mini][0],initialise_erreurs()):
8b5845ff
DL
437 cle_mini = cle
438 return cle_mini
439
440 liste_f = []
441 while not(fini(listes_jours)):
442 typedonnee = mini(listes_jours)
443 # On extrait les données dans les deux listes (jours et données)
444 jour = listes_jours[typedonnee].pop(0)
445 donnee = listes_donnees[typedonnee].pop(0)
446 if liste_f == [] or jour != liste_f[-1]["age"]: # Si le jour est un "nouveau" jour
447 liste_f.append({"age":jour})
448 # On met à jour l'élément
449 liste_f[-1][typedonnee] = donnee
450
451 return liste_f
452
5679dfd0 453
a680b2f7 454### COnversion json vers formulaire
8b5845ff 455# Json -> formulaire HTML
d03279e7 456def fichier_json_vers_configdonnees(chaine,liste_err):
a680b2f7
DL
457 """ prend le json importé (chaine) et l'exporte vers les valeurs du formulaire
458 Renvoyé sous forme de dictionnaire (mais adapté au formulaire web)"""
d03279e7
DL
459 debug("json vers config : Prêt à interpréter le json",liste_err)
460 try:
461 valform = json.loads(chaine)
462 except :
463 erreur("Impossible de lire le fichier json !",liste_err)
464 return {}
be2bf515
DL
465 # Il faut maintenant récupérer les l_jours et l_poids puis les remettre
466 # sous forme de age_i et poids_i
8b5845ff
DL
467
468 listes_jours = {}
469 listes_donnees = {}
470 for typed in CONFIG["liste_typedonnees"]:
471 if typed == "poids": # pour la rétrocompatibilité
472 listes_jours[typed] = valform.get("data_j",[])
473 listes_donnees[typed] = valform.get("data_p",[])
474 else:
475 listes_jours[typed] = valform.get("data_jours_"+typed,[])
476 listes_donnees[typed] = valform.get("data_"+typed,[])
477
478 debug("Avant fusion : listes jours "+str(listes_jours),liste_err)
479 liste_donnees = fusionne_donnees(listes_jours,listes_donnees)
480 debug("Fusion de listes ok. Liste moche : "+str(liste_donnees),liste_err)
481 for i in range(len(liste_donnees)):
482 for (cle,val) in liste_donnees[i].items():
483 valform[cle+"_"+str(i)] = val
be2bf515 484
8b5845ff 485 valform["nb_data"] = max(len(liste_donnees) +2,DEFAUT["nb_data"])
5679dfd0 486
be2bf515 487 return valform
5679dfd0 488
5679dfd0 489
be2bf515 490