extrapolation des courbes et calculs associés
[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
113 if not( 0<=donnee<CONFIG[typedonnee+"_maxi"]):
114 warning(typedonnee+"incohérent(e) : "+str(donnee),liste_err)
115 donnee = 0
116 return donnee
5679dfd0 117
5679dfd0
DL
118
119#########################
120
8b5845ff 121# web -> python
5679dfd0
DL
122def convertit_date_vers_python(chaine,liste_err):
123 """ prend une chaine comme renvoyée par un champ de formulaire date
124 aaaa-mm-jj et en fait une date python
125 renvoie "" si ne marche pas"""
126 liste = chaine.split("-")
127 if len(liste) != 3:
128 warning("La date : "+chaine+" est invalide !",liste_err)
129 return ""
130 else:
30158504 131 debug("Conversion de la date "+chaine+". Découpage : "+str(liste),liste_err)
9cb3c31c
DL
132 try:
133 date = datetime.date(int(liste[0]),int(liste[1]),int(liste[2]))
134 except:
135 date = ""
30158504 136 warning("Impossible de lire la date "+chaine+". Format accepté : aaaa-mm-jj",liste_err)
9cb3c31c 137 return date
8b5845ff
DL
138
139# python -> json
5679dfd0
DL
140def convertit_date_vers_texte(date):
141 """ convertit une date python en format texte aaaa-mm-jj"""
142 if date == "":
143 return ""
144 else:
145 return (str(date.year)+"-"+str(date.month)+"-"+str(date.day))
146
147
148def delta_date(date1,datenaissance):
149 """ renvoie le nombre de jours (entier) entre date1 et datenaissance format "datetime"
150 datenaissance est supposée antérieure. Erreur sinon."""
151 d = date1 - datenaissance
152 jours = d.days
153 if jours<0:
a680b2f7 154 warning("La différence entre les dates est négative... :/")
5679dfd0
DL
155 return -1
156 return jours
157
158
8b5845ff
DL
159################### On regroupe tout ce qui gère les données en une fonction
160
161def web_vers_python(data,liste_err):
162 """ prend en argument le dictionnaire de requête et renvoie la config, et les
163 tableaux de donnée"""
164
165 # Régler la configuration
166 config = gere_configuration(data,liste_err)
167
168 # récupérer les données
169 listes_jours = {}
170 listes_donnees = {}
171 for typed in CONFIG["liste_typedonnees"]:
172 listes_jours[typed],listes_donnees[typed] = gere_donnees(data,config["naissance"],typed,liste_err)
173
174 # Si on a choisi la même échelle de données
175 if config["memechelle"] == "oui":
176 config["non_sauve"]["maxi"] = calcule_max_graphique([j for lj in listes_jours.values() for j in lj])
177 config["non_sauve"]["unite"] = choix_unite(config["non_sauve"]["maxi"])
178
179 return (config,listes_jours,listes_donnees)
180
181
182
a680b2f7 183########### Fonctions qui gèretn les données web vers python
5679dfd0 184
8b5845ff
DL
185def gere_checkbox(chaine):
186 """ prend en arg une chaine, et renvoie "oui" si c'est "on" (sortie de la checkbox)
187 et chaîne vide si n'importe quoi d'autre"""
188 if chaine == "on":
189 return "oui"
190 else:
191 return ""
5679dfd0
DL
192
193def gere_configuration(data,liste_err):
be2bf515 194 """ prend en argument le dictionnaire de requête (configuration imparfaite), et
5679dfd0
DL
195 construit le dictionnaire de configuration qui va bien.
196 Vérifie que chaque entrée est cohérente évidemment."""
a680b2f7 197 # Initialisation
685a5f75 198 configuration = {"non_sauve": {}}
5679dfd0
DL
199
200 # Pour le nom, osef qu'il soit vide
201 nom = data.get("nom","")
202 # Par contre s'il est trop long on le tronque
d03279e7 203 configuration["nom"] = nom[:CONFIG["longueur_max_nom_bebe"]]
5679dfd0
DL
204
205 sexe = data.get("sexe","")
a46e1269 206 if not (sexe in ["F","M","N"]):
5679dfd0 207 warning("Le sexe de l'enfant est invalide ! "+sexe,liste_err)
a46e1269 208 sexe = "N"
5679dfd0
DL
209 configuration["sexe"] = sexe
210
211 naissance = data.get("naissance","")
212 if naissance !="":
213 naissance = convertit_date_vers_python(naissance,liste_err)
214 configuration["naissance"] = naissance
215
b5ac625b
DL
216 prematurite = data.get("prematurite","")
217 j = convertit_jours_vers_python(prematurite,liste_err)
218 configuration["prematurite"] = convertit_age_vers_texte(j)
219
220 configuration["agecorrige"] = gere_checkbox(data.get("agecorrige",""))
221
5679dfd0
DL
222 # Type de courbe. Au pire on met P
223 tyc = data.get("typecourbe","")
224 if not (tyc in ["P","Z"]):
225 tyc = "P"
226 configuration["typecourbe"] = tyc
227
228 # unité
229 unite = data.get("unite","")
9e4c51c7 230 if not (unite in CONFIG["liste_unites"]):
5679dfd0
DL
231 unite = ""
232 #warning("L'unité "+unite+" n'est pas reconnue !",liste_err)
233 configuration["unite"] = unite
9e4c51c7 234
5679dfd0 235 # grille
8b5845ff
DL
236 configuration["grille"] = gere_checkbox(data.get("grille",""))
237
238 # tracer ou non les courbes vides
239 configuration["tracevide"] = gere_checkbox(data.get("tracevide",""))
5679dfd0 240
8b5845ff
DL
241 # Même échelle sur tous les graphiques
242 configuration["memechelle"] = gere_checkbox(data.get("memechelle",""))
c2fe511b 243
8b5845ff 244
5679dfd0
DL
245 # maxi. 0 signifie qu'on veut pas de maxi
246 maxi = data.get("maxi","")
247 if maxi == "":
248 configuration["maxi"] = 0
249 else:
54c71831 250 configuration["maxi"] = int(convertit_jours_vers_python(maxi,liste_err))
5679dfd0
DL
251
252 # dimensions du graphique
be2bf515 253 largeur = data.get("largeur","")
5679dfd0 254 if largeur == "":
915e90bb 255 largeur = DEFAUT["largeur_graphique"]
5679dfd0
DL
256 else:
257 try:
258 largeur = int(largeur)
259 except:
260 warning("La largeur "+largeur+"est invalide !",liste_err)
915e90bb 261 largeur = DEFAUT["largeur_graphique"]
d03279e7
DL
262 if largeur > CONFIG["largeur_graphique_max"]:
263 largeur = CONFIG["largeur_graphique_max"]
264 warning("Largeur du graphique trop grande !",liste_err)
265 elif largeur < CONFIG["largeur_graphique_min"]:
266 largeur = CONFIG["largeur_graphique_min"]
267 warning("Largeur du graphique trop petite !",liste_err)
5679dfd0
DL
268 configuration["largeur"] = largeur
269
be2bf515 270 hauteur = data.get("hauteur","")
5679dfd0 271 if hauteur == "":
915e90bb 272 hauteur = DEFAUT["hauteur_graphique"]
5679dfd0
DL
273 else:
274 try:
275 hauteur = int(hauteur)
276 except:
277 warning("La hauteur "+hauteur+"est invalide !",liste_err)
915e90bb 278 hauteur = DEFAUT["hauteur_graphique"]
d03279e7
DL
279 if hauteur > CONFIG["hauteur_graphique_max"]:
280 hauteur = CONFIG["hauteur_graphique_max"]
281 warning("Hauteur du graphique trop grande !",liste_err)
282 elif hauteur < CONFIG["hauteur_graphique_min"]:
283 hauteur = CONFIG["hauteur_graphique_min"]
284 warning("Hauteur du graphique trop petite !",liste_err)
5679dfd0
DL
285 configuration["hauteur"] = hauteur
286
287 # existence et position de la légende
8b5845ff 288 configuration["legende"] = gere_checkbox(data.get("legende",""))
5679dfd0
DL
289
290 positionlegende = data.get("positionlegende","")
291 if not(positionlegende in ['upper left','upper right','lower left','lower right']):
292 positionlegende = "upper left"
293 configuration["positionlegende"] = positionlegende
fd69b6b5 294
915e90bb
DL
295 configuration["couleurs"] = {}
296 # gérer les couleurs
915e90bb
DL
297 for clecouleur in DEFAUT["couleurs"]:
298 coul = rgb_vers_tuple(data.get("couleur_"+clecouleur,""),CONFIG["couleurs"][clecouleur],liste_err)
299 configuration["couleurs"][clecouleur] = coul
685a5f75 300
cf0d4c8c
DL
301
302 configuration["non_sauve"]["grilleamelio"] = gere_checkbox(data.get("grilleamelio",""))
303
304
305 #### La partie extrapolation n'a pas besoin d'être sauvée
306 configuration["non_sauve"]["prolongercourbes"] = gere_checkbox(data.get("prolongercourbes",""))
307
308 # Valeur par défaut : 1
309 debug(data.get("nbextradata", "aaargh"), liste_err)
310 nbextradata = data.get("nbextradata",1)
311 try:
312 nbextradata = int(nbextradata)
313 except:
314 warning("Le nombre de données sur lequel on extrapole est invalide : "+nbextradata, liste_err)
315 nbextradata = 1
316 configuration["non_sauve"]["nbextradata"] = nbextradata
317
318 if data.get("calculextradata_type","") in CONFIG["liste_typedonnees"]:
319 configuration["non_sauve"]["calculextradata_type"] = data.get("calculextradata_type","")
320 configuration["non_sauve"]["calculextradata_age"] = convertit_jours_vers_python(data.get("calculextradata_age","0j"),liste_err)
321 else:
322 configuration["non_sauve"]["calculextradata_type"] = ""
323 # On ne met rien dans l'âge, pas la peine
324
325 ctyped = data.get("calculextratemps_type","")
326 if ctyped in CONFIG["liste_typedonnees"]:
327 configuration["non_sauve"]["calculextratemps_type"] = ctyped
328 configuration["non_sauve"]["calculextratemps_val"] = convertit_donnee_vers_python(data.get("calculextratemps_val",""), ctyped, liste_err)
329 else:
330 configuration["non_sauve"]["calculextratemps_type"] = ""
fd69b6b5 331
5679dfd0 332 return configuration
5679dfd0 333
cf0d4c8c
DL
334
335
a680b2f7 336## web vers python : données
8b5845ff
DL
337def gere_donnees(data,naissance,typedonnee,liste_err):
338 """ prend en argument le dictionnaire de requête, et la date de
339 naissance (éventuellement vide), et construit deux listes :
340 l_jours et l_data correspondantes.
341 Il faut donner en argument le type de données : voir
342 CONFIG["liste_typedonnees"]"""
343 if typedonnee not in CONFIG["liste_typedonnees"]:
344 warning("gere_donnees : le type de données : "+typedonnee+" est invalide !! Types acceptés : "+str(CONFIG["liste_typedonnees"]),liste_err)
345 return ([],[])
346
347 # On construit une liste de couples d'abord
5679dfd0
DL
348 liste_donnees = []
349
350 i = 0
8b5845ff
DL
351 # On va chercher si y'a des données à donnee_i
352 while typedonnee+"_"+str(i) in data.keys():
353 if data[typedonnee+"_"+str(i)] != "":
354 donnee = convertit_donnee_vers_python(data[typedonnee+"_"+str(i)],typedonnee,liste_err)
5679dfd0
DL
355 age = data.get("age_"+str(i),"")
356 if age !="":
357 age = convertit_jours_vers_python(age,liste_err)
8b5845ff 358 liste_donnees.append((age,donnee))
5679dfd0
DL
359 else:
360 date = data.get("date_"+str(i),"")
361 datep = convertit_date_vers_python(date,liste_err)
362 # on vérifie la date
363 if naissance == "":
364 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)
365 elif datep != "": # la date est valide et on a une date de naissance
366 age = delta_date(datep,naissance)
8b5845ff 367 liste_donnees.append((age,donnee))
5679dfd0
DL
368 i+=1
369
370 # Trier la liste
371 liste_donnees.sort(key=lambda x : x[0])
372
373 # splitter la liste
374 l_jours = [x[0] for x in liste_donnees]
8b5845ff 375 l_donnee = [x[1] for x in liste_donnees]
5679dfd0 376
8b5845ff 377 return (l_jours,l_donnee)
be2bf515
DL
378
379
a680b2f7 380# python vers Json
8b5845ff 381#### export vers json
be2bf515 382
8b5845ff 383def donnees_vers_json(l_jours,l_poids,l_jourst,l_taille,config):
be2bf515 384 """ retourne le json à renvoyer"""
915e90bb 385 gros_dico = copy.deepcopy(config)
be2bf515 386 l_jours2 = [convertit_age_vers_texte(d) for d in l_jours]
8b5845ff 387 l_jourst2 = [convertit_age_vers_texte(d) for d in l_jourst]
be2bf515
DL
388 gros_dico["data_j"] = l_jours2
389 gros_dico["data_p"] = l_poids
8b5845ff
DL
390 gros_dico["data_jours_taille"] = l_jourst2
391 gros_dico["data_taille"] = l_taille
be2bf515
DL
392 # gérer la date de naissance
393 if gros_dico.get("naissance","") != "":
394 gros_dico["naissance"] = convertit_date_vers_texte(gros_dico["naissance"])
395 # gérer l'age maxi
396 gros_dico["maxi"] = convertit_age_vers_texte(gros_dico["maxi"])
fd69b6b5 397 # gérer les couleurs
915e90bb
DL
398 for clecouleur in DEFAUT["couleurs"]:
399 gros_dico["couleurs"][clecouleur] = tuple_vers_rgb(gros_dico["couleurs"][clecouleur])
fd69b6b5 400
8b5845ff
DL
401 # Enlever ce qui ne se sauvegarde pas si y'a
402 if "non_sauve" in gros_dico:
403 del gros_dico["non_sauve"]
5679dfd0 404
be2bf515 405 return json.dumps(gros_dico, indent=2,ensure_ascii=False )
5679dfd0 406
8b5845ff
DL
407def fusionne_donnees(listes_jours,listes_donnees):
408 """ prend en argument deux dicos de listes. Chaque liste de jours est associée à une liste
409 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
410 ("age":truc, "donnee1":truc, "donnee2":truc, ...) triée par ordre de jours. Si jamais une des données est vide,
411 le champ du dictionnaire n'est pas rempli"""
a680b2f7 412
8b5845ff
DL
413 def fini(lj):
414 """ teste si les listes sont toutes vides """
415 for l in lj.values():
416 if l!=[]:
417 return False
418 return True
419
420 def mini(lj):
421 """ renvoie la clé de la liste où il y a le min """
422 cle_mini = CONFIG["liste_typedonnees"][0]
423 for (cle,liste) in lj.items():
424 if lj[cle_mini]== []:
425 cle_mini = cle
426 elif lj[cle] != []:
b5ac625b 427 if convertit_jours_vers_python(lj[cle][0],initialise_erreurs())<convertit_jours_vers_python(lj[cle_mini][0],initialise_erreurs()):
8b5845ff
DL
428 cle_mini = cle
429 return cle_mini
430
431 liste_f = []
432 while not(fini(listes_jours)):
433 typedonnee = mini(listes_jours)
434 # On extrait les données dans les deux listes (jours et données)
435 jour = listes_jours[typedonnee].pop(0)
436 donnee = listes_donnees[typedonnee].pop(0)
437 if liste_f == [] or jour != liste_f[-1]["age"]: # Si le jour est un "nouveau" jour
438 liste_f.append({"age":jour})
439 # On met à jour l'élément
440 liste_f[-1][typedonnee] = donnee
441
442 return liste_f
443
5679dfd0 444
a680b2f7 445### COnversion json vers formulaire
8b5845ff 446# Json -> formulaire HTML
d03279e7 447def fichier_json_vers_configdonnees(chaine,liste_err):
a680b2f7
DL
448 """ prend le json importé (chaine) et l'exporte vers les valeurs du formulaire
449 Renvoyé sous forme de dictionnaire (mais adapté au formulaire web)"""
d03279e7
DL
450 debug("json vers config : Prêt à interpréter le json",liste_err)
451 try:
452 valform = json.loads(chaine)
453 except :
454 erreur("Impossible de lire le fichier json !",liste_err)
455 return {}
be2bf515
DL
456 # Il faut maintenant récupérer les l_jours et l_poids puis les remettre
457 # sous forme de age_i et poids_i
8b5845ff
DL
458
459 listes_jours = {}
460 listes_donnees = {}
461 for typed in CONFIG["liste_typedonnees"]:
462 if typed == "poids": # pour la rétrocompatibilité
463 listes_jours[typed] = valform.get("data_j",[])
464 listes_donnees[typed] = valform.get("data_p",[])
465 else:
466 listes_jours[typed] = valform.get("data_jours_"+typed,[])
467 listes_donnees[typed] = valform.get("data_"+typed,[])
468
469 debug("Avant fusion : listes jours "+str(listes_jours),liste_err)
470 liste_donnees = fusionne_donnees(listes_jours,listes_donnees)
471 debug("Fusion de listes ok. Liste moche : "+str(liste_donnees),liste_err)
472 for i in range(len(liste_donnees)):
473 for (cle,val) in liste_donnees[i].items():
474 valform[cle+"_"+str(i)] = val
be2bf515 475
8b5845ff 476 valform["nb_data"] = max(len(liste_donnees) +2,DEFAUT["nb_data"])
5679dfd0 477
be2bf515 478 return valform
5679dfd0 479
5679dfd0 480
be2bf515 481