Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

issue_milestone.go 19KB

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