You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

milestone_list.go 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package issues
  4. import (
  5. "context"
  6. "strings"
  7. "code.gitea.io/gitea/models/db"
  8. "code.gitea.io/gitea/modules/setting"
  9. api "code.gitea.io/gitea/modules/structs"
  10. "xorm.io/builder"
  11. )
  12. // MilestoneList is a list of milestones offering additional functionality
  13. type MilestoneList []*Milestone
  14. func (milestones MilestoneList) getMilestoneIDs() []int64 {
  15. ids := make([]int64, 0, len(milestones))
  16. for _, ms := range milestones {
  17. ids = append(ids, ms.ID)
  18. }
  19. return ids
  20. }
  21. // GetMilestonesOption contain options to get milestones
  22. type GetMilestonesOption struct {
  23. db.ListOptions
  24. RepoID int64
  25. State api.StateType
  26. Name string
  27. SortType string
  28. }
  29. func (opts GetMilestonesOption) toCond() builder.Cond {
  30. cond := builder.NewCond()
  31. if opts.RepoID != 0 {
  32. cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
  33. }
  34. switch opts.State {
  35. case api.StateClosed:
  36. cond = cond.And(builder.Eq{"is_closed": true})
  37. case api.StateAll:
  38. break
  39. // api.StateOpen:
  40. default:
  41. cond = cond.And(builder.Eq{"is_closed": false})
  42. }
  43. if len(opts.Name) != 0 {
  44. cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name))
  45. }
  46. return cond
  47. }
  48. // GetMilestones returns milestones filtered by GetMilestonesOption's
  49. func GetMilestones(opts GetMilestonesOption) (MilestoneList, int64, error) {
  50. sess := db.GetEngine(db.DefaultContext).Where(opts.toCond())
  51. if opts.Page != 0 {
  52. sess = db.SetSessionPagination(sess, &opts)
  53. }
  54. switch opts.SortType {
  55. case "furthestduedate":
  56. sess.Desc("deadline_unix")
  57. case "leastcomplete":
  58. sess.Asc("completeness")
  59. case "mostcomplete":
  60. sess.Desc("completeness")
  61. case "leastissues":
  62. sess.Asc("num_issues")
  63. case "mostissues":
  64. sess.Desc("num_issues")
  65. case "id":
  66. sess.Asc("id")
  67. default:
  68. sess.Asc("deadline_unix").Asc("id")
  69. }
  70. miles := make([]*Milestone, 0, opts.PageSize)
  71. total, err := sess.FindAndCount(&miles)
  72. return miles, total, err
  73. }
  74. // GetMilestoneIDsByNames returns a list of milestone ids by given names.
  75. // It doesn't filter them by repo, so it could return milestones belonging to different repos.
  76. // It's used for filtering issues via indexer, otherwise it would be useless.
  77. // Since it could return milestones with the same name, so the length of returned ids could be more than the length of names.
  78. func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error) {
  79. var ids []int64
  80. return ids, db.GetEngine(ctx).Table("milestone").
  81. Where(db.BuildCaseInsensitiveIn("name", names)).
  82. Cols("id").
  83. Find(&ids)
  84. }
  85. // SearchMilestones search milestones
  86. func SearchMilestones(ctx context.Context, repoCond builder.Cond, page int, isClosed bool, sortType, keyword string) (MilestoneList, error) {
  87. miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
  88. sess := db.GetEngine(ctx).Where("is_closed = ?", isClosed)
  89. if len(keyword) > 0 {
  90. sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
  91. }
  92. if repoCond.IsValid() {
  93. sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
  94. }
  95. if page > 0 {
  96. sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
  97. }
  98. switch sortType {
  99. case "furthestduedate":
  100. sess.Desc("deadline_unix")
  101. case "leastcomplete":
  102. sess.Asc("completeness")
  103. case "mostcomplete":
  104. sess.Desc("completeness")
  105. case "leastissues":
  106. sess.Asc("num_issues")
  107. case "mostissues":
  108. sess.Desc("num_issues")
  109. default:
  110. sess.Asc("deadline_unix")
  111. }
  112. return miles, sess.Find(&miles)
  113. }
  114. // GetMilestonesByRepoIDs returns a list of milestones of given repositories and status.
  115. func GetMilestonesByRepoIDs(ctx context.Context, repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
  116. return SearchMilestones(
  117. ctx,
  118. builder.In("repo_id", repoIDs),
  119. page,
  120. isClosed,
  121. sortType,
  122. "",
  123. )
  124. }
  125. // LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
  126. func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error {
  127. type totalTimesByMilestone struct {
  128. MilestoneID int64
  129. Time int64
  130. }
  131. if len(milestones) == 0 {
  132. return nil
  133. }
  134. trackedTimes := make(map[int64]int64, len(milestones))
  135. // Get total tracked time by milestone_id
  136. rows, err := db.GetEngine(ctx).Table("issue").
  137. Join("INNER", "milestone", "issue.milestone_id = milestone.id").
  138. Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
  139. Where("tracked_time.deleted = ?", false).
  140. Select("milestone_id, sum(time) as time").
  141. In("milestone_id", milestones.getMilestoneIDs()).
  142. GroupBy("milestone_id").
  143. Rows(new(totalTimesByMilestone))
  144. if err != nil {
  145. return err
  146. }
  147. defer rows.Close()
  148. for rows.Next() {
  149. var totalTime totalTimesByMilestone
  150. err = rows.Scan(&totalTime)
  151. if err != nil {
  152. return err
  153. }
  154. trackedTimes[totalTime.MilestoneID] = totalTime.Time
  155. }
  156. for _, milestone := range milestones {
  157. milestone.TotalTrackedTime = trackedTimes[milestone.ID]
  158. }
  159. return nil
  160. }
  161. // CountMilestones returns number of milestones in given repository with other options
  162. func CountMilestones(ctx context.Context, opts GetMilestonesOption) (int64, error) {
  163. return db.GetEngine(ctx).
  164. Where(opts.toCond()).
  165. Count(new(Milestone))
  166. }
  167. // CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options`
  168. func CountMilestonesByRepoCond(ctx context.Context, repoCond builder.Cond, isClosed bool) (map[int64]int64, error) {
  169. sess := db.GetEngine(ctx).Where("is_closed = ?", isClosed)
  170. if repoCond.IsValid() {
  171. sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
  172. }
  173. countsSlice := make([]*struct {
  174. RepoID int64
  175. Count int64
  176. }, 0, 10)
  177. if err := sess.GroupBy("repo_id").
  178. Select("repo_id AS repo_id, COUNT(*) AS count").
  179. Table("milestone").
  180. Find(&countsSlice); err != nil {
  181. return nil, err
  182. }
  183. countMap := make(map[int64]int64, len(countsSlice))
  184. for _, c := range countsSlice {
  185. countMap[c.RepoID] = c.Count
  186. }
  187. return countMap, nil
  188. }
  189. // CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options`
  190. func CountMilestonesByRepoCondAndKw(ctx context.Context, repoCond builder.Cond, keyword string, isClosed bool) (map[int64]int64, error) {
  191. sess := db.GetEngine(ctx).Where("is_closed = ?", isClosed)
  192. if len(keyword) > 0 {
  193. sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
  194. }
  195. if repoCond.IsValid() {
  196. sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
  197. }
  198. countsSlice := make([]*struct {
  199. RepoID int64
  200. Count int64
  201. }, 0, 10)
  202. if err := sess.GroupBy("repo_id").
  203. Select("repo_id AS repo_id, COUNT(*) AS count").
  204. Table("milestone").
  205. Find(&countsSlice); err != nil {
  206. return nil, err
  207. }
  208. countMap := make(map[int64]int64, len(countsSlice))
  209. for _, c := range countsSlice {
  210. countMap[c.RepoID] = c.Count
  211. }
  212. return countMap, nil
  213. }
  214. // MilestonesStats represents milestone statistic information.
  215. type MilestonesStats struct {
  216. OpenCount, ClosedCount int64
  217. }
  218. // Total returns the total counts of milestones
  219. func (m MilestonesStats) Total() int64 {
  220. return m.OpenCount + m.ClosedCount
  221. }
  222. // GetMilestonesStatsByRepoCond returns milestone statistic information for dashboard by given conditions.
  223. func GetMilestonesStatsByRepoCond(ctx context.Context, repoCond builder.Cond) (*MilestonesStats, error) {
  224. var err error
  225. stats := &MilestonesStats{}
  226. sess := db.GetEngine(ctx).Where("is_closed = ?", false)
  227. if repoCond.IsValid() {
  228. sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
  229. }
  230. stats.OpenCount, err = sess.Count(new(Milestone))
  231. if err != nil {
  232. return nil, err
  233. }
  234. sess = db.GetEngine(ctx).Where("is_closed = ?", true)
  235. if repoCond.IsValid() {
  236. sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
  237. }
  238. stats.ClosedCount, err = sess.Count(new(Milestone))
  239. if err != nil {
  240. return nil, err
  241. }
  242. return stats, nil
  243. }
  244. // GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword.
  245. func GetMilestonesStatsByRepoCondAndKw(ctx context.Context, repoCond builder.Cond, keyword string) (*MilestonesStats, error) {
  246. var err error
  247. stats := &MilestonesStats{}
  248. sess := db.GetEngine(ctx).Where("is_closed = ?", false)
  249. if len(keyword) > 0 {
  250. sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
  251. }
  252. if repoCond.IsValid() {
  253. sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
  254. }
  255. stats.OpenCount, err = sess.Count(new(Milestone))
  256. if err != nil {
  257. return nil, err
  258. }
  259. sess = db.GetEngine(ctx).Where("is_closed = ?", true)
  260. if len(keyword) > 0 {
  261. sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
  262. }
  263. if repoCond.IsValid() {
  264. sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
  265. }
  266. stats.ClosedCount, err = sess.Count(new(Milestone))
  267. if err != nil {
  268. return nil, err
  269. }
  270. return stats, nil
  271. }