Skip to content

Commit ac72ac1

Browse files
authored
Merge pull request #98 from ComputerScienceHouse/eboard-vote-v2
E-Board vote v2
2 parents 8705fe9 + c08e0fe commit ac72ac1

File tree

14 files changed

+230
-72
lines changed

14 files changed

+230
-72
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ VOTE_SLACK_BOT_TOKEN=
3232
### Dev Overrides
3333
`DEV_DISABLE_ACTIVE_FILTERS="true"` will disable the requirements that you be active to vote
3434
`DEV_FORCE_IS_EVALS="true"` will force vote to treat all users as the Evals director
35-
`DEV_FORCE_IS_CHAIR="true"` will force vote to treat all users as the Chairperson
3635

3736
## Linting
3837
These will be checked by CI

docker-compose.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ services:
2222
VOTE_STATE: 27a28540e47ec786b7bdad03f83171b3
2323
DEV_DISABLE_ACTIVE_FILTERS: "${DEV_DISABLE_ACTIVE_FILTERS}"
2424
DEV_FORCE_IS_EVALS: "${DEV_FORCE_IS_EVALS}"
25-
DEV_FORCE_IS_CHAIR: "${DEV_FORCE_IS_CHAIR}"
2625
ports:
2726
- "127.0.0.1:8080:8080"
2827

eboard.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"slices"
7+
"strings"
8+
9+
"github.com/gin-gonic/gin"
10+
)
11+
12+
var votes map[string]float32
13+
var voters []string
14+
15+
var OPTIONS = []string{"Pass", "Fail", "Abstain"}
16+
17+
func HandleGetEboardVote(c *gin.Context) {
18+
user := getUserData(c)
19+
if !slices.Contains(user.Groups, "eboard") {
20+
c.JSON(http.StatusUnauthorized, gin.H{"error": "You need to be E-Board to access this page"})
21+
return
22+
}
23+
if votes == nil {
24+
votes = make(map[string]float32)
25+
}
26+
fmt.Println(votes)
27+
c.HTML(http.StatusOK, "eboard.tmpl", gin.H{
28+
"Username": user,
29+
"Voted": slices.Contains(voters, user.Username),
30+
"Results": votes,
31+
"Options": OPTIONS,
32+
})
33+
}
34+
35+
func HandlePostEboardVote(c *gin.Context) {
36+
user := getUserData(c)
37+
if !slices.Contains(user.Groups, "eboard") {
38+
c.JSON(http.StatusUnauthorized, gin.H{"error": "You need to be E-Board to access this page"})
39+
return
40+
}
41+
if slices.Contains(voters, user.Username) {
42+
c.JSON(http.StatusBadRequest, gin.H{"error": "You cannot vote again!"})
43+
return
44+
}
45+
i := slices.IndexFunc(user.Groups, func(s string) bool {
46+
return strings.Contains(s, "eboard-")
47+
})
48+
if i == -1 {
49+
c.JSON(http.StatusBadRequest, gin.H{"error": "You have the eboard group but not an eboard-[position] group. What is wrong with you?"})
50+
return
51+
}
52+
//get the eboard position, count the members, and divide one whole vote by the number of members in the position
53+
position := user.Groups[i]
54+
positionMembers := oidcClient.GetOIDCGroup(oidcClient.FindOIDCGroupID(position))
55+
weight := 1.0 / float32(len(positionMembers))
56+
fmt.Println(weight)
57+
//post the vote
58+
option := c.PostForm("option")
59+
if option == "" {
60+
c.JSON(http.StatusBadRequest, gin.H{"error": "You need to pick an option"})
61+
}
62+
votes[option] = votes[option] + weight
63+
voters = append(voters, user.Username)
64+
c.Redirect(http.StatusFound, "/eboard")
65+
}
66+
67+
func HandleManageEboardVote(c *gin.Context) {
68+
user := getUserData(c)
69+
if !slices.Contains(user.Groups, "eboard") {
70+
c.JSON(http.StatusUnauthorized, gin.H{"error": "You need to be E-Board to access this page"})
71+
return
72+
}
73+
if c.PostForm("clear_vote") != "" {
74+
votes = make(map[string]float32)
75+
voters = make([]string, 0)
76+
}
77+
c.Redirect(http.StatusFound, "/eboard")
78+
}

