選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

issue_tracked_time.go 17KB

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