Improve design
- improve icons - implement VoteChoice and VoteStatus as real types with Scanner and Value methods
This commit is contained in:
parent
b8b6899cf3
commit
164495c818
7 changed files with 141 additions and 66 deletions
|
@ -155,6 +155,14 @@ func setupFormDecoder() *form.Decoder {
|
|||
|
||||
return v, nil
|
||||
}, new(models.VoteType))
|
||||
decoder.RegisterCustomTypeFunc(func(values []string) (interface{}, error) {
|
||||
v, err := models.VoteChoiceFromString(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not convert value %s: %w", values[0], err)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}, new(models.VoteChoice))
|
||||
|
||||
return decoder
|
||||
}
|
||||
|
|
|
@ -101,50 +101,109 @@ func (v *VoteType) QuorumAndMajority() (int, float32) {
|
|||
return quorumDefault, majorityDefault
|
||||
}
|
||||
|
||||
type VoteStatus int8
|
||||
type VoteStatus struct {
|
||||
Label string
|
||||
Id int8
|
||||
}
|
||||
|
||||
const (
|
||||
voteStatusDeclined VoteStatus = -1
|
||||
voteStatusPending VoteStatus = 0
|
||||
voteStatusApproved VoteStatus = 1
|
||||
voteStatusWithdrawn VoteStatus = -2
|
||||
var (
|
||||
voteStatusDeclined = &VoteStatus{Label: "declined", Id: -1}
|
||||
voteStatusPending = &VoteStatus{Label: "pending", Id: 0}
|
||||
voteStatusApproved = &VoteStatus{Label: "approved", Id: 1}
|
||||
voteStatusWithdrawn = &VoteStatus{Label: "withdrawn", Id: -2}
|
||||
)
|
||||
|
||||
var voteStatusLabels = map[VoteStatus]string{
|
||||
voteStatusDeclined: "declined",
|
||||
voteStatusPending: "pending",
|
||||
voteStatusApproved: "approved",
|
||||
voteStatusWithdrawn: "withdrawn",
|
||||
}
|
||||
|
||||
func (v VoteStatus) String() string {
|
||||
if label, ok := voteStatusLabels[v]; ok {
|
||||
return label
|
||||
func VoteStatusFromInt(id int64) (*VoteStatus, error) {
|
||||
for _, vs := range []*VoteStatus{voteStatusPending, voteStatusApproved, voteStatusWithdrawn, voteStatusDeclined} {
|
||||
if int64(vs.Id) == id {
|
||||
return vs, nil
|
||||
}
|
||||
}
|
||||
|
||||
return unknownVariant
|
||||
return nil, fmt.Errorf("unknown vote status id %d", id)
|
||||
}
|
||||
|
||||
type VoteChoice int
|
||||
func (v *VoteStatus) String() string {
|
||||
return v.Label
|
||||
}
|
||||
|
||||
const (
|
||||
voteAye VoteChoice = 1
|
||||
voteNaye VoteChoice = -1
|
||||
voteAbstain VoteChoice = 0
|
||||
func (v *VoteStatus) Scan(src any) error {
|
||||
value, ok := src.(int64)
|
||||
if !ok {
|
||||
return fmt.Errorf("could not cast %v of %T to uint8", src, src)
|
||||
}
|
||||
|
||||
vs, err := VoteStatusFromInt(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*v = *vs
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VoteStatus) Value() (driver.Value, error) {
|
||||
return int64(v.Id), nil
|
||||
}
|
||||
|
||||
type VoteChoice struct {
|
||||
label string
|
||||
id int8
|
||||
}
|
||||
|
||||
var (
|
||||
VoteAye = &VoteChoice{label: "aye", id: 1}
|
||||
VoteNaye = &VoteChoice{label: "naye", id: -1}
|
||||
VoteAbstain = &VoteChoice{label: "abstain", id: 0}
|
||||
)
|
||||
|
||||
var voteChoiceLabels = map[VoteChoice]string{
|
||||
voteAye: "aye",
|
||||
voteNaye: "naye",
|
||||
voteAbstain: "abstain",
|
||||
}
|
||||
|
||||
func (v VoteChoice) String() string {
|
||||
if label, ok := voteChoiceLabels[v]; ok {
|
||||
return label
|
||||
func VoteChoiceFromString(label string) (*VoteChoice, error) {
|
||||
for _, vc := range []*VoteChoice{VoteAye, VoteNaye, VoteAbstain} {
|
||||
if strings.EqualFold(vc.label, label) {
|
||||
return vc, nil
|
||||
}
|
||||
}
|
||||
|
||||
return unknownVariant
|
||||
return nil, fmt.Errorf("unknown vote choice %s", label)
|
||||
}
|
||||
|
||||
func VoteChoiceFromInt(id int64) (*VoteChoice, error) {
|
||||
for _, vc := range []*VoteChoice{VoteAye, VoteNaye, VoteAbstain} {
|
||||
if int64(vc.id) == id {
|
||||
return vc, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown vote type id %d", id)
|
||||
}
|
||||
|
||||
func (v *VoteChoice) String() string {
|
||||
return v.label
|
||||
}
|
||||
|
||||
func (v *VoteChoice) Scan(src any) error {
|
||||
value, ok := src.(int64)
|
||||
if !ok {
|
||||
return fmt.Errorf("could not cast %v of %T to uint8", src, src)
|
||||
}
|
||||
|
||||
vc, err := VoteChoiceFromInt(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*v = *vc
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VoteChoice) Value() (driver.Value, error) {
|
||||
return int64(v.id), nil
|
||||
}
|
||||
|
||||
func (v *VoteChoice) Equal(other *VoteChoice) bool {
|
||||
return v.id == other.id
|
||||
}
|
||||
|
||||
type VoteSums struct {
|
||||
|
@ -159,7 +218,7 @@ func (v *VoteSums) TotalVotes() int {
|
|||
return v.Ayes + v.Nayes
|
||||
}
|
||||
|
||||
func (v *VoteSums) CalculateResult(quorum int, majority float32) (VoteStatus, string) {
|
||||
func (v *VoteSums) CalculateResult(quorum int, majority float32) (*VoteStatus, string) {
|
||||
if v.VoteCount() < quorum {
|
||||
return voteStatusDeclined, fmt.Sprintf("Needed quorum of %d has not been reached.", quorum)
|
||||
}
|
||||
|
@ -177,7 +236,7 @@ type Motion struct {
|
|||
Proponent int64 `db:"proponent"`
|
||||
Title string
|
||||
Content string
|
||||
Status VoteStatus
|
||||
Status *VoteStatus
|
||||
Due time.Time
|
||||
Modified time.Time
|
||||
Tag string
|
||||
|
@ -366,7 +425,7 @@ func (m *MotionModel) SumsForDecision(ctx context.Context, tx *sqlx.Tx, d *Motio
|
|||
|
||||
for voteRows.Next() {
|
||||
var (
|
||||
vote VoteChoice
|
||||
vote *VoteChoice
|
||||
count int
|
||||
)
|
||||
|
||||
|
@ -379,11 +438,11 @@ func (m *MotionModel) SumsForDecision(ctx context.Context, tx *sqlx.Tx, d *Motio
|
|||
}
|
||||
|
||||
switch vote {
|
||||
case voteAye:
|
||||
case VoteAye:
|
||||
sums.Ayes = count
|
||||
case voteNaye:
|
||||
case VoteNaye:
|
||||
sums.Nayes = count
|
||||
case voteAbstain:
|
||||
case VoteAbstain:
|
||||
sums.Abstains = count
|
||||
}
|
||||
}
|
||||
|
@ -430,7 +489,7 @@ type MotionForDisplay struct {
|
|||
Title string
|
||||
Content string
|
||||
Type *VoteType `db:"votetype"`
|
||||
Status VoteStatus
|
||||
Status *VoteStatus
|
||||
Due time.Time
|
||||
Modified time.Time
|
||||
Sums VoteSums
|
||||
|
@ -439,7 +498,7 @@ type MotionForDisplay struct {
|
|||
|
||||
type VoteForDisplay struct {
|
||||
Name string
|
||||
Vote VoteChoice
|
||||
Vote *VoteChoice
|
||||
}
|
||||
|
||||
type MotionListOptions struct {
|
||||
|
@ -580,7 +639,7 @@ func (m *MotionModel) FillVoteSums(ctx context.Context, decisions []*MotionForDi
|
|||
|
||||
var (
|
||||
decisionID int64
|
||||
vote VoteChoice
|
||||
vote *VoteChoice
|
||||
count int
|
||||
)
|
||||
|
||||
|
@ -589,12 +648,12 @@ func (m *MotionModel) FillVoteSums(ctx context.Context, decisions []*MotionForDi
|
|||
return fmt.Errorf("could not scan row: %w", err)
|
||||
}
|
||||
|
||||
switch vote {
|
||||
case voteAye:
|
||||
switch {
|
||||
case vote.Equal(VoteAye):
|
||||
decisionMap[decisionID].Sums.Ayes = count
|
||||
case voteNaye:
|
||||
case vote.Equal(VoteNaye):
|
||||
decisionMap[decisionID].Sums.Nayes = count
|
||||
case voteAbstain:
|
||||
case vote.Equal(VoteAbstain):
|
||||
decisionMap[decisionID].Sums.Abstains = count
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</h1>
|
||||
{{ with .User }}
|
||||
<div class="ui label">
|
||||
<i class="user icon"></i>
|
||||
<i class="id card outline icon"></i>
|
||||
Authenticated as {{ .Name }} <{{ .Reminder }}>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
{{ define "motion_actions" }}
|
||||
{{ if eq .Status 0 }}
|
||||
<a class="ui compact right labeled green icon button" href="/vote/{{ .Tag }}/aye"><i
|
||||
class="check circle icon"></i> Aye</a>
|
||||
<a class="ui compact right labeled red icon button" href="/vote/{{ .Tag }}/naye"><i
|
||||
class="minus circle icon"></i> Naye</a>
|
||||
<a class="ui compact right labeled grey icon button" href="/vote/{{ .Tag }}/abstain"><i class="circle icon"></i>
|
||||
Abstain</a>
|
||||
<a class="ui compact left labeled icon button" href="/proxy/{{ .Tag }}"><i class="users icon"></i> Proxy
|
||||
Vote</a>
|
||||
<a class="ui compact left labeled icon button" href="/motions/{{ .Tag }}/edit"><i class="edit icon"></i> Modify</a>
|
||||
<a class="ui compact left labeled icon button" href="/motions/{{ .Tag }}/withdraw"><i class="trash icon"></i>
|
||||
Withdraw</a>
|
||||
{{ if eq .Status.Label "pending" }}
|
||||
<a class="ui compact labeled green icon button" href="/vote/{{ .Tag }}/aye">
|
||||
<i class="check circle icon"></i> Aye</a>
|
||||
<a class="ui compact labeled red icon button" href="/vote/{{ .Tag }}/naye">
|
||||
<i class="minus circle icon"></i> Naye</a>
|
||||
<a class="ui compact labeled grey icon button" href="/vote/{{ .Tag }}/abstain">
|
||||
<i class="question circle icon"></i> Abstain</a>
|
||||
<a class="ui compact labeled icon button" href="/proxy/{{ .Tag }}">
|
||||
<i class="users icon"></i> Proxy Vote</a>
|
||||
<a class="ui compact labeled icon button" href="/motions/{{ .Tag }}/edit">
|
||||
<i class="edit icon"></i> Modify</a>
|
||||
<a class="ui compact labeled icon button" href="/motions/{{ .Tag }}/withdraw">
|
||||
<i class="trash icon"></i> Withdraw</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
|
@ -1,6 +1,6 @@
|
|||
{{ define "motion_display" }}
|
||||
<span class="ui {{ template "motion_status_class" .Status }} ribbon label">{{ .Status|toString|title }}</span>
|
||||
<div class="ui label"><i class="ui icon calendar"></i> {{ dateInZone "2006-01-02 15:04:05 UTC" .Modified "UTC" }}</div>
|
||||
<div class="ui label"><i class="ui icon calendar alternate outline"></i> {{ dateInZone "2006-01-02 15:04:05 UTC" .Modified "UTC" }}</div>
|
||||
<h3 class="ui header"><a href="/motions/{{ .Tag }}" title="Details for motion {{ .Tag }}: {{ .Title }}">{{ .Tag }}: {{ .Title }}</a></h3>
|
||||
<p>{{ wrap 76 .Content | nl2br }}</p>
|
||||
<table class="ui small definition table">
|
||||
|
@ -21,15 +21,16 @@
|
|||
<td>Votes:</td>
|
||||
<td>
|
||||
<div class="ui labels">
|
||||
<div class="ui basic label green"><i
|
||||
class="check circle icon"></i>Aye
|
||||
<div class="ui basic label green">
|
||||
<i class="check circle icon"></i>Aye
|
||||
<div class="detail">{{.Sums.Ayes}}</div>
|
||||
</div>
|
||||
<div class="ui basic label red"><i
|
||||
class="minus circle icon"></i>Naye
|
||||
<div class="ui basic label red">
|
||||
<i class="minus circle icon"></i>Naye
|
||||
<div class="detail">{{.Sums.Nayes}}</div>
|
||||
</div>
|
||||
<div class="ui basic label grey"><i class="circle icon"></i>Abstain
|
||||
<div class="ui basic label grey">
|
||||
<i class="question circle icon"></i>Abstain
|
||||
<div class="detail">{{.Sums.Abstains}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
{{ define "motion_status_class" -}}
|
||||
{{ if eq . 0 }}blue{{ else if eq . 1 }}green{{ else if eq . -1 }}red{{ else if eq . -2 }}grey{{ end }}
|
||||
{{- if eq .Label "pending" }}blue
|
||||
{{- else if eq .Label "approved" }}green
|
||||
{{- else if eq .Label "declined" }}red
|
||||
{{- else if eq .Label "withdrawn" }}grey
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
@ -20,7 +20,9 @@
|
|||
{{ end }}
|
||||
{{ if canStartVote $user }}
|
||||
<div class="right item">
|
||||
<a class="ui primary button" href="/newmotion/">New motion</a>
|
||||
<a class="ui primary labeled icon button" href="/newmotion/">
|
||||
<i class="magic icon"></i>
|
||||
New motion</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
|
Loading…
Reference in a new issue