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