Use static assets for HTML templates
[cacert-boardvoting.git] / models.go
1 package main
2
3 import (
4 "database/sql"
5 "fmt"
6 dbmig "git.cacert.org/cacert-boardvoting/db"
7 "github.com/jmoiron/sqlx"
8 "github.com/rubenv/sql-migrate"
9 "time"
10 )
11
12 type sqlKey int
13
14 const (
15 sqlLoadDecisions sqlKey = iota
16 sqlLoadUnvotedDecisions
17 sqlLoadDecisionByTag
18 sqlLoadDecisionById
19 sqlLoadVoteCountsForDecision
20 sqlLoadVotesForDecision
21 sqlLoadEnabledVoterByEmail
22 sqlCountOlderThanDecision
23 sqlCountOlderThanUnvotedDecision
24 sqlCreateDecision
25 sqlUpdateDecision
26 sqlUpdateDecisionStatus
27 sqlSelectClosableDecisions
28 sqlGetNextPendingDecisionDue
29 sqlGetReminderVoters
30 sqlFindUnvotedDecisionsForVoter
31 sqlGetEnabledVoterById
32 sqlCreateVote
33 sqlLoadVote
34 sqlGetVotersForProxy
35 )
36
37 var sqlStatements = map[sqlKey]string{
38 sqlLoadDecisions: `
39 SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title,
40 decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified
41 FROM decisions
42 JOIN voters ON decisions.proponent=voters.id
43 ORDER BY proposed DESC
44 LIMIT 10 OFFSET 10 * $1`,
45 sqlLoadUnvotedDecisions: `
46 SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title,
47 decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified
48 FROM decisions
49 JOIN voters ON decisions.proponent=voters.id
50 WHERE decisions.status = 0 AND decisions.id NOT IN (SELECT votes.decision FROM votes WHERE votes.voter = $1)
51 ORDER BY proposed DESC
52 LIMIT 10 OFFSET 10 * $2;`,
53 sqlLoadDecisionByTag: `
54 SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title,
55 decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified
56 FROM decisions
57 JOIN voters ON decisions.proponent=voters.id
58 WHERE decisions.tag=$1;`,
59 sqlLoadDecisionById: `
60 SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content,
61 decisions.votetype, decisions.status, decisions.due, decisions.modified
62 FROM decisions
63 WHERE decisions.id=$1;`,
64 sqlLoadVoteCountsForDecision: `
65 SELECT vote, COUNT(vote) FROM votes WHERE decision=$1 GROUP BY vote`,
66 sqlLoadVotesForDecision: `
67 SELECT votes.decision, votes.voter, voters.name, votes.vote, votes.voted, votes.notes
68 FROM votes
69 JOIN voters ON votes.voter=voters.id
70 WHERE decision=$1`,
71 sqlLoadEnabledVoterByEmail: `
72 SELECT voters.id, voters.name, voters.enabled, voters.reminder
73 FROM voters
74 JOIN emails ON voters.id=emails.voter
75 WHERE emails.address=$1 AND voters.enabled=1`,
76 sqlGetEnabledVoterById: `
77 SELECT id, name, enabled, reminder
78 FROM voters
79 WHERE enabled=1 AND id=$1`,
80 sqlCountOlderThanDecision: `
81 SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`,
82 sqlCountOlderThanUnvotedDecision: `
83 SELECT COUNT(*) > 0 FROM decisions
84 WHERE proposed < $1 AND status=0 AND id NOT IN (SELECT decision FROM votes WHERE votes.voter=$2)`,
85 sqlCreateDecision: `
86 INSERT INTO decisions (proposed, proponent, title, content, votetype, status, due, modified,tag)
87 VALUES (
88 :proposed, :proponent, :title, :content, :votetype, 0, :due, :proposed,
89 'm' || strftime('%Y%m%d', :proposed) || '.' || (
90 SELECT COUNT(*)+1 AS num
91 FROM decisions
92 WHERE proposed BETWEEN date(:proposed) AND date(:proposed, '1 day')
93 )
94 )`,
95 sqlUpdateDecision: `
96 UPDATE decisions
97 SET proponent=:proponent, title=:title, content=:content, votetype=:votetype, due=:due, modified=:modified
98 WHERE id=:id`,
99 sqlUpdateDecisionStatus: `
100 UPDATE decisions SET status=:status, modified=:modified WHERE id=:id`,
101 sqlSelectClosableDecisions: `
102 SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content,
103 decisions.votetype, decisions.status, decisions.due, decisions.modified
104 FROM decisions
105 WHERE decisions.status=0 AND :now > due`,
106 sqlGetNextPendingDecisionDue: `
107 SELECT due FROM decisions WHERE status=0 ORDER BY due LIMIT 1`,
108 sqlGetVotersForProxy: `
109 SELECT id, name, reminder
110 FROM voters WHERE enabled=1 AND id != $1`,
111 sqlGetReminderVoters: `
112 SELECT id, name, reminder FROM voters WHERE enabled=1 AND reminder!='' AND reminder IS NOT NULL`,
113 sqlFindUnvotedDecisionsForVoter: `
114 SELECT tag, title, votetype, due
115 FROM decisions
116 WHERE status = 0 AND id NOT IN (SELECT decision FROM votes WHERE voter = $1)
117 ORDER BY due ASC`,
118 sqlCreateVote: `
119 INSERT OR REPLACE INTO votes (decision, voter, vote, voted, notes)
120 VALUES (:decision, :voter, :vote, :voted, :notes)`,
121 sqlLoadVote: `
122 SELECT decision, voter, vote, voted, notes
123 FROM votes
124 WHERE decision=$1 AND voter=$2`,
125 }
126
127 type VoteType uint8
128 type VoteStatus int8
129
130 type Decision struct {
131 Id int64
132 Proposed time.Time
133 ProponentId int64 `db:"proponent"`
134 Title string
135 Content string
136 Quorum int
137 Majority int
138 Status VoteStatus
139 Due time.Time
140 Modified time.Time
141 Tag string
142 VoteType VoteType
143 }
144
145 type Voter struct {
146 Id int64
147 Name string
148 Enabled bool
149 Reminder string // reminder email address
150 }
151
152 type VoteChoice int
153
154 const (
155 voteAye = 1
156 voteNaye = -1
157 voteAbstain = 0
158 )
159
160 const (
161 voteTypeMotion = 0
162 voteTypeVeto = 1
163 )
164
165 func (v VoteType) String() string {
166 switch v {
167 case voteTypeMotion:
168 return "motion"
169 case voteTypeVeto:
170 return "veto"
171 default:
172 return "unknown"
173 }
174 }
175
176 func (v VoteType) QuorumAndMajority() (int, int) {
177 switch v {
178 case voteTypeMotion:
179 return 3, 50
180 default:
181 return 1, 99
182 }
183 }
184
185 func (v VoteChoice) String() string {
186 switch v {
187 case voteAye:
188 return "aye"
189 case voteNaye:
190 return "naye"
191 case voteAbstain:
192 return "abstain"
193 default:
194 return "unknown"
195 }
196 }
197
198 var VoteValues = map[string]VoteChoice{
199 "aye": voteAye,
200 "naye": voteNaye,
201 "abstain": voteAbstain,
202 }
203
204 var VoteChoices = map[int64]VoteChoice{
205 1: voteAye,
206 0: voteAbstain,
207 -1: voteNaye,
208 }
209
210 const (
211 voteStatusDeclined = -1
212 voteStatusPending = 0
213 voteStatusApproved = 1
214 voteStatusWithdrawn = -2
215 )
216
217 func (v VoteStatus) String() string {
218 switch v {
219 case voteStatusDeclined:
220 return "declined"
221 case voteStatusPending:
222 return "pending"
223 case voteStatusApproved:
224 return "approved"
225 case voteStatusWithdrawn:
226 return "withdrawn"
227 default:
228 return "unknown"
229 }
230 }
231
232 type Vote struct {
233 DecisionId int64 `db:"decision"`
234 VoterId int64 `db:"voter"`
235 Vote VoteChoice
236 Voted time.Time
237 Notes string
238 }
239
240 type dbHandler struct {
241 db *sqlx.DB
242 }
243
244 var db *dbHandler
245
246 func NewDB(database *sql.DB) *dbHandler {
247 handler := &dbHandler{db: sqlx.NewDb(database, "sqlite3")}
248 failedStatements := make([]string, 0)
249 for _, sqlStatement := range sqlStatements {
250 var stmt *sqlx.Stmt
251 stmt, err := handler.db.Preparex(sqlStatement)
252 if err != nil {
253 log.Critical("ERROR parsing statement %s: %s", sqlStatement, err)
254 failedStatements = append(failedStatements, sqlStatement)
255 }
256 stmt.Close()
257 }
258 if len(failedStatements) > 0 {
259 log.Panicf("%d statements failed to prepare", len(failedStatements))
260 }
261
262 version, err := migrate.Exec(database, "sqlite3", dbmig.Migrations(), migrate.Up)
263 if err != nil {
264 log.Panicf("running database migration failed: %v", err)
265 } else {
266 log.Infof("database is now at version %d", version)
267 }
268 return handler
269 }
270
271 func (d *dbHandler) Close() error {
272 return d.db.Close()
273 }
274
275 func (d *dbHandler) getPreparedNamedStatement(statementKey sqlKey) *sqlx.NamedStmt {
276 statement, err := d.db.PrepareNamed(sqlStatements[statementKey])
277 if err != nil {
278 log.Panicf("Preparing statement failed: %v", err)
279 }
280 return statement
281 }
282
283 func (d *dbHandler) getPreparedStatement(statementKey sqlKey) *sqlx.Stmt {
284 statement, err := d.db.Preparex(sqlStatements[statementKey])
285 if err != nil {
286 log.Panicf("Preparing statement failed: %v", err)
287 }
288 return statement
289 }
290
291 func (v *Vote) Save() (err error) {
292 insertVoteStmt := db.getPreparedNamedStatement(sqlCreateVote)
293 defer insertVoteStmt.Close()
294
295 if _, err = insertVoteStmt.Exec(v); err != nil {
296 log.Errorf("saving vote failed: %v", err)
297 return
298 }
299
300 getVoteStmt := db.getPreparedStatement(sqlLoadVote)
301 defer getVoteStmt.Close()
302
303 if err = getVoteStmt.Get(v, v.DecisionId, v.VoterId); err != nil {
304 log.Errorf("getting inserted vote failed: %v", err)
305 return
306 }
307
308 return
309 }
310
311 type VoteSums struct {
312 Ayes int
313 Nayes int
314 Abstains int
315 }
316
317 func (v *VoteSums) VoteCount() int {
318 return v.Ayes + v.Nayes + v.Abstains
319 }
320
321 func (v *VoteSums) TotalVotes() int {
322 return v.Ayes + v.Nayes
323 }
324
325 func (v *VoteSums) Percent() int {
326 totalVotes := v.TotalVotes()
327 if totalVotes == 0 {
328 return 0
329 }
330 return v.Ayes * 100 / totalVotes
331 }
332
333 func (v *VoteSums) CalculateResult(quorum int, majority int) (status VoteStatus, reasoning string) {
334 if v.VoteCount() < quorum {
335 status, reasoning = voteStatusDeclined, fmt.Sprintf("Needed quorum of %d has not been reached.", quorum)
336 } else if (v.Ayes / v.TotalVotes()) < (majority / 100) {
337 status, reasoning = voteStatusDeclined, fmt.Sprintf("Needed majority of %d%% has not been reached.", majority)
338 } else {
339 status, reasoning = voteStatusApproved, "Quorum and majority have been reached"
340 }
341 return
342 }
343
344 type VoteForDisplay struct {
345 Vote
346 Name string
347 }
348
349 type DecisionForDisplay struct {
350 Decision
351 Proposer string `db:"proposer"`
352 *VoteSums
353 Votes []VoteForDisplay
354 }
355
356 func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err error) {
357 decisionStmt := db.getPreparedStatement(sqlLoadDecisionByTag)
358 defer decisionStmt.Close()
359
360 decision = &DecisionForDisplay{}
361 if err = decisionStmt.Get(decision, tag); err != nil {
362 if err == sql.ErrNoRows {
363 decision = nil
364 err = nil
365 } else {
366 log.Errorf("getting motion %s failed: %v", tag, err)
367 return
368 }
369 }
370 decision.VoteSums, err = decision.Decision.VoteSums()
371 return
372 }
373
374 // FindDecisionsForDisplayOnPage loads a set of decisions from the database.
375 //
376 // This function uses OFFSET for pagination which is not a good idea for larger data sets.
377 //
378 // TODO: migrate to timestamp base pagination
379 func FindDecisionsForDisplayOnPage(page int64, unvoted bool, voter *Voter) (decisions []*DecisionForDisplay, err error) {
380 var decisionsStmt *sqlx.Stmt
381 if unvoted && voter != nil {
382 decisionsStmt = db.getPreparedStatement(sqlLoadUnvotedDecisions)
383 } else {
384 decisionsStmt = db.getPreparedStatement(sqlLoadDecisions)
385 }
386 defer decisionsStmt.Close()
387
388 var rows *sqlx.Rows
389 if unvoted && voter != nil {
390 rows, err = decisionsStmt.Queryx(voter.Id, page-1)
391 } else {
392 rows, err = decisionsStmt.Queryx(page - 1)
393 }
394 if err != nil {
395 log.Errorf("loading motions for page %d failed: %v", page, err)
396 return
397 }
398 defer rows.Close()
399
400 for rows.Next() {
401 var d DecisionForDisplay
402 if err = rows.StructScan(&d); err != nil {
403 log.Errorf("loading motions for page %d failed: %v", page, err)
404 return
405 }
406 d.VoteSums, err = d.Decision.VoteSums()
407 if err != nil {
408 return
409 }
410 decisions = append(decisions, &d)
411 }
412 return
413 }
414
415 func (d *Decision) VoteSums() (sums *VoteSums, err error) {
416 votesStmt := db.getPreparedStatement(sqlLoadVoteCountsForDecision)
417 defer votesStmt.Close()
418
419 voteRows, err := votesStmt.Queryx(d.Id)
420 if err != nil {
421 log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err)
422 return
423 }
424 defer voteRows.Close()
425
426 sums = &VoteSums{}
427 for voteRows.Next() {
428 var vote VoteChoice
429 var count int
430 if err = voteRows.Scan(&vote, &count); err != nil {
431 log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err)
432 return
433 }
434 switch vote {
435 case voteAye:
436 sums.Ayes = count
437 case voteNaye:
438 sums.Nayes = count
439 case voteAbstain:
440 sums.Abstains = count
441 }
442 }
443 return
444 }
445
446 func (d *DecisionForDisplay) LoadVotes() (err error) {
447 votesStmt := db.getPreparedStatement(sqlLoadVotesForDecision)
448 defer votesStmt.Close()
449
450 err = votesStmt.Select(&d.Votes, d.Id)
451 if err != nil {
452 log.Errorf("selecting votes for motion %s failed: %v", d.Tag, err)
453 return
454 }
455 return
456 }
457
458 func (d *Decision) OlderExists(unvoted bool, voter *Voter) (result bool, err error) {
459 if unvoted && voter != nil {
460 olderStmt := db.getPreparedStatement(sqlCountOlderThanUnvotedDecision)
461 defer olderStmt.Close()
462
463 if err = olderStmt.Get(&result, d.Proposed, voter.Id); err != nil {
464 log.Errorf("finding older motions than %s failed: %v", d.Tag, err)
465 return
466 }
467 } else {
468 olderStmt := db.getPreparedStatement(sqlCountOlderThanDecision)
469 defer olderStmt.Close()
470
471 if err = olderStmt.Get(&result, d.Proposed); err != nil {
472 log.Errorf("finding older motions than %s failed: %v", d.Tag, err)
473 return
474 }
475 }
476
477 return
478 }
479
480 func (d *Decision) Create() (err error) {
481 insertDecisionStmt := db.getPreparedNamedStatement(sqlCreateDecision)
482 defer insertDecisionStmt.Close()
483
484 result, err := insertDecisionStmt.Exec(d)
485 if err != nil {
486 log.Errorf("creating motion failed: %v", err)
487 return
488 }
489
490 lastInsertId, err := result.LastInsertId()
491 if err != nil {
492 log.Errorf("getting id of inserted motion failed: %v", err)
493 return
494 }
495 rescheduleChannel <- JobIdCloseDecisions
496
497 getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById)
498 defer getDecisionStmt.Close()
499
500 err = getDecisionStmt.Get(d, lastInsertId)
501 if err != nil {
502 log.Errorf("getting inserted motion failed: %v", err)
503 return
504 }
505
506 return
507 }
508
509 func (d *Decision) LoadWithId() (err error) {
510 getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById)
511 defer getDecisionStmt.Close()
512
513 err = getDecisionStmt.Get(d, d.Id)
514 if err != nil {
515 log.Errorf("loading updated motion failed: %v", err)
516 return
517 }
518
519 return
520 }
521
522 func (d *Decision) Update() (err error) {
523 updateDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecision)
524 defer updateDecisionStmt.Close()
525
526 result, err := updateDecisionStmt.Exec(d)
527 if err != nil {
528 log.Errorf("updating motion failed: %v", err)
529 return
530 }
531 affectedRows, err := result.RowsAffected()
532 if err != nil {
533 log.Error("Problem determining the affected rows")
534 return
535 } else if affectedRows != 1 {
536 log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
537 }
538 rescheduleChannel <- JobIdCloseDecisions
539
540 err = d.LoadWithId()
541 return
542 }
543
544 func (d *Decision) UpdateStatus() (err error) {
545 updateStatusStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus)
546 defer updateStatusStmt.Close()
547
548 result, err := updateStatusStmt.Exec(d)
549 if err != nil {
550 log.Errorf("setting motion status failed: %v", err)
551 return
552 }
553 affectedRows, err := result.RowsAffected()
554 if err != nil {
555 log.Errorf("determining the affected rows failed: %v", err)
556 return
557 } else if affectedRows != 1 {
558 log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
559 }
560 rescheduleChannel <- JobIdCloseDecisions
561
562 err = d.LoadWithId()
563 return
564 }
565
566 func (d *Decision) String() string {
567 return fmt.Sprintf("%s %s (Id %d)", d.Tag, d.Title, d.Id)
568 }
569
570 func FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
571 findVoterStmt := db.getPreparedStatement(sqlLoadEnabledVoterByEmail)
572 defer findVoterStmt.Close()
573
574 voter = &Voter{}
575 if err = findVoterStmt.Get(voter, emailAddress); err != nil {
576 if err != sql.ErrNoRows {
577 log.Errorf("getting voter for address %s failed: %v", emailAddress, err)
578 } else {
579 err = nil
580 voter = nil
581 }
582 }
583 return
584 }
585
586 func (d *Decision) Close() error {
587 quorum, majority := d.VoteType.QuorumAndMajority()
588
589 var voteSums *VoteSums
590 var err error
591
592 if voteSums, err = d.VoteSums(); err != nil {
593 log.Errorf("getting vote sums failed: %v", err)
594 return err
595 }
596 var reasoning string
597 d.Status, reasoning = voteSums.CalculateResult(quorum, majority)
598
599 closeDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus)
600 defer closeDecisionStmt.Close()
601
602 result, err := closeDecisionStmt.Exec(d)
603 if err != nil {
604 log.Errorf("closing vote failed: %v", err)
605 return err
606 }
607 if affectedRows, err := result.RowsAffected(); err != nil {
608 log.Errorf("getting affected rows failed: %v", err)
609 return err
610 } else if affectedRows != 1 {
611 log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
612 }
613
614 NotifyMailChannel <- NewNotificationClosedDecision(d, voteSums, reasoning)
615
616 log.Infof("decision %s closed with result %s: reasoning %s", d.Tag, d.Status, reasoning)
617
618 return nil
619 }
620
621 func CloseDecisions() (err error) {
622 getClosableDecisionsStmt := db.getPreparedNamedStatement(sqlSelectClosableDecisions)
623 defer getClosableDecisionsStmt.Close()
624
625 decisions := make([]*Decision, 0)
626 rows, err := getClosableDecisionsStmt.Queryx(struct{ Now time.Time }{time.Now().UTC()})
627 if err != nil {
628 log.Errorf("fetching closable decisions failed: %v", err)
629 return
630 }
631 defer rows.Close()
632 for rows.Next() {
633 decision := &Decision{}
634 if err = rows.StructScan(decision); err != nil {
635 log.Errorf("scanning row failed: %v", err)
636 return
637 }
638 decisions = append(decisions, decision)
639 }
640 rows.Close()
641
642 for _, decision := range decisions {
643 log.Debugf("found closable decision %s", decision.Tag)
644 if err = decision.Close(); err != nil {
645 log.Errorf("closing decision %s failed: %s", decision.Tag, err)
646 return
647 }
648 }
649
650 return
651 }
652
653 func GetNextPendingDecisionDue() (due *time.Time, err error) {
654 getNextPendingDecisionDueStmt := db.getPreparedStatement(sqlGetNextPendingDecisionDue)
655 defer getNextPendingDecisionDueStmt.Close()
656
657 row := getNextPendingDecisionDueStmt.QueryRow()
658
659 due = &time.Time{}
660 if err = row.Scan(due); err != nil {
661 if err == sql.ErrNoRows {
662 log.Debug("No pending decisions")
663 return nil, nil
664 }
665 log.Errorf("parsing result failed: %v", err)
666 return nil, err
667 }
668
669 return
670 }
671
672 func GetReminderVoters() (voters *[]Voter, err error) {
673 getReminderVotersStmt := db.getPreparedStatement(sqlGetReminderVoters)
674 defer getReminderVotersStmt.Close()
675
676 voterSlice := make([]Voter, 0)
677
678 if err = getReminderVotersStmt.Select(&voterSlice); err != nil {
679 log.Errorf("getting voters failed: %v", err)
680 return
681 }
682 voters = &voterSlice
683
684 return
685 }
686
687 func FindUnvotedDecisionsForVoter(voter *Voter) (decisions *[]Decision, err error) {
688 findUnvotedDecisionsForVoterStmt := db.getPreparedStatement(sqlFindUnvotedDecisionsForVoter)
689 defer findUnvotedDecisionsForVoterStmt.Close()
690
691 decisionsSlice := make([]Decision, 0)
692
693 if err = findUnvotedDecisionsForVoterStmt.Select(&decisionsSlice, voter.Id); err != nil {
694 log.Errorf("getting unvoted decisions failed: %v", err)
695 return
696 }
697 decisions = &decisionsSlice
698
699 return
700 }
701
702 func GetVoterById(id int64) (voter *Voter, err error) {
703 getVoterByIdStmt := db.getPreparedStatement(sqlGetEnabledVoterById)
704 defer getVoterByIdStmt.Close()
705
706 voter = &Voter{}
707 if err = getVoterByIdStmt.Get(voter, id); err != nil {
708 log.Errorf("getting voter failed: %v", err)
709 return
710 }
711
712 return
713 }
714
715 func GetVotersForProxy(proxy *Voter) (voters *[]Voter, err error) {
716 getVotersForProxyStmt := db.getPreparedStatement(sqlGetVotersForProxy)
717 defer getVotersForProxyStmt.Close()
718
719 votersSlice := make([]Voter, 0)
720
721 if err = getVotersForProxyStmt.Select(&votersSlice, proxy.Id); err != nil {
722 log.Errorf("Error getting voters for proxy failed: %v", err)
723 return
724 }
725 voters = &votersSlice
726
727 return
728 }