code.gitea.io/gitea@v1.22.3/models/issues/milestone_list.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package issues 5 6 import ( 7 "context" 8 "strings" 9 10 "code.gitea.io/gitea/models/db" 11 "code.gitea.io/gitea/modules/optional" 12 13 "xorm.io/builder" 14 ) 15 16 // MilestoneList is a list of milestones offering additional functionality 17 type MilestoneList []*Milestone 18 19 func (milestones MilestoneList) getMilestoneIDs() []int64 { 20 ids := make([]int64, 0, len(milestones)) 21 for _, ms := range milestones { 22 ids = append(ids, ms.ID) 23 } 24 return ids 25 } 26 27 // FindMilestoneOptions contain options to get milestones 28 type FindMilestoneOptions struct { 29 db.ListOptions 30 RepoID int64 31 IsClosed optional.Option[bool] 32 Name string 33 SortType string 34 RepoCond builder.Cond 35 RepoIDs []int64 36 } 37 38 func (opts FindMilestoneOptions) ToConds() builder.Cond { 39 cond := builder.NewCond() 40 if opts.RepoID != 0 { 41 cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) 42 } 43 if opts.IsClosed.Has() { 44 cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()}) 45 } 46 if opts.RepoCond != nil && opts.RepoCond.IsValid() { 47 cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond))) 48 } 49 if len(opts.RepoIDs) > 0 { 50 cond = cond.And(builder.In("repo_id", opts.RepoIDs)) 51 } 52 if len(opts.Name) != 0 { 53 cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name)) 54 } 55 56 return cond 57 } 58 59 func (opts FindMilestoneOptions) ToOrders() string { 60 switch opts.SortType { 61 case "furthestduedate": 62 return "deadline_unix DESC" 63 case "leastcomplete": 64 return "completeness ASC" 65 case "mostcomplete": 66 return "completeness DESC" 67 case "leastissues": 68 return "num_issues ASC" 69 case "mostissues": 70 return "num_issues DESC" 71 case "id": 72 return "id ASC" 73 default: 74 return "deadline_unix ASC, id ASC" 75 } 76 } 77 78 // GetMilestoneIDsByNames returns a list of milestone ids by given names. 79 // It doesn't filter them by repo, so it could return milestones belonging to different repos. 80 // It's used for filtering issues via indexer, otherwise it would be useless. 81 // Since it could return milestones with the same name, so the length of returned ids could be more than the length of names. 82 func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error) { 83 var ids []int64 84 return ids, db.GetEngine(ctx).Table("milestone"). 85 Where(db.BuildCaseInsensitiveIn("name", names)). 86 Cols("id"). 87 Find(&ids) 88 } 89 90 // LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request 91 func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error { 92 type totalTimesByMilestone struct { 93 MilestoneID int64 94 Time int64 95 } 96 if len(milestones) == 0 { 97 return nil 98 } 99 trackedTimes := make(map[int64]int64, len(milestones)) 100 101 // Get total tracked time by milestone_id 102 rows, err := db.GetEngine(ctx).Table("issue"). 103 Join("INNER", "milestone", "issue.milestone_id = milestone.id"). 104 Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id"). 105 Where("tracked_time.deleted = ?", false). 106 Select("milestone_id, sum(time) as time"). 107 In("milestone_id", milestones.getMilestoneIDs()). 108 GroupBy("milestone_id"). 109 Rows(new(totalTimesByMilestone)) 110 if err != nil { 111 return err 112 } 113 114 defer rows.Close() 115 116 for rows.Next() { 117 var totalTime totalTimesByMilestone 118 err = rows.Scan(&totalTime) 119 if err != nil { 120 return err 121 } 122 trackedTimes[totalTime.MilestoneID] = totalTime.Time 123 } 124 125 for _, milestone := range milestones { 126 milestone.TotalTrackedTime = trackedTimes[milestone.ID] 127 } 128 return nil 129 } 130 131 // CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options` 132 func CountMilestonesMap(ctx context.Context, opts FindMilestoneOptions) (map[int64]int64, error) { 133 sess := db.GetEngine(ctx).Where(opts.ToConds()) 134 135 countsSlice := make([]*struct { 136 RepoID int64 137 Count int64 138 }, 0, 10) 139 if err := sess.GroupBy("repo_id"). 140 Select("repo_id AS repo_id, COUNT(*) AS count"). 141 Table("milestone"). 142 Find(&countsSlice); err != nil { 143 return nil, err 144 } 145 146 countMap := make(map[int64]int64, len(countsSlice)) 147 for _, c := range countsSlice { 148 countMap[c.RepoID] = c.Count 149 } 150 return countMap, nil 151 } 152 153 // MilestonesStats represents milestone statistic information. 154 type MilestonesStats struct { 155 OpenCount, ClosedCount int64 156 } 157 158 // Total returns the total counts of milestones 159 func (m MilestonesStats) Total() int64 { 160 return m.OpenCount + m.ClosedCount 161 } 162 163 // GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword. 164 func GetMilestonesStatsByRepoCondAndKw(ctx context.Context, repoCond builder.Cond, keyword string) (*MilestonesStats, error) { 165 var err error 166 stats := &MilestonesStats{} 167 168 sess := db.GetEngine(ctx).Where("is_closed = ?", false) 169 if len(keyword) > 0 { 170 sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) 171 } 172 if repoCond.IsValid() { 173 sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) 174 } 175 stats.OpenCount, err = sess.Count(new(Milestone)) 176 if err != nil { 177 return nil, err 178 } 179 180 sess = db.GetEngine(ctx).Where("is_closed = ?", true) 181 if len(keyword) > 0 { 182 sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) 183 } 184 if repoCond.IsValid() { 185 sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) 186 } 187 stats.ClosedCount, err = sess.Count(new(Milestone)) 188 if err != nil { 189 return nil, err 190 } 191 192 return stats, nil 193 }