github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/directeurs/controller.go (about) 1 // Expose les fonctionnalités du portail des directeurs 2 package directeurs 3 4 import ( 5 "bytes" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io/ioutil" 10 "path/filepath" 11 "sort" 12 "strings" 13 "time" 14 15 "github.com/benoitkugler/goACVE/logs" 16 dm "github.com/benoitkugler/goACVE/server/core/datamodel" 17 cd "github.com/benoitkugler/goACVE/server/core/documents" 18 rd "github.com/benoitkugler/goACVE/server/core/rawdata" 19 "github.com/benoitkugler/goACVE/server/core/rawdata/matching" 20 "github.com/benoitkugler/goACVE/server/core/utils/joomeo" 21 "github.com/benoitkugler/goACVE/server/core/utils/mails" 22 "github.com/benoitkugler/goACVE/server/documents" 23 "github.com/benoitkugler/goACVE/server/shared" 24 ) 25 26 const ( 27 DeltaToken = 48 * time.Hour 28 EndPointEquipier = "/equipier" 29 ) 30 31 const ( 32 noLoad loadDataMode = iota // charge uniquement le camp 33 loadInscrits // charge aussi les données nécessaires aux inscrits 34 loadEquipiers // charge aussi les données nécessaires aux équipiers 35 ) 36 37 const ( 38 DownloadAll modeDownloadFicheSanitaire = "all" 39 DownloadOne modeDownloadFicheSanitaire = "one" 40 DownloadAllInOneDocument modeDownloadFicheSanitaire = "all_in_one_document" 41 ) 42 43 var ( 44 HeaderExportInscrits = []rd.Header{ 45 {Field: dm.PersonneNom, Label: "Nom"}, 46 {Field: dm.PersonnePrenom, Label: "Prénom"}, 47 {Field: dm.PersonneSexe, Label: "Sexe"}, 48 {Field: dm.ParticipantAgeDebutCamp, Label: "Age (début de camp)"}, 49 {Field: dm.PersonneDateNaissance, Label: "Date de naissance"}, 50 {Field: dm.ParticipantGroupe, Label: "Groupe"}, 51 {Field: dm.ParticipantAnimateur, Label: "Animateur"}, 52 {Field: dm.ParticipantBus, Label: "Navette"}, 53 {Field: dm.PersonneMail, Label: "Mail du participant"}, 54 {Field: dm.ParticipantOptionPrix, Label: "Option sur le prix"}, 55 {Field: dm.ParticipantMaterielSki, Label: "Matériel de ski"}, 56 {Field: dm.ParticipantMaterielSkiType, Label: "Loueur (matériel ski)"}, 57 } 58 59 HeaderExportInscritsSimple = []rd.Header{ 60 {Field: dm.PersonneNom, Label: "Nom"}, 61 {Field: dm.PersonnePrenom, Label: "Prénom"}, 62 {Field: dm.ParticipantGroupe, Label: "Groupe"}, 63 {Field: dm.ParticipantAnimateur, Label: "Animateur"}, 64 } 65 66 HeaderExportResponsables = []rd.Header{ 67 {Field: dm.ParticipantRespoNomPrenom, Label: "Responsable"}, 68 {Field: dm.ParticipantRespoMail, Label: "Mail"}, 69 {Field: dm.ParticipantRespoTels, Label: "Tel."}, 70 {Field: dm.ParticipantRespoAdresse, Label: "Adresse"}, 71 {Field: dm.ParticipantRespoCodePostal, Label: "Code postal"}, 72 {Field: dm.ParticipantRespoVille, Label: "Ville"}, 73 {Field: dm.ParticipantRespoPays, Label: "Pays"}, 74 } 75 76 HeaderExportResponsablesSimple = []rd.Header{ 77 {Field: dm.ParticipantRespoNomPrenom, Label: "Responsable"}, 78 {Field: dm.ParticipantRespoTels, Label: "Tel."}, 79 } 80 81 HeaderExportEquipiers = []rd.Header{ 82 {Field: dm.PersonneNom, Label: "Nom"}, 83 {Field: dm.PersonnePrenom, Label: "Prénom"}, 84 {Field: dm.EquipierRoles, Label: "Rôle"}, 85 {Field: dm.EquipierDiplome, Label: "Diplôme"}, 86 {Field: dm.EquipierAppro, Label: "Approfondissement"}, 87 {Field: dm.PersonneSexe, Label: "Sexe"}, 88 {Field: dm.PersonneNomJeuneFille, Label: "Nom de jeune fille"}, 89 {Field: dm.PersonneDateNaissance, Label: "Date de naissance"}, 90 {Field: dm.PersonneDepartementNaissance, Label: "Département de naissance"}, 91 {Field: dm.PersonneVilleNaissance, Label: "Ville de naissance"}, 92 {Field: dm.PersonneMail, Label: "Adresse mail"}, 93 {Field: dm.PersonneTels, Label: "Téléphones"}, 94 {Field: dm.PersonneAdresse, Label: "Adresse"}, 95 {Field: dm.PersonneCodePostal, Label: "Code postal"}, 96 {Field: dm.PersonneVille, Label: "Ville"}, 97 {Field: dm.PersonneSecuriteSociale, Label: "Securité sociale"}, 98 {Field: dm.PersonneProfession, Label: "Profession"}, 99 {Field: dm.PersonneEtudiant, Label: "Etudiant"}, 100 {Field: dm.PersonneFonctionnaire, Label: "Fonctionnaire"}, 101 {Field: dm.EquipierPresence, Label: "Présence au séjour"}, 102 } 103 ) 104 105 type loadDataMode uint8 106 107 type export string 108 109 type details string 110 111 type listeVetements string 112 113 type formulaireEquipier string 114 115 type modeDownloadFicheSanitaire string 116 117 type Controller struct { 118 shared.Controller 119 120 joomeo logs.Joomeo 121 ContraintesEquipiers rd.Contraintes 122 defaultListe struct{ Ete, Hiver []rd.Vetement } 123 } 124 125 // NewController créé un controller et charge les ressources 126 func NewController(base shared.Controller, joomeo logs.Joomeo, ressourcesPath string) (Controller, error) { 127 var out Controller 128 out.Controller = base 129 out.joomeo = joomeo 130 131 b, err := ioutil.ReadFile(filepath.Join(ressourcesPath, "liste_vetements.json")) 132 if err != nil { 133 return out, fmt.Errorf("Impossible d'accéder aux listes de vêtements par défaut : %s", err) 134 } 135 if err = json.Unmarshal(b, &out.defaultListe); err != nil { 136 return out, fmt.Errorf("Listes de vêtements par défaut corrompues : %s", err) 137 } 138 139 err = out.initBuiltinContraintes() 140 return out, err 141 } 142 143 // initBuiltinContraintes charge une fois pour toute 144 // les contraintes possibles pour un document d'équipier 145 func (ct *Controller) initBuiltinContraintes() error { 146 // toutes sauf le test nautique 147 rows, err := ct.DB.Query("SELECT * FROM contraintes WHERE builtin <> '' AND builtin <> $1", rd.CTestNautique) 148 if err != nil { 149 return err 150 } 151 ct.ContraintesEquipiers, err = rd.ScanContraintes(rows) 152 return err 153 } 154 155 type Pieces struct { 156 Contraintes rd.Contraintes `json:"contraintes,omitempty"` // contraintes possibles 157 Documents []EquipierDocuments `json:"documents,omitempty"` 158 } 159 160 // EquipierDocuments indique les contraintes et les documents présents 161 // pour un équipier. 162 type EquipierDocuments struct { 163 Contraintes []rd.EquipierContrainte `json:"contraintes,omitempty"` // writable 164 165 IdEquipier int64 `json:"id_equipier,omitempty"` 166 NomPrenom string `json:"nom_prenom,omitempty"` 167 Documents map[int64][]documents.PublicDocument `json:"documents,omitempty"` // id contrainte -> document présents 168 } 169 170 type DetailsWritable struct { 171 Nom rd.String `json:"nom,omitempty"` 172 Lieu rd.String `json:"lieu,omitempty"` 173 NbPlaces rd.Int `json:"nb_places,omitempty"` 174 NeedEquilibreGF rd.Bool `json:"need_equilibre_gf,omitempty"` 175 MaterielSki rd.MaterielSkiCamp `json:"materiel_ski,omitempty"` 176 } 177 178 type Details struct { 179 DetailsWritable 180 Bus rd.BusCamp `json:"bus,omitempty"` 181 DateDebut rd.Date `json:"date_debut,omitempty"` 182 DateFin rd.Date `json:"date_fin,omitempty"` 183 } 184 185 type JoomeoData struct { 186 SpaceUrl string `json:"space_url,omitempty"` 187 Meta joomeo.Album `json:"meta,omitempty"` 188 Contacts []joomeo.ContactPermission `json:"contacts,omitempty"` 189 MailsInscrits []string `json:"mails_inscrits,omitempty"` 190 MailsResponsables []string `json:"mails_responsables,omitempty"` 191 MailsEquipiers []string `json:"mails_equipiers,omitempty"` 192 } 193 194 func (ct Controller) getCamps() ([]shared.CampMeta, error) { 195 camps, err := rd.SelectAllCamps(ct.DB) 196 if err != nil { 197 return nil, err 198 } 199 out := make([]shared.CampMeta, 0, len(camps)) 200 for _, camp := range camps { 201 var c shared.Camp 202 c.From(camp) 203 out = append(out, c.CampMeta) 204 } 205 sort.Slice(out, func(i int, j int) bool { 206 return out[i].Label < out[j].Label 207 }) 208 return out, nil 209 } 210 211 // checkPassword renvoie "" si le mot de passe est faux, 212 // une erreur si la requête est mal formée, 213 // ou un token si tout va bien. 214 func (ct Controller) checkPassword(idCamp int64, password string) (camp rd.Camp, token string, err error) { 215 camp, err = rd.SelectCamp(ct.DB, idCamp) 216 if err != nil { 217 return 218 } 219 if camp.Password == "" { 220 err = errors.New("Le camp n'est pas protégé par un mot de passe. Contactez l'administrateur.") 221 return 222 } 223 if string(camp.Password) != password { 224 return 225 } 226 token, err = ct.creeToken(idCamp) 227 return 228 } 229 230 func (ct Controller) newDriverShared(token string, camp rd.Camp) driverShared { 231 base := dm.BaseLocale{ 232 Camps: rd.Camps{camp.Id: camp}, 233 } 234 return driverShared{ 235 Controller: ct.Controller, 236 joomeo: ct.joomeo, 237 camp: base.NewCamp(camp.Id), 238 token: token, 239 } 240 } 241 242 func (ct Controller) newDriverCampComplet(token string, camp rd.Camp) DriverCampComplet { 243 return DriverCampComplet{ 244 driverShared: ct.newDriverShared(token, camp), 245 contraintesEquipiers: ct.ContraintesEquipiers, 246 } 247 } 248 249 func loadData(d Driver, load loadDataMode) error { 250 var err error 251 switch load { 252 case loadInscrits: 253 err = d.loadDataInscrits() 254 case loadEquipiers: 255 err = d.loadDataEquipiers() 256 } 257 return err 258 } 259 260 // setupRequest s'occupe de l'identification et du chargement des données, 261 // pour tous les camps 262 func (ct Controller) setupRequest(req withBasicAuth, load loadDataMode) (Driver, error) { 263 idCamp, token, err := ct.authentifie(req) 264 if err != nil { 265 return nil, err 266 } 267 camp, err := rd.SelectCamp(ct.DB, idCamp) 268 if err != nil { 269 return nil, err 270 } 271 var out Driver 272 if camp.InscriptionSimple { 273 out = DriverCampSimple{driverShared: ct.newDriverShared(token, camp)} 274 } else { 275 out = ct.newDriverCampComplet(token, camp) 276 } 277 err = loadData(out, load) 278 return out, err 279 } 280 281 // setupRequest s'occupe de l'identification et du chargement des données, 282 // pour tous les camps complets uniquement 283 func (ct Controller) setupRequestComplet(req withBasicAuth, load loadDataMode) (DriverCampComplet, error) { 284 idCamp, token, err := ct.authentifie(req) 285 if err != nil { 286 return DriverCampComplet{}, err 287 } 288 camp, err := rd.SelectCamp(ct.DB, idCamp) 289 if err != nil { 290 return DriverCampComplet{}, err 291 } 292 if camp.InscriptionSimple { 293 return DriverCampComplet{}, fmt.Errorf("Le camp %s ne supporte pas l'opération demandée.", camp.Label()) 294 } 295 out := ct.newDriverCampComplet(token, camp) 296 err = loadData(out, load) 297 return out, err 298 } 299 300 func (rc DriverCampComplet) compileDocs(host string, docs []dm.AccesDocumentPersonne) (map[int64][]documents.PublicDocument, error) { 301 out := make(map[int64][]documents.PublicDocument) 302 for _, doc := range docs { 303 contrainte := doc.GetContrainte() 304 pub, err := documents.PublieDocument(rc.Signing, host, doc.RawData()) 305 if err != nil { 306 return nil, err 307 } 308 out[contrainte.Id] = append(out[contrainte.Id], pub) 309 } 310 return out, nil 311 } 312 313 func (d DriverCampComplet) packageDocs(idsDocs rd.Ids, prefixes map[int64]string) (*bytes.Buffer, error) { 314 archive := cd.NewArchiveZip() 315 if err := documents.LoadDocsAndAdd(d.DB, idsDocs, archive, prefixes); err != nil { 316 return nil, err 317 } 318 return archive.Close() 319 } 320 321 func (d driverShared) getDetails() Details { 322 rc := d.camp.RawData() 323 return Details{ 324 DetailsWritable: DetailsWritable{ 325 Nom: rc.Nom, 326 Lieu: rc.Lieu, 327 NbPlaces: rc.NbPlaces, 328 NeedEquilibreGF: rc.NeedEquilibreGf, 329 MaterielSki: rc.Options.MaterielSki, 330 }, 331 Bus: rc.Options.Bus, 332 DateDebut: rc.DateDebut, 333 DateFin: rc.DateFin, 334 } 335 } 336 337 func (d driverShared) updateDetails(det DetailsWritable) error { 338 if det.Nom == "" || det.Lieu == "" || det.NbPlaces == 0 { 339 return errors.New("Merci de préciser nom, lieu et nombre de places !") 340 } 341 rc := d.camp.RawData() 342 rc.Nom = det.Nom 343 rc.Lieu = det.Lieu 344 rc.NbPlaces = det.NbPlaces 345 rc.NeedEquilibreGf = det.NeedEquilibreGF 346 rc.Options.MaterielSki = det.MaterielSki 347 348 rc, err := rc.Update(d.DB) 349 if err != nil { 350 return err 351 } 352 353 d.camp.Base.Camps[rc.Id] = rc 354 return nil 355 } 356 357 // charge les documents du camp (lettre, pièces jointes bonus) 358 func (d DriverCampComplet) loadDocsCamp() error { 359 rows, err := d.DB.Query(`SELECT documents.* FROM documents 360 JOIN document_camps ON document_camps.id_document = documents.id 361 WHERE document_camps.id_camp = $1`, d.camp.Id) 362 if err != nil { 363 return shared.FormatErr("Le chargement des documents liés au camp a échoué", err) 364 } 365 docs, err := rd.ScanDocuments(rows) 366 if err != nil { 367 return shared.FormatErr("La lecture des documents liés au camp a échoué.", err) 368 } 369 370 liens, err := rd.SelectDocumentCampsByIdCamps(d.DB, d.camp.Id) 371 if err != nil { 372 return shared.FormatErr("La lecture des documents liés au camp a échoué.", err) 373 } 374 d.camp.Base.Documents = docs 375 d.camp.Base.DocumentCamps = liens.ByIdDocument() 376 return nil 377 } 378 379 // getBonusDocs charge les pièces jointes du camp et les partage 380 func (d DriverCampComplet) getBonusDocs(host string) ([]documents.PublicDocument, error) { 381 if err := d.loadDocsCamp(); err != nil { 382 return nil, err 383 } 384 pjs := d.camp.GetRegisteredDocuments(false, true) 385 outDocs := make([]documents.PublicDocument, len(pjs)) 386 var err error 387 for index, doc := range pjs { 388 outDocs[index], err = documents.PublieDocument(d.Signing, host, doc) 389 if err != nil { 390 return nil, err 391 } 392 } 393 return outDocs, nil 394 } 395 396 func (d DriverCampComplet) addBonusDoc(host string, fileName string, fileContent []byte) (pub documents.PublicDocument, err error) { 397 if err = d.loadDocsCamp(); err != nil { 398 return 399 } 400 tx, err := d.DB.Begin() 401 if err != nil { 402 return 403 } 404 405 document := rd.Document{ 406 Description: "Document additionnel du " + d.camp.RawData().Label(), 407 DateHeureModif: rd.Time(time.Now()), 408 } 409 document, err = document.Insert(tx) 410 if err != nil { 411 return pub, shared.Rollback(tx, err) 412 } 413 414 // on insert le lien 415 lien := rd.DocumentCamp{ 416 IdCamp: d.camp.Id, 417 IdDocument: document.Id, 418 IsLettre: false, 419 } 420 err = rd.InsertManyDocumentCamps(tx, lien) 421 if err != nil { 422 return pub, shared.Rollback(tx, err) 423 } 424 425 // on insert le contenu 426 document, err = documents.SaveDocument(tx, document.Id, fileContent, fileName, false) 427 if err != nil { 428 return pub, shared.Rollback(tx, err) 429 } 430 431 // puis on publie 432 pub, err = documents.PublieDocument(d.Signing, host, document) 433 if err != nil { 434 return pub, shared.Rollback(tx, err) 435 } 436 d.camp.Base.Documents[document.Id] = document 437 d.camp.Base.DocumentCamps[lien.IdDocument] = lien 438 err = tx.Commit() 439 return 440 } 441 442 func (d DriverCampComplet) updateEnvois(env rd.Envois) error { 443 if env.Locked { 444 return errors.New("La modification des préférences d'envois est désactivée tant que l'envoi est verrouillé.") 445 } 446 oldLocked := d.camp.RawData().Envois.Locked 447 tx, err := d.DB.Begin() 448 if err != nil { 449 return err 450 } 451 row := tx.QueryRow("UPDATE camps SET envois = $1 WHERE id = $2 RETURNING *", env, d.camp.Id) 452 camp, err := rd.ScanCamp(row) 453 if err != nil { 454 return shared.Rollback(tx, err) 455 } 456 d.camp.Base.Camps[camp.Id] = camp 457 458 wasUnlocked := oldLocked && !camp.Envois.Locked 459 if wasUnlocked { // on prévient par mail le centre d'inscription 460 html, err := mails.NewNotifieEnvoiDocs(camp) 461 if err != nil { 462 return shared.Rollback(tx, err) 463 } 464 err = mails.NewMailer(d.SMTP).SendMail(rd.CoordonnesCentre.Mail, "[ACVE] Envois des documents", html, nil, 465 nil) 466 if err != nil { 467 return shared.Rollback(tx, err) 468 } 469 } 470 err = tx.Commit() 471 return err 472 } 473 474 func (d DriverCampComplet) updateListeVetements(l rd.ListeVetements) error { 475 row := d.DB.QueryRow("UPDATE camps SET liste_vetements = $1 WHERE id = $2 RETURNING *", l, d.camp.Id) 476 camp, err := rd.ScanCamp(row) 477 if err != nil { 478 return err 479 } 480 d.camp.Base.Camps[camp.Id] = camp 481 return nil 482 } 483 484 func (d DriverCampComplet) getLettreDirecteur(host string) (doc documents.PublicDocument, lettre rd.Lettredirecteur, err error) { 485 if err = d.loadDocsCamp(); err != nil { 486 return 487 } 488 letters := d.camp.GetRegisteredDocuments(true, false) 489 if len(letters) > 0 { 490 // il n'y a normalement qu'une seule lettre 491 // sinon, on renvoie une lettre au hasard. 492 doc, err = documents.PublieDocument(d.Signing, host, letters[0]) 493 if err != nil { 494 return 495 } 496 } 497 lettre, _, err = rd.SelectLettredirecteurByIdCamp(d.DB, d.camp.Id) // zero value si le camp n'a pas encore de lettre 498 return 499 } 500 501 // reduitNom remplace les espaces par des _ 502 func reduitNom(fullname string) string { 503 parts := strings.Split(strings.ToLower(fullname), " ") 504 for index, p := range parts { 505 parts[index] = strings.Title(matching.RemoveAccents(strings.TrimSpace(p))) 506 } 507 return strings.Join(parts, "_") 508 }