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 }