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.4KB

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