Improve design

- improve icons
- implement VoteChoice and VoteStatus as real types with Scanner and Value
  methods
main
Jan Dittberner 2 years ago
parent b8b6899cf3
commit 164495c818

@ -155,6 +155,14 @@ func setupFormDecoder() *form.Decoder {
return v, nil return v, nil
}, new(models.VoteType)) }, 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 return decoder
} }

@ -101,50 +101,109 @@ func (v *VoteType) QuorumAndMajority() (int, float32) {
return quorumDefault, majorityDefault return quorumDefault, majorityDefault
} }
type VoteStatus int8 type VoteStatus struct {
Label string
Id int8
}
const ( var (
voteStatusDeclined VoteStatus = -1 voteStatusDeclined = &VoteStatus{Label: "declined", Id: -1}
voteStatusPending VoteStatus = 0 voteStatusPending = &VoteStatus{Label: "pending", Id: 0}
voteStatusApproved VoteStatus = 1 voteStatusApproved = &VoteStatus{Label: "approved", Id: 1}
voteStatusWithdrawn VoteStatus = -2 voteStatusWithdrawn = &VoteStatus{Label: "withdrawn", Id: -2}
) )
var voteStatusLabels = map[VoteStatus]string{ func VoteStatusFromInt(id int64) (*VoteStatus, error) {
voteStatusDeclined: "declined", for _, vs := range []*VoteStatus{voteStatusPending, voteStatusApproved, voteStatusWithdrawn, voteStatusDeclined} {
voteStatusPending: "pending", if int64(vs.Id) == id {
voteStatusApproved: "approved", return vs, nil
voteStatusWithdrawn: "withdrawn", }
}
return nil, fmt.Errorf("unknown vote status id %d", id)
}
func (v *VoteStatus) String() string {
return v.Label
} }
func (v VoteStatus) String() string { func (v *VoteStatus) Scan(src any) error {
if label, ok := voteStatusLabels[v]; ok { value, ok := src.(int64)
return label if !ok {
return fmt.Errorf("could not cast %v of %T to uint8", src, src)
} }
return unknownVariant vs, err := VoteStatusFromInt(value)
if err != nil {
return err
}
*v = *vs
return nil
} }
type VoteChoice int func (v *VoteStatus) Value() (driver.Value, error) {
return int64(v.Id), nil
}
type VoteChoice struct {
label string
id int8
}
const ( var (
voteAye VoteChoice = 1 VoteAye = &VoteChoice{label: "aye", id: 1}
voteNaye VoteChoice = -1 VoteNaye = &VoteChoice{label: "naye", id: -1}
voteAbstain VoteChoice = 0 VoteAbstain = &VoteChoice{label: "abstain", id: 0}
) )
var voteChoiceLabels = map[VoteChoice]string{ func VoteChoiceFromString(label string) (*VoteChoice, error) {
voteAye: "aye", for _, vc := range []*VoteChoice{VoteAye, VoteNaye, VoteAbstain} {
voteNaye: "naye", if strings.EqualFold(vc.label, label) {
voteAbstain: "abstain", return vc, nil
}
}
return nil, fmt.Errorf("unknown vote choice %s", label)
} }
func (v VoteChoice) String() string { func VoteChoiceFromInt(id int64) (*VoteChoice, error) {
if label, ok := voteChoiceLabels[v]; ok { for _, vc := range []*VoteChoice{VoteAye, VoteNaye, VoteAbstain} {
return label if int64(vc.id) == id {
return vc, nil
}
} }
return unknownVariant 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 { type VoteSums struct {
@ -159,7 +218,7 @@ func (v *VoteSums) TotalVotes() int {
return v.Ayes + v.Nayes 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 { if v.VoteCount() < quorum {
return voteStatusDeclined, fmt.Sprintf("Needed quorum of %d has not been reached.", 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"` Proponent int64 `db:"proponent"`
Title string Title string
Content string Content string
Status VoteStatus Status *VoteStatus
Due time.Time Due time.Time
Modified time.Time Modified time.Time
Tag string Tag string
@ -366,7 +425,7 @@ func (m *MotionModel) SumsForDecision(ctx context.Context, tx *sqlx.Tx, d *Motio
for voteRows.Next() { for voteRows.Next() {
var ( var (
vote VoteChoice vote *VoteChoice
count int count int
) )
@ -379,11 +438,11 @@ func (m *MotionModel) SumsForDecision(ctx context.Context, tx *sqlx.Tx, d *Motio
} }
switch vote { switch vote {
case voteAye: case VoteAye:
sums.Ayes = count sums.Ayes = count
case voteNaye: case VoteNaye:
sums.Nayes = count sums.Nayes = count
case voteAbstain: case VoteAbstain:
sums.Abstains = count sums.Abstains = count
} }
} }
@ -430,7 +489,7 @@ type MotionForDisplay struct {
Title string Title string
Content string Content string
Type *VoteType `db:"votetype"` Type *VoteType `db:"votetype"`
Status VoteStatus Status *VoteStatus
Due time.Time Due time.Time
Modified time.Time Modified time.Time
Sums VoteSums Sums VoteSums
@ -439,7 +498,7 @@ type MotionForDisplay struct {
type VoteForDisplay struct { type VoteForDisplay struct {
Name string Name string
Vote VoteChoice Vote *VoteChoice
} }
type MotionListOptions struct { type MotionListOptions struct {
@ -580,7 +639,7 @@ func (m *MotionModel) FillVoteSums(ctx context.Context, decisions []*MotionForDi
var ( var (
decisionID int64 decisionID int64
vote VoteChoice vote *VoteChoice
count int count int
) )
@ -589,12 +648,12 @@ func (m *MotionModel) FillVoteSums(ctx context.Context, decisions []*MotionForDi
return fmt.Errorf("could not scan row: %w", err) return fmt.Errorf("could not scan row: %w", err)
} }
switch vote { switch {
case voteAye: case vote.Equal(VoteAye):
decisionMap[decisionID].Sums.Ayes = count decisionMap[decisionID].Sums.Ayes = count
case voteNaye: case vote.Equal(VoteNaye):
decisionMap[decisionID].Sums.Nayes = count decisionMap[decisionID].Sums.Nayes = count
case voteAbstain: case vote.Equal(VoteAbstain):
decisionMap[decisionID].Sums.Abstains = count decisionMap[decisionID].Sums.Abstains = count
} }
} }

@ -20,7 +20,7 @@
</h1> </h1>
{{ with .User }} {{ with .User }}
<div class="ui label"> <div class="ui label">
<i class="user icon"></i> <i class="id card outline icon"></i>
Authenticated as {{ .Name }} &lt;{{ .Reminder }}&gt; Authenticated as {{ .Name }} &lt;{{ .Reminder }}&gt;
</div> </div>
{{ end }} {{ end }}

@ -1,15 +1,16 @@
{{ define "motion_actions" }} {{ define "motion_actions" }}
{{ if eq .Status 0 }} {{ if eq .Status.Label "pending" }}
<a class="ui compact right labeled green icon button" href="/vote/{{ .Tag }}/aye"><i <a class="ui compact labeled green icon button" href="/vote/{{ .Tag }}/aye">
class="check circle icon"></i> Aye</a> <i class="check circle icon"></i> Aye</a>
<a class="ui compact right labeled red icon button" href="/vote/{{ .Tag }}/naye"><i <a class="ui compact labeled red icon button" href="/vote/{{ .Tag }}/naye">
class="minus circle icon"></i> Naye</a> <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> <a class="ui compact labeled grey icon button" href="/vote/{{ .Tag }}/abstain">
Abstain</a> <i class="question circle icon"></i> Abstain</a>
<a class="ui compact left labeled icon button" href="/proxy/{{ .Tag }}"><i class="users icon"></i> Proxy <a class="ui compact labeled icon button" href="/proxy/{{ .Tag }}">
Vote</a> <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 labeled icon button" href="/motions/{{ .Tag }}/edit">
<a class="ui compact left labeled icon button" href="/motions/{{ .Tag }}/withdraw"><i class="trash icon"></i> <i class="edit icon"></i> Modify</a>
Withdraw</a> <a class="ui compact labeled icon button" href="/motions/{{ .Tag }}/withdraw">
<i class="trash icon"></i> Withdraw</a>
{{ end }} {{ end }}
{{ end }} {{ end }}

@ -1,6 +1,6 @@
{{ define "motion_display" }} {{ define "motion_display" }}
<span class="ui {{ template "motion_status_class" .Status }} ribbon label">{{ .Status|toString|title }}</span> <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> <h3 class="ui header"><a href="/motions/{{ .Tag }}" title="Details for motion {{ .Tag }}: {{ .Title }}">{{ .Tag }}: {{ .Title }}</a></h3>
<p>{{ wrap 76 .Content | nl2br }}</p> <p>{{ wrap 76 .Content | nl2br }}</p>
<table class="ui small definition table"> <table class="ui small definition table">
@ -21,15 +21,16 @@
<td>Votes:</td> <td>Votes:</td>
<td> <td>
<div class="ui labels"> <div class="ui labels">
<div class="ui basic label green"><i <div class="ui basic label green">
class="check circle icon"></i>Aye <i class="check circle icon"></i>Aye
<div class="detail">{{.Sums.Ayes}}</div> <div class="detail">{{.Sums.Ayes}}</div>
</div> </div>
<div class="ui basic label red"><i <div class="ui basic label red">
class="minus circle icon"></i>Naye <i class="minus circle icon"></i>Naye
<div class="detail">{{.Sums.Nayes}}</div> <div class="detail">{{.Sums.Nayes}}</div>
</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 class="detail">{{.Sums.Abstains}}</div>
</div> </div>
</div> </div>

@ -1,3 +1,7 @@
{{ define "motion_status_class" -}} {{ 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 }} {{- end }}

@ -20,7 +20,9 @@
{{ end }} {{ end }}
{{ if canStartVote $user }} {{ if canStartVote $user }}
<div class="right item"> <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> </div>
{{ end }} {{ end }}
{{ end }} {{ end }}

Loading…
Cancel
Save