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