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.

issue_milestone.go 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package models
  5. import (
  6. "context"
  7. "fmt"
  8. "strings"
  9. "time"
  10. "code.gitea.io/gitea/models/db"
  11. "code.gitea.io/gitea/modules/setting"
  12. api "code.gitea.io/gitea/modules/structs"
  13. "code.gitea.io/gitea/modules/timeutil"
  14. "xorm.io/builder"
  15. )
  16. // Milestone represents a milestone of repository.
  17. type Milestone struct {
  18. ID int64 `xorm:"pk autoincr"`
  19. RepoID int64 `xorm:"INDEX"`
  20. Repo *Repository `xorm:"-"`
  21. Name string
  22. Content string `xorm:"TEXT"`
  23. RenderedContent string `xorm:"-"`
  24. IsClosed bool
  25. NumIssues int
  26. NumClosedIssues int
  27. NumOpenIssues int `xorm:"-"`
  28. Completeness int // Percentage(1-100).
  29. IsOverdue bool `xorm:"-"`
  30. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  31. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  32. DeadlineUnix timeutil.TimeStamp
  33. ClosedDateUnix timeutil.TimeStamp
  34. DeadlineString string `xorm:"-"`
  35. TotalTrackedTime int64 `xorm:"-"`
  36. TimeSinceUpdate int64 `xorm:"-"`
  37. }
  38. func init() {
  39. db.RegisterModel(new(Milestone))
  40. }
  41. // BeforeUpdate is invoked from XORM before updating this object.
  42. func (m *Milestone) BeforeUpdate() {
  43. if m.NumIssues > 0 {
  44. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  45. } else {
  46. m.Completeness = 0
  47. }
  48. }
  49. // AfterLoad is invoked from XORM after setting the value of a field of
  50. // this object.
  51. func (m *Milestone) AfterLoad() {
  52. if !m.UpdatedUnix.IsZero() {
  53. m.TimeSinceUpdate = time.Now().Unix() - m.UpdatedUnix.AsTime().Unix()
  54. }
  55. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  56. if m.DeadlineUnix.Year() == 9999 {
  57. return
  58. }
  59. m.DeadlineString = m.DeadlineUnix.Format("2006-01-02")
  60. if m.IsClosed {
  61. m.IsOverdue = m.ClosedDateUnix >= m.DeadlineUnix
  62. } else {
  63. m.IsOverdue = timeutil.TimeStampNow() >= m.DeadlineUnix
  64. }
  65. }
  66. // State returns string representation of milestone status.
  67. func (m *Milestone) State() api.StateType {
  68. if m.IsClosed {
  69. return api.StateClosed
  70. }
  71. return api.StateOpen
  72. }
  73. // NewMilestone creates new milestone of repository.
  74. func NewMilestone(m *Milestone) (err error) {
  75. ctx, committer, err := db.TxContext()
  76. if err != nil {
  77. return err
  78. }
  79. defer committer.Close()
  80. m.Name = strings.TrimSpace(m.Name)
  81. if err = db.Insert(ctx, m); err != nil {
  82. return err
  83. }
  84. if _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID); err != nil {
  85. return err
  86. }
  87. return committer.Commit()
  88. }
  89. func getMilestoneByRepoID(e db.Engine, repoID, id int64) (*Milestone, error) {
  90. m := new(Milestone)
  91. has, err := e.ID(id).Where("repo_id=?", repoID).Get(m)
  92. if err != nil {
  93. return nil, err
  94. } else if !has {
  95. return nil, ErrMilestoneNotExist{ID: id, RepoID: repoID}
  96. }
  97. return m, nil
  98. }
  99. // GetMilestoneByRepoID returns the milestone in a repository.
  100. func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
  101. return getMilestoneByRepoID(db.GetEngine(db.DefaultContext), repoID, id)
  102. }
  103. // GetMilestoneByRepoIDANDName return a milestone if one exist by name and repo
  104. func GetMilestoneByRepoIDANDName(repoID int64, name string) (*Milestone, error) {
  105. var mile Milestone
  106. has, err := db.GetEngine(db.DefaultContext).Where("repo_id=? AND name=?", repoID, name).Get(&mile)
  107. if err != nil {
  108. return nil, err
  109. }
  110. if !has {
  111. return nil, ErrMilestoneNotExist{Name: name, RepoID: repoID}
  112. }
  113. return &mile, nil
  114. }
  115. // GetMilestoneByID returns the milestone via id .
  116. func GetMilestoneByID(id int64) (*Milestone, error) {
  117. return getMilestoneByID(db.GetEngine(db.DefaultContext), id)
  118. }
  119. func getMilestoneByID(e db.Engine, id int64) (*Milestone, error) {
  120. var m Milestone
  121. has, err := e.ID(id).Get(&m)
  122. if err != nil {
  123. return nil, err
  124. } else if !has {
  125. return nil, ErrMilestoneNotExist{ID: id, RepoID: 0}
  126. }
  127. return &m, nil
  128. }
  129. // UpdateMilestone updates information of given milestone.
  130. func UpdateMilestone(m *Milestone, oldIsClosed bool) error {
  131. ctx, committer, err := db.TxContext()
  132. if err != nil {
  133. return err
  134. }
  135. defer committer.Close()
  136. sess := db.GetEngine(ctx)
  137. if m.IsClosed && !oldIsClosed {
  138. m.ClosedDateUnix = timeutil.TimeStampNow()
  139. }
  140. if err := updateMilestone(sess, m); err != nil {
  141. return err
  142. }
  143. // if IsClosed changed, update milestone numbers of repository
  144. if oldIsClosed != m.IsClosed {
  145. if err := updateRepoMilestoneNum(sess, m.RepoID); err != nil {
  146. return err
  147. }
  148. }
  149. return committer.Commit()
  150. }
  151. func updateMilestone(e db.Engine, m *Milestone) error {
  152. m.Name = strings.TrimSpace(m.Name)
  153. _, err := e.ID(m.ID).AllCols().Update(m)
  154. if err != nil {
  155. return err
  156. }
  157. return updateMilestoneCounters(e, m.ID)
  158. }
  159. // updateMilestoneCounters calculates NumIssues, NumClosesIssues and Completeness
  160. func updateMilestoneCounters(e db.Engine, id int64) error {
  161. _, err := e.ID(id).
  162. SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
  163. builder.Eq{"milestone_id": id},
  164. )).
  165. SetExpr("num_closed_issues", builder.Select("count(*)").From("issue").Where(
  166. builder.Eq{
  167. "milestone_id": id,
  168. "is_closed": true,
  169. },
  170. )).
  171. Update(&Milestone{})
  172. if err != nil {
  173. return err
  174. }
  175. _, err = e.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?",
  176. id,
  177. )
  178. return err
  179. }
  180. // ChangeMilestoneStatusByRepoIDAndID changes a milestone open/closed status if the milestone ID is in the repo.
  181. func ChangeMilestoneStatusByRepoIDAndID(repoID, milestoneID int64, isClosed bool) error {
  182. ctx, committer, err := db.TxContext()
  183. if err != nil {
  184. return err
  185. }
  186. defer committer.Close()
  187. sess := db.GetEngine(ctx)
  188. m := &Milestone{
  189. ID: milestoneID,
  190. RepoID: repoID,
  191. }
  192. has, err := sess.ID(milestoneID).Where("repo_id = ?", repoID).Get(m)
  193. if err != nil {
  194. return err
  195. } else if !has {
  196. return ErrMilestoneNotExist{ID: milestoneID, RepoID: repoID}
  197. }
  198. if err := changeMilestoneStatus(sess, m, isClosed); err != nil {
  199. return err
  200. }
  201. return committer.Commit()
  202. }
  203. // ChangeMilestoneStatus changes the milestone open/closed status.
  204. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
  205. ctx, committer, err := db.TxContext()
  206. if err != nil {
  207. return err
  208. }
  209. defer committer.Close()
  210. if err := changeMilestoneStatus(db.GetEngine(ctx), m, isClosed); err != nil {
  211. return err
  212. }
  213. return committer.Commit()
  214. }
  215. func changeMilestoneStatus(e db.Engine, m *Milestone, isClosed bool) error {
  216. m.IsClosed = isClosed
  217. if isClosed {
  218. m.ClosedDateUnix = timeutil.TimeStampNow()
  219. }
  220. count, err := e.ID(m.ID).Where("repo_id = ? AND is_closed = ?", m.RepoID, !isClosed).Cols("is_closed", "closed_date_unix").Update(m)
  221. if err != nil {
  222. return err
  223. }
  224. if count < 1 {
  225. return nil
  226. }
  227. return updateRepoMilestoneNum(e, m.RepoID)
  228. }
  229. func changeMilestoneAssign(ctx context.Context, doer *User, issue *Issue, oldMilestoneID int64) error {
  230. e := db.GetEngine(ctx)
  231. if err := updateIssueCols(e, issue, "milestone_id"); err != nil {
  232. return err
  233. }
  234. if oldMilestoneID > 0 {
  235. if err := updateMilestoneCounters(e, oldMilestoneID); err != nil {
  236. return err
  237. }
  238. }
  239. if issue.MilestoneID > 0 {
  240. if err := updateMilestoneCounters(e, issue.MilestoneID); err != nil {
  241. return err
  242. }
  243. }
  244. if oldMilestoneID > 0 || issue.MilestoneID > 0 {
  245. if err := issue.loadRepo(e); err != nil {
  246. return err
  247. }
  248. opts := &CreateCommentOptions{
  249. Type: CommentTypeMilestone,
  250. Doer: doer,
  251. Repo: issue.Repo,
  252. Issue: issue,
  253. OldMilestoneID: oldMilestoneID,
  254. MilestoneID: issue.MilestoneID,
  255. }
  256. if _, err := createComment(ctx, opts); err != nil {
  257. return err
  258. }
  259. }
  260. return nil
  261. }
  262. // ChangeMilestoneAssign changes assignment of milestone for issue.
  263. func ChangeMilestoneAssign(issue *Issue, doer *User, oldMilestoneID int64) (err error) {
  264. ctx, committer, err := db.TxContext()
  265. if err != nil {
  266. return err
  267. }
  268. defer committer.Close()
  269. if err = changeMilestoneAssign(ctx, doer, issue, oldMilestoneID); err != nil {
  270. return err
  271. }
  272. if err = committer.Commit(); err != nil {
  273. return fmt.Errorf("Commit: %v", err)
  274. }
  275. return nil
  276. }
  277. // DeleteMilestoneByRepoID deletes a milestone from a repository.
  278. func DeleteMilestoneByRepoID(repoID, id int64) error {
  279. m, err := GetMilestoneByRepoID(repoID, id)
  280. if err != nil {
  281. if IsErrMilestoneNotExist(err) {
  282. return nil
  283. }
  284. return err
  285. }
  286. repo, err := GetRepositoryByID(m.RepoID)
  287. if err != nil {
  288. return err
  289. }
  290. ctx, committer, err := db.TxContext()
  291. if err != nil {
  292. return err
  293. }
  294. defer committer.Close()
  295. sess := db.GetEngine(ctx)
  296. if _, err = sess.ID(m.ID).Delete(new(Milestone)); err != nil {
  297. return err
  298. }
  299. numMilestones, err := countRepoMilestones(sess, repo.ID)
  300. if err != nil {
  301. return err
  302. }
  303. numClosedMilestones, err := countRepoClosedMilestones(sess, repo.ID)
  304. if err != nil {
  305. return err
  306. }
  307. repo.NumMilestones = int(numMilestones)
  308. repo.NumClosedMilestones = int(numClosedMilestones)
  309. if _, err = sess.ID(repo.ID).Cols("num_milestones, num_closed_milestones").Update(repo); err != nil {
  310. return err
  311. }
  312. if _, err = db.Exec(ctx, "UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil {
  313. return err
  314. }
  315. return committer.Commit()
  316. }
  317. // MilestoneList is a list of milestones offering additional functionality
  318. type MilestoneList []*Milestone
  319. func (milestones MilestoneList) getMilestoneIDs() []int64 {
  320. ids := make([]int64, 0, len(milestones))
  321. for _, ms := range milestones {
  322. ids = append(ids, ms.ID)
  323. }
  324. return ids
  325. }
  326. // GetMilestonesOption contain options to get milestones
  327. type GetMilestonesOption struct {
  328. db.ListOptions
  329. RepoID int64
  330. State api.StateType
  331. Name string
  332. SortType string
  333. }
  334. func (opts GetMilestonesOption) toCond() builder.Cond {
  335. cond := builder.NewCond()
  336. if opts.RepoID != 0 {
  337. cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
  338. }
  339. switch opts.State {
  340. case api.StateClosed:
  341. cond = cond.And(builder.Eq{"is_closed": true})
  342. case api.StateAll:
  343. break
  344. // api.StateOpen:
  345. default:
  346. cond = cond.And(builder.Eq{"is_closed": false})
  347. }
  348. if len(opts.Name) != 0 {
  349. cond = cond.And(builder.Like{"name", opts.Name})
  350. }
  351. return cond
  352. }
  353. // GetMilestones returns milestones filtered by GetMilestonesOption's
  354. func GetMilestones(opts GetMilestonesOption) (MilestoneList, int64, error) {
  355. sess := db.GetEngine(db.DefaultContext).Where(opts.toCond())
  356. if opts.Page != 0 {
  357. sess = db.SetSessionPagination(sess, &opts)
  358. }
  359. switch opts.SortType {
  360. case "furthestduedate":
  361. sess.Desc("deadline_unix")
  362. case "leastcomplete":
  363. sess.Asc("completeness")
  364. case "mostcomplete":
  365. sess.Desc("completeness")
  366. case "leastissues":
  367. sess.Asc("num_issues")
  368. case "mostissues":
  369. sess.Desc("num_issues")
  370. case "id":
  371. sess.Asc("id")
  372. default:
  373. sess.Asc("deadline_unix").Asc("id")
  374. }
  375. miles := make([]*Milestone, 0, opts.PageSize)
  376. total, err := sess.FindAndCount(&miles)
  377. return miles, total, err
  378. }
  379. // SearchMilestones search milestones
  380. func SearchMilestones(repoCond builder.Cond, page int, isClosed bool, sortType string, keyword string) (MilestoneList, error) {
  381. miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
  382. sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed)
  383. if len(keyword) > 0 {
  384. sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
  385. }
  386. if repoCond.IsValid() {
  387. sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
  388. }
  389. if page > 0 {
  390. sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
  391. }
  392. switch sortType {
  393. case "furthestduedate":
  394. sess.Desc("deadline_unix")
  395. case "leastcomplete":
  396. sess.Asc("completeness")
  397. case "mostcomplete":
  398. sess.Desc("completeness")
  399. case "leastissues":
  400. sess.Asc("num_issues")
  401. case "mostissues":
  402. sess.Desc("num_issues")
  403. default:
  404. sess.Asc("deadline_unix")
  405. }
  406. return miles, sess.Find(&miles)
  407. }
  408. // GetMilestonesByRepoIDs returns a list of milestones of given repositories and status.
  409. func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
  410. return SearchMilestones(
  411. builder.In("repo_id", repoIDs),
  412. page,
  413. isClosed,
  414. sortType,
  415. "",
  416. )
  417. }
  418. // ____ _ _
  419. // / ___|| |_ __ _| |_ ___
  420. // \___ \| __/ _` | __/ __|
  421. // ___) | || (_| | |_\__ \
  422. // |____/ \__\__,_|\__|___/
  423. //
  424. // MilestonesStats represents milestone statistic information.
  425. type MilestonesStats struct {
  426. OpenCount, ClosedCount int64
  427. }
  428. // Total returns the total counts of milestones
  429. func (m MilestonesStats) Total() int64 {
  430. return m.OpenCount + m.ClosedCount
  431. }
  432. // GetMilestonesStatsByRepoCond returns milestone statistic information for dashboard by given conditions.
  433. func GetMilestonesStatsByRepoCond(repoCond builder.Cond) (*MilestonesStats, error) {
  434. var err error
  435. stats := &MilestonesStats{}
  436. sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false)
  437. if repoCond.IsValid() {
  438. sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
  439. }
  440. stats.OpenCount, err = sess.Count(new(Milestone))
  441. if err != nil {
  442. return nil, err
  443. }
  444. sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true)
  445. if repoCond.IsValid() {
  446. sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
  447. }
  448. stats.ClosedCount, err = sess.Count(new(Milestone))
  449. if err != nil {
  450. return nil, err
  451. }
  452. return stats, nil
  453. }
  454. // GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword.
  455. func GetMilestonesStatsByRepoCondAndKw(repoCond builder.Cond, keyword string) (*MilestonesStats, error) {
  456. var err error
  457. stats := &MilestonesStats{}
  458. sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false)
  459. if len(keyword) > 0 {
  460. sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
  461. }
  462. if repoCond.IsValid() {
  463. sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
  464. }
  465. stats.OpenCount, err = sess.Count(new(Milestone))
  466. if err != nil {
  467. return nil, err
  468. }
  469. sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true)
  470. if len(keyword) > 0 {
  471. sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
  472. }
  473. if repoCond.IsValid() {
  474. sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
  475. }
  476. stats.ClosedCount, err = sess.Count(new(Milestone))
  477. if err != nil {
  478. return nil, err
  479. }
  480. return stats, nil
  481. }
  482. func countRepoMilestones(e db.Engine, repoID int64) (int64, error) {
  483. return e.
  484. Where("repo_id=?", repoID).
  485. Count(new(Milestone))
  486. }
  487. func countRepoClosedMilestones(e db.Engine, repoID int64) (int64, error) {
  488. return e.
  489. Where("repo_id=? AND is_closed=?", repoID, true).
  490. Count(new(Milestone))
  491. }
  492. // CountRepoClosedMilestones returns number of closed milestones in given repository.
  493. func CountRepoClosedMilestones(repoID int64) (int64, error) {
  494. return countRepoClosedMilestones(db.GetEngine(db.DefaultContext), repoID)
  495. }
  496. // CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options`
  497. func CountMilestonesByRepoCond(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) {
  498. sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed)
  499. if repoCond.IsValid() {
  500. sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
  501. }
  502. countsSlice := make([]*struct {
  503. RepoID int64
  504. Count int64
  505. }, 0, 10)
  506. if err := sess.GroupBy("repo_id").
  507. Select("repo_id AS repo_id, COUNT(*) AS count").
  508. Table("milestone").
  509. Find(&countsSlice); err != nil {
  510. return nil, err
  511. }
  512. countMap := make(map[int64]int64, len(countsSlice))
  513. for _, c := range countsSlice {
  514. countMap[c.RepoID] = c.Count
  515. }
  516. return countMap, nil
  517. }
  518. // CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options`
  519. func CountMilestonesByRepoCondAndKw(repoCond builder.Cond, keyword string, isClosed bool) (map[int64]int64, error) {
  520. sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed)
  521. if len(keyword) > 0 {
  522. sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
  523. }
  524. if repoCond.IsValid() {
  525. sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
  526. }
  527. countsSlice := make([]*struct {
  528. RepoID int64
  529. Count int64
  530. }, 0, 10)
  531. if err := sess.GroupBy("repo_id").
  532. Select("repo_id AS repo_id, COUNT(*) AS count").
  533. Table("milestone").
  534. Find(&countsSlice); err != nil {
  535. return nil, err
  536. }
  537. countMap := make(map[int64]int64, len(countsSlice))
  538. for _, c := range countsSlice {
  539. countMap[c.RepoID] = c.Count
  540. }
  541. return countMap, nil
  542. }
  543. func updateRepoMilestoneNum(e db.Engine, repoID int64) error {
  544. _, err := e.Exec("UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?",
  545. repoID,
  546. repoID,
  547. true,
  548. repoID,
  549. )
  550. return err
  551. }
  552. // _____ _ _ _____ _
  553. // |_ _| __ __ _ ___| | _____ __| |_ _(_)_ __ ___ ___ ___
  554. // | || '__/ _` |/ __| |/ / _ \/ _` | | | | | '_ ` _ \ / _ \/ __|
  555. // | || | | (_| | (__| < __/ (_| | | | | | | | | | | __/\__ \
  556. // |_||_| \__,_|\___|_|\_\___|\__,_| |_| |_|_| |_| |_|\___||___/
  557. //
  558. func (milestones MilestoneList) loadTotalTrackedTimes(e db.Engine) error {
  559. type totalTimesByMilestone struct {
  560. MilestoneID int64
  561. Time int64
  562. }
  563. if len(milestones) == 0 {
  564. return nil
  565. }
  566. trackedTimes := make(map[int64]int64, len(milestones))
  567. // Get total tracked time by milestone_id
  568. rows, err := e.Table("issue").
  569. Join("INNER", "milestone", "issue.milestone_id = milestone.id").
  570. Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
  571. Where("tracked_time.deleted = ?", false).
  572. Select("milestone_id, sum(time) as time").
  573. In("milestone_id", milestones.getMilestoneIDs()).
  574. GroupBy("milestone_id").
  575. Rows(new(totalTimesByMilestone))
  576. if err != nil {
  577. return err
  578. }
  579. defer rows.Close()
  580. for rows.Next() {
  581. var totalTime totalTimesByMilestone
  582. err = rows.Scan(&totalTime)
  583. if err != nil {
  584. return err
  585. }
  586. trackedTimes[totalTime.MilestoneID] = totalTime.Time
  587. }
  588. for _, milestone := range milestones {
  589. milestone.TotalTrackedTime = trackedTimes[milestone.ID]
  590. }
  591. return nil
  592. }
  593. func (m *Milestone) loadTotalTrackedTime(e db.Engine) error {
  594. type totalTimesByMilestone struct {
  595. MilestoneID int64
  596. Time int64
  597. }
  598. totalTime := &totalTimesByMilestone{MilestoneID: m.ID}
  599. has, err := e.Table("issue").
  600. Join("INNER", "milestone", "issue.milestone_id = milestone.id").
  601. Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
  602. Where("tracked_time.deleted = ?", false).
  603. Select("milestone_id, sum(time) as time").
  604. Where("milestone_id = ?", m.ID).
  605. GroupBy("milestone_id").
  606. Get(totalTime)
  607. if err != nil {
  608. return err
  609. } else if !has {
  610. return nil
  611. }
  612. m.TotalTrackedTime = totalTime.Time
  613. return nil
  614. }
  615. // LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
  616. func (milestones MilestoneList) LoadTotalTrackedTimes() error {
  617. return milestones.loadTotalTrackedTimes(db.GetEngine(db.DefaultContext))
  618. }
  619. // LoadTotalTrackedTime loads the tracked time for the milestone
  620. func (m *Milestone) LoadTotalTrackedTime() error {
  621. return m.loadTotalTrackedTime(db.GetEngine(db.DefaultContext))
  622. }