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.

tracked_time.go 8.9KB

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