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  }