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