feat: restrict candidates of a poll using a whitelist

That whitelist is a sequence of issues IDs (Index),
separated by commas (,), each optionally prefixed by a hash (#).

See https://github.com/go-gitea/gitea/issues/8115#issuecomment-1441096541
Dominique Merle 1 year ago
parent 28966fdf87
commit 46e95c2264

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

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

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

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

@ -28,7 +28,7 @@ func TestTally(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, poll)
pnt := &PollNaiveTallier{}
pnt := &PollCandidTallier{}
// No judgments yet

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

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

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

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

@ -33,24 +33,16 @@
<option value="emote6-1">🤮 😒 😐 🙂 😀 🤩</option>
</select>
</div>
<div class="field disabled">
<div class="field">
<label>{{ .locale.Tr "repo.polls.candidates.label" }}</label>
<label>
<input type="radio" name="candidates_pool" value="issues_merges" checked>
{{ .locale.Tr "repo.polls.candidates.pool.issues_merges" }}
</label>
<label>
<input type="radio" name="candidates_pool" value="issues">
{{ .locale.Tr "repo.polls.candidates.pool.issues" }}
</label>
<label>
<input type="radio" name="candidates_pool" value="merges">
{{ .locale.Tr "repo.polls.candidates.pool.merges" }}
</label>
<label>
<input type="radio" name="candidates_pool" value="indices">
<input type="radio" name="candidates_pool" value="whitelist">
{{ .locale.Tr "repo.polls.candidates.pool.indices" }}
<input class="inline-input" type="text" name="candidates_pool_indices" title="{{ .locale.Tr "repo.polls.candidates.pool.indices.help" }}" placeholder="1, 9, 12">
<input class="inline-input" type="text" name="candidates_whitelist" title="{{ .locale.Tr "repo.polls.candidates.pool.indices.help" }}" placeholder="1, 9, 12">
</label>
</div>
</div>

Loading…
Cancel
Save