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

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