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