Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add /milestones endpoint #8733

Merged
merged 46 commits into from
Dec 15, 2019
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
227a970
in progress checkpoint
bhalbright Oct 22, 2019
9fe22ba
an initial crack at the milestones showing up; the expected milestone…
bhalbright Oct 23, 2019
614f95e
in progress checkpoint, ui looks better and got the repo name to show…
bhalbright Oct 24, 2019
7dae2df
in-progress work on adding time tracking totals to the milestones on …
bhalbright Oct 26, 2019
6a2136e
adding time tracking totals to milestrones for the dashboard view
bhalbright Oct 26, 2019
f770682
fixed issue with in your repositories total on milestones dashboard
bhalbright Oct 27, 2019
2f6a674
change to use PageIsMilestonesDashboard for the dashboard active item…
bhalbright Oct 27, 2019
961accc
fixed/removed some comments
bhalbright Oct 27, 2019
4a67ddc
Merge branch 'master' of https://github.com/go-gitea/gitea into miles…
bhalbright Oct 27, 2019
b6f5da8
todo comments on integration tests to write
bhalbright Oct 29, 2019
8bbe415
checking error on LoadTotalTrackedTime in home.go
bhalbright Oct 30, 2019
767d2f1
Merge branch 'master' of https://github.com/go-gitea/gitea into miles…
bhalbright Oct 31, 2019
e352efe
added integration test for CountMilestonesByRepo
bhalbright Nov 1, 2019
1a4efb2
Merge branch 'master' of https://github.com/go-gitea/gitea into miles…
bhalbright Nov 1, 2019
ba0f7ff
added test TestGetMilestonesByRepoIDs
bhalbright Nov 2, 2019
9097d45
Merge branch 'master' of https://github.com/go-gitea/gitea into miles…
bhalbright Nov 2, 2019
04628dc
added more milestone tests, removed the YourRepositoryCount property …
bhalbright Nov 2, 2019
afcce09
added test for Milestones endpoint
bhalbright Nov 2, 2019
96d1e11
fixed unit test
bhalbright Nov 2, 2019
70f4614
fixed unit test
bhalbright Nov 3, 2019
92289d2
added unit test for milestones endpoint where a repo is selected
bhalbright Nov 3, 2019
02c2bc4
removed view type param from milestones endpoint because it wasn't be…
bhalbright Nov 23, 2019
ffe6d80
Merge branch 'master' of https://github.com/go-gitea/gitea into miles…
bhalbright Nov 23, 2019
d25e1f3
Merge branch 'master' of https://github.com/go-gitea/gitea into miles…
bhalbright Dec 7, 2019
ce00c53
Merge branch 'master' into milestones
lunny Dec 8, 2019
e1bc0f3
Merge branch 'master' of https://github.com/go-gitea/gitea into miles…
bhalbright Dec 8, 2019
8e731a4
Merge branch 'master' of https://github.com/go-gitea/gitea into miles…
bhalbright Dec 9, 2019
875e512
added configuration to enable/disable the milestones dashboard endpoi…
bhalbright Dec 9, 2019
32bcda0
Merge branch 'master' of https://github.com/go-gitea/gitea into miles…
bhalbright Dec 10, 2019
3a71b07
changed "CountMilestonesByRepo" to "CountMilestonesByRepoIDs" so the …
bhalbright Dec 10, 2019
a178e54
Merge branch 'master' into milestones
lunny Dec 14, 2019
e6cae8a
Merge remote-tracking branch 'user/bhalbright/milestones' into milest…
6543 Dec 14, 2019
489a660
fix-stats-count
6543 Dec 14, 2019
f2bbc6e
fix test
6543 Dec 14, 2019
3220b65
add-integration-tests
6543 Dec 14, 2019
eb674fc
make multi selectable
6543 Dec 14, 2019
612f0ef
Merge branch 'master' of https://github.com/go-gitea/gitea into miles…
bhalbright Dec 14, 2019
ecfa69f
Merge branch 'milestones-multiselectable' of https://github.com/6543-…
bhalbright Dec 14, 2019
aaad768
Update integrations/links_test.go
bhalbright Dec 14, 2019
f264f5d
Update integrations/links_test.go
bhalbright Dec 14, 2019
cd60aa9
Merge branch 'master' into milestones
lunny Dec 15, 2019
dd15e48
Merge branch 'master' into milestones
zeripath Dec 15, 2019
b33d30a
Update integrations/links_test.go
bhalbright Dec 15, 2019
218aa5b
Update integrations/links_test.go
bhalbright Dec 15, 2019
5d161b3
Merge branch 'master' of https://github.com/go-gitea/gitea into miles…
bhalbright Dec 15, 2019
def5416
Merge branch 'master' into milestones
zeripath Dec 15, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions custom/conf/app.ini.sample
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,8 @@ DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME = true
NO_REPLY_ADDRESS = noreply.example.org
; Show Registration button
SHOW_REGISTRATION_BUTTON = true
; Show milestones dashboard page - a view of all the user's milestones
SHOW_MILESTONES_DASHBOARD_PAGE = true
; Default value for AutoWatchNewRepos
; When adding a repo to a team or creating a new repo all team members will watch the
; repo automatically if enabled
Expand Down
1 change: 1 addition & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ relation to port exhaustion.
- `EMAIL_DOMAIN_WHITELIST`: **\<empty\>**: If non-empty, list of domain names that can only be used to register
on this instance.
- `SHOW_REGISTRATION_BUTTON`: **! DISABLE\_REGISTRATION**: Show Registration Button
- `SHOW_MILESTONES_DASHBOARD_PAGE`: **true** Enable this to show the milestones dashboard page - a view of all the user's milestones
- `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created
- `AUTO_WATCH_ON_CHANGES`: **false**: Enable this to make users watch a repository after their first commit to it
- `DEFAULT_ORG_VISIBILITY`: **public**: Set default visibility mode for organisations, either "public", "limited" or "private".
Expand Down
112 changes: 110 additions & 2 deletions models/issue_milestone.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import (

// Milestone represents a milestone of repository.
type Milestone struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
Repo *Repository `xorm:"-"`
Name string
Content string `xorm:"TEXT"`
RenderedContent string `xorm:"-"`
Expand Down Expand Up @@ -177,11 +178,38 @@ func (milestones MilestoneList) loadTotalTrackedTimes(e Engine) error {
return nil
}

func (m *Milestone) loadTotalTrackedTime(e Engine) error {
type totalTimesByMilestone struct {
MilestoneID int64
Time int64
}
totalTime := &totalTimesByMilestone{MilestoneID: m.ID}
has, err := e.Table("issue").
Join("INNER", "milestone", "issue.milestone_id = milestone.id").
Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
Select("milestone_id, sum(time) as time").
Where("milestone_id = ?", m.ID).
GroupBy("milestone_id").
Get(totalTime)
if err != nil {
return err
} else if !has {
return nil
}
m.TotalTrackedTime = totalTime.Time
return nil
}

// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
func (milestones MilestoneList) LoadTotalTrackedTimes() error {
return milestones.loadTotalTrackedTimes(x)
}

// LoadTotalTrackedTime loads the tracked time for the milestone
func (m *Milestone) LoadTotalTrackedTime() error {
return m.loadTotalTrackedTime(x)
}

func (milestones MilestoneList) getMilestoneIDs() []int64 {
var ids = make([]int64, 0, len(milestones))
for _, ms := range milestones {
Expand Down Expand Up @@ -465,3 +493,83 @@ func DeleteMilestoneByRepoID(repoID, id int64) error {
}
return sess.Commit()
}

// CountMilestonesByRepoIDs map from repoIDs to number of milestones matching the options`
func CountMilestonesByRepoIDs(repoIDs []int64, isClosed bool) (map[int64]int64, error) {
sess := x.Where("is_closed = ?", isClosed)
sess.In("repo_id", repoIDs)

countsSlice := make([]*struct {
RepoID int64
Count int64
}, 0, 10)
if err := sess.GroupBy("repo_id").
Select("repo_id AS repo_id, COUNT(*) AS count").
Table("milestone").
Find(&countsSlice); err != nil {
return nil, err
}

countMap := make(map[int64]int64, len(countsSlice))
for _, c := range countsSlice {
countMap[c.RepoID] = c.Count
}
return countMap, nil
}