main.go

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,19 @@ var CONDITIONAL_GATEKEEP_URL = os.Getenv("VOTE_CONDITIONAL_URL")
3030
var VOTE_HOST = os.Getenv("VOTE_HOST")
3131

3232
// Dev mode flags
33-
var DEV_DISABLE_ACTIVE_FILTERS = os.Getenv("DEV_DISABLE_ACTIVE_FILTERS") == "true"
34-
var DEV_FORCE_IS_EVALS = os.Getenv("DEV_FORCE_IS_EVALS") == "true"
35-
var DEV_FORCE_IS_CHAIR = os.Getenv("DEV_FORCE_IS_CHAIR") == "true"
33+
var DEV_DISABLE_ACTIVE_FILTERS bool = os.Getenv("DEV_DISABLE_ACTIVE_FILTERS") == "true"
34+
var DEV_FORCE_IS_EVALS bool = os.Getenv("DEV_FORCE_IS_EVALS") == "true"
3635

3736
func inc(x int) string {
3837
return strconv.Itoa(x + 1)
3938
}
4039

41-
// GetVoterCount Gets the number of people eligible to vote in a poll
40+
// Gets the number of people eligible to vote in a poll
4241
func GetVoterCount(poll database.Poll) int {
4342
return len(poll.AllowedUsers)
4443
}
4544

