github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/vote/admin.go (about)

     1  package vote
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"path"
     7  	"sort"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/benoitkugler/goACVE/server/core/rawdata"
    12  	"github.com/benoitkugler/goACVE/server/core/utils/mails"
    13  	"github.com/benoitkugler/goACVE/logs"
    14  	"github.com/benoitkugler/goACVE/server/shared"
    15  )
    16  
    17  // --------------------- page administrateur ----------------------------
    18  
    19  const mailSubject = "[ACVE] Vote de l'association"
    20  
    21  func lienVote(host string, id int64) string {
    22  	lien := logs.LienVote.Offusc(id)
    23  	return shared.BuildUrl(host, path.Join(EndpointVote, lien), nil)
    24  }
    25  
    26  type idVoteTime struct {
    27  	idVote int64
    28  	time   time.Time
    29  }
    30  
    31  type timedVotes map[idVoteTime]bool
    32  
    33  func (m timedVotes) last() time.Time {
    34  	var out time.Time
    35  	for k := range m {
    36  		if k.time.After(out) {
    37  			out = k.time
    38  		}
    39  	}
    40  	return out
    41  }
    42  
    43  var critereMembre = fmt.Sprintf(`rang_membre_asso IN ('%s', '%s', '%s') AND (is_temporaire IS NULL OR is_temporaire = false)`,
    44  	rawdata.RMAMembre, rawdata.RMACA, rawdata.RMABureau)
    45  
    46  func (ct Controller) getMembres(host string) ([]Membre, error) {
    47  	// a synchroniser avec bilans
    48  	rows, err := ct.db.Query("SELECT * FROM personnes WHERE " + critereMembre)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	personnes, err := rawdata.ScanPersonnes(rows)
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  	row := ct.db.QueryRow("SELECT count(*) FROM votes")
    57  	var nbVotes int
    58  	if err := row.Scan(&nbVotes); err != nil {
    59  		return nil, err
    60  	}
    61  	vps, err := SelectAllVotePersonnes(ct.db)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  
    66  	// pour chaque membre, combien de votes faits
    67  	tmp := make(map[int64]timedVotes)
    68  	for _, vp := range vps {
    69  		l := tmp[vp.IdPersonne]
    70  		if l == nil {
    71  			l = make(timedVotes)
    72  		}
    73  		l[idVoteTime{idVote: vp.IdVote, time: vp.Time}] = true
    74  		tmp[vp.IdPersonne] = l
    75  	}
    76  
    77  	var out []Membre
    78  	for _, pers := range personnes {
    79  		votePers := tmp[pers.Id]
    80  		out = append(out, Membre{
    81  			NomPrenom: pers.FNom() + " " + pers.FPrenom(),
    82  			Mail:      pers.Mail,
    83  			Lien:      lienVote(host, pers.Id),
    84  			Participation: Participation{
    85  				Actuels: len(votePers),
    86  				Total:   nbVotes,
    87  				Last:    votePers.last(),
    88  			},
    89  		})
    90  	}
    91  	sort.Slice(out, func(i, j int) bool {
    92  		return out[i].NomPrenom < out[j].NomPrenom
    93  	})
    94  	return out, nil
    95  }
    96  
    97  type nomLien struct {
    98  	NomPrenom string
    99  	Lien      string
   100  }
   101  
   102  type mailArgs struct {
   103  	Title             string
   104  	FooterTitle       string
   105  	FooterInfos       string
   106  	SignatureMail     string
   107  	MessagePersoLines []string
   108  	Liens             []nomLien
   109  }
   110  
   111  func (ct Controller) genereMail(liens []nomLien) (string, error) {
   112  	args := mailArgs{
   113  		Title:         "Vote en ligne",
   114  		FooterTitle:   rawdata.Asso.Title,
   115  		FooterInfos:   rawdata.Asso.Infos,
   116  		SignatureMail: "<i>Ceci est un mail automatique, merci de ne pas y répondre.</i>",
   117  		Liens:         liens,
   118  	}
   119  	var out bytes.Buffer
   120  	err := ct.templateMail.ExecuteTemplate(&out, "base.html", args)
   121  	return out.String(), err
   122  }
   123  
   124  func (ct Controller) inviteOne(membre Membre) error {
   125  	mailHtml, err := ct.genereMail([]nomLien{{NomPrenom: membre.NomPrenom, Lien: membre.Lien}})
   126  	if err != nil {
   127  		return err
   128  	}
   129  	return mails.NewMailer(ct.smtp).SendMail(membre.Mail.String(), mailSubject, mailHtml, nil, nil)
   130  }
   131  
   132  func (ct Controller) inviteAll(host string) error {
   133  	membres, err := ct.getMembres(host)
   134  	if err != nil {
   135  		return err
   136  	}
   137  	// on regroupe les personnes par adresse mail
   138  	parMail := make(map[string][]nomLien)
   139  	for _, membre := range membres {
   140  		mail := membre.Mail.TrimSpace().ToLower()
   141  		parMail[mail] = append(parMail[mail], nomLien{NomPrenom: membre.NomPrenom, Lien: membre.Lien})
   142  	}
   143  	// on ignore les mails vides
   144  	delete(parMail, "")
   145  
   146  	pool, err := mails.NewPool(ct.smtp, nil)
   147  	if err != nil {
   148  		return err
   149  	}
   150  	defer pool.Close()
   151  	var errs []string
   152  	for to, personnes := range parMail {
   153  		mailHtml, err := ct.genereMail(personnes)
   154  		if err != nil {
   155  			errs = append(errs, err.Error())
   156  			continue
   157  		}
   158  		err = pool.SendMail(to, mailSubject, mailHtml, nil, nil)
   159  		if err != nil {
   160  			errs = append(errs, err.Error())
   161  			continue
   162  		}
   163  	}
   164  	if len(errs) > 0 {
   165  		return fmt.Errorf("Certains mails n'ont pas été envoyés : <br/> %s", strings.Join(errs, "<br/>"))
   166  	}
   167  	return nil
   168  }
   169  
   170  func (ct Controller) addDecompte(votes []VoteCandidats) ([]VoteAdmin, error) {
   171  	actions, err := SelectAllVotePersonnes(ct.db)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  	// on compte les actions par votes
   176  	allActions := make(map[int64]int)
   177  	for _, ac := range actions {
   178  		allActions[ac.IdVote]++
   179  	}
   180  
   181  	voix, err := SelectAllVotePersonneCandidats(ct.db)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  	// on compte les voix par candidats
   186  	allVoix := make(map[int64]int)
   187  	for _, v := range voix {
   188  		allVoix[v.IdCandidat]++
   189  	}
   190  
   191  	out := make([]VoteAdmin, len(votes))
   192  	// on récupère les voix
   193  	for i, v := range votes {
   194  		va := VoteAdmin{VoteCandidats: v, Voix: make(map[int64]int), Participation: allActions[v.Id]}
   195  		for _, candidat := range va.Candidats {
   196  			va.Voix[candidat.Id] = allVoix[candidat.Id]
   197  		}
   198  		out[i] = va
   199  	}
   200  	return out, nil
   201  }
   202  
   203  func (ct Controller) getVotes() ([]VoteCandidats, error) {
   204  	votes, err := SelectAllVotes(ct.db)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  	candidats, err := SelectAllCandidats(ct.db)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	// on regroupe les candidats par vote
   214  	tmp := make(map[int64][]Candidat) // id vote -> candidats
   215  	for _, candidat := range candidats {
   216  		tmp[candidat.IdVote] = append(tmp[candidat.IdVote], candidat)
   217  	}
   218  	var out []VoteCandidats
   219  	for _, vote := range votes {
   220  		cds := tmp[vote.Id]
   221  		sort.Slice(cds, func(i, j int) bool {
   222  			return cds[i].Id < cds[j].Id
   223  		})
   224  		out = append(out, VoteCandidats{
   225  			Vote:      vote,
   226  			Candidats: cds,
   227  		})
   228  	}
   229  	sort.Slice(out, func(i, j int) bool {
   230  		return out[i].Id < out[j].Id
   231  	})
   232  	return out, nil
   233  }
   234  
   235  func (ct Controller) getVotesWithDecompte() ([]VoteAdmin, error) {
   236  	votes, err := ct.getVotes()
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  	return ct.addDecompte(votes)
   241  }
   242  
   243  // vote est complété
   244  func (ct Controller) createVote(vote VoteCandidats) error {
   245  	tx, err := ct.db.Begin()
   246  	if err != nil {
   247  		return err
   248  	}
   249  	vote.Vote, err = vote.Vote.Insert(tx)
   250  	if err != nil {
   251  		return shared.Rollback(tx, err)
   252  	}
   253  	for _, c := range vote.Candidats {
   254  		c.IdVote = vote.Id // on utilise l'id nouvellement attribué
   255  		_, err := c.Insert(tx)
   256  		if err != nil {
   257  			return shared.Rollback(tx, err)
   258  		}
   259  	}
   260  	return tx.Commit()
   261  }
   262  
   263  // modification seulement si aucune voix
   264  func (ct Controller) updateVote(vote VoteCandidats) error {
   265  	voix, err := SelectVotePersonneByIdVote(ct.db, vote.Id)
   266  	if err != nil {
   267  		return err
   268  	}
   269  	if len(voix) > 0 {
   270  		return fmt.Errorf("Ce vote a déjà reçu des suffrages, les modifications sont donc désactivées.")
   271  	}
   272  
   273  	tx, err := ct.db.Begin()
   274  	if err != nil {
   275  		return err
   276  	}
   277  	vote.Vote, err = vote.Vote.Update(tx)
   278  	if err != nil {
   279  		return shared.Rollback(tx, err)
   280  	}
   281  	// on remet à zéro les choix
   282  	_, err = tx.Exec("DELETE FROM candidats WHERE id_vote = $1", vote.Id)
   283  	if err != nil {
   284  		return shared.Rollback(tx, err)
   285  	}
   286  	// puis on insère les nouveaux
   287  	for _, c := range vote.Candidats {
   288  		c.IdVote = vote.Id
   289  		_, err := c.Insert(tx)
   290  		if err != nil {
   291  			return shared.Rollback(tx, err)
   292  		}
   293  	}
   294  	return tx.Commit()
   295  }
   296  
   297  func (ct Controller) lockVote(params LockVote) (Vote, error) {
   298  	row := ct.db.QueryRow("UPDATE votes SET is_locked = $1 WHERE id = $2 RETURNING *", params.IsLocked, params.IdVote)
   299  	return ScanVote(row)
   300  }
   301  
   302  func (ct Controller) clearVote(id int64) error {
   303  	tx, err := ct.db.Begin()
   304  	if err != nil {
   305  		return err
   306  	}
   307  	_, err = tx.Exec("DELETE FROM vote_personne_candidats WHERE id_vote = $1", id)
   308  	if err != nil {
   309  		return shared.Rollback(tx, err)
   310  	}
   311  	_, err = tx.Exec("DELETE FROM vote_personnes WHERE id_vote = $1", id)
   312  	if err != nil {
   313  		return shared.Rollback(tx, err)
   314  	}
   315  	err = tx.Commit()
   316  	return err
   317  }
   318  
   319  func (ct Controller) deleteVote(id int64) error {
   320  	tx, err := ct.db.Begin()
   321  	if err != nil {
   322  		return err
   323  	}
   324  	// liens
   325  	_, err = tx.Exec("DELETE FROM vote_personne_candidats WHERE id_vote = $1", id)
   326  	if err != nil {
   327  		return shared.Rollback(tx, err)
   328  	}
   329  	_, err = tx.Exec("DELETE FROM vote_personnes WHERE id_vote = $1", id)
   330  	if err != nil {
   331  		return shared.Rollback(tx, err)
   332  	}
   333  	_, err = tx.Exec("DELETE FROM candidats WHERE id_vote = $1", id)
   334  	if err != nil {
   335  		return shared.Rollback(tx, err)
   336  	}
   337  	_, err = Vote{Id: id}.Delete(tx)
   338  	if err != nil {
   339  		return shared.Rollback(tx, err)
   340  	}
   341  	return tx.Commit()
   342  }
   343  
   344  func (ct Controller) exportBilanVotes() ([]byte, error) {
   345  	votes, err := ct.getVotesWithDecompte()
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  
   350  	// on compte les voix exprimées
   351  	votePersonnes, err := SelectAllVotePersonnes(ct.db)
   352  	if err != nil {
   353  		return nil, err
   354  	}
   355  	tmp := make(map[int64]int)
   356  	for _, vp := range votePersonnes {
   357  		tmp[vp.IdVote]++
   358  	}
   359  
   360  	row := ct.db.QueryRow("SELECT count(*) FROM personnes WHERE " + critereMembre)
   361  	var nbTotal int
   362  	if err := row.Scan(&nbTotal); err != nil {
   363  		return nil, err
   364  	}
   365  
   366  	var vps []voteParticipants
   367  	for _, vote := range votes {
   368  		if !vote.IsLocked {
   369  			continue // on ignore les votes non verouillés
   370  		}
   371  		vps = append(vps, voteParticipants{
   372  			VoteAdmin:      vote,
   373  			nbParticipants: tmp[vote.Id],
   374  			nbTotal:        nbTotal,
   375  		})
   376  	}
   377  	if len(vps) == 0 {
   378  		return nil, fmt.Errorf("Aucun vote à exporter. Merci de <b>clôturer</b> les votes que vous voulez exporter.")
   379  	}
   380  	return genereBilanVotes(vps)
   381  }