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