mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-22 11:46:40 +00:00
[bugfix] poll vote count fixes (#2444)
* don't drop all vote counts if hideCounts is set, refactors poll option extraction slightly * omit voters_count when not set * make voters_count a ptr to ensure it is omit unless definitely needed * handle case of expires_at, voters_count and option.votes_count being nilable * faster isNil check * remove omitempty tags since mastodon API marks things as nullable but still sets them in outgoing json
This commit is contained in:
parent
2191c7dee5
commit
ac48192562
|
@ -1102,19 +1102,11 @@ func ExtractPoll(poll Pollable) (*gtsmodel.Poll, error) {
|
||||||
var closed time.Time
|
var closed time.Time
|
||||||
|
|
||||||
// Extract the options (votes if any) and 'multiple choice' flag.
|
// Extract the options (votes if any) and 'multiple choice' flag.
|
||||||
options, votes, multi, err := ExtractPollOptions(poll)
|
options, multi, hideCounts, err := extractPollOptions(poll)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if counts have been hidden from us.
|
|
||||||
hideCounts := len(options) != len(votes)
|
|
||||||
|
|
||||||
if hideCounts {
|
|
||||||
// Simply provide zeroed slice.
|
|
||||||
votes = make([]int, len(options))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the poll closed time,
|
// Extract the poll closed time,
|
||||||
// it's okay for this to be zero.
|
// it's okay for this to be zero.
|
||||||
closedSlice := GetClosed(poll)
|
closedSlice := GetClosed(poll)
|
||||||
|
@ -1138,53 +1130,87 @@ func ExtractPoll(poll Pollable) (*gtsmodel.Poll, error) {
|
||||||
voters := GetVotersCount(poll)
|
voters := GetVotersCount(poll)
|
||||||
|
|
||||||
return >smodel.Poll{
|
return >smodel.Poll{
|
||||||
Options: options,
|
Options: optionNames(options),
|
||||||
Multiple: &multi,
|
Multiple: &multi,
|
||||||
HideCounts: &hideCounts,
|
HideCounts: &hideCounts,
|
||||||
Votes: votes,
|
Votes: optionVotes(options),
|
||||||
Voters: &voters,
|
Voters: &voters,
|
||||||
ExpiresAt: endTime,
|
ExpiresAt: endTime,
|
||||||
ClosedAt: closed,
|
ClosedAt: closed,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractPollOptions extracts poll option name strings, and the 'multiple choice flag' property value from Pollable.
|
// pollOption is a simple type
|
||||||
func ExtractPollOptions(poll Pollable) (names []string, votes []int, multi bool, err error) {
|
// to unify a poll option name
|
||||||
|
// with the number of votes.
|
||||||
|
type pollOption struct {
|
||||||
|
Name string
|
||||||
|
Votes int
|
||||||
|
}
|
||||||
|
|
||||||
|
// optionNames extracts name strings from a slice of poll options.
|
||||||
|
func optionNames(in []pollOption) []string {
|
||||||
|
out := make([]string, len(in))
|
||||||
|
for i := range in {
|
||||||
|
out[i] = in[i].Name
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// optionVotes extracts vote counts from a slice of poll options.
|
||||||
|
func optionVotes(in []pollOption) []int {
|
||||||
|
out := make([]int, len(in))
|
||||||
|
for i := range in {
|
||||||
|
out[i] = in[i].Votes
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractPollOptions extracts poll option name strings, the 'multiple choice flag', and 'hideCounts' intrinsic flag properties value from Pollable.
|
||||||
|
func extractPollOptions(poll Pollable) (options []pollOption, multi bool, hide bool, err error) {
|
||||||
var errs gtserror.MultiError
|
var errs gtserror.MultiError
|
||||||
|
|
||||||
// Iterate the oneOf property and gather poll single-choice options.
|
// Iterate the oneOf property and gather poll single-choice options.
|
||||||
IterateOneOf(poll, func(iter vocab.ActivityStreamsOneOfPropertyIterator) {
|
IterateOneOf(poll, func(iter vocab.ActivityStreamsOneOfPropertyIterator) {
|
||||||
name, count, err := extractPollOption(iter.GetType())
|
name, votes, err := extractPollOption(iter.GetType())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Append(err)
|
errs.Append(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
names = append(names, name)
|
if votes == nil {
|
||||||
if count != nil {
|
hide = true
|
||||||
votes = append(votes, *count)
|
votes = new(int)
|
||||||
}
|
}
|
||||||
|
options = append(options, pollOption{
|
||||||
|
Name: name,
|
||||||
|
Votes: *votes,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
if len(names) > 0 || len(errs) > 0 {
|
if len(options) > 0 || len(errs) > 0 {
|
||||||
return names, votes, false, errs.Combine()
|
return options, false, hide, errs.Combine()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate the anyOf property and gather poll multi-choice options.
|
// Iterate the anyOf property and gather poll multi-choice options.
|
||||||
IterateAnyOf(poll, func(iter vocab.ActivityStreamsAnyOfPropertyIterator) {
|
IterateAnyOf(poll, func(iter vocab.ActivityStreamsAnyOfPropertyIterator) {
|
||||||
name, count, err := extractPollOption(iter.GetType())
|
name, votes, err := extractPollOption(iter.GetType())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Append(err)
|
errs.Append(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
names = append(names, name)
|
if votes == nil {
|
||||||
if count != nil {
|
hide = true
|
||||||
votes = append(votes, *count)
|
votes = new(int)
|
||||||
}
|
}
|
||||||
|
options = append(options, pollOption{
|
||||||
|
Name: name,
|
||||||
|
Votes: *votes,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
if len(names) > 0 || len(errs) > 0 {
|
if len(options) > 0 || len(errs) > 0 {
|
||||||
return names, votes, true, errs.Combine()
|
return options, true, hide, errs.Combine()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil, false, errors.New("poll without options")
|
return nil, false, false, errors.New("poll without options")
|
||||||
}
|
}
|
||||||
|
|
||||||
// IterateOneOf will attempt to extract oneOf property from given interface, and passes each iterated item to function.
|
// IterateOneOf will attempt to extract oneOf property from given interface, and passes each iterated item to function.
|
||||||
|
|
|
@ -28,7 +28,7 @@ type Poll struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|
||||||
// When the poll ends. (ISO 8601 Datetime).
|
// When the poll ends. (ISO 8601 Datetime).
|
||||||
ExpiresAt string `json:"expires_at"`
|
ExpiresAt *string `json:"expires_at"`
|
||||||
|
|
||||||
// Is the poll currently expired?
|
// Is the poll currently expired?
|
||||||
Expired bool `json:"expired"`
|
Expired bool `json:"expired"`
|
||||||
|
@ -40,7 +40,7 @@ type Poll struct {
|
||||||
VotesCount int `json:"votes_count"`
|
VotesCount int `json:"votes_count"`
|
||||||
|
|
||||||
// How many unique accounts have voted on a multiple-choice poll.
|
// How many unique accounts have voted on a multiple-choice poll.
|
||||||
VotersCount int `json:"voters_count"`
|
VotersCount *int `json:"voters_count"`
|
||||||
|
|
||||||
// When called with a user token, has the authorized user voted?
|
// When called with a user token, has the authorized user voted?
|
||||||
//
|
//
|
||||||
|
@ -68,7 +68,7 @@ type PollOption struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
|
|
||||||
// The number of received votes for this option.
|
// The number of received votes for this option.
|
||||||
VotesCount int `json:"votes_count"`
|
VotesCount *int `json:"votes_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PollRequest models a request to create a poll.
|
// PollRequest models a request to create a poll.
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
@ -172,6 +173,13 @@ func increment(i int) int {
|
||||||
return i + 1
|
return i + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isNil will safely check if 'v' is nil without
|
||||||
|
// dealing with weird Go interface nil bullshit.
|
||||||
|
func isNil(i interface{}) bool {
|
||||||
|
type eface struct{ _, data unsafe.Pointer }
|
||||||
|
return (*eface)(unsafe.Pointer(&i)).data == nil
|
||||||
|
}
|
||||||
|
|
||||||
func LoadTemplateFunctions(engine *gin.Engine) {
|
func LoadTemplateFunctions(engine *gin.Engine) {
|
||||||
engine.SetFuncMap(template.FuncMap{
|
engine.SetFuncMap(template.FuncMap{
|
||||||
"escape": escape,
|
"escape": escape,
|
||||||
|
@ -185,5 +193,6 @@ func LoadTemplateFunctions(engine *gin.Engine) {
|
||||||
"emojify": emojify,
|
"emojify": emojify,
|
||||||
"acctInstance": acctInstance,
|
"acctInstance": acctInstance,
|
||||||
"increment": increment,
|
"increment": increment,
|
||||||
|
"isNil": isNil,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -686,9 +686,9 @@ func (c *Converter) StatusToWebStatus(
|
||||||
webPollOptions := make([]apimodel.WebPollOption, len(poll.Options))
|
webPollOptions := make([]apimodel.WebPollOption, len(poll.Options))
|
||||||
for i, option := range poll.Options {
|
for i, option := range poll.Options {
|
||||||
var voteShare float32
|
var voteShare float32
|
||||||
if totalVotes != 0 &&
|
|
||||||
option.VotesCount != 0 {
|
if totalVotes != 0 && option.VotesCount != nil {
|
||||||
voteShare = (float32(option.VotesCount) / float32(totalVotes)) * 100
|
voteShare = float32(*option.VotesCount) / float32(totalVotes) * 100
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format to two decimal points and ditch any
|
// Format to two decimal points and ditch any
|
||||||
|
@ -1432,11 +1432,11 @@ func (c *Converter) PollToAPIPoll(ctx context.Context, requester *gtsmodel.Accou
|
||||||
var (
|
var (
|
||||||
options []apimodel.PollOption
|
options []apimodel.PollOption
|
||||||
totalVotes int
|
totalVotes int
|
||||||
totalVoters int
|
totalVoters *int
|
||||||
voted *bool
|
hasVoted *bool
|
||||||
ownChoices *[]int
|
ownChoices *[]int
|
||||||
isAuthor bool
|
isAuthor bool
|
||||||
expiresAt string
|
expiresAt *string
|
||||||
emojis []apimodel.Emoji
|
emojis []apimodel.Emoji
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1462,57 +1462,62 @@ func (c *Converter) PollToAPIPoll(ctx context.Context, requester *gtsmodel.Accou
|
||||||
// Set choices by requester.
|
// Set choices by requester.
|
||||||
ownChoices = &vote.Choices
|
ownChoices = &vote.Choices
|
||||||
|
|
||||||
// Update default totals in the
|
// Update default total in the
|
||||||
// case that counts are hidden.
|
// case that counts are hidden
|
||||||
|
// (so we just show our own).
|
||||||
totalVotes = len(vote.Choices)
|
totalVotes = len(vote.Choices)
|
||||||
totalVoters = 1
|
|
||||||
for _, choice := range *ownChoices {
|
|
||||||
options[choice].VotesCount++
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Requester is defined but hasn't made
|
// Requester hasn't yet voted, use
|
||||||
// a choice. Init slice to serialize as `[]`.
|
// empty slice to serialize as `[]`.
|
||||||
ownChoices = util.Ptr(make([]int, 0))
|
ownChoices = &[]int{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if requester is author of source status.
|
// Check if requester is author of source status.
|
||||||
isAuthor = (requester.ID == poll.Status.AccountID)
|
isAuthor = (requester.ID == poll.Status.AccountID)
|
||||||
|
|
||||||
// Requester is defined so voted should be defined too.
|
// Set whether requester has voted in poll (or = author).
|
||||||
voted = util.Ptr((isAuthor || len(*ownChoices) > 0))
|
hasVoted = util.Ptr((isAuthor || len(*ownChoices) > 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
if isAuthor || !*poll.HideCounts {
|
if isAuthor || !*poll.HideCounts {
|
||||||
// A remote status,
|
// Only in the case that hide counts is
|
||||||
// the simple route!
|
// disabled, or the requester is the author
|
||||||
//
|
// do we actually populate the vote counts.
|
||||||
// Pull cached remote values.
|
|
||||||
totalVoters = (*poll.Voters)
|
|
||||||
|
|
||||||
// When this is status author, or hide counts
|
if *poll.Multiple {
|
||||||
// is disabled, set the counts known per vote,
|
// The total number of voters are only
|
||||||
// and accumulate all the vote totals.
|
// provided in the case of a multiple
|
||||||
|
// choice poll. All else leaves it nil.
|
||||||
|
totalVoters = poll.Voters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate per-vote counts
|
||||||
|
// and overall total vote count.
|
||||||
for i, count := range poll.Votes {
|
for i, count := range poll.Votes {
|
||||||
options[i].VotesCount = count
|
if options[i].VotesCount == nil {
|
||||||
|
options[i].VotesCount = new(int)
|
||||||
|
}
|
||||||
|
(*options[i].VotesCount) += count
|
||||||
totalVotes += count
|
totalVotes += count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !poll.ExpiresAt.IsZero() {
|
if !poll.ExpiresAt.IsZero() {
|
||||||
// Calculate poll expiry string (if set).
|
// Calculate poll expiry string (if set).
|
||||||
expiresAt = util.FormatISO8601(poll.ExpiresAt)
|
str := util.FormatISO8601(poll.ExpiresAt)
|
||||||
|
expiresAt = &str
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to inherit emojis
|
var err error
|
||||||
// from parent status.
|
|
||||||
if pStatus := poll.Status; pStatus != nil {
|
// Try to inherit emojis from parent status.
|
||||||
var err error
|
emojis, err = c.convertEmojisToAPIEmojis(ctx,
|
||||||
emojis, err = c.convertEmojisToAPIEmojis(ctx, pStatus.Emojis, pStatus.EmojiIDs)
|
poll.Status.Emojis,
|
||||||
if err != nil {
|
poll.Status.EmojiIDs,
|
||||||
// Fall back to empty slice.
|
)
|
||||||
log.Errorf(ctx, "error converting emojis from parent status: %v", err)
|
if err != nil {
|
||||||
emojis = make([]apimodel.Emoji, 0)
|
log.Errorf(ctx, "error converting emojis from parent status: %v", err)
|
||||||
}
|
emojis = []apimodel.Emoji{} // fallback to empty slice.
|
||||||
}
|
}
|
||||||
|
|
||||||
return &apimodel.Poll{
|
return &apimodel.Poll{
|
||||||
|
@ -1522,7 +1527,7 @@ func (c *Converter) PollToAPIPoll(ctx context.Context, requester *gtsmodel.Accou
|
||||||
Multiple: (*poll.Multiple),
|
Multiple: (*poll.Multiple),
|
||||||
VotesCount: totalVotes,
|
VotesCount: totalVotes,
|
||||||
VotersCount: totalVoters,
|
VotersCount: totalVoters,
|
||||||
Voted: voted,
|
Voted: hasVoted,
|
||||||
OwnVotes: ownChoices,
|
OwnVotes: ownChoices,
|
||||||
Options: options,
|
Options: options,
|
||||||
Emojis: emojis,
|
Emojis: emojis,
|
||||||
|
|
|
@ -47,6 +47,9 @@
|
||||||
<meter aria-hidden="true" id="poll-{{- $pollOption.PollID -}}-option-{{- increment $index -}}" min="0" max="100" value="{{- $pollOption.VoteShare -}}">{{- $pollOption.VoteShare -}}%</meter>
|
<meter aria-hidden="true" id="poll-{{- $pollOption.PollID -}}-option-{{- increment $index -}}" min="0" max="100" value="{{- $pollOption.VoteShare -}}">{{- $pollOption.VoteShare -}}%</meter>
|
||||||
<div class="sr-only">Option {{ increment $index }}: <span lang="{{ .LanguageTag.TagStr }}">{{ emojify .Emojis (noescape $pollOption.Title) -}}</span></div>
|
<div class="sr-only">Option {{ increment $index }}: <span lang="{{ .LanguageTag.TagStr }}">{{ emojify .Emojis (noescape $pollOption.Title) -}}</span></div>
|
||||||
<div class="poll-vote-summary">
|
<div class="poll-vote-summary">
|
||||||
|
{{- if isNil $pollOption.VotesCount }}
|
||||||
|
Results not yet published.
|
||||||
|
{{- else -}}
|
||||||
<span class="poll-vote-share">{{- $pollOption.VoteShareStr -}}%</span>
|
<span class="poll-vote-share">{{- $pollOption.VoteShareStr -}}%</span>
|
||||||
<span class="poll-vote-count">
|
<span class="poll-vote-count">
|
||||||
{{- if eq $pollOption.VotesCount 1 -}}
|
{{- if eq $pollOption.VotesCount 1 -}}
|
||||||
|
@ -55,6 +58,7 @@
|
||||||
{{- $pollOption.VotesCount }} votes
|
{{- $pollOption.VotesCount }} votes
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</span>
|
</span>
|
||||||
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
Loading…
Reference in a new issue