diff --git a/models/poll.go b/models/poll.go index bf78f1726..46f74ffd9 100644 --- a/models/poll.go +++ b/models/poll.go @@ -6,11 +6,14 @@ package models import ( "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "fmt" + "regexp" + "strconv" "strings" ) @@ -22,17 +25,31 @@ type Poll struct { AuthorID int64 `xorm:"INDEX"` Author *user_model.User `xorm:"-"` //Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository. - // When the poll is applied to all the issues, the subject should be an issue's trait. - // eg: Quality, Importance, Urgency, Wholeness, Relevance… + + // The Subject of the poll should be short + // When the poll uses issues as candidates, the subject should be an issue's trait. + // eg: Quality, Importance, Urgency, Wholeness, Relevance, Enthusiasm… Subject string `xorm:"name"` // Description may be used to describe at length the constitutive details of that poll. // Eg: Rationale, Deliberation Consequences, Schedule, Modus Operandi… // It can be written in the usual gitea-flavored markdown. Description string `xorm:"TEXT"` RenderedDescription string `xorm:"-"` - Ref string // Do we need this? Are we even using it? WHat is it? - Gradation string `xorm:"-"` + Ref string // Do we need this? Are we even using it? What is it? + + Gradation string `xorm:"-"` + + // Examples of a Candidates Whitelist + // 15 + // 1, 12, #13 + // Ideas: + // 3-13, 15-99 + // tag=bug, tag=bugfix, 21878 + // or perhaps + // [bug], [bugfix], 21878 + // but ideally an issue _search bar_ command + CandidatesWhitelist string `xorm:"candidates_whitelist"` AreCandidatesIssues bool // unused -- is this even the way to go ? DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"` @@ -45,6 +62,10 @@ type Poll struct { //Judgments JudgmentList `xorm:"-"` } +func init() { + db.RegisterModel(new(Poll)) +} + // PollList is a list of polls offering additional functionality (perhaps) type PollList []*Poll @@ -109,6 +130,7 @@ func (poll *Poll) GetGradeColorCode(grade uint8) (_ string) { } } +// GetCandidatesIDs collects the IDs of Candidates having received at least one judgment. func (poll *Poll) GetCandidatesIDs() (_ []int64, err error) { ids := make([]int64, 0, 10) x := db.GetEngine(db.DefaultContext) @@ -122,6 +144,40 @@ func (poll *Poll) GetCandidatesIDs() (_ []int64, err error) { return ids, nil } +// AllowsIssueAsCandidate checks if this Poll may use the provided Issue as Candidate +func (poll *Poll) AllowsIssueAsCandidate(issue *issues_model.Issue) bool { + + if poll.CandidatesWhitelist == "" { + return true + } + + // Idea: Instead of handling our parsing here, perhaps use the search bar utils ? + csvSeperator := regexp.MustCompile(`\s*,\s*`) + issueIdRegex := regexp.MustCompile(`#?(?P[0-9]+)`) + issueIdIndex := issueIdRegex.SubexpIndex("id") + + candidatesFilters := csvSeperator.Split(poll.CandidatesWhitelist, -1) + for _, candidateFilter := range candidatesFilters { + + issueIdMatches := issueIdRegex.FindStringSubmatch(candidateFilter) + if nil != issueIdMatches { + acceptedIssueID, err := strconv.Atoi(issueIdMatches[issueIdIndex]) + if nil == err { + if issue.Index == int64(acceptedIssueID) { + fmt.Println("YEP") + return true + } + } + } + + // … perhaps match tags as well, and other filters ? → search bar !?! + // /!. CURRENT IMPLEMENTATION ABOVE WILL CHOKE ON COMMAS IN TAG NAMES + } + + fmt.Println("NOPE") + return false +} + func (poll *Poll) GetJudgmentOnCandidate(judge *user_model.User, candidateID int64) (judgment *PollJudgment) { x := db.GetEngine(db.DefaultContext) judgment, err := getJudgmentOfJudgeOnPollCandidate(x, judge.ID, poll.ID, candidateID) @@ -177,10 +233,11 @@ func (poll *Poll) CountGrades(candidateID int64, grade uint8) (_ uint64, err err type CreatePollOptions struct { //Type PollType // for inline polls with their own candidates? - Author *user_model.User - Repo *repo_model.Repository - Subject string - Description string + Author *user_model.User + Repo *repo_model.Repository + Subject string + Description string + CandidatesWhitelist string //Grades string } @@ -213,6 +270,7 @@ func createPoll(e db.Engine, opts *CreatePollOptions) (_ *Poll, err error) { Repo: opts.Repo, Subject: opts.Subject, Description: opts.Description, + CandidatesWhitelist: opts.CandidatesWhitelist, AreCandidatesIssues: true, } if _, err = e.Insert(poll); err != nil { @@ -246,6 +304,8 @@ func GetPolls(repoID int64, page int) (PollList, error) { //sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed) if page > 0 { sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum) + } else { + sess = sess.Limit(setting.UI.IssuePagingNum) } return polls, sess.Find(&polls) @@ -264,8 +324,31 @@ func getPollByRepoID(e db.Engine, repoID, id int64) (*Poll, error) { // GetPollByRepoID returns the poll in a repository. func GetPollByRepoID(repoID, id int64) (*Poll, error) { - x := db.GetEngine(db.DefaultContext) - return getPollByRepoID(x, repoID, id) + return getPollByRepoID(db.GetEngine(db.DefaultContext), repoID, id) +} + +// GetPollsOnIssue returns the (paginated) list of polls active on a given issue. +func GetPollsOnIssue(issue *issues_model.Issue) (PollList, error) { + polls := make([]*Poll, 0, setting.UI.IssuePagingNum) + e := db.GetEngine(db.DefaultContext) + sess := e.Where("repo_id = ?", issue.RepoID) + //sess := e.Where("repo_id = ? AND is_closed = ?", issue, isClosed) + // TODO: exclude closed polls + // TODO: sort by decreasing priority, and then by decreasing creation date + + err := sess.Find(&polls) + if nil != err { + return polls, err + } + + filteredPolls := make([]*Poll, 0, setting.UI.IssuePagingNum) + for _, poll := range polls { + if poll.AllowsIssueAsCandidate(issue) { + filteredPolls = append(filteredPolls, poll) + } + } + + return filteredPolls, nil } // _ _ _ _ @@ -326,6 +409,8 @@ func DeletePollByRepoID(repoID, id int64) error { return err } + // FIXME: check if user is allowed to do this + ctx, committer, err := db.TxContext() if err != nil { return err diff --git a/models/poll_deliberator.go b/models/poll_deliberator.go index adad41ac7..0528409ec 100644 --- a/models/poll_deliberator.go +++ b/models/poll_deliberator.go @@ -22,7 +22,7 @@ type MajorityJudgmentDeliberator struct { // Deliberate ranks Candidates and gathers statistics into a PollResult func (deli *MajorityJudgmentDeliberator) Deliberate(poll *Poll) (_ *PollResult, err error) { - naiveTallier := &PollNaiveTallier{} + naiveTallier := &PollCandidTallier{} pollTally, err := naiveTallier.Tally(poll) if nil != err { return nil, err diff --git a/models/poll_judgment.go b/models/poll_judgment.go index 8820f8f42..a491f9425 100644 --- a/models/poll_judgment.go +++ b/models/poll_judgment.go @@ -20,23 +20,28 @@ type PollJudgment struct { Poll *Poll `xorm:"-"` JudgeID int64 `xorm:"INDEX UNIQUE(poll_judge_candidate)"` Judge *user_model.User `xorm:"-"` - // Either an Issue ID or an index in the list of Candidates (for inline polls) + // CandidateID is either an Issue ID or an index in the list of Candidates (for inline polls) + // One solution would be to use positive numbers for Issues, and negative for Inline, but that's horrible, + // and it prevents adding other types as Candidates later on. I need to RTFM, but which one ? CandidateID int64 `xorm:"UNIQUE(poll_judge_candidate)"` - // There may be other graduations + // Grade holds the level of the grade of this judgment // 0 = to reject // 1 = poor // 2 = passable // 3 = good // 4 = very good // 5 = excellent - // Make sure 0 always means *something* in your graduation - // Various graduations are provided . + // There may be other gradings, but make sure 0 always means *something* in your grading Grade uint8 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } +func init() { + db.RegisterModel(new(PollJudgment)) +} + type CreateJudgmentOptions struct { Poll *Poll Judge *user_model.User diff --git a/models/poll_tally.go b/models/poll_tally.go index d06a2146e..dfa8291b0 100644 --- a/models/poll_tally.go +++ b/models/poll_tally.go @@ -81,7 +81,7 @@ func (pct *PollCandidateTally) GetMedian() (_ uint8) { return grade.Grade } } - println("warning: GetMedian defaulting to 0") + //println("warning: PollCandidateTally.GetMedian defaulting to 0") return uint8(0) } @@ -153,16 +153,9 @@ type PollTallier interface { Tally(poll *Poll) (tally *PollTally, err error) } -// _ _ _ -// | \ | | __ _(_)_ _____ -// | \| |/ _` | \ \ / / _ \ -// | |\ | (_| | |\ V / __/ -// |_| \_|\__,_|_| \_/ \___| -// - -type PollNaiveTallier struct{} +type PollCandidTallier struct{} -func (tallier *PollNaiveTallier) Tally(poll *Poll) (_ *PollTally, err error) { +func (tallier *PollCandidTallier) Tally(poll *Poll) (_ *PollTally, err error) { gradation := poll.GetGradationList() diff --git a/models/poll_tally_test.go b/models/poll_tally_test.go index 9a1a1c1fa..8423aafd1 100644 --- a/models/poll_tally_test.go +++ b/models/poll_tally_test.go @@ -28,7 +28,7 @@ func TestTally(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, poll) - pnt := &PollNaiveTallier{} + pnt := &PollCandidTallier{} // No judgments yet diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 1b216df55..2edf11024 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1772,7 +1772,7 @@ func ViewIssue(ctx *context.Context) { return } // Get Polls - ctx.Data["Polls"], err = models.GetPolls(ctx.Repo.Repository.ID, 0) + ctx.Data["Polls"], err = models.GetPollsOnIssue(issue) if err != nil { ctx.ServerError("IssueView.GetPolls", err) return diff --git a/routers/web/repo/poll.go b/routers/web/repo/poll.go index 5441909a5..c98d931fc 100644 --- a/routers/web/repo/poll.go +++ b/routers/web/repo/poll.go @@ -56,8 +56,6 @@ func NewPoll(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.polls.new") ctx.Data["PageIsPolls"] = true - //ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language()) - ctx.HTML(200, tplPollsNew) } @@ -71,11 +69,17 @@ func NewPollPost(ctx *context.Context) { return } + candidatesWhitelist := "" + if form.CandidatesPool == "whitelist" { + candidatesWhitelist = form.CandidatesWhitelist + } + if _, err := models.CreatePoll(&models.CreatePollOptions{ - Author: ctx.Doer, - Repo: ctx.Repo.Repository, - Subject: form.Subject, - Description: form.Description, + Author: ctx.Doer, + Repo: ctx.Repo.Repository, + Subject: form.Subject, + Description: form.Description, + CandidatesWhitelist: candidatesWhitelist, }); err != nil { ctx.ServerError("CreatePoll", err) return diff --git a/routers/web/repo/poll_judgment.go b/routers/web/repo/poll_judgment.go index 41e69ea83..5a1d4b25a 100644 --- a/routers/web/repo/poll_judgment.go +++ b/routers/web/repo/poll_judgment.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. +// Copyright 2020 The Gitea Community. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -6,6 +6,7 @@ package repo import ( "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" "path" @@ -34,11 +35,22 @@ func EmitJudgment(ctx *context.Context) { return } + // TODO: handle the plurality of types of candidates + issue, errI := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, candidateID) + if nil != errI { + ctx.NotFound("EmitJudgment.GetIssueByIndex", errI) + return + } + if false == poll.AllowsIssueAsCandidate(issue) { + ctx.NotFound("EmitJudgment.AllowsIssueAsCandidate", errP) + return + } + judgment := poll.GetJudgmentOnCandidate(judge, candidateID) if nil != judgment { if judgment.Grade == grade { - // Delete a judgment if it exists and is the same as submitted. + // Rule: Delete a judgment if it exists and is the same as submitted. (toggle behavior) // Not obvious nor usual behavior, but pretty handy for now. errD := models.DeleteJudgment(&models.DeleteJudgmentOptions{ Poll: poll, diff --git a/services/forms/poll_form.go b/services/forms/poll_form.go index a0f8e0428..ffaeae5ab 100644 --- a/services/forms/poll_form.go +++ b/services/forms/poll_form.go @@ -11,8 +11,10 @@ import ( // CreatePollForm creates a new poll type CreatePollForm struct { - Subject string `binding:"Required;MaxSize(128)"` // 128 is duplicated in the template - Description string + Subject string `binding:"Required;MaxSize(128)"` // 128 is duplicated in the template + Description string + CandidatesPool string + CandidatesWhitelist string } // Validate the form fields diff --git a/templates/repo/polls/new.tmpl b/templates/repo/polls/new.tmpl index 7fc95c155..b68e47e39 100644 --- a/templates/repo/polls/new.tmpl +++ b/templates/repo/polls/new.tmpl @@ -33,24 +33,16 @@ -
+
- -