// GetMilestonesByRepoIDs returns a list of milestones of given repositories and status.
func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
sess := x.Where("is_closed = ?", isClosed)
sess.In("repo_id", repoIDs)
if page > 0 {
sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
}

switch sortType {
case "furthestduedate":
sess.Desc("deadline_unix")
case "leastcomplete":
sess.Asc("completeness")
case "mostcomplete":
sess.Desc("completeness")
case "leastissues":
sess.Asc("num_issues")
case "mostissues":
sess.Desc("num_issues")
default:
sess.Asc("deadline_unix")
}
return miles, sess.Find(&miles)
}

// UserMilestoneStats represents milestone statistic information.
type UserMilestoneStats struct {
OpenCount, ClosedCount int64
}

// GetUserMilestoneStats returns milestone statistic information for dashboard by given conditions.
func GetUserMilestoneStats(userID int64, repoID int64, userRepoIDs []int64) (*UserMilestoneStats, error) {
var err error
stats := &UserMilestoneStats{}

cond := builder.NewCond()
if repoID > 0 {
cond = cond.And(builder.Eq{"repo_id": repoID})
}

stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
And(builder.In("repo_id", userRepoIDs)).
Count(new(Milestone))
if err != nil {
return nil, err
}
stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
And(builder.In("repo_id", userRepoIDs)).
Count(new(Milestone))
if err != nil {
return nil, err
}

return stats, nil
}
86 changes: 86 additions & 0 deletions models/issue_milestone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,89 @@ func TestMilestoneList_LoadTotalTrackedTimes(t *testing.T) {

assert.Equal(t, miles[0].TotalTrackedTime, int64(3662))
}

