// Copyright 2018 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package repo import ( "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" auth "code.gitea.io/gitea/modules/forms" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "xorm.io/builder" ) const ( tplMilestone base.TplName = "repo/issue/milestones" tplMilestoneNew base.TplName = "repo/issue/milestone_new" tplMilestoneIssues base.TplName = "repo/issue/milestone_issues" ) // Milestones render milestones page func Milestones(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.milestones") ctx.Data["PageIsIssueList"] = true ctx.Data["PageIsMilestones"] = true isShowClosed := ctx.Query("state") == "closed" stats, err := models.GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"id": ctx.Repo.Repository.ID})) if err != nil { ctx.ServerError("MilestoneStats", err) return } ctx.Data["OpenCount"] = stats.OpenCount ctx.Data["ClosedCount"] = stats.ClosedCount sortType := ctx.Query("sort") page := ctx.QueryInt("page") if page <= 1 { page = 1 } var total int var state structs.StateType if !isShowClosed { total = int(stats.OpenCount) state = structs.StateOpen } else { total = int(stats.ClosedCount) state = structs.StateClosed } miles, err := models.GetMilestones(models.GetMilestonesOption{ ListOptions: models.ListOptions{ Page: page, PageSize: setting.UI.IssuePagingNum, }, RepoID: ctx.Repo.Repository.ID, State: state, SortType: sortType, }) if err != nil { ctx.ServerError("GetMilestones", err) return } if ctx.Repo.Repository.IsTimetrackerEnabled() { if err := miles.LoadTotalTrackedTimes(); err != nil { ctx.ServerError("LoadTotalTrackedTimes", err) return } } for _, m := range miles { m.RenderedContent = string(markdown.Render([]byte(m.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) } ctx.Data["Milestones"] = miles if isShowClosed { ctx.Data["State"] = "closed" } else { ctx.Data["State"] = "open" } ctx.Data["SortType"] = sortType ctx.Data["IsShowClosed"] = isShowClosed pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5) pager.AddParam(ctx, "state", "State") ctx.Data["Page"] = pager ctx.HTML(200, tplMilestone) } // NewMilestone render creating milestone page func NewMilestone(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.milestones.new") ctx.Data["PageIsIssueList"] = true ctx.Data["PageIsMilestones"] = true ctx.HTML(200, tplMilestoneNew) } // NewMilestonePost response for creating milestone func NewMilestonePost(ctx *context.Context) { form := web.GetForm(ctx).(*auth.CreateMilestoneForm) ctx.Data["Title"] = ctx.Tr("repo.milestones.new") ctx.Data["PageIsIssueList"] = true ctx.Data["PageIsMilestones"] = true if ctx.HasError() { ctx.HTML(200, tplMilestoneNew) return } if len(form.Deadline) == 0 { form.Deadline = "9999-12-31" } deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local) if err != nil { ctx.Data["Err_Deadline"] = true ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) return } deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location()) if err = models.NewMilestone(&models.Milestone{ RepoID: ctx.Repo.Repository.ID, Name: form.Title, Content: form.Content, DeadlineUnix: timeutil.TimeStamp(deadline.Unix()), }); err != nil { ctx.ServerError("NewMilestone", err) return } ctx.Flash.Success(ctx.Tr("repo.milestones.create_success", form.Title)) ctx.Redirect(ctx.Repo.RepoLink + "/milestones") } // EditMilestone render edting milestone page func EditMilestone(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.milestones.edit") ctx.Data["PageIsMilestones"] = true ctx.Data["PageIsEditMilestone"] = true m, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) if err != nil { if models.IsErrMilestoneNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetMilestoneByRepoID", err) } return } ctx.Data["title"] = m.Name ctx.Data["content"] = m.Content if len(m.DeadlineString) > 0 { ctx.Data["deadline"] = m.DeadlineString } ctx.HTML(200, tplMilestoneNew) } // EditMilestonePost response for edting milestone func EditMilestonePost(ctx *context.Context) { form := web.GetForm(ctx).(*auth.CreateMilestoneForm) ctx.Data["Title"] = ctx.Tr("repo.milestones.edit") ctx.Data["PageIsMilestones"] = true ctx.Data["PageIsEditMilestone"] = true if ctx.HasError() { ctx.HTML(200, tplMilestoneNew) return } if len(form.Deadline) == 0 { form.Deadline = "9999-12-31" } deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local) if err != nil { ctx.Data["Err_Deadline"] = true ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) return } deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location()) m, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) if err != nil { if models.IsErrMilestoneNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetMilestoneByRepoID", err) } return } m.Name = form.Title m.Content = form.Content m.DeadlineUnix = timeutil.TimeStamp(deadline.Unix()) if err = models.UpdateMilestone(m, m.IsClosed); err != nil { ctx.ServerError("UpdateMilestone", err) return } ctx.Flash.Success(ctx.Tr("repo.milestones.edit_success", m.Name)) ctx.Redirect(ctx.Repo.RepoLink + "/milestones") } // ChangeMilestoneStatus response for change a milestone's status func ChangeMilestoneStatus(ctx *context.Context) { toClose := false switch ctx.Params(":action") { case "open": toClose = false case "close": toClose = true default: ctx.Redirect(ctx.Repo.RepoLink + "/milestones") } id := ctx.ParamsInt64(":id") if err := models.ChangeMilestoneStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil { if models.IsErrMilestoneNotExist(err) { ctx.NotFound("", err) } else { ctx.ServerError("ChangeMilestoneStatusByIDAndRepoID", err) } return } ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=" + ctx.Params(":action")) } // DeleteMilestone delete a milestone func DeleteMilestone(ctx *context.Context) { if err := models.DeleteMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil { ctx.Flash.Error("DeleteMilestoneByRepoID: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("repo.milestones.deletion_success")) } ctx.JSON(200, map[string]interface{}{ "redirect": ctx.Repo.RepoLink + "/milestones", }) } // MilestoneIssuesAndPulls lists all the issues and pull requests of the milestone func MilestoneIssuesAndPulls(ctx *context.Context) { milestoneID := ctx.ParamsInt64(":id") milestone, err := models.GetMilestoneByID(milestoneID) if err != nil { if models.IsErrMilestoneNotExist(err) { ctx.NotFound("GetMilestoneByID", err) return } ctx.ServerError("GetMilestoneByID", err) return } milestone.RenderedContent = string(markdown.Render([]byte(milestone.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) ctx.Data["Title"] = milestone.Name ctx.Data["Milestone"] = milestone issues(ctx, milestoneID, 0, util.OptionalBoolNone) ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false) ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true) ctx.HTML(200, tplMilestoneIssues) }