You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

issue_tracked_time.go 15KB

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