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_tracked_time.go 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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. "time"
  7. "code.gitea.io/gitea/models/db"
  8. "code.gitea.io/gitea/modules/setting"
  9. "xorm.io/builder"
  10. )
  11. // TrackedTime represents a time that was spent for a specific issue.
  12. type TrackedTime struct {
  13. ID int64 `xorm:"pk autoincr"`
  14. IssueID int64 `xorm:"INDEX"`
  15. Issue *Issue `xorm:"-"`
  16. UserID int64 `xorm:"INDEX"`
  17. User *User `xorm:"-"`
  18. Created time.Time `xorm:"-"`
  19. CreatedUnix int64 `xorm:"created"`
  20. Time int64 `xorm:"NOT NULL"`
  21. Deleted bool `xorm:"NOT NULL DEFAULT false"`
  22. }
  23. func init() {
  24. db.RegisterModel(new(TrackedTime))
  25. }
  26. // TrackedTimeList is a List of TrackedTime's
  27. type TrackedTimeList []*TrackedTime
  28. // AfterLoad is invoked from XORM after setting the values of all fields of this object.
  29. func (t *TrackedTime) AfterLoad() {
  30. t.Created = time.Unix(t.CreatedUnix, 0).In(setting.DefaultUILocation)
  31. }
  32. // LoadAttributes load Issue, User
  33. func (t *TrackedTime) LoadAttributes() (err error) {
  34. return t.loadAttributes(db.GetEngine(db.DefaultContext))
  35. }
  36. func (t *TrackedTime) loadAttributes(e db.Engine) (err error) {
  37. if t.Issue == nil {
  38. t.Issue, err = getIssueByID(e, t.IssueID)
  39. if err != nil {
  40. return
  41. }
  42. err = t.Issue.loadRepo(e)
  43. if err != nil {
  44. return
  45. }
  46. }
  47. if t.User == nil {
  48. t.User, err = getUserByID(e, t.UserID)
  49. if err != nil {
  50. return
  51. }
  52. }
  53. return
  54. }
  55. // LoadAttributes load Issue, User
  56. func (tl TrackedTimeList) LoadAttributes() (err error) {
  57. for _, t := range tl {
  58. if err = t.LoadAttributes(); err != nil {
  59. return err
  60. }
  61. }
  62. return
  63. }
  64. // FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored.
  65. type FindTrackedTimesOptions struct {
  66. db.ListOptions
  67. IssueID int64
  68. UserID int64
  69. RepositoryID int64
  70. MilestoneID int64
  71. CreatedAfterUnix int64
  72. CreatedBeforeUnix int64
  73. }
  74. // toCond will convert each condition into a xorm-Cond
  75. func (opts *FindTrackedTimesOptions) toCond() builder.Cond {
  76. cond := builder.NewCond().And(builder.Eq{"tracked_time.deleted": false})
  77. if opts.IssueID != 0 {
  78. cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
  79. }
  80. if opts.UserID != 0 {
  81. cond = cond.And(builder.Eq{"user_id": opts.UserID})
  82. }
  83. if opts.RepositoryID != 0 {
  84. cond = cond.And(builder.Eq{"issue.repo_id": opts.RepositoryID})
  85. }
  86. if opts.MilestoneID != 0 {
  87. cond = cond.And(builder.Eq{"issue.milestone_id": opts.MilestoneID})
  88. }
  89. if opts.CreatedAfterUnix != 0 {
  90. cond = cond.And(builder.Gte{"tracked_time.created_unix": opts.CreatedAfterUnix})
  91. }
  92. if opts.CreatedBeforeUnix != 0 {
  93. cond = cond.And(builder.Lte{"tracked_time.created_unix": opts.CreatedBeforeUnix})
  94. }
  95. return cond
  96. }
  97. // toSession will convert the given options to a xorm Session by using the conditions from toCond and joining with issue table if required
  98. func (opts *FindTrackedTimesOptions) toSession(e db.Engine) db.Engine {
  99. sess := e
  100. if opts.RepositoryID > 0 || opts.MilestoneID > 0 {
  101. sess = e.Join("INNER", "issue", "issue.id = tracked_time.issue_id")
  102. }
  103. sess = sess.Where(opts.toCond())
  104. if opts.Page != 0 {
  105. sess = db.SetEnginePagination(sess, opts)
  106. }
  107. return sess
  108. }
  109. func getTrackedTimes(e db.Engine, options *FindTrackedTimesOptions) (trackedTimes TrackedTimeList, err error) {
  110. err = options.toSession(e).Find(&trackedTimes)
  111. return
  112. }
  113. // GetTrackedTimes returns all tracked times that fit to the given options.
  114. func GetTrackedTimes(opts *FindTrackedTimesOptions) (TrackedTimeList, error) {
  115. return getTrackedTimes(db.GetEngine(db.DefaultContext), opts)
  116. }
  117. // CountTrackedTimes returns count of tracked times that fit to the given options.
  118. func CountTrackedTimes(opts *FindTrackedTimesOptions) (int64, error) {
  119. sess := db.GetEngine(db.DefaultContext).Where(opts.toCond())
  120. if opts.RepositoryID > 0 || opts.MilestoneID > 0 {
  121. sess = sess.Join("INNER", "issue", "issue.id = tracked_time.issue_id")
  122. }
  123. return sess.Count(&TrackedTime{})
  124. }
  125. func getTrackedSeconds(e db.Engine, opts FindTrackedTimesOptions) (trackedSeconds int64, err error) {
  126. return opts.toSession(e).SumInt(&TrackedTime{}, "time")
  127. }
  128. // GetTrackedSeconds return sum of seconds
  129. func GetTrackedSeconds(opts FindTrackedTimesOptions) (int64, error) {
  130. return getTrackedSeconds(db.GetEngine(db.DefaultContext), opts)
  131. }
  132. // AddTime will add the given time (in seconds) to the issue
  133. func AddTime(user *User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) {
  134. sess := db.NewSession(db.DefaultContext)
  135. defer sess.Close()
  136. if err := sess.Begin(); err != nil {
  137. return nil, err
  138. }
  139. t, err := addTime(sess, user, issue, amount, created)
  140. if err != nil {
  141. return nil, err
  142. }
  143. if err := issue.loadRepo(sess); err != nil {
  144. return nil, err
  145. }
  146. if _, err := createComment(sess, &CreateCommentOptions{
  147. Issue: issue,
  148. Repo: issue.Repo,
  149. Doer: user,
  150. Content: SecToTime(amount),
  151. Type: CommentTypeAddTimeManual,
  152. TimeID: t.ID,
  153. }); err != nil {
  154. return nil, err
  155. }
  156. return t, sess.Commit()
  157. }
  158. func addTime(e db.Engine, user *User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) {
  159. if created.IsZero() {
  160. created = time.Now()
  161. }
  162. tt := &TrackedTime{
  163. IssueID: issue.ID,
  164. UserID: user.ID,
  165. Time: amount,
  166. Created: created,
  167. }
  168. if _, err := e.Insert(tt); err != nil {
  169. return nil, err
  170. }
  171. return tt, nil
  172. }
  173. // TotalTimes returns the spent time for each user by an issue
  174. func TotalTimes(options *FindTrackedTimesOptions) (map[*User]string, error) {
  175. trackedTimes, err := GetTrackedTimes(options)
  176. if err != nil {
  177. return nil, err
  178. }
  179. // Adding total time per user ID
  180. totalTimesByUser := make(map[int64]int64)
  181. for _, t := range trackedTimes {
  182. totalTimesByUser[t.UserID] += t.Time
  183. }
  184. totalTimes := make(map[*User]string)
  185. // Fetching User and making time human readable
  186. for userID, total := range totalTimesByUser {
  187. user, err := GetUserByID(userID)
  188. if err != nil {
  189. if IsErrUserNotExist(err) {
  190. continue
  191. }
  192. return nil, err
  193. }
  194. totalTimes[user] = SecToTime(total)
  195. }
  196. return totalTimes, nil
  197. }
  198. // DeleteIssueUserTimes deletes times for issue
  199. func DeleteIssueUserTimes(issue *Issue, user *User) error {
  200. sess := db.NewSession(db.DefaultContext)
  201. defer sess.Close()
  202. if err := sess.Begin(); err != nil {
  203. return err
  204. }
  205. opts := FindTrackedTimesOptions{
  206. IssueID: issue.ID,
  207. UserID: user.ID,
  208. }
  209. removedTime, err := deleteTimes(sess, opts)
  210. if err != nil {
  211. return err
  212. }
  213. if removedTime == 0 {
  214. return ErrNotExist{}
  215. }
  216. if err := issue.loadRepo(sess); err != nil {
  217. return err
  218. }
  219. if _, err := createComment(sess, &CreateCommentOptions{
  220. Issue: issue,
  221. Repo: issue.Repo,
  222. Doer: user,
  223. Content: "- " + SecToTime(removedTime),
  224. Type: CommentTypeDeleteTimeManual,
  225. }); err != nil {
  226. return err
  227. }
  228. return sess.Commit()
  229. }
  230. // DeleteTime delete a specific Time
  231. func DeleteTime(t *TrackedTime) error {
  232. sess := db.NewSession(db.DefaultContext)
  233. defer sess.Close()
  234. if err := sess.Begin(); err != nil {
  235. return err
  236. }
  237. if err := t.loadAttributes(sess); err != nil {
  238. return err
  239. }
  240. if err := deleteTime(sess, t); err != nil {
  241. return err
  242. }
  243. if _, err := createComment(sess, &CreateCommentOptions{
  244. Issue: t.Issue,
  245. Repo: t.Issue.Repo,
  246. Doer: t.User,
  247. Content: "- " + SecToTime(t.Time),
  248. Type: CommentTypeDeleteTimeManual,
  249. }); err != nil {
  250. return err
  251. }
  252. return sess.Commit()
  253. }
  254. func deleteTimes(e db.Engine, opts FindTrackedTimesOptions) (removedTime int64, err error) {
  255. removedTime, err = getTrackedSeconds(e, opts)
  256. if err != nil || removedTime == 0 {
  257. return
  258. }
  259. _, err = opts.toSession(e).Table("tracked_time").Cols("deleted").Update(&TrackedTime{Deleted: true})
  260. return
  261. }
  262. func deleteTime(e db.Engine, t *TrackedTime) error {
  263. if t.Deleted {
  264. return ErrNotExist{ID: t.ID}
  265. }
  266. t.Deleted = true
  267. _, err := e.ID(t.ID).Cols("deleted").Update(t)
  268. return err
  269. }
  270. // GetTrackedTimeByID returns raw TrackedTime without loading attributes by id
  271. func GetTrackedTimeByID(id int64) (*TrackedTime, error) {
  272. time := new(TrackedTime)
  273. has, err := db.GetEngine(db.DefaultContext).ID(id).Get(time)
  274. if err != nil {
  275. return nil, err
  276. } else if !has {
  277. return nil, ErrNotExist{ID: id}
  278. }
  279. return time, nil
  280. }