code.gitea.io/gitea@v1.21.7/routers/web/repo/milestone.go (about)

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