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_stopwatch.go 7.2KB

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