Add /milestones endpoint (#8733)
Create a /milestones endpoint which basically serves as a dashboard view for milestones, very similar to the /issues or /pulls page. Closes #8232
This commit is contained in:
parent
7cc16740a5
commit
f6b29012e0
14 changed files with 568 additions and 7 deletions
|
@ -254,6 +254,13 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
}
|
||||
}
|
||||
|
||||
reqMilestonesDashboardPageEnabled := func(ctx *context.Context) {
|
||||
if !setting.Service.ShowMilestonesDashboardPage {
|
||||
ctx.Error(403)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
m.Use(user.GetNotificationCount)
|
||||
|
||||
// FIXME: not all routes need go through same middlewares.
|
||||
|
@ -276,6 +283,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
m.Combo("/install", routers.InstallInit).Get(routers.Install).
|
||||
Post(bindIgnErr(auth.InstallForm{}), routers.InstallPost)
|
||||
m.Get("/^:type(issues|pulls)$", reqSignIn, user.Issues)
|
||||
m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones)
|
||||
|
||||
// ***** START: User *****
|
||||
m.Group("/user", func() {
|
||||
|
@ -556,6 +564,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
m.Group("/:org", func() {
|
||||
m.Get("/dashboard", user.Dashboard)
|
||||
m.Get("/^:type(issues|pulls)$", user.Issues)
|
||||
m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
|
||||
m.Get("/members", org.Members)
|
||||
m.Get("/members/action/:action", org.MembersAction)
|
||||
|
||||
|
|
|
@ -18,17 +18,20 @@ import (
|
|||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp"
|
||||
"github.com/keybase/go-crypto/openpgp/armor"
|
||||
"github.com/unknwon/com"
|
||||
)
|
||||
|
||||
const (
|
||||
tplDashboard base.TplName = "user/dashboard/dashboard"
|
||||
tplIssues base.TplName = "user/dashboard/issues"
|
||||
tplProfile base.TplName = "user/profile"
|
||||
tplDashboard base.TplName = "user/dashboard/dashboard"
|
||||
tplIssues base.TplName = "user/dashboard/issues"
|
||||
tplMilestones base.TplName = "user/dashboard/milestones"
|
||||
tplProfile base.TplName = "user/profile"
|
||||
)
|
||||
|
||||
// getDashboardContextUser finds out dashboard is viewing as which context user.
|
||||
|
@ -150,6 +153,190 @@ func Dashboard(ctx *context.Context) {
|
|||
ctx.HTML(200, tplDashboard)
|
||||
}
|
||||
|
||||
// Milestones render the user milestones page
|
||||
func Milestones(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("milestones")
|
||||
ctx.Data["PageIsMilestonesDashboard"] = true
|
||||
|
||||
ctxUser := getDashboardContextUser(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
sortType := ctx.Query("sort")
|
||||
page := ctx.QueryInt("page")
|
||||
if page <= 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
reposQuery := ctx.Query("repos")
|
||||
isShowClosed := ctx.Query("state") == "closed"
|
||||
|
||||
// Get repositories.
|
||||
var err error
|
||||
var userRepoIDs []int64
|
||||
if ctxUser.IsOrganization() {
|
||||
env, err := ctxUser.AccessibleReposEnv(ctx.User.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("AccessibleReposEnv", err)
|
||||
return
|
||||
}
|
||||
userRepoIDs, err = env.RepoIDs(1, ctxUser.NumRepos)
|
||||
if err != nil {
|
||||
ctx.ServerError("env.RepoIDs", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
unitType := models.UnitTypeIssues
|
||||
userRepoIDs, err = ctxUser.GetAccessRepoIDs(unitType)
|
||||
if err != nil {
|
||||
ctx.ServerError("ctxUser.GetAccessRepoIDs", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(userRepoIDs) == 0 {
|
||||
userRepoIDs = []int64{-1}
|
||||
}
|
||||
|
||||
var repoIDs []int64
|
||||
if issueReposQueryPattern.MatchString(reposQuery) {
|
||||
// remove "[" and "]" from string
|
||||
reposQuery = reposQuery[1 : len(reposQuery)-1]
|
||||
//for each ID (delimiter ",") add to int to repoIDs
|
||||
reposSet := false
|
||||
for _, rID := range strings.Split(reposQuery, ",") {
|
||||
// Ensure nonempty string entries
|
||||
if rID != "" && rID != "0" {
|
||||
reposSet = true
|
||||
rIDint64, err := strconv.ParseInt(rID, 10, 64)
|
||||
if err == nil && com.IsSliceContainsInt64(userRepoIDs, rIDint64) {
|
||||
repoIDs = append(repoIDs, rIDint64)
|
||||
}
|
||||
}
|
||||
}
|
||||
if reposSet && len(repoIDs) == 0 {
|
||||
// force an empty result
|
||||
repoIDs = []int64{-1}
|
||||
}
|
||||
} else {
|
||||
log.Error("issueReposQueryPattern not match with query")
|
||||
}
|
||||
|
||||
if len(repoIDs) == 0 {
|
||||
repoIDs = userRepoIDs
|
||||
}
|
||||
|
||||
counts, err := models.CountMilestonesByRepoIDs(userRepoIDs, isShowClosed)
|
||||
if err != nil {
|
||||
ctx.ServerError("CountMilestonesByRepoIDs", err)
|
||||
return
|
||||
}
|
||||
|
||||
milestones, err := models.GetMilestonesByRepoIDs(repoIDs, page, isShowClosed, sortType)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMilestonesByRepoIDs", err)
|
||||
return
|
||||
}
|
||||
|
||||
showReposMap := make(map[int64]*models.Repository, len(counts))
|
||||
for rID := range counts {
|
||||
if rID == -1 {
|
||||
break
|
||||
}
|
||||
repo, err := models.GetRepositoryByID(rID)
|
||||
if err != nil {
|
||||
if models.IsErrRepoNotExist(err) {
|
||||
ctx.NotFound("GetRepositoryByID", err)
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.ServerError("GetRepositoryByID", fmt.Errorf("[%d]%v", rID, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
showReposMap[rID] = repo
|
||||
|
||||
// Check if user has access to given repository.
|
||||
perm, err := models.GetUserRepoPermission(repo, ctxUser)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", fmt.Errorf("[%d]%v", rID, err))
|
||||
return
|
||||
}
|
||||
|
||||
if !perm.CanRead(models.UnitTypeIssues) {
|
||||
if log.IsTrace() {
|
||||
log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+
|
||||
"User in repo has Permissions: %-+v",
|
||||
ctxUser,
|
||||
models.UnitTypeIssues,
|
||||
repo,
|
||||
perm)
|
||||
}
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
showRepos := models.RepositoryListOfMap(showReposMap)
|
||||
sort.Sort(showRepos)
|
||||
if err = showRepos.LoadAttributes(); err != nil {
|
||||
ctx.ServerError("LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, m := range milestones {
|
||||
m.Repo = showReposMap[m.RepoID]
|
||||
m.RenderedContent = string(markdown.Render([]byte(m.Content), m.Repo.Link(), m.Repo.ComposeMetas()))
|
||||
if m.Repo.IsTimetrackerEnabled() {
|
||||
err := m.LoadTotalTrackedTime()
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadTotalTrackedTime", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
milestoneStats, err := models.GetMilestonesStats(repoIDs)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMilestoneStats", err)
|
||||
return
|
||||
}
|
||||
|
||||
totalMilestoneStats, err := models.GetMilestonesStats(userRepoIDs)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMilestoneStats", err)
|
||||
return
|
||||
}
|
||||
|
||||
var pagerCount int
|
||||
if isShowClosed {
|
||||
ctx.Data["State"] = "closed"
|
||||
ctx.Data["Total"] = totalMilestoneStats.ClosedCount
|
||||
pagerCount = int(milestoneStats.ClosedCount)
|
||||
} else {
|
||||
ctx.Data["State"] = "open"
|
||||
ctx.Data["Total"] = totalMilestoneStats.OpenCount
|
||||
pagerCount = int(milestoneStats.OpenCount)
|
||||
}
|
||||
|
||||
ctx.Data["Milestones"] = milestones
|
||||
ctx.Data["Repos"] = showRepos
|
||||
ctx.Data["Counts"] = counts
|
||||
ctx.Data["MilestoneStats"] = milestoneStats
|
||||
ctx.Data["SortType"] = sortType
|
||||
if len(repoIDs) != len(userRepoIDs) {
|
||||
ctx.Data["RepoIDs"] = repoIDs
|
||||
}
|
||||
ctx.Data["IsShowClosed"] = isShowClosed
|
||||
|
||||
pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5)
|
||||
pager.AddParam(ctx, "repos", "RepoIDs")
|
||||
pager.AddParam(ctx, "sort", "SortType")
|
||||
pager.AddParam(ctx, "state", "State")
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(200, tplMilestones)
|
||||
}
|
||||
|
||||
// Regexp for repos query
|
||||
var issueReposQueryPattern = regexp.MustCompile(`^\[\d+(,\d+)*,?\]$`)
|
||||
|
||||
|
|
|
@ -31,3 +31,42 @@ func TestIssues(t *testing.T) {
|
|||
assert.Len(t, ctx.Data["Issues"], 1)
|
||||
assert.Len(t, ctx.Data["Repos"], 1)
|
||||
}
|
||||
|
||||
func TestMilestones(t *testing.T) {
|
||||
setting.UI.IssuePagingNum = 1
|
||||
assert.NoError(t, models.LoadFixtures())
|
||||
|
||||
ctx := test.MockContext(t, "milestones")
|
||||
test.LoadUser(t, ctx, 2)
|
||||
ctx.SetParams("sort", "issues")
|
||||
ctx.Req.Form.Set("state", "closed")
|
||||
ctx.Req.Form.Set("sort", "furthestduedate")
|
||||
Milestones(ctx)
|
||||
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
|
||||
assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
|
||||
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
|
||||
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
|
||||
assert.EqualValues(t, 1, ctx.Data["Total"])
|
||||
assert.Len(t, ctx.Data["Milestones"], 1)
|
||||
assert.Len(t, ctx.Data["Repos"], 1)
|
||||
}
|
||||
|
||||
func TestMilestonesForSpecificRepo(t *testing.T) {
|
||||
setting.UI.IssuePagingNum = 1
|
||||
assert.NoError(t, models.LoadFixtures())
|
||||
|
||||
ctx := test.MockContext(t, "milestones")
|
||||
test.LoadUser(t, ctx, 2)
|
||||
ctx.SetParams("sort", "issues")
|
||||
ctx.SetParams("repo", "1")
|
||||
ctx.Req.Form.Set("state", "closed")
|
||||
ctx.Req.Form.Set("sort", "furthestduedate")
|
||||
Milestones(ctx)
|
||||
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
|
||||
assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
|
||||
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
|
||||
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
|
||||
assert.EqualValues(t, 1, ctx.Data["Total"])
|
||||
assert.Len(t, ctx.Data["Milestones"], 1)
|
||||
assert.Len(t, ctx.Data["Repos"], 1)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue