2017-09-12 09:48:13 +03:00
// Copyright 2017 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2017-09-12 09:48:13 +03:00
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"
2022-12-04 20:48:18 +03:00
"errors"
2023-06-23 15:12:39 +03:00
"fmt"
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"
2024-03-02 18:42:31 +03:00
"code.gitea.io/gitea/modules/optional"
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"
2023-10-19 17:08:31 +03:00
"xorm.io/xorm"
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
2023-07-22 17:14:27 +03:00
func ( t * TrackedTime ) LoadAttributes ( ctx context . Context ) ( err error ) {
2022-12-04 20:48:18 +03:00
// Load the issue
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 )
2022-12-04 20:48:18 +03:00
if err != nil && ! errors . Is ( err , util . ErrNotExist ) {
return err
2019-12-27 23:30:58 +03:00
}
2022-12-04 20:48:18 +03:00
}
// Now load the repo for the issue (which we may have just loaded)
if t . Issue != nil {
2022-04-08 12:11:15 +03:00
err = t . Issue . LoadRepo ( ctx )
2022-12-04 20:48:18 +03:00
if err != nil && ! errors . Is ( err , util . ErrNotExist ) {
return err
2019-12-27 23:30:58 +03:00
}
}
2022-12-04 20:48:18 +03:00
// Load the user
2019-12-27 23:30:58 +03:00
if t . User == nil {
2022-12-03 05:48:26 +03:00
t . User , err = user_model . GetUserByID ( ctx , t . UserID )
2019-12-27 23:30:58 +03:00
if err != nil {
2022-12-04 20:48:18 +03:00
if ! errors . Is ( err , util . ErrNotExist ) {
return err
}
t . User = user_model . NewGhostUser ( )
2019-12-27 23:30:58 +03:00
}
}
2022-12-04 20:48:18 +03:00
return nil
2019-12-27 23:30:58 +03:00
}
// LoadAttributes load Issue, User
2023-07-22 17:14:27 +03:00
func ( tl TrackedTimeList ) LoadAttributes ( ctx context . Context ) error {
2019-12-27 23:30:58 +03:00
for _ , t := range tl {
2023-07-22 17:14:27 +03:00
if err := t . LoadAttributes ( ctx ) ; err != nil {
2019-12-27 23:30:58 +03:00
return err
}
}
2022-12-04 20:48:18 +03:00
return nil
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
2024-01-15 05:19:25 +03:00
func ( opts * FindTrackedTimesOptions ) ToConds ( ) 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
}
2024-01-15 05:19:25 +03:00
func ( opts * FindTrackedTimesOptions ) ToJoins ( ) [ ] db . JoinFunc {
if opts . RepositoryID > 0 || opts . MilestoneID > 0 {
return [ ] db . JoinFunc {
func ( e db . Engine ) error {
e . Join ( "INNER" , "issue" , "issue.id = tracked_time.issue_id" )
return nil
} ,
}
}
return nil
}
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
2024-01-15 05:19:25 +03:00
sess = sess . Where ( opts . ToConds ( ) )
2020-01-24 22:00:29 +03:00
if opts . Page != 0 {
2024-01-15 05:19:25 +03:00
sess = db . SetSessionPagination ( 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.
2023-07-22 17:14:27 +03:00
func CountTrackedTimes ( ctx context . Context , opts * FindTrackedTimesOptions ) ( int64 , error ) {
2024-01-15 05:19:25 +03:00
sess := db . GetEngine ( ctx ) . Where ( opts . ToConds ( ) )
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
2023-07-22 17:14:27 +03:00
func AddTime ( ctx context . Context , user * user_model . User , issue * Issue , amount int64 , created time . Time ) ( * TrackedTime , error ) {
ctx , committer , err := db . TxContext ( ctx )
2021-11-19 16:39:57 +03:00
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-12-10 05:46:31 +03:00
if _ , err := CreateComment ( ctx , & CreateCommentOptions {
2023-06-23 15:12:39 +03:00
Issue : issue ,
Repo : issue . Repo ,
Doer : user ,
// Content before v1.21 did store the formated string instead of seconds,
// so use "|" as delimeter to mark the new format
Content : fmt . Sprintf ( "|%d" , 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
}
2023-09-29 16:04:14 +03:00
// TotalTimesForEachUser returns the spent time in seconds for each user by an issue
2023-10-03 13:30:41 +03:00
func TotalTimesForEachUser ( ctx context . Context , options * FindTrackedTimesOptions ) ( map [ * user_model . User ] int64 , error ) {
trackedTimes , err := GetTrackedTimes ( ctx , 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
}
2023-06-19 19:40:06 +03:00
totalTimes := make ( map [ * user_model . User ] int64 )
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 {
2023-10-03 13:30:41 +03:00
user , err := user_model . GetUserByID ( ctx , 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
}
2023-06-19 19:40:06 +03:00
totalTimes [ user ] = 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
2023-10-03 13:30:41 +03:00
func DeleteIssueUserTimes ( ctx context . Context , issue * Issue , user * user_model . User ) error {
ctx , committer , err := db . TxContext ( ctx )
2021-11-19 16:39:57 +03:00
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-10-18 08:50:37 +03:00
return db . ErrNotExist { Resource : "tracked_time" }
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-12-10 05:46:31 +03:00
if _ , err := CreateComment ( ctx , & CreateCommentOptions {
2023-06-23 15:12:39 +03:00
Issue : issue ,
Repo : issue . Repo ,
Doer : user ,
// Content before v1.21 did store the formated string instead of seconds,
// so use "|" as delimeter to mark the new format
Content : fmt . Sprintf ( "|%d" , 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
2023-10-03 13:30:41 +03:00
func DeleteTime ( ctx context . Context , t * TrackedTime ) error {
ctx , committer , err := db . TxContext ( ctx )
2021-11-19 16:39:57 +03:00
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
2023-07-22 17:14:27 +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-12-10 05:46:31 +03:00
if _ , err := CreateComment ( ctx , & CreateCommentOptions {
2023-06-23 15:12:39 +03:00
Issue : t . Issue ,
Repo : t . Issue . Repo ,
Doer : t . User ,
// Content before v1.21 did store the formated string instead of seconds,
// so use "|" as delimeter to mark the new format
Content : fmt . Sprintf ( "|%d" , 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 {
2023-07-09 14:58:06 +03:00
return removedTime , err
2019-12-27 23:30:58 +03:00
}
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-10-18 08:50:37 +03:00
return db . ErrNotExist { Resource : "tracked_time" , 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
2023-10-03 13:30:41 +03:00
func GetTrackedTimeByID ( ctx context . Context , id int64 ) ( * TrackedTime , error ) {
2020-05-09 17:18:44 +03:00
time := new ( TrackedTime )
2023-10-03 13:30:41 +03:00
has , err := db . GetEngine ( ctx ) . ID ( id ) . Get ( time )
2019-12-27 23:30:58 +03:00
if err != nil {
return nil , err
} else if ! has {
2022-10-18 08:50:37 +03:00
return nil , db . ErrNotExist { Resource : "tracked_time" , ID : id }
2019-12-27 23:30:58 +03:00
}
return time , nil
}
2023-10-19 17:08:31 +03:00
// GetIssueTotalTrackedTime returns the total tracked time for issues by given conditions.
2024-03-02 18:42:31 +03:00
func GetIssueTotalTrackedTime ( ctx context . Context , opts * IssuesOptions , isClosed optional . Option [ bool ] ) ( int64 , error ) {
2023-10-19 17:08:31 +03:00
if len ( opts . IssueIDs ) <= MaxQueryParameters {
return getIssueTotalTrackedTimeChunk ( ctx , opts , isClosed , opts . IssueIDs )
}
// If too long a list of IDs is provided,
// we get the statistics in smaller chunks and get accumulates
var accum int64
for i := 0 ; i < len ( opts . IssueIDs ) ; {
chunk := i + MaxQueryParameters
if chunk > len ( opts . IssueIDs ) {
chunk = len ( opts . IssueIDs )
}
time , err := getIssueTotalTrackedTimeChunk ( ctx , opts , isClosed , opts . IssueIDs [ i : chunk ] )
if err != nil {
return 0 , err
}
accum += time
i = chunk
}
return accum , nil
}
2024-03-02 18:42:31 +03:00
func getIssueTotalTrackedTimeChunk ( ctx context . Context , opts * IssuesOptions , isClosed optional . Option [ bool ] , issueIDs [ ] int64 ) ( int64 , error ) {
2023-10-19 17:08:31 +03:00
sumSession := func ( opts * IssuesOptions , issueIDs [ ] int64 ) * xorm . Session {
sess := db . GetEngine ( ctx ) .
Table ( "tracked_time" ) .
Where ( "tracked_time.deleted = ?" , false ) .
Join ( "INNER" , "issue" , "tracked_time.issue_id = issue.id" )
return applyIssuesOptions ( sess , opts , issueIDs )
}
type trackedTime struct {
Time int64
}
2024-01-15 18:07:22 +03:00
session := sumSession ( opts , issueIDs )
2024-03-02 18:42:31 +03:00
if isClosed . Has ( ) {
session = session . And ( "issue.is_closed = ?" , isClosed . Value ( ) )
2024-01-15 18:07:22 +03:00
}
return session . SumInt ( new ( trackedTime ) , "tracked_time.time" )
2023-10-19 17:08:31 +03:00
}