46-
// CalculateQuorum Calculates the number of votes required for quorum in a poll
45+
// Calculates the number of votes required for quorum in a poll
4746
func CalculateQuorum(poll database.Poll) int {
4847
voterCount := GetVoterCount(poll)
4948
return int(math.Ceil(float64(voterCount) * poll.QuorumType))
@@ -96,12 +95,15 @@ func main() {
9695
r.GET("/auth/callback", csh.AuthCallback)
9796
r.GET("/auth/logout", csh.AuthLogout)
9897

98+
r.GET("/eboard", csh.AuthWrapper(HandleGetEboardVote))
99+
r.POST("/eboard", csh.AuthWrapper(HandlePostEboardVote))
100+
r.POST("/eboard/manage", csh.AuthWrapper(HandleManageEboardVote))
101+
99102
// TODO: change ALL the response codes to use http.(actual description)
100103
r.GET("/", csh.AuthWrapper(func(c *gin.Context) {
101-
cl, _ := c.Get("cshauth")
102-
claims := cl.(cshAuth.CSHClaims)
103104
// This is intentionally left unprotected
104105
// A user may be unable to vote but should still be able to see a list of polls
106+
user := getUserData(c)
105107

106108
polls, err := database.GetOpenPolls(c)
107109
if err != nil {
@@ -114,8 +116,9 @@ func main() {
114116

115117
c.HTML(http.StatusOK, "index.tmpl", gin.H{
116118
"Polls": polls,
117-
"Username": claims.UserInfo.Username,
118-
"FullName": claims.UserInfo.FullName,
119+
"Username": user.Username,
120+
"FullName": user.FullName,
121+
"EBoard": slices.Contains(user.Groups, "eboard"),
119122
})
120123
}))
121124

@@ -200,19 +203,8 @@ func main() {
200203
AllowWriteIns: c.PostForm("allowWriteIn") == "true",
201204
Hidden: c.PostForm("hidden") == "true",
202205
}
203-
switch c.PostForm("pollType") {
204-
case "rankedChoice":
206+
if c.PostForm("rankedChoice") == "true" {
205207
poll.VoteType = database.POLL_TYPE_RANKED
206-
case "eboard":
207-
eboard := oidcClient.GetEBoard()
208-
var usernames []string
209-
for _, member := range eboard {
210-
usernames = append(usernames, member.Username)
211-
}
212-
poll.AllowedUsers = usernames
213-
poll.AllowWriteIns = false
214-
poll.Hidden = true
215-
poll.Gatekeep = false
216208
}
217209

218210
switch c.PostForm("options") {
@@ -361,6 +353,7 @@ func main() {
361353

362354
vote.Options[option] = optionRank
363355
}
356+
364357
// process write-in
365358
if c.PostForm("writeinOption") != "" && c.PostForm("writein") != "" {
366359
for candidate := range vote.Options {
@@ -380,8 +373,8 @@ func main() {
380373
}
381374
vote.Options[c.PostForm("writeinOption")] = rank
382375
}
383-
// Perform checks, vote does not change beyond this
384376

377+
// Perform checks, vote does not change beyond this
385378
optionCount := len(vote.Options)
386379
voted := make([]bool, optionCount)
387380

@@ -394,6 +387,10 @@ func main() {
394387
// Duplicate ranks and range check
395388
for _, rank := range vote.Options {
396389
if rank > 0 && rank <= optionCount {
390+
if rank > optionCount {
391+
c.JSON(http.StatusBadRequest, gin.H{"error": "Rank choice is more than the amount of candidates ranked"})
392+
return
393+
}
397394
if voted[rank-1] {
398395
c.JSON(http.StatusBadRequest, gin.H{"error": "You ranked two or more candidates at the same level"})
399396
return
@@ -562,10 +559,6 @@ func isEvals(user cshAuth.CSHUserInfo) bool {
562559
return DEV_FORCE_IS_EVALS || slices.Contains(user.Groups, "eboard-evaluations")
563560
}
564561

565-
func isChair(user cshAuth.CSHUserInfo) bool {
566-
return DEV_FORCE_IS_CHAIR || slices.Contains(user.Groups, "eboard-chairman")
567-
}
568-
569562
// canVote determines whether a user can cast a vote.
570563
//
571564
// returns an integer value: 0 is success, 1 is database error, 3 is not active, 4 is gatekept, 9 is already voted
@@ -591,6 +584,12 @@ func canVote(user cshAuth.CSHUserInfo, poll database.Poll, allowedUsers []string
591584
return 0
592585
}
593586

587+
func getUserData(c *gin.Context) cshAuth.CSHUserInfo {
588+
cl, _ := c.Get("cshauth")
589+
user := cl.(cshAuth.CSHClaims).UserInfo
590+
return user
591+
}
592+
594593
func uniquePolls(polls []*database.Poll) []*database.Poll {
595594
var unique []*database.Poll
596595
for _, poll := range polls {

templates/closed.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<!-- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" /> -->
66
<link
77
rel="stylesheet"
8-
href="https://assets.csh.rit.edu/csh-material-bootstrap/4.3.1/dist/csh-material-bootstrap.min.css"
8+
href="https://assets.csh.rit.edu/csh-material-bootstrap/4.6.2/dist/csh-material-bootstrap.min.css"
99
media="screen"
1010
/>
1111

templates/create.tmpl

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<title>CSH Vote</title>
55
<!-- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" /> -->
6-
<link rel="stylesheet" href="https://assets.csh.rit.edu/csh-material-bootstrap/4.3.1/dist/csh-material-bootstrap.min.css" media="screen">
6+
<link rel="stylesheet" href="https://assets.csh.rit.edu/csh-material-bootstrap/4.6.2/dist/csh-material-bootstrap.min.css" media="screen">
77

88
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
99

@@ -59,26 +59,11 @@
5959
</div>
6060
<div class="form-group">
6161
<input
62-
type="radio"
63-
name="pollType"
64-
value="rankedChoice"
62+
type="checkbox"
63+
name="rankedChoice"
64+
value="true"
6565
>
6666
<span>Ranked Choice Vote</span>
67-
<input
68-
type="radio"
69-
name="pollType"
70-
value="eboard"
71-
class="ml-3"
72-
>
73-
<span>E-Board Vote</span>
74-
<input
75-
type="radio"
76-
name="pollType"
77-
value="simple"
78-
class="ml-3"
79-
checked="checked"
80-
>
81-
<span>Simple (Single-Choice) Vote</span>
8267
</div>
8368
<div class="form-group">
8469
<input

templates/eboard.tmpl

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>CSH Vote</title>
5+
<link rel="stylesheet"
6+
href="https://assets.csh.rit.edu/csh-material-bootstrap/4.6.2/dist/csh-material-bootstrap.min.css"
7+
media="screen">
8+
9+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
10+
11+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
12+
</head>
13+
<body>
14+
{{ template "nav" . }}
15+
<div class="container main p-5">
16+
<h2>E-Board Vote</h2>
17+
<div class="row">
18+
<div class="col-md-8">
19+
{{ if .Voted }}
20+
{{ range $option, $count := .Results }}
21+
<div id="{{ $option }}" style="font-size: 1.25rem; line-height: 1.25">
22+
{{ $option }}: {{ $count }}
23+
</div>
24+
<br/>
25+
{{ end }}
26+
{{ else }}
27+
<form method="POST">
28+
{{ range $i, $option := .Options }}
29+
<div class="form-check">
30+
<input class="form-check-input" type="radio" name="option"
31+
id="{{ $option }}"
32+
value="{{ $option }}" required/>
33+
<label style="font-size: 1.25rem; line-height: 1.25; padding-left: 4px;"
34+
class="form-check-label"
35+
for="{{ $option }}">{{ $option }}</label>
36+
</div>
37+
<br/>
38+
{{ end }}
39+
<button type="submit" class="btn btn-primary">Submit</button>
40+
</form>
41+
{{ end }}
42+
</div>
43+
<div class="col-md-4">
44+
<form action="/eboard/manage" method="POST" onsubmit="location.reload()">
45+
<input class="d-none" name="clear_vote" value="true">
46+
<button type="submit" class="btn btn-warning">Clear Votes</button>
47+
</form>
48+
</div>
49+
</div>
50+
</div>
51+
</body>
52+
</html>

templates/hidden.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<!-- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" /> -->
66
<link
77
rel="stylesheet"
8-
href="https://assets.csh.rit.edu/csh-material-bootstrap/4.3.1/dist/csh-material-bootstrap.min.css"
8+
href="https://assets.csh.rit.edu/csh-material-bootstrap/4.6.2/dist/csh-material-bootstrap.min.css"
99
media="screen"
1010
/>
1111

templates/index.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<!-- <link rel="stylesheet" href="https://themeswitcher.csh.rit.edu/api/get" /> -->
66
<link
77
rel="stylesheet"
8-
href="https://assets.csh.rit.edu/csh-material-bootstrap/4.3.1/dist/csh-material-bootstrap.min.css"
8+
href="https://assets.csh.rit.edu/csh-material-bootstrap/4.6.2/dist/csh-material-bootstrap.min.css"
99
media="screen"
1010
/>
1111

templates/nav.tmpl

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
{{ define "nav" }}
2-
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
2+
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
33
<div class="container">
4-
<a class="navbar-brand" href="/">Vote</a>
4+
<a class="navbar-brand" href="/">Vote</a>
55

6-
<button class="navbar-toggler collapsed" type="button" data-toggle="collapse" data-target="#navbar"
7-
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
8-
<span class="navbar-toggler-icon"></span>
9-
</button>
10-
<div id="navbar" class="collapse navbar-collapse">
11-
<a class="nav-item nav-link text-light" href="/"><i class="bi bi-exclamation-circle-fill"></i> Open Polls</a>
12-
<a class="nav-item nav-link text-light" href="/closed"><i class="bi bi-archive-fill"></i> Closed Polls</a>
13-
<a class="nav-item nav-link text-light" href="/create"><i class="bi bi-plus-circle-fill"></i> Create Poll</a>
14-
<div class="nav navbar-nav ml-auto">
15-
<div class="navbar-user">
16-
<img src="https://profiles.csh.rit.edu/image/{{ .Username }}" />
17-
<span class="text-light">{{ .FullName }}</span>
18-
<a href="/auth/logout" style="color: #c3c3c3;"><i>(logout)</i></a>
19-
</div>
20-
</div>
6+
<button class="navbar-toggler collapsed" type="button" data-toggle="collapse" data-target="#navbar"
7+
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
8+
<span class="navbar-toggler-icon"></span>
9+
</button>
10+
<div id="navbar" class="collapse navbar-collapse">
11+
<a class="nav-item nav-link text-light" href="/"><i class="bi bi-exclamation-circle-fill"></i> Open Polls</a>
12+
<a class="nav-item nav-link text-light" href="/closed"><i class="bi bi-archive-fill"></i> Closed Polls</a>
13+
<a class="nav-item nav-link text-light" href="/create"><i class="bi bi-plus-circle-fill"></i> Create Poll</a>
14+
{{ if .EBoard }}
15+
<a class="nav-item nav-link text-light" href="/eboard"> E-Board</a>
16+
{{ end }}
17+
<div class="nav navbar-nav ml-auto">
18+
<div class="navbar-user">
19+
<img src="https://profiles.csh.rit.edu/image/{{ .Username }}" />
20+
<span class="text-light">{{ .FullName }}</span>
21+
<a href="/auth/logout" style="color: #c3c3c3;"><i>(logout)</i></a>
22+
</div>
2123
</div>
24+
</div>
2225
</div>
2326

2427
<script>
@@ -31,6 +34,6 @@
3134
});
3235
});
3336
</script>
34-
</nav>
37+
</nav>
3538

3639
{{ end }}

0 commit comments

Comments
 (0)