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 repo 6 7import ( 8 "fmt" 9 "net/http" 10 "time" 11 12 "code.gitea.io/gitea/models" 13 "code.gitea.io/gitea/models/unit" 14 user_model "code.gitea.io/gitea/models/user" 15 "code.gitea.io/gitea/modules/context" 16 "code.gitea.io/gitea/modules/convert" 17 api "code.gitea.io/gitea/modules/structs" 18 "code.gitea.io/gitea/modules/web" 19 "code.gitea.io/gitea/routers/api/v1/utils" 20) 21 22// ListTrackedTimes list all the tracked times of an issue 23func ListTrackedTimes(ctx *context.APIContext) { 24 // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/times issue issueTrackedTimes 25 // --- 26 // summary: List an issue's tracked times 27 // produces: 28 // - application/json 29 // parameters: 30 // - name: owner 31 // in: path 32 // description: owner of the repo 33 // type: string 34 // required: true 35 // - name: repo 36 // in: path 37 // description: name of the repo 38 // type: string 39 // required: true 40 // - name: index 41 // in: path 42 // description: index of the issue 43 // type: integer 44 // format: int64 45 // required: true 46 // - name: user 47 // in: query 48 // description: optional filter by user (available for issue managers) 49 // type: string 50 // - name: since 51 // in: query 52 // description: Only show times updated after the given time. This is a timestamp in RFC 3339 format 53 // type: string 54 // format: date-time 55 // - name: before 56 // in: query 57 // description: Only show times updated before the given time. This is a timestamp in RFC 3339 format 58 // type: string 59 // format: date-time 60 // - name: page 61 // in: query 62 // description: page number of results to return (1-based) 63 // type: integer 64 // - name: limit 65 // in: query 66 // description: page size of results 67 // type: integer 68 // responses: 69 // "200": 70 // "$ref": "#/responses/TrackedTimeList" 71 // "404": 72 // "$ref": "#/responses/notFound" 73 74 if !ctx.Repo.Repository.IsTimetrackerEnabled() { 75 ctx.NotFound("Timetracker is disabled") 76 return 77 } 78 issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 79 if err != nil { 80 if models.IsErrIssueNotExist(err) { 81 ctx.NotFound(err) 82 } else { 83 ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) 84 } 85 return 86 } 87 88 opts := &models.FindTrackedTimesOptions{ 89 ListOptions: utils.GetListOptions(ctx), 90 RepositoryID: ctx.Repo.Repository.ID, 91 IssueID: issue.ID, 92 } 93 94 qUser := ctx.FormTrim("user") 95 if qUser != "" { 96 user, err := user_model.GetUserByName(qUser) 97 if user_model.IsErrUserNotExist(err) { 98 ctx.Error(http.StatusNotFound, "User does not exist", err) 99 } else if err != nil { 100 ctx.Error(http.StatusInternalServerError, "GetUserByName", err) 101 return 102 } 103 opts.UserID = user.ID 104 } 105 106 if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil { 107 ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) 108 return 109 } 110 111 cantSetUser := !ctx.User.IsAdmin && 112 opts.UserID != ctx.User.ID && 113 !ctx.IsUserRepoWriter([]unit.Type{unit.TypeIssues}) 114 115 if cantSetUser { 116 if opts.UserID == 0 { 117 opts.UserID = ctx.User.ID 118 } else { 119 ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights")) 120 return 121 } 122 } 123 124 count, err := models.CountTrackedTimes(opts) 125 if err != nil { 126 ctx.InternalServerError(err) 127 return 128 } 129 130 trackedTimes, err := models.GetTrackedTimes(opts) 131 if err != nil { 132 ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err) 133 return 134 } 135 if err = trackedTimes.LoadAttributes(); err != nil { 136 ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) 137 return 138 } 139 140 ctx.SetTotalCountHeader(count) 141 ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(trackedTimes)) 142} 143 144// AddTime add time manual to the given issue 145func AddTime(ctx *context.APIContext) { 146 // swagger:operation Post /repos/{owner}/{repo}/issues/{index}/times issue issueAddTime 147 // --- 148 // summary: Add tracked time to a issue 149 // consumes: 150 // - application/json 151 // produces: 152 // - application/json 153 // parameters: 154 // - name: owner 155 // in: path 156 // description: owner of the repo 157 // type: string 158 // required: true 159 // - name: repo 160 // in: path 161 // description: name of the repo 162 // type: string 163 // required: true 164 // - name: index 165 // in: path 166 // description: index of the issue 167 // type: integer 168 // format: int64 169 // required: true 170 // - name: body 171 // in: body 172 // schema: 173 // "$ref": "#/definitions/AddTimeOption" 174 // responses: 175 // "200": 176 // "$ref": "#/responses/TrackedTime" 177 // "400": 178 // "$ref": "#/responses/error" 179 // "403": 180 // "$ref": "#/responses/forbidden" 181 form := web.GetForm(ctx).(*api.AddTimeOption) 182 issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 183 if err != nil { 184 if models.IsErrIssueNotExist(err) { 185 ctx.NotFound(err) 186 } else { 187 ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) 188 } 189 return 190 } 191 192 if !ctx.Repo.CanUseTimetracker(issue, ctx.User) { 193 if !ctx.Repo.Repository.IsTimetrackerEnabled() { 194 ctx.Error(http.StatusBadRequest, "", "time tracking disabled") 195 return 196 } 197 ctx.Status(http.StatusForbidden) 198 return 199 } 200 201 user := ctx.User 202 if form.User != "" { 203 if (ctx.IsUserRepoAdmin() && ctx.User.Name != form.User) || ctx.User.IsAdmin { 204 //allow only RepoAdmin, Admin and User to add time 205 user, err = user_model.GetUserByName(form.User) 206 if err != nil { 207 ctx.Error(http.StatusInternalServerError, "GetUserByName", err) 208 } 209 } 210 } 211 212 created := time.Time{} 213 if !form.Created.IsZero() { 214 created = form.Created 215 } 216 217 trackedTime, err := models.AddTime(user, issue, form.Time, created) 218 if err != nil { 219 ctx.Error(http.StatusInternalServerError, "AddTime", err) 220 return 221 } 222 if err = trackedTime.LoadAttributes(); err != nil { 223 ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) 224 return 225 } 226 ctx.JSON(http.StatusOK, convert.ToTrackedTime(trackedTime)) 227} 228 229// ResetIssueTime reset time manual to the given issue 230func ResetIssueTime(ctx *context.APIContext) { 231 // swagger:operation Delete /repos/{owner}/{repo}/issues/{index}/times issue issueResetTime 232 // --- 233 // summary: Reset a tracked time of an issue 234 // consumes: 235 // - application/json 236 // produces: 237 // - application/json 238 // parameters: 239 // - name: owner 240 // in: path 241 // description: owner of the repo 242 // type: string 243 // required: true 244 // - name: repo 245 // in: path 246 // description: name of the repo 247 // type: string 248 // required: true 249 // - name: index 250 // in: path 251 // description: index of the issue to add tracked time to 252 // type: integer 253 // format: int64 254 // required: true 255 // responses: 256 // "204": 257 // "$ref": "#/responses/empty" 258 // "400": 259 // "$ref": "#/responses/error" 260 // "403": 261 // "$ref": "#/responses/forbidden" 262 263 issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 264 if err != nil { 265 if models.IsErrIssueNotExist(err) { 266 ctx.NotFound(err) 267 } else { 268 ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) 269 } 270 return 271 } 272 273 if !ctx.Repo.CanUseTimetracker(issue, ctx.User) { 274 if !ctx.Repo.Repository.IsTimetrackerEnabled() { 275 ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"}) 276 return 277 } 278 ctx.Status(http.StatusForbidden) 279 return 280 } 281 282 err = models.DeleteIssueUserTimes(issue, ctx.User) 283 if err != nil { 284 if models.IsErrNotExist(err) { 285 ctx.Error(http.StatusNotFound, "DeleteIssueUserTimes", err) 286 } else { 287 ctx.Error(http.StatusInternalServerError, "DeleteIssueUserTimes", err) 288 } 289 return 290 } 291 ctx.Status(204) 292} 293 294// DeleteTime delete a specific time by id 295func DeleteTime(ctx *context.APIContext) { 296 // swagger:operation Delete /repos/{owner}/{repo}/issues/{index}/times/{id} issue issueDeleteTime 297 // --- 298 // summary: Delete specific tracked time 299 // consumes: 300 // - application/json 301 // produces: 302 // - application/json 303 // parameters: 304 // - name: owner 305 // in: path 306 // description: owner of the repo 307 // type: string 308 // required: true 309 // - name: repo 310 // in: path 311 // description: name of the repo 312 // type: string 313 // required: true 314 // - name: index 315 // in: path 316 // description: index of the issue 317 // type: integer 318 // format: int64 319 // required: true 320 // - name: id 321 // in: path 322 // description: id of time to delete 323 // type: integer 324 // format: int64 325 // required: true 326 // responses: 327 // "204": 328 // "$ref": "#/responses/empty" 329 // "400": 330 // "$ref": "#/responses/error" 331 // "403": 332 // "$ref": "#/responses/forbidden" 333 334 issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 335 if err != nil { 336 if models.IsErrIssueNotExist(err) { 337 ctx.NotFound(err) 338 } else { 339 ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) 340 } 341 return 342 } 343 344 if !ctx.Repo.CanUseTimetracker(issue, ctx.User) { 345 if !ctx.Repo.Repository.IsTimetrackerEnabled() { 346 ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"}) 347 return 348 } 349 ctx.Status(http.StatusForbidden) 350 return 351 } 352 353 time, err := models.GetTrackedTimeByID(ctx.ParamsInt64(":id")) 354 if err != nil { 355 if models.IsErrNotExist(err) { 356 ctx.NotFound(err) 357 return 358 } 359 ctx.Error(http.StatusInternalServerError, "GetTrackedTimeByID", err) 360 return 361 } 362 if time.Deleted { 363 ctx.NotFound(fmt.Errorf("tracked time [%d] already deleted", time.ID)) 364 return 365 } 366 367 if !ctx.User.IsAdmin && time.UserID != ctx.User.ID { 368 //Only Admin and User itself can delete their time 369 ctx.Status(http.StatusForbidden) 370 return 371 } 372 373 err = models.DeleteTime(time) 374 if err != nil { 375 ctx.Error(http.StatusInternalServerError, "DeleteTime", err) 376 return 377 } 378 ctx.Status(http.StatusNoContent) 379} 380 381// ListTrackedTimesByUser lists all tracked times of the user 382func ListTrackedTimesByUser(ctx *context.APIContext) { 383 // swagger:operation GET /repos/{owner}/{repo}/times/{user} repository userTrackedTimes 384 // --- 385 // summary: List a user's tracked times in a repo 386 // deprecated: true 387 // produces: 388 // - application/json 389 // parameters: 390 // - name: owner 391 // in: path 392 // description: owner of the repo 393 // type: string 394 // required: true 395 // - name: repo 396 // in: path 397 // description: name of the repo 398 // type: string 399 // required: true 400 // - name: user 401 // in: path 402 // description: username of user 403 // type: string 404 // required: true 405 // responses: 406 // "200": 407 // "$ref": "#/responses/TrackedTimeList" 408 // "400": 409 // "$ref": "#/responses/error" 410 // "403": 411 // "$ref": "#/responses/forbidden" 412 413 if !ctx.Repo.Repository.IsTimetrackerEnabled() { 414 ctx.Error(http.StatusBadRequest, "", "time tracking disabled") 415 return 416 } 417 user, err := user_model.GetUserByName(ctx.Params(":timetrackingusername")) 418 if err != nil { 419 if user_model.IsErrUserNotExist(err) { 420 ctx.NotFound(err) 421 } else { 422 ctx.Error(http.StatusInternalServerError, "GetUserByName", err) 423 } 424 return 425 } 426 if user == nil { 427 ctx.NotFound() 428 return 429 } 430 431 if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin && ctx.User.ID != user.ID { 432 ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights")) 433 return 434 } 435 436 opts := &models.FindTrackedTimesOptions{ 437 UserID: user.ID, 438 RepositoryID: ctx.Repo.Repository.ID, 439 } 440 441 trackedTimes, err := models.GetTrackedTimes(opts) 442 if err != nil { 443 ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err) 444 return 445 } 446 if err = trackedTimes.LoadAttributes(); err != nil { 447 ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) 448 return 449 } 450 ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(trackedTimes)) 451} 452 453// ListTrackedTimesByRepository lists all tracked times of the repository 454func ListTrackedTimesByRepository(ctx *context.APIContext) { 455 // swagger:operation GET /repos/{owner}/{repo}/times repository repoTrackedTimes 456 // --- 457 // summary: List a repo's tracked times 458 // produces: 459 // - application/json 460 // parameters: 461 // - name: owner 462 // in: path 463 // description: owner of the repo 464 // type: string 465 // required: true 466 // - name: repo 467 // in: path 468 // description: name of the repo 469 // type: string 470 // required: true 471 // - name: user 472 // in: query 473 // description: optional filter by user (available for issue managers) 474 // type: string 475 // - name: since 476 // in: query 477 // description: Only show times updated after the given time. This is a timestamp in RFC 3339 format 478 // type: string 479 // format: date-time 480 // - name: before 481 // in: query 482 // description: Only show times updated before the given time. This is a timestamp in RFC 3339 format 483 // type: string 484 // format: date-time 485 // - name: page 486 // in: query 487 // description: page number of results to return (1-based) 488 // type: integer 489 // - name: limit 490 // in: query 491 // description: page size of results 492 // type: integer 493 // responses: 494 // "200": 495 // "$ref": "#/responses/TrackedTimeList" 496 // "400": 497 // "$ref": "#/responses/error" 498 // "403": 499 // "$ref": "#/responses/forbidden" 500 501 if !ctx.Repo.Repository.IsTimetrackerEnabled() { 502 ctx.Error(http.StatusBadRequest, "", "time tracking disabled") 503 return 504 } 505 506 opts := &models.FindTrackedTimesOptions{ 507 ListOptions: utils.GetListOptions(ctx), 508 RepositoryID: ctx.Repo.Repository.ID, 509 } 510 511 // Filters 512 qUser := ctx.FormTrim("user") 513 if qUser != "" { 514 user, err := user_model.GetUserByName(qUser) 515 if user_model.IsErrUserNotExist(err) { 516 ctx.Error(http.StatusNotFound, "User does not exist", err) 517 } else if err != nil { 518 ctx.Error(http.StatusInternalServerError, "GetUserByName", err) 519 return 520 } 521 opts.UserID = user.ID 522 } 523 524 var err error 525 if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil { 526 ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) 527 return 528 } 529 530 cantSetUser := !ctx.User.IsAdmin && 531 opts.UserID != ctx.User.ID && 532 !ctx.IsUserRepoWriter([]unit.Type{unit.TypeIssues}) 533 534 if cantSetUser { 535 if opts.UserID == 0 { 536 opts.UserID = ctx.User.ID 537 } else { 538 ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights")) 539 return 540 } 541 } 542 543 count, err := models.CountTrackedTimes(opts) 544 if err != nil { 545 ctx.InternalServerError(err) 546 return 547 } 548 549 trackedTimes, err := models.GetTrackedTimes(opts) 550 if err != nil { 551 ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err) 552 return 553 } 554 if err = trackedTimes.LoadAttributes(); err != nil { 555 ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) 556 return 557 } 558 559 ctx.SetTotalCountHeader(count) 560 ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(trackedTimes)) 561} 562 563// ListMyTrackedTimes lists all tracked times of the current user 564func ListMyTrackedTimes(ctx *context.APIContext) { 565 // swagger:operation GET /user/times user userCurrentTrackedTimes 566 // --- 567 // summary: List the current user's tracked times 568 // parameters: 569 // - name: page 570 // in: query 571 // description: page number of results to return (1-based) 572 // type: integer 573 // - name: limit 574 // in: query 575 // description: page size of results 576 // type: integer 577 // produces: 578 // - application/json 579 // parameters: 580 // - name: since 581 // in: query 582 // description: Only show times updated after the given time. This is a timestamp in RFC 3339 format 583 // type: string 584 // format: date-time 585 // - name: before 586 // in: query 587 // description: Only show times updated before the given time. This is a timestamp in RFC 3339 format 588 // type: string 589 // format: date-time 590 // responses: 591 // "200": 592 // "$ref": "#/responses/TrackedTimeList" 593 594 opts := &models.FindTrackedTimesOptions{ 595 ListOptions: utils.GetListOptions(ctx), 596 UserID: ctx.User.ID, 597 } 598 599 var err error 600 if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil { 601 ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) 602 return 603 } 604 605 count, err := models.CountTrackedTimes(opts) 606 if err != nil { 607 ctx.InternalServerError(err) 608 return 609 } 610 611 trackedTimes, err := models.GetTrackedTimes(opts) 612 if err != nil { 613 ctx.Error(http.StatusInternalServerError, "GetTrackedTimesByUser", err) 614 return 615 } 616 617 if err = trackedTimes.LoadAttributes(); err != nil { 618 ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) 619 return 620 } 621 622 ctx.SetTotalCountHeader(count) 623 ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(trackedTimes)) 624} 625