code.gitea.io/gitea@v1.22.3/models/issues/stopwatch.go (about)

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