2023-09-09 00:09:23 +03:00
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
"context"
"strings"
"code.gitea.io/gitea/models/db"
2024-03-02 18:42:31 +03:00
"code.gitea.io/gitea/modules/optional"
2023-09-09 00:09:23 +03:00
"xorm.io/builder"
)
// MilestoneList is a list of milestones offering additional functionality
type MilestoneList [ ] * Milestone
func ( milestones MilestoneList ) getMilestoneIDs ( ) [ ] int64 {
ids := make ( [ ] int64 , 0 , len ( milestones ) )
for _ , ms := range milestones {
ids = append ( ids , ms . ID )
}
return ids
}
2023-12-11 11:56:48 +03:00
// FindMilestoneOptions contain options to get milestones
type FindMilestoneOptions struct {
2023-09-09 00:09:23 +03:00
db . ListOptions
RepoID int64
2024-03-02 18:42:31 +03:00
IsClosed optional . Option [ bool ]
2023-09-09 00:09:23 +03:00
Name string
SortType string
2023-12-11 11:56:48 +03:00
RepoCond builder . Cond
RepoIDs [ ] int64
2023-09-09 00:09:23 +03:00
}
2023-12-11 11:56:48 +03:00
func ( opts FindMilestoneOptions ) ToConds ( ) builder . Cond {
2023-09-09 00:09:23 +03:00
cond := builder . NewCond ( )
if opts . RepoID != 0 {
cond = cond . And ( builder . Eq { "repo_id" : opts . RepoID } )
}
2024-03-02 18:42:31 +03:00
if opts . IsClosed . Has ( ) {
cond = cond . And ( builder . Eq { "is_closed" : opts . IsClosed . Value ( ) } )
2023-12-11 11:56:48 +03:00
}
if opts . RepoCond != nil && opts . RepoCond . IsValid ( ) {
cond = cond . And ( builder . In ( "repo_id" , builder . Select ( "id" ) . From ( "repository" ) . Where ( opts . RepoCond ) ) )
}
if len ( opts . RepoIDs ) > 0 {
cond = cond . And ( builder . In ( "repo_id" , opts . RepoIDs ) )
2023-09-09 00:09:23 +03:00
}
if len ( opts . Name ) != 0 {
cond = cond . And ( db . BuildCaseInsensitiveLike ( "name" , opts . Name ) )
}
return cond
}
2023-12-11 11:56:48 +03:00
func ( opts FindMilestoneOptions ) ToOrders ( ) string {
2023-09-09 00:09:23 +03:00
switch opts . SortType {
case "furthestduedate" :
2023-12-11 11:56:48 +03:00
return "deadline_unix DESC"
2023-09-09 00:09:23 +03:00
case "leastcomplete" :
2023-12-11 11:56:48 +03:00
return "completeness ASC"
2023-09-09 00:09:23 +03:00
case "mostcomplete" :
2023-12-11 11:56:48 +03:00
return "completeness DESC"
2023-09-09 00:09:23 +03:00
case "leastissues" :
2023-12-11 11:56:48 +03:00
return "num_issues ASC"
2023-09-09 00:09:23 +03:00
case "mostissues" :
2023-12-11 11:56:48 +03:00
return "num_issues DESC"
2023-09-09 00:09:23 +03:00
case "id" :
2023-12-11 11:56:48 +03:00
return "id ASC"
2024-07-16 11:08:54 +03:00
case "name" :
return "name DESC"
2023-09-09 00:09:23 +03:00
default :
2024-07-16 11:08:54 +03:00
return "deadline_unix ASC, name ASC"
2023-09-09 00:09:23 +03:00
}
}
// GetMilestoneIDsByNames returns a list of milestone ids by given names.
// It doesn't filter them by repo, so it could return milestones belonging to different repos.
// It's used for filtering issues via indexer, otherwise it would be useless.
// Since it could return milestones with the same name, so the length of returned ids could be more than the length of names.
func GetMilestoneIDsByNames ( ctx context . Context , names [ ] string ) ( [ ] int64 , error ) {
var ids [ ] int64
return ids , db . GetEngine ( ctx ) . Table ( "milestone" ) .
Where ( db . BuildCaseInsensitiveIn ( "name" , names ) ) .
Cols ( "id" ) .
Find ( & ids )
}
2023-09-16 17:39:12 +03:00
// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
func ( milestones MilestoneList ) LoadTotalTrackedTimes ( ctx context . Context ) error {
2023-09-09 00:09:23 +03:00
type totalTimesByMilestone struct {
MilestoneID int64
Time int64
}
if len ( milestones ) == 0 {
return nil
}
trackedTimes := make ( map [ int64 ] int64 , len ( milestones ) )
// Get total tracked time by milestone_id
rows , err := db . GetEngine ( ctx ) . Table ( "issue" ) .
Join ( "INNER" , "milestone" , "issue.milestone_id = milestone.id" ) .
Join ( "LEFT" , "tracked_time" , "tracked_time.issue_id = issue.id" ) .
Where ( "tracked_time.deleted = ?" , false ) .
Select ( "milestone_id, sum(time) as time" ) .
In ( "milestone_id" , milestones . getMilestoneIDs ( ) ) .
GroupBy ( "milestone_id" ) .
Rows ( new ( totalTimesByMilestone ) )
if err != nil {
return err
}
defer rows . Close ( )
for rows . Next ( ) {
var totalTime totalTimesByMilestone
err = rows . Scan ( & totalTime )
if err != nil {
return err
}
trackedTimes [ totalTime . MilestoneID ] = totalTime . Time
}
for _ , milestone := range milestones {
milestone . TotalTrackedTime = trackedTimes [ milestone . ID ]
}
return nil
}
// CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options`
2023-12-11 11:56:48 +03:00
func CountMilestonesMap ( ctx context . Context , opts FindMilestoneOptions ) ( map [ int64 ] int64 , error ) {
sess := db . GetEngine ( ctx ) . Where ( opts . ToConds ( ) )
2023-09-09 00:09:23 +03:00
countsSlice := make ( [ ] * struct {
RepoID int64
Count int64
} , 0 , 10 )
if err := sess . GroupBy ( "repo_id" ) .
Select ( "repo_id AS repo_id, COUNT(*) AS count" ) .
Table ( "milestone" ) .
Find ( & countsSlice ) ; err != nil {
return nil , err
}
countMap := make ( map [ int64 ] int64 , len ( countsSlice ) )
for _ , c := range countsSlice {
countMap [ c . RepoID ] = c . Count
}
return countMap , nil
}
// MilestonesStats represents milestone statistic information.
type MilestonesStats struct {
OpenCount , ClosedCount int64
}
// Total returns the total counts of milestones
func ( m MilestonesStats ) Total ( ) int64 {
return m . OpenCount + m . ClosedCount
}
// GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword.
2023-09-16 17:39:12 +03:00
func GetMilestonesStatsByRepoCondAndKw ( ctx context . Context , repoCond builder . Cond , keyword string ) ( * MilestonesStats , error ) {
2023-09-09 00:09:23 +03:00
var err error
stats := & MilestonesStats { }
2023-09-16 17:39:12 +03:00
sess := db . GetEngine ( ctx ) . Where ( "is_closed = ?" , false )
2023-09-09 00:09:23 +03:00
if len ( keyword ) > 0 {
sess = sess . And ( builder . Like { "UPPER(name)" , strings . ToUpper ( keyword ) } )
}
if repoCond . IsValid ( ) {
sess . And ( builder . In ( "repo_id" , builder . Select ( "id" ) . From ( "repository" ) . Where ( repoCond ) ) )
}
stats . OpenCount , err = sess . Count ( new ( Milestone ) )
if err != nil {
return nil , err
}
2023-09-16 17:39:12 +03:00
sess = db . GetEngine ( ctx ) . Where ( "is_closed = ?" , true )
2023-09-09 00:09:23 +03:00
if len ( keyword ) > 0 {
sess = sess . And ( builder . Like { "UPPER(name)" , strings . ToUpper ( keyword ) } )
}
if repoCond . IsValid ( ) {
sess . And ( builder . In ( "repo_id" , builder . Select ( "id" ) . From ( "repository" ) . Where ( repoCond ) ) )
}
stats . ClosedCount , err = sess . Count ( new ( Milestone ) )
if err != nil {
return nil , err
}
return stats , nil
}