Ignore count of applied migrations from migrate.Exec
[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 _, err := migrate.Exec(database, "sqlite3", dbmig.Migrations(), migrate.Up)
263 if err != nil {
264 log.Panicf("running database migration failed: %v", err)
265 }
266 return handler
267 }
268
269 func (d *dbHandler) Close() error {
270 return d.db.Close()
271 }
272
273 func (d *dbHandler) getPreparedNamedStatement(statementKey sqlKey) *sqlx.NamedStmt {
274 statement, err := d.db.PrepareNamed(sqlStatements[statementKey])
275 if err != nil {
276 log.Panicf("Preparing statement failed: %v", err)
277 }
278 return statement
279 }
280
281 func (d *dbHandler) getPreparedStatement(statementKey sqlKey) *sqlx.Stmt {
282 statement, err := d.db.Preparex(sqlStatements[statementKey])
283 if err != nil {
284 log.Panicf("Preparing statement failed: %v", err)
285 }
286 return statement
287 }
288
289 func (v *Vote) Save() (err error) {
290 insertVoteStmt := db.getPreparedNamedStatement(sqlCreateVote)
291 defer insertVoteStmt.Close()
292
293 if _, err = insertVoteStmt.Exec(v); err != nil {
294 log.Errorf("saving vote failed: %v", err)
295 return
296 }
297
298 getVoteStmt := db.getPreparedStatement(sqlLoadVote)
299 defer getVoteStmt.Close()
300
301 if err = getVoteStmt.Get(v, v.DecisionId, v.VoterId); err != nil {
302 log.Errorf("getting inserted vote failed: %v", err)
303 return
304 }
305
306 return
307 }
308
309 type VoteSums struct {
310 Ayes int
311 Nayes int
312 Abstains int
313 }
314
315 func (v *VoteSums) VoteCount() int {
316 return v.Ayes + v.Nayes + v.Abstains
317 }
318
319 func (v *VoteSums) TotalVotes() int {
320 return v.Ayes + v.Nayes
321 }
322
323 func (v *VoteSums) Percent() int {
324 totalVotes := v.TotalVotes()
325 if totalVotes == 0 {
326 return 0
327 }
328 return v.Ayes * 100 / totalVotes
329 }
330
331 func (v *VoteSums) CalculateResult(quorum int, majority int) (status VoteStatus, reasoning string) {
332 if v.VoteCount() < quorum {
333 status, reasoning = voteStatusDeclined, fmt.Sprintf("Needed quorum of %d has not been reached.", quorum)
334 } else if (v.Ayes / v.TotalVotes()) < (majority / 100) {
335 status, reasoning = voteStatusDeclined, fmt.Sprintf("Needed majority of %d%% has not been reached.", majority)
336 } else {
337 status, reasoning = voteStatusApproved, "Quorum and majority have been reached"
338 }
339 return
340 }
341
342 type VoteForDisplay struct {
343 Vote
344 Name string
345 }
346
347 type DecisionForDisplay struct {
348 Decision
349 Proposer string `db:"proposer"`
350 *VoteSums
351 Votes []VoteForDisplay
352 }
353
354 func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err error) {
355 decisionStmt := db.getPreparedStatement(sqlLoadDecisionByTag)
356 defer decisionStmt.Close()
357
358 decision = &DecisionForDisplay{}
359 if err = decisionStmt.Get(decision, tag); err != nil {
360 if err == sql.ErrNoRows {
361 decision = nil
362 err = nil
363 } else {
364 log.Errorf("getting motion %s failed: %v", tag, err)
365 return
366 }
367 }
368 decision.VoteSums, err = decision.Decision.VoteSums()
369 return
370 }
371
372 // FindDecisionsForDisplayOnPage loads a set of decisions from the database.
373 //
374 // This function uses OFFSET for pagination which is not a good idea for larger data sets.
375 //
376 // TODO: migrate to timestamp base pagination
377 func FindDecisionsForDisplayOnPage(page int64, unvoted bool, voter *Voter) (decisions []*DecisionForDisplay, err error) {
378 var decisionsStmt *sqlx.Stmt
379 if unvoted && voter != nil {
380 decisionsStmt = db.getPreparedStatement(sqlLoadUnvotedDecisions)
381 } else {
382 decisionsStmt = db.getPreparedStatement(sqlLoadDecisions)
383 }
384 defer decisionsStmt.Close()
385
386 var rows *sqlx.Rows
387 if unvoted && voter != nil {
388 rows, err = decisionsStmt.Queryx(voter.Id, page-1)
389 } else {
390 rows, err = decisionsStmt.Queryx(page - 1)
391 }
392 if err != nil {
393 log.Errorf("loading motions for page %d failed: %v", page, err)
394 return
395 }
396 defer rows.Close()
397
398 for rows.Next() {
399 var d DecisionForDisplay
400 if err = rows.StructScan(&d); err != nil {
401 log.Errorf("loading motions for page %d failed: %v", page, err)
402 return
403 }
404 d.VoteSums, err = d.Decision.VoteSums()
405 if err != nil {
406 return
407 }
408 decisions = append(decisions, &d)
409 }
410 return
411 }
412
413 func (d *Decision) VoteSums() (sums *VoteSums, err error) {
414 votesStmt := db.getPreparedStatement(sqlLoadVoteCountsForDecision)
415 defer votesStmt.Close()
416
417 voteRows, err := votesStmt.Queryx(d.Id)
418 if err != nil {
419 log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err)
420 return
421 }
422 defer voteRows.Close()
423
424 sums = &VoteSums{}
425 for voteRows.Next() {
426 var vote VoteChoice
427 var count int
428 if err = voteRows.Scan(&vote, &count); err != nil {
429 log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err)
430 return
431 }
432 switch vote {
433 case voteAye:
434 sums.Ayes = count
435 case voteNaye:
436 sums.Nayes = count
437 case voteAbstain:
438 sums.Abstains = count
439 }
440 }
441 return
442 }
443
444 func (d *DecisionForDisplay) LoadVotes() (err error) {
445 votesStmt := db.getPreparedStatement(sqlLoadVotesForDecision)
446 defer votesStmt.Close()
447
448 err = votesStmt.Select(&d.Votes, d.Id)
449 if err != nil {
450 log.Errorf("selecting votes for motion %s failed: %v", d.Tag, err)
451 return
452 }
453 return
454 }
455
456 func (d *Decision) OlderExists(unvoted bool, voter *Voter) (result bool, err error) {
457 if unvoted && voter != nil {
458 olderStmt := db.getPreparedStatement(sqlCountOlderThanUnvotedDecision)
459 defer olderStmt.Close()
460
461 if err = olderStmt.Get(&result, d.Proposed, voter.Id); err != nil {
462 log.Errorf("finding older motions than %s failed: %v", d.Tag, err)
463 return
464 }
465 } else {
466 olderStmt := db.getPreparedStatement(sqlCountOlderThanDecision)
467 defer olderStmt.Close()
468
469 if err = olderStmt.Get(&result, d.Proposed); err != nil {
470 log.Errorf("finding older motions than %s failed: %v", d.Tag, err)
471 return
472 }
473 }
474
475 return
476 }
477
478 func (d *Decision) Create() (err error) {
479 insertDecisionStmt := db.getPreparedNamedStatement(sqlCreateDecision)
480 defer insertDecisionStmt.Close()
481
482 result, err := insertDecisionStmt.Exec(d)
483 if err != nil {
484 log.Errorf("creating motion failed: %v", err)
485 return
486 }
487
488 lastInsertId, err := result.LastInsertId()
489 if err != nil {
490 log.Errorf("getting id of inserted motion failed: %v", err)
491 return
492 }
493 rescheduleChannel <- JobIdCloseDecisions
494
495 getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById)
496 defer getDecisionStmt.Close()
497
498 err = getDecisionStmt.Get(d, lastInsertId)
499 if err != nil {
500 log.Errorf("getting inserted motion failed: %v", err)
501 return
502 }
503
504 return
505 }
506
507 func (d *Decision) LoadWithId() (err error) {
508 getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById)
509 defer getDecisionStmt.Close()
510
511 err = getDecisionStmt.Get(d, d.Id)
512 if err != nil {
513 log.Errorf("loading updated motion failed: %v", err)
514 return
515 }
516
517 return
518 }
519
520 func (d *Decision) Update() (err error) {
521 updateDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecision)
522 defer updateDecisionStmt.Close()
523
524 result, err := updateDecisionStmt.Exec(d)
525 if err != nil {
526 log.Errorf("updating motion failed: %v", err)
527 return
528 }
529 affectedRows, err := result.RowsAffected()
530 if err != nil {
531 log.Error("Problem determining the affected rows")
532 return
533 } else if affectedRows != 1 {
534 log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
535 }
536 rescheduleChannel <- JobIdCloseDecisions
537
538 err = d.LoadWithId()
539 return
540 }
541
542 func (d *Decision) UpdateStatus() (err error) {
543 updateStatusStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus)
544 defer updateStatusStmt.Close()
545
546 result, err := updateStatusStmt.Exec(d)
547 if err != nil {
548 log.Errorf("setting motion status failed: %v", err)
549 return
550 }
551 affectedRows, err := result.RowsAffected()
552 if err != nil {
553 log.Errorf("determining the affected rows failed: %v", err)
554 return
555 } else if affectedRows != 1 {
556 log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
557 }
558 rescheduleChannel <- JobIdCloseDecisions
559
560 err = d.LoadWithId()
561 return
562 }
563
564 func (d *Decision) String() string {
565 return fmt.Sprintf("%s %s (Id %d)", d.Tag, d.Title, d.Id)
566 }
567
568 func FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
569 findVoterStmt := db.getPreparedStatement(sqlLoadEnabledVoterByEmail)
570 defer findVoterStmt.Close()
571
572 voter = &Voter{}
573 if err = findVoterStmt.Get(voter, emailAddress); err != nil {
574 if err != sql.ErrNoRows {
575 log.Errorf("getting voter for address %s failed: %v", emailAddress, err)
576 } else {
577 err = nil
578 voter = nil
579 }
580 }
581 return
582 }
583
584 func (d *Decision) Close() error {
585 quorum, majority := d.VoteType.QuorumAndMajority()
586
587 var voteSums *VoteSums
588 var err error
589
590 if voteSums, err = d.VoteSums(); err != nil {
591 log.Errorf("getting vote sums failed: %v", err)
592 return err
593 }
594 var reasoning string
595 d.Status, reasoning = voteSums.CalculateResult(quorum, majority)
596
597 closeDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus)
598 defer closeDecisionStmt.Close()
599
600 result, err := closeDecisionStmt.Exec(d)
601 if err != nil {
602 log.Errorf("closing vote failed: %v", err)
603 return err
604 }
605 if affectedRows, err := result.RowsAffected(); err != nil {
606 log.Errorf("getting affected rows failed: %v", err)
607 return err
608 } else if affectedRows != 1 {
609 log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
610 }
611
612 NotifyMailChannel <- NewNotificationClosedDecision(d, voteSums, reasoning)
613
614 log.Infof("decision %s closed with result %s: reasoning %s", d.Tag, d.Status, reasoning)
615
616 return nil
617 }
618
619 func CloseDecisions() (err error) {
620 getClosableDecisionsStmt := db.getPreparedNamedStatement(sqlSelectClosableDecisions)
621 defer getClosableDecisionsStmt.Close()
622
623 decisions := make([]*Decision, 0)
624 rows, err := getClosableDecisionsStmt.Queryx(struct{ Now time.Time }{time.Now().UTC()})
625 if err != nil {
626 log.Errorf("fetching closable decisions failed: %v", err)
627 return
628 }
629 defer rows.Close()
630 for rows.Next() {
631 decision := &Decision{}
632 if err = rows.StructScan(decision); err != nil {
633 log.Errorf("scanning row failed: %v", err)
634 return
635 }
636 decisions = append(decisions, decision)
637 }
638 rows.Close()
639
640 for _, decision := range decisions {
641 log.Debugf("found closable decision %s", decision.Tag)
642 if err = decision.Close(); err != nil {
643 log.Errorf("closing decision %s failed: %s", decision.Tag, err)
644 return
645 }
646 }
647
648 return
649 }
650
651 func GetNextPendingDecisionDue() (due *time.Time, err error) {
652 getNextPendingDecisionDueStmt := db.getPreparedStatement(sqlGetNextPendingDecisionDue)
653 defer getNextPendingDecisionDueStmt.Close()
654
655 row := getNextPendingDecisionDueStmt.QueryRow()
656
657 due = &time.Time{}
658 if err = row.Scan(due); err != nil {
659 if err == sql.ErrNoRows {
660 log.Debug("No pending decisions")
661 return nil, nil
662 }
663 log.Errorf("parsing result failed: %v", err)
664 return nil, err
665 }
666
667 return
668 }
669
670 func GetReminderVoters() (voters *[]Voter, err error) {
671 getReminderVotersStmt := db.getPreparedStatement(sqlGetReminderVoters)
672 defer getReminderVotersStmt.Close()
673
674 voterSlice := make([]Voter, 0)
675
676 if err = getReminderVotersStmt.Select(&voterSlice); err != nil {
677 log.Errorf("getting voters failed: %v", err)
678 return
679 }
680 voters = &voterSlice
681
682 return
683 }
684
685 func FindUnvotedDecisionsForVoter(voter *Voter) (decisions *[]Decision, err error) {
686 findUnvotedDecisionsForVoterStmt := db.getPreparedStatement(sqlFindUnvotedDecisionsForVoter)
687 defer findUnvotedDecisionsForVoterStmt.Close()
688
689 decisionsSlice := make([]Decision, 0)
690
691 if err = findUnvotedDecisionsForVoterStmt.Select(&decisionsSlice, voter.Id); err != nil {
692 log.Errorf("getting unvoted decisions failed: %v", err)
693 return
694 }
695 decisions = &decisionsSlice
696
697 return
698 }
699
700 func GetVoterById(id int64) (voter *Voter, err error) {
701 getVoterByIdStmt := db.getPreparedStatement(sqlGetEnabledVoterById)
702 defer getVoterByIdStmt.Close()
703
704 voter = &Voter{}
705 if err = getVoterByIdStmt.Get(voter, id); err != nil {
706 log.Errorf("getting voter failed: %v", err)
707 return
708 }
709
710 return
711 }
712
713 func GetVotersForProxy(proxy *Voter) (voters *[]Voter, err error) {
714 getVotersForProxyStmt := db.getPreparedStatement(sqlGetVotersForProxy)
715 defer getVotersForProxyStmt.Close()
716
717 votersSlice := make([]Voter, 0)
718
719 if err = getVotersForProxyStmt.Select(&votersSlice, proxy.Id); err != nil {
720 log.Errorf("Error getting voters for proxy failed: %v", err)
721 return
722 }
723 voters = &votersSlice
724
725 return
726 }