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.go 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. // Copyright 2016 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package repo
  6. import (
  7. "fmt"
  8. "net/http"
  9. "strings"
  10. "time"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/context"
  13. "code.gitea.io/gitea/modules/convert"
  14. issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
  15. "code.gitea.io/gitea/modules/log"
  16. "code.gitea.io/gitea/modules/setting"
  17. api "code.gitea.io/gitea/modules/structs"
  18. "code.gitea.io/gitea/modules/timeutil"
  19. "code.gitea.io/gitea/modules/util"
  20. "code.gitea.io/gitea/routers/api/v1/utils"
  21. issue_service "code.gitea.io/gitea/services/issue"
  22. )
  23. // SearchIssues searches for issues across the repositories that the user has access to
  24. func SearchIssues(ctx *context.APIContext) {
  25. // swagger:operation GET /repos/issues/search issue issueSearchIssues
  26. // ---
  27. // summary: Search for issues across the repositories that the user has access to
  28. // produces:
  29. // - application/json
  30. // parameters:
  31. // - name: state
  32. // in: query
  33. // description: whether issue is open or closed
  34. // type: string
  35. // - name: labels
  36. // in: query
  37. // description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
  38. // type: string
  39. // - name: q
  40. // in: query
  41. // description: search string
  42. // type: string
  43. // - name: priority_repo_id
  44. // in: query
  45. // description: repository to prioritize in the results
  46. // type: integer
  47. // format: int64
  48. // - name: type
  49. // in: query
  50. // description: filter by type (issues / pulls) if set
  51. // type: string
  52. // - name: page
  53. // in: query
  54. // description: page number of requested issues
  55. // type: integer
  56. // responses:
  57. // "200":
  58. // "$ref": "#/responses/IssueList"
  59. var isClosed util.OptionalBool
  60. switch ctx.Query("state") {
  61. case "closed":
  62. isClosed = util.OptionalBoolTrue
  63. case "all":
  64. isClosed = util.OptionalBoolNone
  65. default:
  66. isClosed = util.OptionalBoolFalse
  67. }
  68. // find repos user can access (for issue search)
  69. repoIDs := make([]int64, 0)
  70. opts := &models.SearchRepoOptions{
  71. ListOptions: models.ListOptions{
  72. PageSize: 15,
  73. },
  74. Private: false,
  75. AllPublic: true,
  76. TopicOnly: false,
  77. Collaborate: util.OptionalBoolNone,
  78. OrderBy: models.SearchOrderByRecentUpdated,
  79. Actor: ctx.User,
  80. }
  81. if ctx.IsSigned {
  82. opts.Private = true
  83. opts.AllLimited = true
  84. }
  85. issueCount := 0
  86. for page := 1; ; page++ {
  87. opts.Page = page
  88. repos, count, err := models.SearchRepositoryByName(opts)
  89. if err != nil {
  90. ctx.Error(http.StatusInternalServerError, "SearchRepositoryByName", err)
  91. return
  92. }
  93. if len(repos) == 0 {
  94. break
  95. }
  96. log.Trace("Processing next %d repos of %d", len(repos), count)
  97. for _, repo := range repos {
  98. switch isClosed {
  99. case util.OptionalBoolTrue:
  100. issueCount += repo.NumClosedIssues
  101. case util.OptionalBoolFalse:
  102. issueCount += repo.NumOpenIssues
  103. case util.OptionalBoolNone:
  104. issueCount += repo.NumIssues
  105. }
  106. repoIDs = append(repoIDs, repo.ID)
  107. }
  108. }
  109. var issues []*models.Issue
  110. keyword := strings.Trim(ctx.Query("q"), " ")
  111. if strings.IndexByte(keyword, 0) >= 0 {
  112. keyword = ""
  113. }
  114. var issueIDs []int64
  115. var labelIDs []int64
  116. var err error
  117. if len(keyword) > 0 && len(repoIDs) > 0 {
  118. issueIDs, err = issue_indexer.SearchIssuesByKeyword(repoIDs, keyword)
  119. }
  120. labels := ctx.Query("labels")
  121. if splitted := strings.Split(labels, ","); labels != "" && len(splitted) > 0 {
  122. labelIDs, err = models.GetLabelIDsInReposByNames(repoIDs, splitted)
  123. if err != nil {
  124. ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err)
  125. return
  126. }
  127. }
  128. var isPull util.OptionalBool
  129. switch ctx.Query("type") {
  130. case "pulls":
  131. isPull = util.OptionalBoolTrue
  132. case "issues":
  133. isPull = util.OptionalBoolFalse
  134. default:
  135. isPull = util.OptionalBoolNone
  136. }
  137. // Only fetch the issues if we either don't have a keyword or the search returned issues
  138. // This would otherwise return all issues if no issues were found by the search.
  139. if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
  140. issues, err = models.Issues(&models.IssuesOptions{
  141. ListOptions: models.ListOptions{
  142. Page: ctx.QueryInt("page"),
  143. PageSize: setting.UI.IssuePagingNum,
  144. },
  145. RepoIDs: repoIDs,
  146. IsClosed: isClosed,
  147. IssueIDs: issueIDs,
  148. LabelIDs: labelIDs,
  149. SortType: "priorityrepo",
  150. PriorityRepoID: ctx.QueryInt64("priority_repo_id"),
  151. IsPull: isPull,
  152. })
  153. }
  154. if err != nil {
  155. ctx.Error(http.StatusInternalServerError, "Issues", err)
  156. return
  157. }
  158. ctx.SetLinkHeader(issueCount, setting.UI.IssuePagingNum)
  159. ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
  160. }
  161. // ListIssues list the issues of a repository
  162. func ListIssues(ctx *context.APIContext) {
  163. // swagger:operation GET /repos/{owner}/{repo}/issues issue issueListIssues
  164. // ---
  165. // summary: List a repository's issues
  166. // produces:
  167. // - application/json
  168. // parameters:
  169. // - name: owner
  170. // in: path
  171. // description: owner of the repo
  172. // type: string
  173. // required: true
  174. // - name: repo
  175. // in: path
  176. // description: name of the repo
  177. // type: string
  178. // required: true
  179. // - name: state
  180. // in: query
  181. // description: whether issue is open or closed
  182. // type: string
  183. // - name: labels
  184. // in: query
  185. // description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
  186. // type: string
  187. // - name: q
  188. // in: query
  189. // description: search string
  190. // type: string
  191. // - name: type
  192. // in: query
  193. // description: filter by type (issues / pulls) if set
  194. // type: string
  195. // - name: page
  196. // in: query
  197. // description: page number of results to return (1-based)
  198. // type: integer
  199. // - name: limit
  200. // in: query
  201. // description: page size of results, maximum page size is 50
  202. // type: integer
  203. // responses:
  204. // "200":
  205. // "$ref": "#/responses/IssueList"
  206. var isClosed util.OptionalBool
  207. switch ctx.Query("state") {
  208. case "closed":
  209. isClosed = util.OptionalBoolTrue
  210. case "all":
  211. isClosed = util.OptionalBoolNone
  212. default:
  213. isClosed = util.OptionalBoolFalse
  214. }
  215. var issues []*models.Issue
  216. keyword := strings.Trim(ctx.Query("q"), " ")
  217. if strings.IndexByte(keyword, 0) >= 0 {
  218. keyword = ""
  219. }
  220. var issueIDs []int64
  221. var labelIDs []int64
  222. var err error
  223. if len(keyword) > 0 {
  224. issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{ctx.Repo.Repository.ID}, keyword)
  225. }
  226. if splitted := strings.Split(ctx.Query("labels"), ","); len(splitted) > 0 {
  227. labelIDs, err = models.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
  228. if err != nil {
  229. ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err)
  230. return
  231. }
  232. }
  233. listOptions := utils.GetListOptions(ctx)
  234. if ctx.QueryInt("limit") == 0 {
  235. listOptions.PageSize = setting.UI.IssuePagingNum
  236. }
  237. var isPull util.OptionalBool
  238. switch ctx.Query("type") {
  239. case "pulls":
  240. isPull = util.OptionalBoolTrue
  241. case "issues":
  242. isPull = util.OptionalBoolFalse
  243. default:
  244. isPull = util.OptionalBoolNone
  245. }
  246. // Only fetch the issues if we either don't have a keyword or the search returned issues
  247. // This would otherwise return all issues if no issues were found by the search.
  248. if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
  249. issues, err = models.Issues(&models.IssuesOptions{
  250. ListOptions: listOptions,
  251. RepoIDs: []int64{ctx.Repo.Repository.ID},
  252. IsClosed: isClosed,
  253. IssueIDs: issueIDs,
  254. LabelIDs: labelIDs,
  255. IsPull: isPull,
  256. })
  257. }
  258. if err != nil {
  259. ctx.Error(http.StatusInternalServerError, "Issues", err)
  260. return
  261. }
  262. ctx.SetLinkHeader(ctx.Repo.Repository.NumIssues, listOptions.PageSize)
  263. ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
  264. }
  265. // GetIssue get an issue of a repository
  266. func GetIssue(ctx *context.APIContext) {
  267. // swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
  268. // ---
  269. // summary: Get an issue
  270. // produces:
  271. // - application/json
  272. // parameters:
  273. // - name: owner
  274. // in: path
  275. // description: owner of the repo
  276. // type: string
  277. // required: true
  278. // - name: repo
  279. // in: path
  280. // description: name of the repo
  281. // type: string
  282. // required: true
  283. // - name: index
  284. // in: path
  285. // description: index of the issue to get
  286. // type: integer
  287. // format: int64
  288. // required: true
  289. // responses:
  290. // "200":
  291. // "$ref": "#/responses/Issue"
  292. // "404":
  293. // "$ref": "#/responses/notFound"
  294. issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  295. if err != nil {
  296. if models.IsErrIssueNotExist(err) {
  297. ctx.NotFound()
  298. } else {
  299. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  300. }
  301. return
  302. }
  303. ctx.JSON(http.StatusOK, convert.ToAPIIssue(issue))
  304. }
  305. // CreateIssue create an issue of a repository
  306. func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) {
  307. // swagger:operation POST /repos/{owner}/{repo}/issues issue issueCreateIssue
  308. // ---
  309. // summary: Create an issue. If using deadline only the date will be taken into account, and time of day ignored.
  310. // consumes:
  311. // - application/json
  312. // produces:
  313. // - application/json
  314. // parameters:
  315. // - name: owner
  316. // in: path
  317. // description: owner of the repo
  318. // type: string
  319. // required: true
  320. // - name: repo
  321. // in: path
  322. // description: name of the repo
  323. // type: string
  324. // required: true
  325. // - name: body
  326. // in: body
  327. // schema:
  328. // "$ref": "#/definitions/CreateIssueOption"
  329. // responses:
  330. // "201":
  331. // "$ref": "#/responses/Issue"
  332. // "403":
  333. // "$ref": "#/responses/forbidden"
  334. // "412":
  335. // "$ref": "#/responses/error"
  336. // "422":
  337. // "$ref": "#/responses/validationError"
  338. var deadlineUnix timeutil.TimeStamp
  339. if form.Deadline != nil && ctx.Repo.CanWrite(models.UnitTypeIssues) {
  340. deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
  341. }
  342. issue := &models.Issue{
  343. RepoID: ctx.Repo.Repository.ID,
  344. Repo: ctx.Repo.Repository,
  345. Title: form.Title,
  346. PosterID: ctx.User.ID,
  347. Poster: ctx.User,
  348. Content: form.Body,
  349. DeadlineUnix: deadlineUnix,
  350. }
  351. var assigneeIDs = make([]int64, 0)
  352. var err error
  353. if ctx.Repo.CanWrite(models.UnitTypeIssues) {
  354. issue.MilestoneID = form.Milestone
  355. assigneeIDs, err = models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees)
  356. if err != nil {
  357. if models.IsErrUserNotExist(err) {
  358. ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
  359. } else {
  360. ctx.Error(http.StatusInternalServerError, "AddAssigneeByName", err)
  361. }
  362. return
  363. }
  364. // Check if the passed assignees is assignable
  365. for _, aID := range assigneeIDs {
  366. assignee, err := models.GetUserByID(aID)
  367. if err != nil {
  368. ctx.Error(http.StatusInternalServerError, "GetUserByID", err)
  369. return
  370. }
  371. valid, err := models.CanBeAssigned(assignee, ctx.Repo.Repository, false)
  372. if err != nil {
  373. ctx.Error(http.StatusInternalServerError, "canBeAssigned", err)
  374. return
  375. }
  376. if !valid {
  377. ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name})
  378. return
  379. }
  380. }
  381. } else {
  382. // setting labels is not allowed if user is not a writer
  383. form.Labels = make([]int64, 0)
  384. }
  385. if err := issue_service.NewIssue(ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
  386. if models.IsErrUserDoesNotHaveAccessToRepo(err) {
  387. ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
  388. return
  389. }
  390. ctx.Error(http.StatusInternalServerError, "NewIssue", err)
  391. return
  392. }
  393. if form.Closed {
  394. if err := issue_service.ChangeStatus(issue, ctx.User, true); err != nil {
  395. if models.IsErrDependenciesLeft(err) {
  396. ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
  397. return
  398. }
  399. ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
  400. return
  401. }
  402. }
  403. // Refetch from database to assign some automatic values
  404. issue, err = models.GetIssueByID(issue.ID)
  405. if err != nil {
  406. ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
  407. return
  408. }
  409. ctx.JSON(http.StatusCreated, convert.ToAPIIssue(issue))
  410. }
  411. // EditIssue modify an issue of a repository
  412. func EditIssue(ctx *context.APIContext, form api.EditIssueOption) {
  413. // swagger:operation PATCH /repos/{owner}/{repo}/issues/{index} issue issueEditIssue
  414. // ---
  415. // summary: Edit an issue. If using deadline only the date will be taken into account, and time of day ignored.
  416. // consumes:
  417. // - application/json
  418. // produces:
  419. // - application/json
  420. // parameters:
  421. // - name: owner
  422. // in: path
  423. // description: owner of the repo
  424. // type: string
  425. // required: true
  426. // - name: repo
  427. // in: path
  428. // description: name of the repo
  429. // type: string
  430. // required: true
  431. // - name: index
  432. // in: path
  433. // description: index of the issue to edit
  434. // type: integer
  435. // format: int64
  436. // required: true
  437. // - name: body
  438. // in: body
  439. // schema:
  440. // "$ref": "#/definitions/EditIssueOption"
  441. // responses:
  442. // "201":
  443. // "$ref": "#/responses/Issue"
  444. // "403":
  445. // "$ref": "#/responses/forbidden"
  446. // "404":
  447. // "$ref": "#/responses/notFound"
  448. // "412":
  449. // "$ref": "#/responses/error"
  450. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  451. if err != nil {
  452. if models.IsErrIssueNotExist(err) {
  453. ctx.NotFound()
  454. } else {
  455. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  456. }
  457. return
  458. }
  459. issue.Repo = ctx.Repo.Repository
  460. canWrite := ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
  461. err = issue.LoadAttributes()
  462. if err != nil {
  463. ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
  464. return
  465. }
  466. if !issue.IsPoster(ctx.User.ID) && !canWrite {
  467. ctx.Status(http.StatusForbidden)
  468. return
  469. }
  470. if len(form.Title) > 0 {
  471. issue.Title = form.Title
  472. }
  473. if form.Body != nil {
  474. issue.Content = *form.Body
  475. }
  476. // Update or remove the deadline, only if set and allowed
  477. if (form.Deadline != nil || form.RemoveDeadline != nil) && canWrite {
  478. var deadlineUnix timeutil.TimeStamp
  479. if (form.RemoveDeadline == nil || !*form.RemoveDeadline) && !form.Deadline.IsZero() {
  480. deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
  481. 23, 59, 59, 0, form.Deadline.Location())
  482. deadlineUnix = timeutil.TimeStamp(deadline.Unix())
  483. }
  484. if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.User); err != nil {
  485. ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
  486. return
  487. }
  488. issue.DeadlineUnix = deadlineUnix
  489. }
  490. // Add/delete assignees
  491. // Deleting is done the GitHub way (quote from their api documentation):
  492. // https://developer.github.com/v3/issues/#edit-an-issue
  493. // "assignees" (array): Logins for Users to assign to this issue.
  494. // Pass one or more user logins to replace the set of assignees on this Issue.
  495. // Send an empty array ([]) to clear all assignees from the Issue.
  496. if canWrite && (form.Assignees != nil || form.Assignee != nil) {
  497. oneAssignee := ""
  498. if form.Assignee != nil {
  499. oneAssignee = *form.Assignee
  500. }
  501. err = issue_service.UpdateAssignees(issue, oneAssignee, form.Assignees, ctx.User)
  502. if err != nil {
  503. ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
  504. return
  505. }
  506. }
  507. if canWrite && form.Milestone != nil &&
  508. issue.MilestoneID != *form.Milestone {
  509. oldMilestoneID := issue.MilestoneID
  510. issue.MilestoneID = *form.Milestone
  511. if err = issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
  512. ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
  513. return
  514. }
  515. }
  516. if err = models.UpdateIssueByAPI(issue); err != nil {
  517. ctx.Error(http.StatusInternalServerError, "UpdateIssueByAPI", err)
  518. return
  519. }
  520. if form.State != nil {
  521. if err = issue_service.ChangeStatus(issue, ctx.User, api.StateClosed == api.StateType(*form.State)); err != nil {
  522. if models.IsErrDependenciesLeft(err) {
  523. ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
  524. return
  525. }
  526. ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
  527. return
  528. }
  529. }
  530. // Refetch from database to assign some automatic values
  531. issue, err = models.GetIssueByID(issue.ID)
  532. if err != nil {
  533. ctx.InternalServerError(err)
  534. return
  535. }
  536. if err = issue.LoadMilestone(); err != nil {
  537. ctx.InternalServerError(err)
  538. return
  539. }
  540. ctx.JSON(http.StatusCreated, convert.ToAPIIssue(issue))
  541. }
  542. // UpdateIssueDeadline updates an issue deadline
  543. func UpdateIssueDeadline(ctx *context.APIContext, form api.EditDeadlineOption) {
  544. // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/deadline issue issueEditIssueDeadline
  545. // ---
  546. // summary: Set an issue deadline. If set to null, the deadline is deleted. If using deadline only the date will be taken into account, and time of day ignored.
  547. // consumes:
  548. // - application/json
  549. // produces:
  550. // - application/json
  551. // parameters:
  552. // - name: owner
  553. // in: path
  554. // description: owner of the repo
  555. // type: string
  556. // required: true
  557. // - name: repo
  558. // in: path
  559. // description: name of the repo
  560. // type: string
  561. // required: true
  562. // - name: index
  563. // in: path
  564. // description: index of the issue to create or update a deadline on
  565. // type: integer
  566. // format: int64
  567. // required: true
  568. // - name: body
  569. // in: body
  570. // schema:
  571. // "$ref": "#/definitions/EditDeadlineOption"
  572. // responses:
  573. // "201":
  574. // "$ref": "#/responses/IssueDeadline"
  575. // "403":
  576. // "$ref": "#/responses/forbidden"
  577. // "404":
  578. // "$ref": "#/responses/notFound"
  579. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  580. if err != nil {
  581. if models.IsErrIssueNotExist(err) {
  582. ctx.NotFound()
  583. } else {
  584. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  585. }
  586. return
  587. }
  588. if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
  589. ctx.Error(http.StatusForbidden, "", "Not repo writer")
  590. return
  591. }
  592. var deadlineUnix timeutil.TimeStamp
  593. var deadline time.Time
  594. if form.Deadline != nil && !form.Deadline.IsZero() {
  595. deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
  596. 23, 59, 59, 0, form.Deadline.Location())
  597. deadlineUnix = timeutil.TimeStamp(deadline.Unix())
  598. }
  599. if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.User); err != nil {
  600. ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
  601. return
  602. }
  603. ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline})
  604. }