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.

stopwatch.go 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package issues
  4. import (
  5. "context"
  6. "fmt"
  7. "time"
  8. "code.gitea.io/gitea/models/db"
  9. "code.gitea.io/gitea/models/repo"
  10. user_model "code.gitea.io/gitea/models/user"
  11. "code.gitea.io/gitea/modules/timeutil"
  12. "code.gitea.io/gitea/modules/util"
  13. )
  14. // ErrIssueStopwatchNotExist represents an error that stopwatch is not exist
  15. type ErrIssueStopwatchNotExist struct {
  16. UserID int64
  17. IssueID int64
  18. }
  19. func (err ErrIssueStopwatchNotExist) Error() string {
  20. return fmt.Sprintf("issue stopwatch doesn't exist[uid: %d, issue_id: %d", err.UserID, err.IssueID)
  21. }
  22. func (err ErrIssueStopwatchNotExist) Unwrap() error {
  23. return util.ErrNotExist
  24. }
  25. // ErrIssueStopwatchAlreadyExist represents an error that stopwatch is already exist
  26. type ErrIssueStopwatchAlreadyExist struct {
  27. UserID int64
  28. IssueID int64
  29. }
  30. func (err ErrIssueStopwatchAlreadyExist) Error() string {
  31. return fmt.Sprintf("issue stopwatch already exists[uid: %d, issue_id: %d", err.UserID, err.IssueID)
  32. }
  33. func (err ErrIssueStopwatchAlreadyExist) Unwrap() error {
  34. return util.ErrAlreadyExist
  35. }
  36. // Stopwatch represents a stopwatch for time tracking.
  37. type Stopwatch struct {
  38. ID int64 `xorm:"pk autoincr"`
  39. IssueID int64 `xorm:"INDEX"`
  40. UserID int64 `xorm:"INDEX"`
  41. CreatedUnix timeutil.TimeStamp `xorm:"created"`
  42. }
  43. func init() {
  44. db.RegisterModel(new(Stopwatch))
  45. }
  46. // Seconds returns the amount of time passed since creation, based on local server time
  47. func (s Stopwatch) Seconds() int64 {
  48. return int64(timeutil.TimeStampNow() - s.CreatedUnix)
  49. }
  50. // Duration returns a human-readable duration string based on local server time
  51. func (s Stopwatch) Duration() string {
  52. return util.SecToTime(s.Seconds())
  53. }
  54. func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
  55. sw = new(Stopwatch)
  56. exists, err = db.GetEngine(ctx).
  57. Where("user_id = ?", userID).
  58. And("issue_id = ?", issueID).
  59. Get(sw)
  60. return sw, exists, err
  61. }
  62. // UserIDCount is a simple coalition of UserID and Count
  63. type UserStopwatch struct {
  64. UserID int64
  65. StopWatches []*Stopwatch
  66. }
  67. // GetUIDsAndNotificationCounts between the two provided times
  68. func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) {
  69. sws := []*Stopwatch{}
  70. if err := db.GetEngine(ctx).Where("issue_id != 0").Find(&sws); err != nil {
  71. return nil, err
  72. }
  73. if len(sws) == 0 {
  74. return []*UserStopwatch{}, nil
  75. }
  76. lastUserID := int64(-1)
  77. res := []*UserStopwatch{}
  78. for _, sw := range sws {
  79. if lastUserID == sw.UserID {
  80. lastUserStopwatch := res[len(res)-1]
  81. lastUserStopwatch.StopWatches = append(lastUserStopwatch.StopWatches, sw)
  82. } else {
  83. res = append(res, &UserStopwatch{
  84. UserID: sw.UserID,
  85. StopWatches: []*Stopwatch{sw},
  86. })
  87. }
  88. }
  89. return res, nil
  90. }
  91. // GetUserStopwatches return list of all stopwatches of a user
  92. func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) {
  93. sws := make([]*Stopwatch, 0, 8)
  94. sess := db.GetEngine(ctx).Where("stopwatch.user_id = ?", userID)
  95. if listOptions.Page != 0 {
  96. sess = db.SetSessionPagination(sess, &listOptions)
  97. }
  98. err := sess.Find(&sws)
  99. if err != nil {
  100. return nil, err
  101. }
  102. return sws, nil
  103. }
  104. // CountUserStopwatches return count of all stopwatches of a user
  105. func CountUserStopwatches(ctx context.Context, userID int64) (int64, error) {
  106. return db.GetEngine(ctx).Where("user_id = ?", userID).Count(&Stopwatch{})
  107. }
  108. // StopwatchExists returns true if the stopwatch exists
  109. func StopwatchExists(ctx context.Context, userID, issueID int64) bool {
  110. _, exists, _ := getStopwatch(ctx, userID, issueID)
  111. return exists
  112. }
  113. // HasUserStopwatch returns true if the user has a stopwatch
  114. func HasUserStopwatch(ctx context.Context, userID int64) (exists bool, sw *Stopwatch, issue *Issue, err error) {
  115. type stopwatchIssueRepo struct {
  116. Stopwatch `xorm:"extends"`
  117. Issue `xorm:"extends"`
  118. repo.Repository `xorm:"extends"`
  119. }
  120. swIR := new(stopwatchIssueRepo)
  121. exists, err = db.GetEngine(ctx).
  122. Table("stopwatch").
  123. Where("user_id = ?", userID).
  124. Join("INNER", "issue", "issue.id = stopwatch.issue_id").
  125. Join("INNER", "repository", "repository.id = issue.repo_id").
  126. Get(swIR)
  127. if exists {
  128. sw = &swIR.Stopwatch
  129. issue = &swIR.Issue
  130. issue.Repo = &swIR.Repository
  131. }
  132. return exists, sw, issue, err
  133. }
  134. // FinishIssueStopwatchIfPossible if stopwatch exist then finish it otherwise ignore
  135. func FinishIssueStopwatchIfPossible(ctx context.Context, user *user_model.User, issue *Issue) error {
  136. _, exists, err := getStopwatch(ctx, user.ID, issue.ID)
  137. if err != nil {
  138. return err
  139. }
  140. if !exists {
  141. return nil
  142. }
  143. return FinishIssueStopwatch(ctx, user, issue)
  144. }
  145. // CreateOrStopIssueStopwatch create an issue stopwatch if it's not exist, otherwise finish it
  146. func CreateOrStopIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error {
  147. _, exists, err := getStopwatch(ctx, user.ID, issue.ID)
  148. if err != nil {
  149. return err
  150. }
  151. if exists {
  152. return FinishIssueStopwatch(ctx, user, issue)
  153. }
  154. return CreateIssueStopwatch(ctx, user, issue)
  155. }
  156. // FinishIssueStopwatch if stopwatch exist then finish it otherwise return an error
  157. func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error {
  158. sw, exists, err := getStopwatch(ctx, user.ID, issue.ID)
  159. if err != nil {
  160. return err
  161. }
  162. if !exists {
  163. return ErrIssueStopwatchNotExist{
  164. UserID: user.ID,
  165. IssueID: issue.ID,
  166. }
  167. }
  168. // Create tracked time out of the time difference between start date and actual date
  169. timediff := time.Now().Unix() - int64(sw.CreatedUnix)
  170. // Create TrackedTime
  171. tt := &TrackedTime{
  172. Created: time.Now(),
  173. IssueID: issue.ID,
  174. UserID: user.ID,
  175. Time: timediff,
  176. }
  177. if err := db.Insert(ctx, tt); err != nil {
  178. return err
  179. }
  180. if err := issue.LoadRepo(ctx); err != nil {
  181. return err
  182. }
  183. if _, err := CreateComment(ctx, &CreateCommentOptions{
  184. Doer: user,
  185. Issue: issue,
  186. Repo: issue.Repo,
  187. Content: util.SecToTime(timediff),
  188. Type: CommentTypeStopTracking,
  189. TimeID: tt.ID,
  190. }); err != nil {
  191. return err
  192. }
  193. _, err = db.DeleteByBean(ctx, sw)
  194. return err
  195. }
  196. // CreateIssueStopwatch creates a stopwatch if not exist, otherwise return an error
  197. func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error {
  198. if err := issue.LoadRepo(ctx); err != nil {
  199. return err
  200. }
  201. // if another stopwatch is running: stop it
  202. exists, _, otherIssue, err := HasUserStopwatch(ctx, user.ID)
  203. if err != nil {
  204. return err
  205. }
  206. if exists {
  207. if err := FinishIssueStopwatch(ctx, user, otherIssue); err != nil {
  208. return err
  209. }
  210. }
  211. // Create stopwatch
  212. sw := &Stopwatch{
  213. UserID: user.ID,
  214. IssueID: issue.ID,
  215. }
  216. if err := db.Insert(ctx, sw); err != nil {
  217. return err
  218. }
  219. if err := issue.LoadRepo(ctx); err != nil {
  220. return err
  221. }
  222. if _, err := CreateComment(ctx, &CreateCommentOptions{
  223. Doer: user,
  224. Issue: issue,
  225. Repo: issue.Repo,
  226. Type: CommentTypeStartTracking,
  227. }); err != nil {
  228. return err
  229. }
  230. return nil
  231. }
  232. // CancelStopwatch removes the given stopwatch and logs it into issue's timeline.
  233. func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error {
  234. ctx, committer, err := db.TxContext(ctx)
  235. if err != nil {
  236. return err
  237. }
  238. defer committer.Close()
  239. if err := cancelStopwatch(ctx, user, issue); err != nil {
  240. return err
  241. }
  242. return committer.Commit()
  243. }
  244. func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error {
  245. e := db.GetEngine(ctx)
  246. sw, exists, err := getStopwatch(ctx, user.ID, issue.ID)
  247. if err != nil {
  248. return err
  249. }
  250. if exists {
  251. if _, err := e.Delete(sw); err != nil {
  252. return err
  253. }
  254. if err := issue.LoadRepo(ctx); err != nil {
  255. return err
  256. }
  257. if _, err := CreateComment(ctx, &CreateCommentOptions{
  258. Doer: user,
  259. Issue: issue,
  260. Repo: issue.Repo,
  261. Type: CommentTypeCancelTracking,
  262. }); err != nil {
  263. return err
  264. }
  265. }
  266. return nil
  267. }