github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/directeurs/equipiers.go (about) 1 package directeurs 2 3 import ( 4 "bytes" 5 "database/sql" 6 "errors" 7 "fmt" 8 "path" 9 "sort" 10 "strings" 11 12 dm "github.com/benoitkugler/goACVE/server/core/datamodel" 13 rd "github.com/benoitkugler/goACVE/server/core/rawdata" 14 "github.com/benoitkugler/goACVE/server/core/rawdata/matching" 15 "github.com/benoitkugler/goACVE/server/core/utils/mails" 16 "github.com/benoitkugler/goACVE/server/core/utils/table" 17 "github.com/benoitkugler/goACVE/server/documents" 18 "github.com/benoitkugler/goACVE/server/shared" 19 ) 20 21 type ResultatRecherche struct { 22 Id int64 `json:"id"` 23 Nom rd.String `json:"nom"` 24 Prenom rd.String `json:"prenom"` 25 DateNaissance string `json:"date_naissance"` 26 Pertinence uint8 `json:"pertinence"` // en % 27 } 28 29 type EquipierDirecteur struct { 30 shared.BaseEquipier 31 32 Id int64 `json:"id"` 33 LienFormulaire string `json:"lien_formulaire"` 34 Roles rd.Roles `json:"roles"` 35 IsSb rd.Bool `json:"is_sb"` 36 } 37 38 func FromPersonneEquipier(personne rd.Personne, equipier rd.Equipier) EquipierDirecteur { 39 out := EquipierDirecteur{BaseEquipier: shared.FromPersonneEquipier(personne, equipier)} 40 41 out.Id = equipier.Id 42 out.Roles = equipier.Roles 43 44 return out 45 } 46 47 func (e EquipierDirecteur) ToPersonneEquipier(personne *rd.BasePersonne, equipier *rd.Equipier) { 48 e.BaseEquipier.ToPersonneEquipier(personne, equipier) 49 equipier.Roles = e.Roles 50 } 51 52 func (rc DriverCampComplet) scanDataEquipiers() (rd.Personnes, rd.Equipiers, error) { 53 equipiers, err := rd.SelectEquipiersByIdCamps(rc.DB, rc.camp.Id) 54 if err != nil { 55 return nil, nil, err 56 } 57 58 rows, err := rc.DB.Query(`SELECT personnes.* FROM personnes 59 JOIN equipiers ON equipiers.id_personne = personnes.id 60 WHERE equipiers.id = ANY($1)`, equipiers.Ids().AsSQL()) 61 if err != nil { 62 return nil, nil, err 63 } 64 personnes, err := rd.ScanPersonnes(rows) 65 return personnes, equipiers, err 66 } 67 68 // renvoie les données nécessaires à la base locale, pour les équipiers. 69 func (rc DriverCampComplet) loadDataEquipiers() error { 70 personnes, equipiers, err := rc.scanDataEquipiers() 71 72 // contraintes des équipiers 73 equipierContraintes, err := rd.SelectEquipierContraintesByIdEquipiers(rc.DB, equipiers.Ids()...) 74 if err != nil { 75 return err 76 } 77 78 rc.camp.Base.Personnes = personnes 79 rc.camp.Base.Equipiers = equipiers 80 rc.camp.Base.EquipierContraintes = equipierContraintes.ByIdEquipier() 81 return err 82 } 83 84 // charge en plus les données nécessaires aux pièces justificatives 85 func (d DriverCampComplet) loadDataDocumentsEquipiers() error { 86 base := d.camp.Base 87 88 // documents présents 89 docs, err := d.LoadDocuments(base.Personnes.Ids()) 90 if err != nil { 91 return err 92 } 93 94 // les contraintes (possibles) sont déjà chargées une fois pour toute 95 contraintes := make(rd.Contraintes) 96 for _, contrainte := range d.contraintesEquipiers { 97 contraintes[contrainte.Id] = contrainte 98 } 99 100 base.Documents = docs.Documents 101 base.DocumentPersonnes = docs.Liens 102 base.Contraintes = contraintes 103 return nil 104 } 105 106 // getPiecesJustificatives compile les documents et exigences 107 func (d DriverCampComplet) getPiecesJustificatives(host string) (Pieces, error) { 108 err := d.loadDataDocumentsEquipiers() 109 if err != nil { 110 return Pieces{}, err 111 } 112 113 out := Pieces{Contraintes: d.contraintesEquipiers} 114 115 cache := d.camp.Base.ResoudDocumentsPersonnes() 116 equipiers := d.camp.GetEquipe(nil) 117 out.Documents = make([]EquipierDocuments, len(equipiers)) 118 for index, equipier := range equipiers { 119 docs := equipier.GetPersonne().GetDocuments(cache) 120 publicDocs, err := d.compileDocs(host, docs) 121 if err != nil { 122 return Pieces{}, err 123 } 124 out.Documents[index] = EquipierDocuments{ 125 Contraintes: equipier.GetContraintes(), 126 IdEquipier: equipier.Id, 127 NomPrenom: equipier.GetPersonne().RawData().NomPrenom().String(), 128 Documents: publicDocs, 129 } 130 } 131 sort.Slice(out.Documents, func(i int, j int) bool { 132 return out.Documents[i].NomPrenom < out.Documents[j].NomPrenom 133 }) 134 return out, nil 135 } 136 137 // setExigenceDocument met à jour une exigence sur un équipier 138 // la base locale n'est pas mise à jour 139 func (d DriverCampComplet) setExigenceDocument(params UpdateContrainteEquipierIn) error { 140 if _, isKnow := d.contraintesEquipiers[params.IdContrainte]; !isKnow { 141 return fmt.Errorf("La catégorie du document à fournir est inconnue (id %d)", params.IdContrainte) 142 } 143 144 tx, err := d.DB.Begin() 145 if err != nil { 146 return err 147 } 148 149 // on supprime l'éventuelle contrainte actuelle pour l' équipier ... 150 ct := rd.EquipierContrainte{IdContrainte: params.IdContrainte, IdEquipier: params.IdEquipier} 151 err = ct.Delete(tx) 152 if err != nil { 153 return shared.Rollback(tx, err) 154 } 155 156 // ... puis on inscrit la nouvelle si nécessaire 157 if params.Demande != rd.OBNon { 158 ct.Optionnel = params.Demande == rd.OBPeutEtre 159 err = rd.InsertManyEquipierContraintes(tx, ct) 160 if err != nil { 161 return shared.Rollback(tx, err) 162 } 163 } 164 165 err = tx.Commit() 166 return err 167 } 168 169 func (d DriverCampComplet) creeDocumentEquipier(host string, idEquipier int64, params documents.ParamsNewDocument) (documents.PublicDocument, error) { 170 return documents.CreeDocumentEquipier(d.Signing, d.DB, host, idEquipier, params) 171 } 172 173 func (d DriverCampComplet) downloadDocumentsEquipiers(onlyRequis bool) (*bytes.Buffer, error) { 174 if err := d.loadDataDocumentsEquipiers(); err != nil { 175 return nil, err 176 } 177 idsDocs, prefixes := d.analyseDocsRequis(onlyRequis) 178 return d.packageDocs(idsDocs, prefixes) 179 } 180 181 // requiert d'avoir chargé les données documents 182 func (d DriverCampComplet) analyseDocsRequis(onlyRequis bool) ([]int64, map[int64]string) { 183 criblePers := map[int64]rd.Set{} // id personne -> id contraintes 184 for _, equipier := range d.camp.GetEquipe(nil) { 185 idPersonne := equipier.GetPersonne().Id 186 if onlyRequis { 187 criblePers[idPersonne] = equipier.GetContraintes().AsIds().AsSet() 188 } else { 189 criblePers[idPersonne] = d.contraintesEquipiers.Ids().AsSet() 190 } 191 } 192 var docsIds rd.Ids 193 prefixes := map[int64]string{} 194 for _, doc := range d.camp.Base.DocumentPersonnes { 195 if criblePers[doc.IdPersonne][doc.IdContrainte] { 196 docsIds = append(docsIds, doc.IdDocument) 197 prefixes[doc.IdDocument] = (d.contraintesEquipiers[doc.IdContrainte].Nom + " " + d.camp.Base.Personnes[doc.IdPersonne].NomPrenom()).String() 198 } 199 } 200 return docsIds, prefixes 201 } 202 203 // chercheSimilaires effectue le plus efficacement possible 204 // une recherche contre tous les profils connus 205 func (ct Controller) chercheSimilaires(in matching.PatternsSimilarite) ([]ResultatRecherche, error) { 206 personnes, err := matching.SelectAllPatternSimilaires(ct.DB) 207 if err != nil { 208 return nil, err 209 } 210 211 scoreMax, res := matching.ChercheSimilaires(personnes, in) 212 out := make([]ResultatRecherche, len(res)) 213 for index, p := range res { 214 out[index] = ResultatRecherche{ 215 Id: p.Personne.Id, 216 Nom: p.Personne.Nom, 217 Prenom: p.Personne.Prenom, 218 DateNaissance: p.Personne.DateNaissance.String(), 219 Pertinence: uint8(p.Score * 100 / scoreMax), 220 } 221 } 222 return out, nil 223 } 224 225 func (d DriverCampComplet) getEquipe(host string) ([]EquipierDirecteur, error) { 226 pts := d.camp.GetEquipe(nil) 227 out := make([]EquipierDirecteur, len(pts)) 228 cache := d.camp.Base.ResoudDocumentsPersonnes() 229 for index, equipier := range pts { 230 accesPers := equipier.GetPersonne() 231 pers := accesPers.RawData() 232 out[index] = FromPersonneEquipier(pers, equipier.RawData()) 233 out[index].IsSb = accesPers.IsSB(cache) 234 s, err := shared.EncodeID(d.Signing, shared.OrEquipier, equipier.Id) 235 if err != nil { 236 return nil, err 237 } 238 out[index].LienFormulaire = shared.BuildUrl(host, path.Join(EndPointEquipier, s), nil) 239 } 240 return out, nil 241 } 242 243 type errorDirecteur string 244 245 func (e errorDirecteur) Error() string { 246 return fmt.Sprintf("Le séjour a déjà un directeur : %s", string(e)) 247 } 248 249 // Ne commit pas ni ne rollback, mais met à jour la base locale. 250 // Des contraintes de documents par défaut sont ajoutées. 251 func (d DriverCampComplet) creeEquipier(idPersonne int64, roles rd.Roles, tx *sql.Tx) (rd.Equipier, error) { 252 if roles.Is(rd.RDirecteur) { 253 // on vérifie qu'un directeur n'est pas déjà présent 254 if dir, has := d.camp.GetDirecteur(); has { 255 return rd.Equipier{}, errorDirecteur(dir.RawData().NomPrenom()) 256 } 257 } 258 equipier := rd.Equipier{ 259 IdPersonne: idPersonne, 260 Roles: roles, 261 IdCamp: d.camp.Id, 262 } 263 equipier, err := equipier.Insert(tx) 264 if err != nil { 265 return equipier, err 266 } 267 pers, err := rd.SelectPersonne(tx, idPersonne) 268 if err != nil { 269 return equipier, err 270 } 271 272 equipierContraintes, err := shared.AddEquipierDefautContraintes(tx, d.contraintesEquipiers, equipier) 273 if err != nil { 274 return equipier, err 275 } 276 d.camp.Base.Personnes[pers.Id] = pers 277 d.camp.Base.Equipiers[equipier.Id] = equipier 278 d.camp.Base.EquipierContraintes[equipier.Id] = equipierContraintes 279 return equipier, nil 280 } 281 282 // rattacheEquipier ajoute un participant et met à jour la base locale. 283 func (d DriverCampComplet) rattacheEquipier(idPersonne int64, roles rd.Roles) (rd.Equipier, error) { 284 tx, err := d.DB.Begin() 285 if err != nil { 286 return rd.Equipier{}, err 287 } 288 out, err := d.creeEquipier(idPersonne, roles, tx) 289 if err != nil { 290 return out, shared.Rollback(tx, err) 291 } 292 err = tx.Commit() 293 return out, err 294 } 295 296 func (d DriverCampComplet) ajouteEquipierTmp(data matching.PatternsSimilarite, roles rd.Roles) (rd.Equipier, error) { 297 newPers := rd.Personne{ 298 BasePersonne: rd.BasePersonne{ 299 Nom: data.Nom, 300 Prenom: data.Prenom, 301 Sexe: data.Sexe, 302 DateNaissance: data.DateNaissance, 303 Mail: data.Mail, 304 NomJeuneFille: data.NomJeuneFille, 305 }, 306 IsTemporaire: true, 307 } 308 tx, err := d.DB.Begin() 309 if err != nil { 310 return rd.Equipier{}, err 311 } 312 newPers, err = newPers.Insert(tx) 313 if err != nil { 314 return rd.Equipier{}, shared.Rollback(tx, err) 315 } 316 equipier, err := d.creeEquipier(newPers.Id, roles, tx) 317 if err != nil { 318 errFinale := shared.Rollback(tx, err) 319 if errD, isDirecteur := err.(errorDirecteur); isDirecteur { 320 // affiche uniquement l'erreur directeur 321 errFinale = errD 322 } 323 return rd.Equipier{}, errFinale 324 } 325 err = tx.Commit() 326 return equipier, err 327 } 328 329 func (d DriverCampComplet) modifieEquipier(eq EquipierDirecteur) error { 330 if err := eq.Roles.Check(); err != nil { 331 return err 332 } 333 if eq.Roles.Is(rd.RDirecteur) { 334 // on vérifie qu'un directeur différent n'est pas déjà présent 335 if dir, has := d.camp.GetDirecteurEquipier(); has && dir.Id != eq.Id { 336 return errorDirecteur(dir.GetPersonne().RawData().NomPrenom()) 337 } 338 } 339 340 tx, err := d.DB.Begin() 341 if err != nil { 342 return err 343 } 344 equipier, err := rd.SelectEquipier(tx, eq.Id) 345 if err != nil { 346 return shared.Rollback(tx, err) 347 } 348 pers, err := rd.SelectPersonne(tx, equipier.IdPersonne) 349 if err != nil { 350 return shared.Rollback(tx, err) 351 } 352 eq.ToPersonneEquipier(&pers.BasePersonne, &equipier) 353 354 equipier, err = equipier.Update(tx) 355 if err != nil { 356 return shared.Rollback(tx, err) 357 } 358 pers, err = pers.Update(tx) 359 if err != nil { 360 return shared.Rollback(tx, err) 361 } 362 // on commit puis on met à jour la base locale 363 err = tx.Commit() 364 d.camp.Base.Equipiers[equipier.Id] = equipier 365 d.camp.Base.Personnes[pers.Id] = pers 366 return err 367 } 368 369 func (d DriverCampComplet) deleteEquipier(id int64) error { 370 tx, err := d.DB.Begin() 371 if err != nil { 372 return err 373 } 374 equipier, err := rd.SelectEquipier(tx, id) 375 if err != nil { 376 return shared.Rollback(tx, err) 377 } 378 379 // on supprime les contraintes 380 _, err = rd.DeleteEquipierContraintesByIdEquipiers(tx, id) 381 if err != nil { 382 return shared.Rollback(tx, err) 383 } 384 385 // puis l'équipier 386 _, err = rd.DeleteEquipierById(tx, equipier.Id) 387 if err != nil { 388 return shared.Rollback(tx, err) 389 } 390 391 // et enfin la personne sous-jacente, si besoin 392 isTmp, idDocs, err := shared.DeletePersonne(tx, equipier.IdPersonne) 393 if err != nil { 394 return shared.Rollback(tx, err) 395 } 396 397 // on commit puis on met à jour la base locale 398 if err = tx.Commit(); err != nil { 399 return err 400 } 401 402 delete(d.camp.Base.Equipiers, id) 403 for _, idDoc := range idDocs { 404 delete(d.camp.Base.Documents, idDoc) 405 delete(d.camp.Base.DocumentPersonnes, idDoc) 406 } 407 if isTmp { 408 delete(d.camp.Base.Personnes, equipier.IdPersonne) 409 } 410 return nil 411 } 412 413 func (d DriverCampComplet) exportListeEquipiers() (*bytes.Buffer, error) { 414 e := d.camp.GetEquipe(nil) 415 eqs := make(rd.Table, len(e)) 416 for index, part := range e { 417 eqs[index] = part.AsItem() 418 } 419 return table.GenereListeEquipe(HeaderExportEquipiers, eqs, false) 420 } 421 422 func (d DriverCampComplet) genereMailInvitationEquipier(host string, equipier dm.AccesEquipier) (to, htmlBody, replyTo string, err error) { 423 pers := equipier.GetPersonne().RawData() 424 camp := equipier.GetCamp() 425 cis, err := shared.EncodeID(d.Signing, shared.OrEquipier, equipier.Id) 426 if err != nil { 427 err = shared.FormatErr("Une erreur interne empêche le cryptage des informations.", err) 428 return 429 } 430 link := shared.BuildUrl(host, path.Join(EndPointEquipier, cis), nil) 431 direc, has := camp.GetDirecteur() 432 directeur, replyTo := "", "" 433 if has { 434 directeur = direc.RawData().FPrenom() 435 replyTo = direc.RawData().Mail.String() 436 } 437 html, err := mails.NewInviteEquipier(camp.RawData(), directeur, pers, link) 438 if err != nil { 439 err = shared.FormatErr("La création du mail a échoué.", err) 440 return 441 } 442 return pers.Mail.String(), html, replyTo, nil 443 } 444 445 // inviteOneEquipier envoie un mail et met à jour le champ 446 // invitation (sur la base locale aussi) 447 func (d DriverCampComplet) inviteOneEquipier(host string, equipier dm.AccesEquipier, mailer mails.Mailer) error { 448 to, html, replyTo, err := d.genereMailInvitationEquipier(host, equipier) 449 if err != nil { 450 return err 451 } 452 453 subject := fmt.Sprintf("[ACVE] Equipier - %s", d.camp.RawData().Label()) 454 err = mailer.SendMail(to, subject, html, nil, mails.CustomReplyTo(replyTo)) 455 if err != nil { 456 return shared.FormatErr(fmt.Sprintf("L'envoi d'un mail d'invitation à %s a échoué.", to), err) 457 } 458 459 // si l'équipier a déjà rempli son formulaire, on ne veut pas "revenir en arrière" sur une nouvelle invitation 460 newState := rd.Invite 461 if equipier.RawData().InvitationEquipier == rd.Verifie { 462 newState = rd.Verifie 463 } 464 row := d.DB.QueryRow("UPDATE equipiers SET invitation_equipier = $1 WHERE id = $2 RETURNING *", newState, equipier.Id) 465 out, err := rd.ScanEquipier(row) 466 if err != nil { 467 return shared.FormatErr("L'invitation a bien été envoyée, mais la mise à jour de la base de données a échoué.", err) 468 } 469 d.camp.Base.Equipiers[equipier.Id] = out 470 return nil 471 } 472 473 // inviteFormulaireEquipier envoie un mail et met à jour le champ 474 // invitation (sur la base locale aussi) 475 func (d DriverCampComplet) inviteFormulaireEquipier(host string, id int64) error { 476 eq := d.camp.Base.NewEquipier(id) 477 return d.inviteOneEquipier(host, eq, mails.NewMailer(d.SMTP)) 478 } 479 480 // inviteFormulairesEquipiers envoie les mails et met à jour le champ invitation 481 // (aussi sur la base locale) 482 func (d DriverCampComplet) inviteFormulairesEquipiers(host string, onlyNew bool) error { 483 pool, err := mails.NewPool(d.SMTP, nil) 484 if err != nil { 485 return err 486 } 487 defer pool.Close() 488 var errsMails []string 489 for _, equipier := range d.camp.GetEquipe(nil) { 490 inv := equipier.RawData().InvitationEquipier 491 if (onlyNew && inv == rd.NonInvite) || (!onlyNew && inv <= rd.Invite) { 492 err := d.inviteOneEquipier(host, equipier, pool) 493 if err != nil { 494 // on enregistre l'erreur et on continue 495 errsMails = append(errsMails, err.Error()) 496 } 497 } 498 } 499 if len(errsMails) > 0 { 500 err := "Des erreurs pendant l'envoi des mails ont été rencontrées : <br/>" + strings.Join(errsMails, "<br/>") 501 return errors.New(err) 502 } 503 return nil 504 }