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.

milestone.go 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. // Copyright 2018 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repo
  4. import (
  5. "net/http"
  6. "net/url"
  7. "time"
  8. "code.gitea.io/gitea/models/db"
  9. issues_model "code.gitea.io/gitea/models/issues"
  10. "code.gitea.io/gitea/modules/base"
  11. "code.gitea.io/gitea/modules/context"
  12. "code.gitea.io/gitea/modules/markup"
  13. "code.gitea.io/gitea/modules/markup/markdown"
  14. "code.gitea.io/gitea/modules/setting"
  15. "code.gitea.io/gitea/modules/structs"
  16. "code.gitea.io/gitea/modules/timeutil"
  17. "code.gitea.io/gitea/modules/util"
  18. "code.gitea.io/gitea/modules/web"
  19. "code.gitea.io/gitea/services/forms"
  20. "code.gitea.io/gitea/services/issue"
  21. "xorm.io/builder"
  22. )
  23. const (
  24. tplMilestone base.TplName = "repo/issue/milestones"
  25. tplMilestoneNew base.TplName = "repo/issue/milestone_new"
  26. tplMilestoneIssues base.TplName = "repo/issue/milestone_issues"
  27. )
  28. // Milestones render milestones page
  29. func Milestones(ctx *context.Context) {
  30. ctx.Data["Title"] = ctx.Tr("repo.milestones")
  31. ctx.Data["PageIsIssueList"] = true
  32. ctx.Data["PageIsMilestones"] = true
  33. isShowClosed := ctx.FormString("state") == "closed"
  34. sortType := ctx.FormString("sort")
  35. keyword := ctx.FormTrim("q")
  36. page := ctx.FormInt("page")
  37. if page <= 1 {
  38. page = 1
  39. }
  40. state := structs.StateOpen
  41. if isShowClosed {
  42. state = structs.StateClosed
  43. }
  44. miles, total, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{
  45. ListOptions: db.ListOptions{
  46. Page: page,
  47. PageSize: setting.UI.IssuePagingNum,
  48. },
  49. RepoID: ctx.Repo.Repository.ID,
  50. State: state,
  51. SortType: sortType,
  52. Name: keyword,
  53. })
  54. if err != nil {
  55. ctx.ServerError("GetMilestones", err)
  56. return
  57. }
  58. stats, err := issues_model.GetMilestonesStatsByRepoCondAndKw(builder.And(builder.Eq{"id": ctx.Repo.Repository.ID}), keyword)
  59. if err != nil {
  60. ctx.ServerError("GetMilestoneStats", err)
  61. return
  62. }
  63. ctx.Data["OpenCount"] = stats.OpenCount
  64. ctx.Data["ClosedCount"] = stats.ClosedCount
  65. if ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
  66. if err := miles.LoadTotalTrackedTimes(); err != nil {
  67. ctx.ServerError("LoadTotalTrackedTimes", err)
  68. return
  69. }
  70. }
  71. for _, m := range miles {
  72. m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
  73. URLPrefix: ctx.Repo.RepoLink,
  74. Metas: ctx.Repo.Repository.ComposeMetas(),
  75. GitRepo: ctx.Repo.GitRepo,
  76. Ctx: ctx,
  77. }, m.Content)
  78. if err != nil {
  79. ctx.ServerError("RenderString", err)
  80. return
  81. }
  82. }
  83. ctx.Data["Milestones"] = miles
  84. if isShowClosed {
  85. ctx.Data["State"] = "closed"
  86. } else {
  87. ctx.Data["State"] = "open"
  88. }
  89. ctx.Data["SortType"] = sortType
  90. ctx.Data["Keyword"] = keyword
  91. ctx.Data["IsShowClosed"] = isShowClosed
  92. pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5)
  93. pager.AddParam(ctx, "state", "State")
  94. pager.AddParam(ctx, "q", "Keyword")
  95. ctx.Data["Page"] = pager
  96. ctx.HTML(http.StatusOK, tplMilestone)
  97. }
  98. // NewMilestone render creating milestone page
  99. func NewMilestone(ctx *context.Context) {
  100. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  101. ctx.Data["PageIsIssueList"] = true
  102. ctx.Data["PageIsMilestones"] = true
  103. ctx.HTML(http.StatusOK, tplMilestoneNew)
  104. }
  105. // NewMilestonePost response for creating milestone
  106. func NewMilestonePost(ctx *context.Context) {
  107. form := web.GetForm(ctx).(*forms.CreateMilestoneForm)
  108. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  109. ctx.Data["PageIsIssueList"] = true
  110. ctx.Data["PageIsMilestones"] = true
  111. if ctx.HasError() {
  112. ctx.HTML(http.StatusOK, tplMilestoneNew)
  113. return
  114. }
  115. if len(form.Deadline) == 0 {
  116. form.Deadline = "9999-12-31"
  117. }
  118. deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local)
  119. if err != nil {
  120. ctx.Data["Err_Deadline"] = true
  121. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
  122. return
  123. }
  124. deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location())
  125. if err = issues_model.NewMilestone(&issues_model.Milestone{
  126. RepoID: ctx.Repo.Repository.ID,
  127. Name: form.Title,
  128. Content: form.Content,
  129. DeadlineUnix: timeutil.TimeStamp(deadline.Unix()),
  130. }); err != nil {
  131. ctx.ServerError("NewMilestone", err)
  132. return
  133. }
  134. ctx.Flash.Success(ctx.Tr("repo.milestones.create_success", form.Title))
  135. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  136. }
  137. // EditMilestone render edting milestone page
  138. func EditMilestone(ctx *context.Context) {
  139. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  140. ctx.Data["PageIsMilestones"] = true
  141. ctx.Data["PageIsEditMilestone"] = true
  142. m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
  143. if err != nil {
  144. if issues_model.IsErrMilestoneNotExist(err) {
  145. ctx.NotFound("", nil)
  146. } else {
  147. ctx.ServerError("GetMilestoneByRepoID", err)
  148. }
  149. return
  150. }
  151. ctx.Data["title"] = m.Name
  152. ctx.Data["content"] = m.Content
  153. if len(m.DeadlineString) > 0 {
  154. ctx.Data["deadline"] = m.DeadlineString
  155. }
  156. ctx.HTML(http.StatusOK, tplMilestoneNew)
  157. }
  158. // EditMilestonePost response for edting milestone
  159. func EditMilestonePost(ctx *context.Context) {
  160. form := web.GetForm(ctx).(*forms.CreateMilestoneForm)
  161. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  162. ctx.Data["PageIsMilestones"] = true
  163. ctx.Data["PageIsEditMilestone"] = true
  164. if ctx.HasError() {
  165. ctx.HTML(http.StatusOK, tplMilestoneNew)
  166. return
  167. }
  168. if len(form.Deadline) == 0 {
  169. form.Deadline = "9999-12-31"
  170. }
  171. deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local)
  172. if err != nil {
  173. ctx.Data["Err_Deadline"] = true
  174. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
  175. return
  176. }
  177. deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location())
  178. m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
  179. if err != nil {
  180. if issues_model.IsErrMilestoneNotExist(err) {
  181. ctx.NotFound("", nil)
  182. } else {
  183. ctx.ServerError("GetMilestoneByRepoID", err)
  184. }
  185. return
  186. }
  187. m.Name = form.Title
  188. m.Content = form.Content
  189. m.DeadlineUnix = timeutil.TimeStamp(deadline.Unix())
  190. if err = issues_model.UpdateMilestone(m, m.IsClosed); err != nil {
  191. ctx.ServerError("UpdateMilestone", err)
  192. return
  193. }
  194. ctx.Flash.Success(ctx.Tr("repo.milestones.edit_success", m.Name))
  195. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  196. }
  197. // ChangeMilestoneStatus response for change a milestone's status
  198. func ChangeMilestoneStatus(ctx *context.Context) {
  199. toClose := false
  200. switch ctx.Params(":action") {
  201. case "open":
  202. toClose = false
  203. case "close":
  204. toClose = true
  205. default:
  206. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  207. }
  208. id := ctx.ParamsInt64(":id")
  209. if err := issues_model.ChangeMilestoneStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil {
  210. if issues_model.IsErrMilestoneNotExist(err) {
  211. ctx.NotFound("", err)
  212. } else {
  213. ctx.ServerError("ChangeMilestoneStatusByIDAndRepoID", err)
  214. }
  215. return
  216. }
  217. ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=" + url.QueryEscape(ctx.Params(":action")))
  218. }
  219. // DeleteMilestone delete a milestone
  220. func DeleteMilestone(ctx *context.Context) {
  221. if err := issues_model.DeleteMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil {
  222. ctx.Flash.Error("DeleteMilestoneByRepoID: " + err.Error())
  223. } else {
  224. ctx.Flash.Success(ctx.Tr("repo.milestones.deletion_success"))
  225. }
  226. ctx.JSON(http.StatusOK, map[string]any{
  227. "redirect": ctx.Repo.RepoLink + "/milestones",
  228. })
  229. }
  230. // MilestoneIssuesAndPulls lists all the issues and pull requests of the milestone
  231. func MilestoneIssuesAndPulls(ctx *context.Context) {
  232. milestoneID := ctx.ParamsInt64(":id")
  233. milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
  234. if err != nil {
  235. if issues_model.IsErrMilestoneNotExist(err) {
  236. ctx.NotFound("GetMilestoneByID", err)
  237. return
  238. }
  239. ctx.ServerError("GetMilestoneByID", err)
  240. return
  241. }
  242. milestone.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
  243. URLPrefix: ctx.Repo.RepoLink,
  244. Metas: ctx.Repo.Repository.ComposeMetas(),
  245. GitRepo: ctx.Repo.GitRepo,
  246. Ctx: ctx,
  247. }, milestone.Content)
  248. if err != nil {
  249. ctx.ServerError("RenderString", err)
  250. return
  251. }
  252. ctx.Data["Title"] = milestone.Name
  253. ctx.Data["Milestone"] = milestone
  254. issues(ctx, milestoneID, 0, util.OptionalBoolNone)
  255. ret, _ := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
  256. ctx.Data["NewIssueChooseTemplate"] = len(ret) > 0
  257. ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)
  258. ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true)
  259. ctx.HTML(http.StatusOK, tplMilestoneIssues)
  260. }