06ae20cb3e
* Forums are disowned on user account deletion (their owner_id=0) * A forum without an owner shows a notice at the bottom with a link to petition to adopt the forum. It goes to the Contact form with a special subject. * Note: there is no easy way to re-assign ownership yet other than a direct database query. * Code cleanup * Alphabetize the DB.AutoMigrate tables. * Delete more things on user deletion: forum_memberships, admin_group_users * Vacuum worker to clean up orphaned polls after the threads are removed
173 lines
4.1 KiB
Go
173 lines
4.1 KiB
Go
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.nonshy.com/nonshy/website/pkg/config"
|
|
)
|
|
|
|
// Poll table for user surveys posted in the forums.
|
|
type Poll struct {
|
|
ID uint64 `gorm:"primaryKey"`
|
|
|
|
// Poll options
|
|
Choices string // line-separated choices
|
|
MultipleChoice bool // User can vote multiple choices
|
|
CustomAnswers bool // Users can contribute a custom response
|
|
|
|
Expires bool // if it stops accepting new votes
|
|
ExpiresAt time.Time // when it stops accepting new votes
|
|
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// CreatePoll initializes a poll.
|
|
//
|
|
// expires is in days (0 = doesn't expire)
|
|
func CreatePoll(choices []string, expires int) *Poll {
|
|
return &Poll{
|
|
Choices: strings.Join(choices, "\n"),
|
|
Expires: expires > 0,
|
|
ExpiresAt: time.Now().Add(time.Duration(expires) * 24 * time.Hour),
|
|
}
|
|
}
|
|
|
|
// GetPoll by ID.
|
|
func GetPoll(id uint64) (*Poll, error) {
|
|
m := &Poll{}
|
|
result := DB.First(&m, id)
|
|
return m, result.Error
|
|
}
|
|
|
|
// Options returns a conveniently formatted listing of the options.
|
|
func (p *Poll) Options() []string {
|
|
return strings.Split(p.Choices, "\n")
|
|
}
|
|
|
|
// IsExpired returns if the poll has ended.
|
|
func (p *Poll) IsExpired() bool {
|
|
return p.Expires && time.Now().After(p.ExpiresAt)
|
|
}
|
|
|
|
// InputType returns "radio" or "checkbox" for multiple choice polls.
|
|
func (p *Poll) InputType() string {
|
|
if p.MultipleChoice {
|
|
return "checkbox"
|
|
}
|
|
return "radio"
|
|
}
|
|
|
|
// Result returns metadata about a poll's status and results, for frontend assist.
|
|
func (p *Poll) Result(currentUser *User) PollResult {
|
|
var (
|
|
result = PollResult{
|
|
AcceptingVotes: true,
|
|
CurrentUserVote: []string{},
|
|
Results: map[string]int{},
|
|
ResultsPercent: map[string]float64{},
|
|
ResultsClass: map[string]string{},
|
|
}
|
|
votes = p.GetAllVotes()
|
|
distinctAnswers int
|
|
)
|
|
|
|
// Populate the CSS classes.
|
|
for i, answer := range p.Options() {
|
|
result.ResultsClass[answer] = config.PollProgressBarClasses[i%len(config.PollProgressBarClasses)]
|
|
}
|
|
|
|
result.TotalVotes = len(votes)
|
|
for _, res := range votes {
|
|
if res.UserID == currentUser.ID {
|
|
result.CurrentUserVote = append(result.CurrentUserVote, res.Answer)
|
|
result.AcceptingVotes = false
|
|
}
|
|
|
|
if _, ok := result.Results[res.Answer]; !ok {
|
|
distinctAnswers++
|
|
result.Results[res.Answer] = 0
|
|
result.ResultsPercent[res.Answer] = 0
|
|
}
|
|
result.Results[res.Answer]++
|
|
}
|
|
|
|
// Compute the percent splits.
|
|
if result.TotalVotes > 0 {
|
|
for answer, count := range result.Results {
|
|
result.ResultsPercent[answer] = float64(count) / float64(result.TotalVotes)
|
|
}
|
|
}
|
|
|
|
// Expired polls don't accept answers.
|
|
if p.IsExpired() {
|
|
result.AcceptingVotes = false
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Save Poll.
|
|
func (p *Poll) Save() error {
|
|
result := DB.Save(p)
|
|
return result.Error
|
|
}
|
|
|
|
// Delete Poll, which also deletes its PollVotes.
|
|
func (p *Poll) Delete() error {
|
|
|
|
// Delete votes first.
|
|
if result := DB.Exec(
|
|
"DELETE FROM poll_votes WHERE poll_id = ?",
|
|
p.ID,
|
|
); result.Error != nil {
|
|
return fmt.Errorf("deleting votes: %s", result.Error)
|
|
}
|
|
|
|
result := DB.Delete(p)
|
|
return result.Error
|
|
}
|
|
|
|
// PollResult holds metadata about the poll result for frontend display.
|
|
type PollResult struct {
|
|
AcceptingVotes bool // user voted or it expired
|
|
CurrentUserVote []string // current user's selection, if any
|
|
Results map[string]int // answers and their %
|
|
ResultsPercent map[string]float64
|
|
ResultsClass map[string]string // progress bar classes
|
|
TotalVotes int
|
|
}
|
|
|
|
func (pr PollResult) GetPercent(answer string) string {
|
|
value := pr.ResultsPercent[answer]
|
|
return fmt.Sprintf("%.1f", value*100)
|
|
}
|
|
|
|
func (pr PollResult) GetClass(answer string) string {
|
|
return pr.ResultsClass[answer]
|
|
}
|
|
|
|
// GetOrphanedPolls gets all (up to 500) polls that don't have Threads pointing to them.
|
|
func GetOrphanedPolls() ([]*Poll, int64, error) {
|
|
var (
|
|
count int64
|
|
ps = []*Poll{}
|
|
)
|
|
|
|
query := DB.Model(&Poll{}).Where(`
|
|
NOT EXISTS (
|
|
SELECT 1 FROM threads
|
|
WHERE threads.poll_id = polls.id
|
|
)
|
|
`)
|
|
query.Count(&count)
|
|
res := query.Limit(500).Find(&ps)
|
|
if res.Error != nil {
|
|
return nil, 0, res.Error
|
|
}
|
|
|
|
return ps, count, res.Error
|
|
}
|