func TestCountMilestonesByRepoIDs(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
milestonesCount := func(repoID int64) (int, int) {
repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
return repo.NumOpenMilestones, repo.NumClosedMilestones
}
repo1OpenCount, repo1ClosedCount := milestonesCount(1)
repo2OpenCount, repo2ClosedCount := milestonesCount(2)

openCounts, err := CountMilestonesByRepoIDs([]int64{1, 2}, false)
assert.NoError(t, err)
assert.EqualValues(t, repo1OpenCount, openCounts[1])
assert.EqualValues(t, repo2OpenCount, openCounts[2])

closedCounts, err := CountMilestonesByRepoIDs([]int64{1, 2}, true)
assert.NoError(t, err)
assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
assert.EqualValues(t, repo2ClosedCount, closedCounts[2])
}

func TestGetMilestonesByRepoIDs(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
test := func(sortType string, sortCond func(*Milestone) int) {
for _, page := range []int{0, 1} {
openMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, false, sortType)
assert.NoError(t, err)
assert.Len(t, openMilestones, repo1.NumOpenMilestones+repo2.NumOpenMilestones)
values := make([]int, len(openMilestones))
for i, milestone := range openMilestones {
values[i] = sortCond(milestone)
}
assert.True(t, sort.IntsAreSorted(values))

closedMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, true, sortType)
assert.NoError(t, err)
assert.Len(t, closedMilestones, repo1.NumClosedMilestones+repo2.NumClosedMilestones)
values = make([]int, len(closedMilestones))
for i, milestone := range closedMilestones {
values[i] = sortCond(milestone)
}
assert.True(t, sort.IntsAreSorted(values))
}
}
test("furthestduedate", func(milestone *Milestone) int {
return -int(milestone.DeadlineUnix)
})
test("leastcomplete", func(milestone *Milestone) int {
return milestone.Completeness
})
test("mostcomplete", func(milestone *Milestone) int {
return -milestone.Completeness
})
test("leastissues", func(milestone *Milestone) int {
return milestone.NumIssues
})
test("mostissues", func(milestone *Milestone) int {
return -milestone.NumIssues
})
test("soonestduedate", func(milestone *Milestone) int {
return int(milestone.DeadlineUnix)
})
}

func TestLoadTotalTrackedTime(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
milestone := AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone)

assert.NoError(t, milestone.LoadTotalTrackedTime())

assert.Equal(t, milestone.TotalTrackedTime, int64(3662))
}

func TestGetUserMilestoneStats(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)

milestoneStats, err := GetUserMilestoneStats(user.ID, repo1.ID, []int64{repo1.ID, repo2.ID})
assert.NoError(t, err)
assert.EqualValues(t, repo1.NumOpenMilestones, milestoneStats.OpenCount)
assert.EqualValues(t, repo1.NumClosedMilestones, milestoneStats.ClosedCount)
}
1 change: 1 addition & 0 deletions modules/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ func Contexter() macaron.Handler {
ctx.Data["IsLandingPageOrganizations"] = setting.LandingPageURL == setting.LandingPageOrganizations

ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton
ctx.Data["ShowMilestonesDashboardPage"] = setting.Service.ShowMilestonesDashboardPage
ctx.Data["ShowFooterBranding"] = setting.ShowFooterBranding
ctx.Data["ShowFooterVersion"] = setting.ShowFooterVersion

Expand Down
2 changes: 2 additions & 0 deletions modules/setting/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var Service struct {
DisableRegistration bool
AllowOnlyExternalRegistration bool
ShowRegistrationButton bool
ShowMilestonesDashboardPage bool
RequireSignInView bool
EnableNotifyMail bool
EnableBasicAuth bool
Expand Down Expand Up @@ -62,6 +63,7 @@ func newService() {
Service.AllowOnlyExternalRegistration = sec.Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").MustBool()
Service.EmailDomainWhitelist = sec.Key("EMAIL_DOMAIN_WHITELIST").Strings(",")
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
Expand Down
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ forks = Forks
activities = Activities
pull_requests = Pull Requests
issues = Issues
milestones = Milestones

cancel = Cancel
add = Add
Expand Down
9 changes: 9 additions & 0 deletions routers/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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() {
Expand Down Expand Up @@ -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)

Expand Down
Loading