Improve design

- improve icons
- implement VoteChoice and VoteStatus as real types with Scanner and Value
  methods
This commit is contained in:
Jan Dittberner 2022-05-27 14:42:39 +02:00
parent b8b6899cf3
commit 164495c818
7 changed files with 141 additions and 66 deletions

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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 }} &lt;{{ .Reminder }}&gt;
</div>
{{ end }}

View file

@ -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 }}

View file

@ -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>

View file

@ -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 }}

View file

@ -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 }}