2017-09-12 09:48:13 +03:00
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2022-06-13 12:37:59 +03:00
package issues
2017-09-12 09:48:13 +03:00
import (
2021-12-10 04:27:50 +03:00
"context"
2017-09-12 09:48:13 +03:00
"time"
2021-09-19 14:49:59 +03:00
"code.gitea.io/gitea/models/db"
2021-11-24 12:49:20 +03:00
user_model "code.gitea.io/gitea/models/user"
2017-12-14 02:45:31 +03:00
"code.gitea.io/gitea/modules/setting"
2022-02-15 19:50:10 +03:00
"code.gitea.io/gitea/modules/util"
2017-11-01 05:25:14 +03:00
2019-06-23 18:22:43 +03:00
"xorm.io/builder"
2017-09-12 09:48:13 +03:00
)
// TrackedTime represents a time that was spent for a specific issue.
type TrackedTime struct {
2021-11-24 12:49:20 +03:00
ID int64 ` xorm:"pk autoincr" `
IssueID int64 ` xorm:"INDEX" `
Issue * Issue ` xorm:"-" `
UserID int64 ` xorm:"INDEX" `
User * user_model . User ` xorm:"-" `
Created time . Time ` xorm:"-" `
CreatedUnix int64 ` xorm:"created" `
Time int64 ` xorm:"NOT NULL" `
Deleted bool ` xorm:"NOT NULL DEFAULT false" `
2017-09-12 09:48:13 +03:00
}
2021-09-19 14:49:59 +03:00
func init ( ) {
db . RegisterModel ( new ( TrackedTime ) )
}
2019-12-27 23:30:58 +03:00
// TrackedTimeList is a List of TrackedTime's
type TrackedTimeList [ ] * TrackedTime
2017-10-01 19:52:35 +03:00
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
func ( t * TrackedTime ) AfterLoad ( ) {
2019-08-15 17:46:21 +03:00
t . Created = time . Unix ( t . CreatedUnix , 0 ) . In ( setting . DefaultUILocation )
2017-09-12 09:48:13 +03:00
}
2019-12-27 23:30:58 +03:00
// LoadAttributes load Issue, User
func ( t * TrackedTime ) LoadAttributes ( ) ( err error ) {
2021-12-10 04:27:50 +03:00
return t . loadAttributes ( db . DefaultContext )
2019-12-27 23:30:58 +03:00
}
2021-12-10 04:27:50 +03:00
func ( t * TrackedTime ) loadAttributes ( ctx context . Context ) ( err error ) {
2019-12-27 23:30:58 +03:00
if t . Issue == nil {
2022-06-13 12:37:59 +03:00
t . Issue , err = GetIssueByID ( ctx , t . IssueID )
2019-12-27 23:30:58 +03:00
if err != nil {
return
}
2022-04-08 12:11:15 +03:00
err = t . Issue . LoadRepo ( ctx )
2019-12-27 23:30:58 +03:00
if err != nil {
return
}
}
if t . User == nil {
2022-05-20 17:08:52 +03:00
t . User , err = user_model . GetUserByIDCtx ( ctx , t . UserID )
2019-12-27 23:30:58 +03:00
if err != nil {
return
}
}
2022-06-20 13:02:49 +03:00
return err
2019-12-27 23:30:58 +03:00
}
// LoadAttributes load Issue, User
func ( tl TrackedTimeList ) LoadAttributes ( ) ( err error ) {
for _ , t := range tl {
if err = t . LoadAttributes ( ) ; err != nil {
return err
}
}
2022-06-20 13:02:49 +03:00
return err
2019-12-27 23:30:58 +03:00
}
2017-09-12 09:48:13 +03:00
// FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored.
type FindTrackedTimesOptions struct {
2021-09-24 14:32:56 +03:00
db . ListOptions
2020-01-09 00:14:00 +03:00
IssueID int64
UserID int64
RepositoryID int64
MilestoneID int64
CreatedAfterUnix int64
CreatedBeforeUnix int64
2017-09-12 09:48:13 +03:00
}
2021-08-12 15:43:08 +03:00
// toCond will convert each condition into a xorm-Cond
func ( opts * FindTrackedTimesOptions ) toCond ( ) builder . Cond {
2019-12-27 23:30:58 +03:00
cond := builder . NewCond ( ) . And ( builder . Eq { "tracked_time.deleted" : false } )
2017-09-12 09:48:13 +03:00
if opts . IssueID != 0 {
cond = cond . And ( builder . Eq { "issue_id" : opts . IssueID } )
}
if opts . UserID != 0 {
cond = cond . And ( builder . Eq { "user_id" : opts . UserID } )
}
if opts . RepositoryID != 0 {
cond = cond . And ( builder . Eq { "issue.repo_id" : opts . RepositoryID } )
}
2018-04-29 08:58:47 +03:00
if opts . MilestoneID != 0 {
cond = cond . And ( builder . Eq { "issue.milestone_id" : opts . MilestoneID } )
}
2020-01-09 00:14:00 +03:00
if opts . CreatedAfterUnix != 0 {
cond = cond . And ( builder . Gte { "tracked_time.created_unix" : opts . CreatedAfterUnix } )
}
if opts . CreatedBeforeUnix != 0 {
cond = cond . And ( builder . Lte { "tracked_time.created_unix" : opts . CreatedBeforeUnix } )
}
2017-09-12 09:48:13 +03:00
return cond
}
2021-08-12 15:43:08 +03:00
// toSession will convert the given options to a xorm Session by using the conditions from toCond and joining with issue table if required
2021-09-19 14:49:59 +03:00
func ( opts * FindTrackedTimesOptions ) toSession ( e db . Engine ) db . Engine {
2020-01-24 22:00:29 +03:00
sess := e
2018-04-29 08:58:47 +03:00
if opts . RepositoryID > 0 || opts . MilestoneID > 0 {
2020-01-24 22:00:29 +03:00
sess = e . Join ( "INNER" , "issue" , "issue.id = tracked_time.issue_id" )
2018-04-29 08:58:47 +03:00
}
2020-01-24 22:00:29 +03:00
2021-08-12 15:43:08 +03:00
sess = sess . Where ( opts . toCond ( ) )
2020-01-24 22:00:29 +03:00
if opts . Page != 0 {
2021-09-24 14:32:56 +03:00
sess = db . SetEnginePagination ( sess , opts )
2020-01-24 22:00:29 +03:00
}
return sess
2018-04-29 08:58:47 +03:00
}
2019-12-27 23:30:58 +03:00
// GetTrackedTimes returns all tracked times that fit to the given options.
2022-05-20 17:08:52 +03:00
func GetTrackedTimes ( ctx context . Context , options * FindTrackedTimesOptions ) ( trackedTimes TrackedTimeList , err error ) {
err = options . toSession ( db . GetEngine ( ctx ) ) . Find ( & trackedTimes )
2022-06-20 13:02:49 +03:00
return trackedTimes , err
2019-12-27 23:30:58 +03:00
}
2021-08-12 15:43:08 +03:00
// CountTrackedTimes returns count of tracked times that fit to the given options.
func CountTrackedTimes ( opts * FindTrackedTimesOptions ) ( int64 , error ) {
2021-09-23 18:45:36 +03:00
sess := db . GetEngine ( db . DefaultContext ) . Where ( opts . toCond ( ) )
2021-08-12 15:43:08 +03:00
if opts . RepositoryID > 0 || opts . MilestoneID > 0 {
sess = sess . Join ( "INNER" , "issue" , "issue.id = tracked_time.issue_id" )
}
return sess . Count ( & TrackedTime { } )
}
2019-12-27 23:30:58 +03:00
// GetTrackedSeconds return sum of seconds
2022-05-20 17:08:52 +03:00
func GetTrackedSeconds ( ctx context . Context , opts FindTrackedTimesOptions ) ( trackedSeconds int64 , err error ) {
return opts . toSession ( db . GetEngine ( ctx ) ) . SumInt ( & TrackedTime { } , "time" )
2019-12-27 23:30:58 +03:00
}
2017-09-12 09:48:13 +03:00
// AddTime will add the given time (in seconds) to the issue
2021-11-24 12:49:20 +03:00
func AddTime ( user * user_model . User , issue * Issue , amount int64 , created time . Time ) ( * TrackedTime , error ) {
2021-11-19 16:39:57 +03:00
ctx , committer , err := db . TxContext ( )
if err != nil {
2019-12-27 23:30:58 +03:00
return nil , err
2017-09-12 09:48:13 +03:00
}
2021-11-19 16:39:57 +03:00
defer committer . Close ( )
2019-12-27 23:30:58 +03:00
2022-05-20 17:08:52 +03:00
t , err := addTime ( ctx , user , issue , amount , created )
2019-12-27 23:30:58 +03:00
if err != nil {
2017-09-12 09:48:13 +03:00
return nil , err
}
2019-12-27 23:30:58 +03:00
2022-04-08 12:11:15 +03:00
if err := issue . LoadRepo ( ctx ) ; err != nil {
2018-12-13 18:55:43 +03:00
return nil , err
}
2019-12-27 23:30:58 +03:00
2022-04-08 12:11:15 +03:00
if _ , err := CreateCommentCtx ( ctx , & CreateCommentOptions {
2017-09-12 09:48:13 +03:00
Issue : issue ,
Repo : issue . Repo ,
Doer : user ,
2022-02-15 19:50:10 +03:00
Content : util . SecToTime ( amount ) ,
2017-09-12 09:48:13 +03:00
Type : CommentTypeAddTimeManual ,
2021-02-19 13:52:11 +03:00
TimeID : t . ID ,
2017-09-12 09:48:13 +03:00
} ) ; err != nil {
return nil , err
}
2019-12-27 23:30:58 +03:00
2021-11-19 16:39:57 +03:00
return t , committer . Commit ( )
2019-12-27 23:30:58 +03:00
}
2022-05-20 17:08:52 +03:00
func addTime ( ctx context . Context , user * user_model . User , issue * Issue , amount int64 , created time . Time ) ( * TrackedTime , error ) {
2019-12-27 23:30:58 +03:00
if created . IsZero ( ) {
created = time . Now ( )
}
tt := & TrackedTime {
IssueID : issue . ID ,
UserID : user . ID ,
Time : amount ,
Created : created ,
}
2022-05-20 17:08:52 +03:00
return tt , db . Insert ( ctx , tt )
2017-09-12 09:48:13 +03:00
}
// TotalTimes returns the spent time for each user by an issue
2021-11-24 12:49:20 +03:00
func TotalTimes ( options * FindTrackedTimesOptions ) ( map [ * user_model . User ] string , error ) {
2022-05-20 17:08:52 +03:00
trackedTimes , err := GetTrackedTimes ( db . DefaultContext , options )
2017-09-12 09:48:13 +03:00
if err != nil {
return nil , err
}
2021-03-14 21:52:12 +03:00
// Adding total time per user ID
2017-09-12 09:48:13 +03:00
totalTimesByUser := make ( map [ int64 ] int64 )
for _ , t := range trackedTimes {
totalTimesByUser [ t . UserID ] += t . Time
}
2021-11-24 12:49:20 +03:00
totalTimes := make ( map [ * user_model . User ] string )
2021-03-14 21:52:12 +03:00
// Fetching User and making time human readable
2017-09-12 09:48:13 +03:00
for userID , total := range totalTimesByUser {
2021-11-24 12:49:20 +03:00
user , err := user_model . GetUserByID ( userID )
2017-09-12 09:48:13 +03:00
if err != nil {
2021-11-24 12:49:20 +03:00
if user_model . IsErrUserNotExist ( err ) {
2017-09-12 09:48:13 +03:00
continue
}
return nil , err
}
2022-02-15 19:50:10 +03:00
totalTimes [ user ] = util . SecToTime ( total )
2017-09-12 09:48:13 +03:00
}
return totalTimes , nil
}
2019-12-27 23:30:58 +03:00
// DeleteIssueUserTimes deletes times for issue
2021-11-24 12:49:20 +03:00
func DeleteIssueUserTimes ( issue * Issue , user * user_model . User ) error {
2021-11-19 16:39:57 +03:00
ctx , committer , err := db . TxContext ( )
if err != nil {
2019-12-27 23:30:58 +03:00
return err
}
2021-11-19 16:39:57 +03:00
defer committer . Close ( )
2019-12-27 23:30:58 +03:00
opts := FindTrackedTimesOptions {
IssueID : issue . ID ,
UserID : user . ID ,
}
2022-05-20 17:08:52 +03:00
removedTime , err := deleteTimes ( ctx , opts )
2019-12-27 23:30:58 +03:00
if err != nil {
return err
}
if removedTime == 0 {
2022-05-08 16:46:34 +03:00
return db . ErrNotExist { }
2019-12-27 23:30:58 +03:00
}
2022-04-08 12:11:15 +03:00
if err := issue . LoadRepo ( ctx ) ; err != nil {
2019-12-27 23:30:58 +03:00
return err
}
2022-04-08 12:11:15 +03:00
if _ , err := CreateCommentCtx ( ctx , & CreateCommentOptions {
2019-12-27 23:30:58 +03:00
Issue : issue ,
Repo : issue . Repo ,
Doer : user ,
2022-02-15 19:50:10 +03:00
Content : "- " + util . SecToTime ( removedTime ) ,
2019-12-27 23:30:58 +03:00
Type : CommentTypeDeleteTimeManual ,
} ) ; err != nil {
return err
}
2021-11-19 16:39:57 +03:00
return committer . Commit ( )
2019-12-27 23:30:58 +03:00
}
// DeleteTime delete a specific Time
func DeleteTime ( t * TrackedTime ) error {
2021-11-19 16:39:57 +03:00
ctx , committer , err := db . TxContext ( )
if err != nil {
2019-12-27 23:30:58 +03:00
return err
}
2021-11-19 16:39:57 +03:00
defer committer . Close ( )
2019-12-27 23:30:58 +03:00
2021-12-10 04:27:50 +03:00
if err := t . loadAttributes ( ctx ) ; err != nil {
2020-05-09 17:18:44 +03:00
return err
}
2022-05-20 17:08:52 +03:00
if err := deleteTime ( ctx , t ) ; err != nil {
2019-12-27 23:30:58 +03:00
return err
}
2022-04-08 12:11:15 +03:00
if _ , err := CreateCommentCtx ( ctx , & CreateCommentOptions {
2019-12-27 23:30:58 +03:00
Issue : t . Issue ,
Repo : t . Issue . Repo ,
Doer : t . User ,
2022-02-15 19:50:10 +03:00
Content : "- " + util . SecToTime ( t . Time ) ,
2019-12-27 23:30:58 +03:00
Type : CommentTypeDeleteTimeManual ,
} ) ; err != nil {
return err
}
2021-11-19 16:39:57 +03:00
return committer . Commit ( )
2019-12-27 23:30:58 +03:00
}
2022-05-20 17:08:52 +03:00
func deleteTimes ( ctx context . Context , opts FindTrackedTimesOptions ) ( removedTime int64 , err error ) {
removedTime , err = GetTrackedSeconds ( ctx , opts )
2019-12-27 23:30:58 +03:00
if err != nil || removedTime == 0 {
return
}
2022-05-20 17:08:52 +03:00
_ , err = opts . toSession ( db . GetEngine ( ctx ) ) . Table ( "tracked_time" ) . Cols ( "deleted" ) . Update ( & TrackedTime { Deleted : true } )
2022-06-20 13:02:49 +03:00
return removedTime , err
2019-12-27 23:30:58 +03:00
}
2022-05-20 17:08:52 +03:00
func deleteTime ( ctx context . Context , t * TrackedTime ) error {
2019-12-27 23:30:58 +03:00
if t . Deleted {
2022-05-08 16:46:34 +03:00
return db . ErrNotExist { ID : t . ID }
2019-12-27 23:30:58 +03:00
}
t . Deleted = true
2022-05-20 17:08:52 +03:00
_ , err := db . GetEngine ( ctx ) . ID ( t . ID ) . Cols ( "deleted" ) . Update ( t )
2019-12-27 23:30:58 +03:00
return err
}
// GetTrackedTimeByID returns raw TrackedTime without loading attributes by id
func GetTrackedTimeByID ( id int64 ) ( * TrackedTime , error ) {
2020-05-09 17:18:44 +03:00
time := new ( TrackedTime )
2021-09-23 18:45:36 +03:00
has , err := db . GetEngine ( db . DefaultContext ) . ID ( id ) . Get ( time )
2019-12-27 23:30:58 +03:00
if err != nil {
return nil , err
} else if ! has {
2022-05-08 16:46:34 +03:00
return nil , db . ErrNotExist { ID : id }
2019-12-27 23:30:58 +03:00
}
return time , nil
}