mirror of
https://github.com/go-gitea/gitea
synced 2025-07-22 18:28:37 +00:00
Allow filtering issues by any assignee (#33343)
This is the opposite of the "No assignee" filter, it will match all issues that have at least one assignee. Before  After  --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -5,11 +5,13 @@ package bleve
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/indexer"
|
||||
indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
|
||||
inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve"
|
||||
"code.gitea.io/gitea/modules/indexer/issues/internal"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
@@ -246,12 +248,20 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id"))
|
||||
}
|
||||
|
||||
if options.PosterID.Has() {
|
||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.PosterID.Value(), "poster_id"))
|
||||
if options.PosterID != "" {
|
||||
// "(none)" becomes 0, it means no poster
|
||||
posterIDInt64, _ := strconv.ParseInt(options.PosterID, 10, 64)
|
||||
queries = append(queries, inner_bleve.NumericEqualityQuery(posterIDInt64, "poster_id"))
|
||||
}
|
||||
|
||||
if options.AssigneeID.Has() {
|
||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.AssigneeID.Value(), "assignee_id"))
|
||||
if options.AssigneeID != "" {
|
||||
if options.AssigneeID == "(any)" {
|
||||
queries = append(queries, inner_bleve.NumericRangeInclusiveQuery(optional.Some[int64](1), optional.None[int64](), "assignee_id"))
|
||||
} else {
|
||||
// "(none)" becomes 0, it means no assignee
|
||||
assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64)
|
||||
queries = append(queries, inner_bleve.NumericEqualityQuery(assigneeIDInt64, "assignee_id"))
|
||||
}
|
||||
}
|
||||
|
||||
if options.MentionID.Has() {
|
||||
|
@@ -54,7 +54,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
||||
RepoIDs: options.RepoIDs,
|
||||
AllPublic: options.AllPublic,
|
||||
RepoCond: nil,
|
||||
AssigneeID: optional.Some(convertID(options.AssigneeID)),
|
||||
AssigneeID: options.AssigneeID,
|
||||
PosterID: options.PosterID,
|
||||
MentionedID: convertID(options.MentionID),
|
||||
ReviewRequestedID: convertID(options.ReviewRequestedID),
|
||||
|
@@ -45,11 +45,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
||||
searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0)
|
||||
}
|
||||
|
||||
if opts.AssigneeID.Value() == db.NoConditionID {
|
||||
searchOpt.AssigneeID = optional.Some[int64](0) // FIXME: this is inconsistent from other places, 0 means "no assignee"
|
||||
} else if opts.AssigneeID.Value() != 0 {
|
||||
searchOpt.AssigneeID = opts.AssigneeID
|
||||
}
|
||||
searchOpt.AssigneeID = opts.AssigneeID
|
||||
|
||||
// See the comment of issues_model.SearchOptions for the reason why we need to convert
|
||||
convertID := func(id int64) optional.Option[int64] {
|
||||
|
@@ -212,12 +212,22 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
||||
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value()))
|
||||
}
|
||||
|
||||
if options.PosterID.Has() {
|
||||
query.Must(elastic.NewTermQuery("poster_id", options.PosterID.Value()))
|
||||
if options.PosterID != "" {
|
||||
// "(none)" becomes 0, it means no poster
|
||||
posterIDInt64, _ := strconv.ParseInt(options.PosterID, 10, 64)
|
||||
query.Must(elastic.NewTermQuery("poster_id", posterIDInt64))
|
||||
}
|
||||
|
||||
if options.AssigneeID.Has() {
|
||||
query.Must(elastic.NewTermQuery("assignee_id", options.AssigneeID.Value()))
|
||||
if options.AssigneeID != "" {
|
||||
if options.AssigneeID == "(any)" {
|
||||
q := elastic.NewRangeQuery("assignee_id")
|
||||
q.Gte(1)
|
||||
query.Must(q)
|
||||
} else {
|
||||
// "(none)" becomes 0, it means no assignee
|
||||
assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64)
|
||||
query.Must(elastic.NewTermQuery("assignee_id", assigneeIDInt64))
|
||||
}
|
||||
}
|
||||
|
||||
if options.MentionID.Has() {
|
||||
|
@@ -44,6 +44,7 @@ func TestDBSearchIssues(t *testing.T) {
|
||||
t.Run("search issues with order", searchIssueWithOrder)
|
||||
t.Run("search issues in project", searchIssueInProject)
|
||||
t.Run("search issues with paginator", searchIssueWithPaginator)
|
||||
t.Run("search issues with any assignee", searchIssueWithAnyAssignee)
|
||||
}
|
||||
|
||||
func searchIssueWithKeyword(t *testing.T) {
|
||||
@@ -176,19 +177,19 @@ func searchIssueByID(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
opts: SearchOptions{
|
||||
PosterID: optional.Some(int64(1)),
|
||||
PosterID: "1",
|
||||
},
|
||||
expectedIDs: []int64{11, 6, 3, 2, 1},
|
||||
},
|
||||
{
|
||||
opts: SearchOptions{
|
||||
AssigneeID: optional.Some(int64(1)),
|
||||
AssigneeID: "1",
|
||||
},
|
||||
expectedIDs: []int64{6, 1},
|
||||
},
|
||||
{
|
||||
// NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it will set AssigneeID to 0 when it is passed as -1.
|
||||
opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: optional.Some(db.NoConditionID)}),
|
||||
// NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it handles the filter correctly
|
||||
opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: "(none)"}),
|
||||
expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2},
|
||||
},
|
||||
{
|
||||
@@ -462,3 +463,25 @@ func searchIssueWithPaginator(t *testing.T) {
|
||||
assert.Equal(t, test.expectedTotal, total)
|
||||
}
|
||||
}
|
||||
|
||||
func searchIssueWithAnyAssignee(t *testing.T) {
|
||||
tests := []struct {
|
||||
opts SearchOptions
|
||||
expectedIDs []int64
|
||||
expectedTotal int64
|
||||
}{
|
||||
{
|
||||
SearchOptions{
|
||||
AssigneeID: "(any)",
|
||||
},
|
||||
[]int64{17, 6, 1},
|
||||
3,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
issueIDs, total, err := SearchIssues(t.Context(), &test.opts)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expectedIDs, issueIDs)
|
||||
assert.Equal(t, test.expectedTotal, total)
|
||||
}
|
||||
}
|
||||
|
@@ -97,9 +97,8 @@ type SearchOptions struct {
|
||||
ProjectID optional.Option[int64] // project the issues belong to
|
||||
ProjectColumnID optional.Option[int64] // project column the issues belong to
|
||||
|
||||
PosterID optional.Option[int64] // poster of the issues
|
||||
|
||||
AssigneeID optional.Option[int64] // assignee of the issues, zero means no assignee
|
||||
PosterID string // poster of the issues, "(none)" or "(any)" or a user ID
|
||||
AssigneeID string // assignee of the issues, "(none)" or "(any)" or a user ID
|
||||
|
||||
MentionID optional.Option[int64] // mentioned user of the issues
|
||||
|
||||
|
@@ -379,7 +379,7 @@ var cases = []*testIndexerCase{
|
||||
Paginator: &db.ListOptions{
|
||||
PageSize: 5,
|
||||
},
|
||||
PosterID: optional.Some(int64(1)),
|
||||
PosterID: "1",
|
||||
},
|
||||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
||||
assert.Len(t, result.Hits, 5)
|
||||
@@ -397,7 +397,7 @@ var cases = []*testIndexerCase{
|
||||
Paginator: &db.ListOptions{
|
||||
PageSize: 5,
|
||||
},
|
||||
AssigneeID: optional.Some(int64(1)),
|
||||
AssigneeID: "1",
|
||||
},
|
||||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
||||
assert.Len(t, result.Hits, 5)
|
||||
@@ -415,7 +415,7 @@ var cases = []*testIndexerCase{
|
||||
Paginator: &db.ListOptions{
|
||||
PageSize: 5,
|
||||
},
|
||||
AssigneeID: optional.Some(int64(0)),
|
||||
AssigneeID: "(none)",
|
||||
},
|
||||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
||||
assert.Len(t, result.Hits, 5)
|
||||
@@ -647,6 +647,21 @@ var cases = []*testIndexerCase{
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "SearchAnyAssignee",
|
||||
SearchOptions: &internal.SearchOptions{
|
||||
AssigneeID: "(any)",
|
||||
},
|
||||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
||||
assert.Len(t, result.Hits, 180)
|
||||
for _, v := range result.Hits {
|
||||
assert.GreaterOrEqual(t, data[v.ID].AssigneeID, int64(1))
|
||||
}
|
||||
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
|
||||
return v.AssigneeID >= 1
|
||||
}), result.Total)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type testIndexerCase struct {
|
||||
|
@@ -187,12 +187,20 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
||||
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value()))
|
||||
}
|
||||
|
||||
if options.PosterID.Has() {
|
||||
query.And(inner_meilisearch.NewFilterEq("poster_id", options.PosterID.Value()))
|
||||
if options.PosterID != "" {
|
||||
// "(none)" becomes 0, it means no poster
|
||||
posterIDInt64, _ := strconv.ParseInt(options.PosterID, 10, 64)
|
||||
query.And(inner_meilisearch.NewFilterEq("poster_id", posterIDInt64))
|
||||
}
|
||||
|
||||
if options.AssigneeID.Has() {
|
||||
query.And(inner_meilisearch.NewFilterEq("assignee_id", options.AssigneeID.Value()))
|
||||
if options.AssigneeID != "" {
|
||||
if options.AssigneeID == "(any)" {
|
||||
query.And(inner_meilisearch.NewFilterGte("assignee_id", 1))
|
||||
} else {
|
||||
// "(none)" becomes 0, it means no assignee
|
||||
assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64)
|
||||
query.And(inner_meilisearch.NewFilterEq("assignee_id", assigneeIDInt64))
|
||||
}
|
||||
}
|
||||
|
||||
if options.MentionID.Has() {
|
||||
|
Reference in New Issue
Block a user