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