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