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

Worktime tracking for the organization level #19808

Merged
merged 11 commits into from
Feb 2, 2025
88 changes: 88 additions & 0 deletions models/organization/org_times.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2022 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.
// SPDX-License-Identifier: MIT

package organization

import (
"code.gitea.io/gitea/models/db"

"xorm.io/builder"
kkovacs marked this conversation as resolved.
Show resolved Hide resolved
)

// ResultTimesByRepos is a struct for DB query results
type ResultTimesByRepos struct {
Name string
SumTime int64
}

// ResultTimesByMilestones is a struct for DB query results
type ResultTimesByMilestones struct {
RepoName string
Name string
ID string
SumTime int64
HideRepoName bool
}

// ResultTimesByMembers is a struct for DB query results
type ResultTimesByMembers struct {
Name string
SumTime int64
}

// GetTimesByRepos fetches data from DB to serve TimesByRepos.
func GetTimesByRepos(org *Organization, unixfrom, unixto int64) (results []ResultTimesByRepos, err error) {
// Get the data from the DB
err = db.GetEngine(db.DefaultContext).
Select("repository.name, SUM(tracked_time.time) AS sum_time").
Table("tracked_time").
Join("INNER", "issue", "tracked_time.issue_id = issue.id").
Join("INNER", "repository", "issue.repo_id = repository.id").
Where(builder.Eq{"repository.owner_id": org.ID}).
And(builder.Eq{"tracked_time.deleted": false}).
And(builder.Gte{"tracked_time.created_unix": unixfrom}).
And(builder.Lte{"tracked_time.created_unix": unixto}).
GroupBy("repository.id").
OrderBy("repository.name").
Find(&results)
return results, err
}

// GetTimesByMilestones gets the actual data from the DB to serve TimesByMilestones.
func GetTimesByMilestones(org *Organization, unixfrom, unixto int64) (results []ResultTimesByMilestones, err error) {
err = db.GetEngine(db.DefaultContext).
Select("repository.name AS repo_name, milestone.name, milestone.id, SUM(tracked_time.time) AS sum_time").
Table("tracked_time").
Join("INNER", "issue", "tracked_time.issue_id = issue.id").
Join("INNER", "repository", "issue.repo_id = repository.id").
Join("LEFT", "milestone", "issue.milestone_id = milestone.id").
Where(builder.Eq{"repository.owner_id": org.ID}).
And(builder.Eq{"tracked_time.deleted": false}).
And(builder.Gte{"tracked_time.created_unix": unixfrom}).
And(builder.Lte{"tracked_time.created_unix": unixto}).
GroupBy("repository.id, milestone.id").
OrderBy("repository.name, milestone.deadline_unix, milestone.id").
Find(&results)

return results, err
}

// getTimesByMembers gets the actual data from the DB to serve TimesByMembers.
func GetTimesByMembers(org *Organization, unixfrom, unixto int64) (results []ResultTimesByMembers, err error) {
err = db.GetEngine(db.DefaultContext).
Select("user.name, SUM(tracked_time.time) AS sum_time").
Table("tracked_time").
Join("INNER", "issue", "tracked_time.issue_id = issue.id").
Join("INNER", "repository", "issue.repo_id = repository.id").
Join("INNER", "user", "tracked_time.user_id = user.id").
Where(builder.Eq{"repository.owner_id": org.ID}).
And(builder.Eq{"tracked_time.deleted": false}).
And(builder.Gte{"tracked_time.created_unix": unixfrom}).
And(builder.Lte{"tracked_time.created_unix": unixto}).
GroupBy("user.id").
OrderBy("sum_time DESC").
Find(&results)
return results, err
}
292 changes: 292 additions & 0 deletions models/organization/org_times_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
// Copyright 2022 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.
// SPDX-License-Identifier: MIT

package organization_test

import (
"testing"

"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/unittest"

_ "code.gitea.io/gitea/models/issues"

"github.com/stretchr/testify/assert"
)

// TestTimesPrepareDB prepares the database for the following tests.
func TestTimesPrepareDB(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
}

// TestTimesByRepos tests TimesByRepos functionality
func TestTimesByRepos(t *testing.T) {
kases := []struct {
name string
unixfrom int64
unixto int64
orgname int64
expected []organization.ResultTimesByRepos
}{
{
name: "Full sum for org 1",
unixfrom: 0,
unixto: 9223372036854775807,
orgname: 1,
expected: []organization.ResultTimesByRepos(nil),
},
{
name: "Full sum for org 2",
unixfrom: 0,
unixto: 9223372036854775807,
orgname: 2,
expected: []organization.ResultTimesByRepos{
{
Name: "repo1",
SumTime: 4083,
},
{
Name: "repo2",
SumTime: 75,
},
},
},
{
name: "Simple time bound",
unixfrom: 946684801,
unixto: 946684802,
orgname: 2,
expected: []organization.ResultTimesByRepos{
{
Name: "repo1",
SumTime: 3662,
},
},
},
{
name: "Both times inclusive",
unixfrom: 946684801,
unixto: 946684801,
orgname: 2,
expected: []organization.ResultTimesByRepos{
{
Name: "repo1",
SumTime: 3661,
},
},
},
{
name: "Should ignore deleted",
unixfrom: 947688814,
unixto: 947688815,
orgname: 2,
expected: []organization.ResultTimesByRepos{
{
Name: "repo2",
SumTime: 71,
},
},
},
}

// Run test kases
for _, kase := range kases {
t.Run(kase.name, func(t *testing.T) {
org, err := organization.GetOrgByID(db.DefaultContext, kase.orgname)
assert.NoError(t, err)
results, err := organization.GetTimesByRepos(org, kase.unixfrom, kase.unixto)
assert.NoError(t, err)
assert.Equal(t, kase.expected, results)
})
}
}

