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 5package models 6 7import ( 8 "context" 9 "time" 10 11 "code.gitea.io/gitea/models/db" 12 user_model "code.gitea.io/gitea/models/user" 13 "code.gitea.io/gitea/modules/setting" 14 15 "xorm.io/builder" 16) 17 18// TrackedTime represents a time that was spent for a specific issue. 19type TrackedTime struct { 20 ID int64 `xorm:"pk autoincr"` 21 IssueID int64 `xorm:"INDEX"` 22 Issue *Issue `xorm:"-"` 23 UserID int64 `xorm:"INDEX"` 24 User *user_model.User `xorm:"-"` 25 Created time.Time `xorm:"-"` 26 CreatedUnix int64 `xorm:"created"` 27 Time int64 `xorm:"NOT NULL"` 28 Deleted bool `xorm:"NOT NULL DEFAULT false"` 29} 30 31func init() { 32 db.RegisterModel(new(TrackedTime)) 33} 34 35// TrackedTimeList is a List of TrackedTime's 36type TrackedTimeList []*TrackedTime 37 38// AfterLoad is invoked from XORM after setting the values of all fields of this object. 39func (t *TrackedTime) AfterLoad() { 40 t.Created = time.Unix(t.CreatedUnix, 0).In(setting.DefaultUILocation) 41} 42 43// LoadAttributes load Issue, User 44func (t *TrackedTime) LoadAttributes() (err error) { 45 return t.loadAttributes(db.DefaultContext) 46} 47 48func (t *TrackedTime) loadAttributes(ctx context.Context) (err error) { 49 e := db.GetEngine(ctx) 50 if t.Issue == nil { 51 t.Issue, err = getIssueByID(e, t.IssueID) 52 if err != nil { 53 return 54 } 55 err = t.Issue.loadRepo(ctx) 56 if err != nil { 57 return 58 } 59 } 60 if t.User == nil { 61 t.User, err = user_model.GetUserByIDEngine(e, t.UserID) 62 if err != nil { 63 return 64 } 65 } 66 return 67} 68 69// LoadAttributes load Issue, User 70func (tl TrackedTimeList) LoadAttributes() (err error) { 71 for _, t := range tl { 72 if err = t.LoadAttributes(); err != nil { 73 return err 74 } 75 } 76 return 77} 78 79// FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored. 80type FindTrackedTimesOptions struct { 81 db.ListOptions 82 IssueID int64 83 UserID int64 84 RepositoryID int64 85 MilestoneID int64 86 CreatedAfterUnix int64 87 CreatedBeforeUnix int64 88} 89 90// toCond will convert each condition into a xorm-Cond 91func (opts *FindTrackedTimesOptions) toCond() builder.Cond { 92 cond := builder.NewCond().And(builder.Eq{"tracked_time.deleted": false}) 93 if opts.IssueID != 0 { 94 cond = cond.And(builder.Eq{"issue_id": opts.IssueID}) 95 } 96 if opts.UserID != 0 { 97 cond = cond.And(builder.Eq{"user_id": opts.UserID}) 98 } 99 if opts.RepositoryID != 0 { 100 cond = cond.And(builder.Eq{"issue.repo_id": opts.RepositoryID}) 101 } 102 if opts.MilestoneID != 0 { 103 cond = cond.And(builder.Eq{"issue.milestone_id": opts.MilestoneID}) 104 } 105 if opts.CreatedAfterUnix != 0 { 106 cond = cond.And(builder.Gte{"tracked_time.created_unix": opts.CreatedAfterUnix}) 107 } 108 if opts.CreatedBeforeUnix != 0 { 109 cond = cond.And(builder.Lte{"tracked_time.created_unix": opts.CreatedBeforeUnix}) 110 } 111 return cond 112} 113 114// toSession will convert the given options to a xorm Session by using the conditions from toCond and joining with issue table if required 115func (opts *FindTrackedTimesOptions) toSession(e db.Engine) db.Engine { 116 sess := e 117 if opts.RepositoryID > 0 || opts.MilestoneID > 0 { 118 sess = e.Join("INNER", "issue", "issue.id = tracked_time.issue_id") 119 } 120 121 sess = sess.Where(opts.toCond()) 122 123 if opts.Page != 0 { 124 sess = db.SetEnginePagination(sess, opts) 125 } 126 127 return sess 128} 129 130func getTrackedTimes(e db.Engine, options *FindTrackedTimesOptions) (trackedTimes TrackedTimeList, err error) { 131 err = options.toSession(e).Find(&trackedTimes) 132 return 133} 134 135// GetTrackedTimes returns all tracked times that fit to the given options. 136func GetTrackedTimes(opts *FindTrackedTimesOptions) (TrackedTimeList, error) { 137 return getTrackedTimes(db.GetEngine(db.DefaultContext), opts) 138} 139 140// CountTrackedTimes returns count of tracked times that fit to the given options. 141func CountTrackedTimes(opts *FindTrackedTimesOptions) (int64, error) { 142 sess := db.GetEngine(db.DefaultContext).Where(opts.toCond()) 143 if opts.RepositoryID > 0 || opts.MilestoneID > 0 { 144 sess = sess.Join("INNER", "issue", "issue.id = tracked_time.issue_id") 145 } 146 return sess.Count(&TrackedTime{}) 147} 148 149func getTrackedSeconds(e db.Engine, opts FindTrackedTimesOptions) (trackedSeconds int64, err error) { 150 return opts.toSession(e).SumInt(&TrackedTime{}, "time") 151} 152 153// GetTrackedSeconds return sum of seconds 154func GetTrackedSeconds(opts FindTrackedTimesOptions) (int64, error) { 155 return getTrackedSeconds(db.GetEngine(db.DefaultContext), opts) 156} 157 158// AddTime will add the given time (in seconds) to the issue 159func AddTime(user *user_model.User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) { 160 ctx, committer, err := db.TxContext() 161 if err != nil { 162 return nil, err 163 } 164 defer committer.Close() 165 sess := db.GetEngine(ctx) 166 167 t, err := addTime(sess, user, issue, amount, created) 168 if err != nil { 169 return nil, err 170 } 171 172 if err := issue.loadRepo(ctx); err != nil { 173 return nil, err 174 } 175 176 if _, err := createComment(ctx, &CreateCommentOptions{ 177 Issue: issue, 178 Repo: issue.Repo, 179 Doer: user, 180 Content: SecToTime(amount), 181 Type: CommentTypeAddTimeManual, 182 TimeID: t.ID, 183 }); err != nil { 184 return nil, err 185 } 186 187 return t, committer.Commit() 188} 189 190func addTime(e db.Engine, user *user_model.User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) { 191 if created.IsZero() { 192 created = time.Now() 193 } 194 tt := &TrackedTime{ 195 IssueID: issue.ID, 196 UserID: user.ID, 197 Time: amount, 198 Created: created, 199 } 200 if _, err := e.Insert(tt); err != nil { 201 return nil, err 202 } 203 204 return tt, nil 205} 206 207// TotalTimes returns the spent time for each user by an issue 208func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string, error) { 209 trackedTimes, err := GetTrackedTimes(options) 210 if err != nil { 211 return nil, err 212 } 213 // Adding total time per user ID 214 totalTimesByUser := make(map[int64]int64) 215 for _, t := range trackedTimes { 216 totalTimesByUser[t.UserID] += t.Time 217 } 218 219 totalTimes := make(map[*user_model.User]string) 220 // Fetching User and making time human readable 221 for userID, total := range totalTimesByUser { 222 user, err := user_model.GetUserByID(userID) 223 if err != nil { 224 if user_model.IsErrUserNotExist(err) { 225 continue 226 } 227 return nil, err 228 } 229 totalTimes[user] = SecToTime(total) 230 } 231 return totalTimes, nil 232} 233 234// DeleteIssueUserTimes deletes times for issue 235func DeleteIssueUserTimes(issue *Issue, user *user_model.User) error { 236 ctx, committer, err := db.TxContext() 237 if err != nil { 238 return err 239 } 240 defer committer.Close() 241 sess := db.GetEngine(ctx) 242 243 opts := FindTrackedTimesOptions{ 244 IssueID: issue.ID, 245 UserID: user.ID, 246 } 247 248 removedTime, err := deleteTimes(sess, opts) 249 if err != nil { 250 return err 251 } 252 if removedTime == 0 { 253 return ErrNotExist{} 254 } 255 256 if err := issue.loadRepo(ctx); err != nil { 257 return err 258 } 259 if _, err := createComment(ctx, &CreateCommentOptions{ 260 Issue: issue, 261 Repo: issue.Repo, 262 Doer: user, 263 Content: "- " + SecToTime(removedTime), 264 Type: CommentTypeDeleteTimeManual, 265 }); err != nil { 266 return err 267 } 268 269 return committer.Commit() 270} 271 272// DeleteTime delete a specific Time 273func DeleteTime(t *TrackedTime) error { 274 ctx, committer, err := db.TxContext() 275 if err != nil { 276 return err 277 } 278 defer committer.Close() 279 280 if err := t.loadAttributes(ctx); err != nil { 281 return err 282 } 283 284 if err := deleteTime(db.GetEngine(ctx), t); err != nil { 285 return err 286 } 287 288 if _, err := createComment(ctx, &CreateCommentOptions{ 289 Issue: t.Issue, 290 Repo: t.Issue.Repo, 291 Doer: t.User, 292 Content: "- " + SecToTime(t.Time), 293 Type: CommentTypeDeleteTimeManual, 294 }); err != nil { 295 return err 296 } 297 298 return committer.Commit() 299} 300 301func deleteTimes(e db.Engine, opts FindTrackedTimesOptions) (removedTime int64, err error) { 302 removedTime, err = getTrackedSeconds(e, opts) 303 if err != nil || removedTime == 0 { 304 return 305 } 306 307 _, err = opts.toSession(e).Table("tracked_time").Cols("deleted").Update(&TrackedTime{Deleted: true}) 308 return 309} 310 311func deleteTime(e db.Engine, t *TrackedTime) error { 312 if t.Deleted { 313 return ErrNotExist{ID: t.ID} 314 } 315 t.Deleted = true 316 _, err := e.ID(t.ID).Cols("deleted").Update(t) 317 return err 318} 319 320// GetTrackedTimeByID returns raw TrackedTime without loading attributes by id 321func GetTrackedTimeByID(id int64) (*TrackedTime, error) { 322 time := new(TrackedTime) 323 has, err := db.GetEngine(db.DefaultContext).ID(id).Get(time) 324 if err != nil { 325 return nil, err 326 } else if !has { 327 return nil, ErrNotExist{ID: id} 328 } 329 return time, nil 330} 331