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