// TestTimesByMilestones tests TimesByMilestones functionality
func TestTimesByMilestones(t *testing.T) {
kases := []struct {
name string
unixfrom int64
unixto int64
orgname int64
expected []organization.ResultTimesByMilestones
}{
{
name: "Full sum for org 1",
unixfrom: 0,
unixto: 9223372036854775807,
orgname: 1,
expected: []organization.ResultTimesByMilestones(nil),
},
{
name: "Full sum for org 2",
unixfrom: 0,
unixto: 9223372036854775807,
orgname: 2,
expected: []organization.ResultTimesByMilestones{
{
RepoName: "repo1",
Name: "",
ID: "",
SumTime: 401,
HideRepoName: false,
},
{
RepoName: "repo1",
Name: "milestone1",
ID: "1",
SumTime: 3682,
HideRepoName: false,
},
{
RepoName: "repo2",
Name: "",
ID: "",
SumTime: 75,
HideRepoName: false,
},
},
},
{
name: "Simple time bound",
unixfrom: 946684801,
unixto: 946684802,
orgname: 2,
expected: []organization.ResultTimesByMilestones{
{
RepoName: "repo1",
Name: "milestone1",
ID: "1",
SumTime: 3662,
HideRepoName: false,
},
},
},
{
name: "Both times inclusive",
unixfrom: 946684801,
unixto: 946684801,
orgname: 2,
expected: []organization.ResultTimesByMilestones{
{
RepoName: "repo1",
Name: "milestone1",
ID: "1",
SumTime: 3661,
HideRepoName: false,
},
},
},
{
name: "Should ignore deleted",
unixfrom: 947688814,
unixto: 947688815,
orgname: 2,
expected: []organization.ResultTimesByMilestones{
{
RepoName: "repo2",
Name: "",
ID: "",
SumTime: 71,
HideRepoName: false,
},
},
},
}

// Run test kases
for _, kase := range kases {
t.Run(kase.name, func(t *testing.T) {
org, err := organization.GetOrgByID(db.DefaultContext, kase.orgname)
assert.NoError(t, err)
results, err := organization.GetTimesByMilestones(org, kase.unixfrom, kase.unixto)
assert.NoError(t, err)
assert.Equal(t, kase.expected, results)
})
}
}

// TestTimesByMembers tests TimesByMembers functionality
func TestTimesByMembers(t *testing.T) {
kases := []struct {
name string
unixfrom int64
unixto int64
orgname int64
expected []organization.ResultTimesByMembers
}{
{
name: "Full sum for org 1",
unixfrom: 0,
unixto: 9223372036854775807,
orgname: 1,
expected: []organization.ResultTimesByMembers(nil),
},
{
// Test case: Sum of times forever in org no. 2
name: "Full sum for org 2",
unixfrom: 0,
unixto: 9223372036854775807,
orgname: 2,
expected: []organization.ResultTimesByMembers{
{
Name: "user2",
SumTime: 3666,
},
{
Name: "user1",
SumTime: 491,
},
},
},
{
name: "Simple time bound",
unixfrom: 946684801,
unixto: 946684802,
orgname: 2,
expected: []organization.ResultTimesByMembers{
{
Name: "user2",
SumTime: 3662,
},
},
},
{
name: "Both times inclusive",
unixfrom: 946684801,
unixto: 946684801,
orgname: 2,
expected: []organization.ResultTimesByMembers{
{
Name: "user2",
SumTime: 3661,
},
},
},
{
name: "Should ignore deleted",
unixfrom: 947688814,
unixto: 947688815,
orgname: 2,
expected: []organization.ResultTimesByMembers{
{
Name: "user1",
SumTime: 71,
},
},
},
}

// Run test kases
for _, kase := range kases {
t.Run(kase.name, func(t *testing.T) {
org, err := organization.GetOrgByID(db.DefaultContext, kase.orgname)
assert.NoError(t, err)
results, err := organization.GetTimesByMembers(org, kase.unixfrom, kase.unixto)
assert.NoError(t, err)
assert.Equal(t, kase.expected, results)
})
}
}
1 change: 1 addition & 0 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ func NewFuncMap() []template.FuncMap {
"Printf": fmt.Sprintf,
"Escape": Escape,
"Sec2Time": util.SecToTime,
"Sec2Hour": util.SecToHour,
"ParseDeadline": func(deadline string) []string {
return strings.Split(deadline, "|")
},
Expand Down
Loading