mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 02:46:04 +01:00 
			
		
		
		
	Dear Gitea team, first of all, thanks for the great work you're doing with this project. I'm planning to introduce Gitea at a client site, and noticed that while there is time recording, there are no project-manager-friendly reports to actually make use of that data, as were also mentioned by others in #4870 #8684 and #13531. Since I had a little time last weekend, I had put together something that I hope to be a useful contribution to this great project (while of course useful for me too). This PR adds a new "Worktime" tab to the Organisation level. There is a date range selector (by default set to the current month), and there are three possible views: - by repository, - by milestone, and - by team member. Happy to receive any feedback! There are several possible future improvements of course (predefined date ranges, charts, a member time sheet, matrix of repos/members, etc) but I hope that even in this relatively simple state this would be useful to lots of people. <img width="1161" alt="Screen Shot 2022-05-25 at 22 12 58" src="https://user-images.githubusercontent.com/118010/170366976-af00c7af-c4f3-4117-86d7-00356d6797a5.png"> Keep up the good work! Kristof --------- Co-authored-by: user <user@kk-git1> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
		
			
				
	
	
		
			294 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			294 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2025 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package integration_test
 | |
| 
 | |
| import (
 | |
| 	"testing"
 | |
| 
 | |
| 	"code.gitea.io/gitea/models/db"
 | |
| 	"code.gitea.io/gitea/models/organization"
 | |
| 	"code.gitea.io/gitea/models/unittest"
 | |
| 
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| )
 | |
| 
 | |
| // TestTimesByRepos tests TimesByRepos functionality
 | |
| func testTimesByRepos(t *testing.T) {
 | |
| 	kases := []struct {
 | |
| 		name     string
 | |
| 		unixfrom int64
 | |
| 		unixto   int64
 | |
| 		orgname  int64
 | |
| 		expected []organization.WorktimeSumByRepos
 | |
| 	}{
 | |
| 		{
 | |
| 			name:     "Full sum for org 1",
 | |
| 			unixfrom: 0,
 | |
| 			unixto:   9223372036854775807,
 | |
| 			orgname:  1,
 | |
| 			expected: []organization.WorktimeSumByRepos(nil),
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Full sum for org 2",
 | |
| 			unixfrom: 0,
 | |
| 			unixto:   9223372036854775807,
 | |
| 			orgname:  2,
 | |
| 			expected: []organization.WorktimeSumByRepos{
 | |
| 				{
 | |
| 					RepoName: "repo1",
 | |
| 					SumTime:  4083,
 | |
| 				},
 | |
| 				{
 | |
| 					RepoName: "repo2",
 | |
| 					SumTime:  75,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Simple time bound",
 | |
| 			unixfrom: 946684801,
 | |
| 			unixto:   946684802,
 | |
| 			orgname:  2,
 | |
| 			expected: []organization.WorktimeSumByRepos{
 | |
| 				{
 | |
| 					RepoName: "repo1",
 | |
| 					SumTime:  3662,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Both times inclusive",
 | |
| 			unixfrom: 946684801,
 | |
| 			unixto:   946684801,
 | |
| 			orgname:  2,
 | |
| 			expected: []organization.WorktimeSumByRepos{
 | |
| 				{
 | |
| 					RepoName: "repo1",
 | |
| 					SumTime:  3661,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Should ignore deleted",
 | |
| 			unixfrom: 947688814,
 | |
| 			unixto:   947688815,
 | |
| 			orgname:  2,
 | |
| 			expected: []organization.WorktimeSumByRepos{
 | |
| 				{
 | |
| 					RepoName: "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.GetWorktimeByRepos(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.WorktimeSumByMilestones
 | |
| 	}{
 | |
| 		{
 | |
| 			name:     "Full sum for org 1",
 | |
| 			unixfrom: 0,
 | |
| 			unixto:   9223372036854775807,
 | |
| 			orgname:  1,
 | |
| 			expected: []organization.WorktimeSumByMilestones(nil),
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Full sum for org 2",
 | |
| 			unixfrom: 0,
 | |
| 			unixto:   9223372036854775807,
 | |
| 			orgname:  2,
 | |
| 			expected: []organization.WorktimeSumByMilestones{
 | |
| 				{
 | |
| 					RepoName:      "repo1",
 | |
| 					MilestoneName: "",
 | |
| 					MilestoneID:   0,
 | |
| 					SumTime:       401,
 | |
| 					HideRepoName:  false,
 | |
| 				},
 | |
| 				{
 | |
| 					RepoName:      "repo1",
 | |
| 					MilestoneName: "milestone1",
 | |
| 					MilestoneID:   1,
 | |
| 					SumTime:       3682,
 | |
| 					HideRepoName:  true,
 | |
| 				},
 | |
| 				{
 | |
| 					RepoName:      "repo2",
 | |
| 					MilestoneName: "",
 | |
| 					MilestoneID:   0,
 | |
| 					SumTime:       75,
 | |
| 					HideRepoName:  false,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Simple time bound",
 | |
| 			unixfrom: 946684801,
 | |
| 			unixto:   946684802,
 | |
| 			orgname:  2,
 | |
| 			expected: []organization.WorktimeSumByMilestones{
 | |
| 				{
 | |
| 					RepoName:      "repo1",
 | |
| 					MilestoneName: "milestone1",
 | |
| 					MilestoneID:   1,
 | |
| 					SumTime:       3662,
 | |
| 					HideRepoName:  false,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Both times inclusive",
 | |
| 			unixfrom: 946684801,
 | |
| 			unixto:   946684801,
 | |
| 			orgname:  2,
 | |
| 			expected: []organization.WorktimeSumByMilestones{
 | |
| 				{
 | |
| 					RepoName:      "repo1",
 | |
| 					MilestoneName: "milestone1",
 | |
| 					MilestoneID:   1,
 | |
| 					SumTime:       3661,
 | |
| 					HideRepoName:  false,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Should ignore deleted",
 | |
| 			unixfrom: 947688814,
 | |
| 			unixto:   947688815,
 | |
| 			orgname:  2,
 | |
| 			expected: []organization.WorktimeSumByMilestones{
 | |
| 				{
 | |
| 					RepoName:      "repo2",
 | |
| 					MilestoneName: "",
 | |
| 					MilestoneID:   0,
 | |
| 					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)
 | |
| 			require.NoError(t, err)
 | |
| 			results, err := organization.GetWorktimeByMilestones(org, kase.unixfrom, kase.unixto)
 | |
| 			if 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.WorktimeSumByMembers
 | |
| 	}{
 | |
| 		{
 | |
| 			name:     "Full sum for org 1",
 | |
| 			unixfrom: 0,
 | |
| 			unixto:   9223372036854775807,
 | |
| 			orgname:  1,
 | |
| 			expected: []organization.WorktimeSumByMembers(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.WorktimeSumByMembers{
 | |
| 				{
 | |
| 					UserName: "user2",
 | |
| 					SumTime:  3666,
 | |
| 				},
 | |
| 				{
 | |
| 					UserName: "user1",
 | |
| 					SumTime:  491,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Simple time bound",
 | |
| 			unixfrom: 946684801,
 | |
| 			unixto:   946684802,
 | |
| 			orgname:  2,
 | |
| 			expected: []organization.WorktimeSumByMembers{
 | |
| 				{
 | |
| 					UserName: "user2",
 | |
| 					SumTime:  3662,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Both times inclusive",
 | |
| 			unixfrom: 946684801,
 | |
| 			unixto:   946684801,
 | |
| 			orgname:  2,
 | |
| 			expected: []organization.WorktimeSumByMembers{
 | |
| 				{
 | |
| 					UserName: "user2",
 | |
| 					SumTime:  3661,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Should ignore deleted",
 | |
| 			unixfrom: 947688814,
 | |
| 			unixto:   947688815,
 | |
| 			orgname:  2,
 | |
| 			expected: []organization.WorktimeSumByMembers{
 | |
| 				{
 | |
| 					UserName: "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.GetWorktimeByMembers(org, kase.unixfrom, kase.unixto)
 | |
| 			assert.NoError(t, err)
 | |
| 			assert.Equal(t, kase.expected, results)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestOrgWorktime(t *testing.T) {
 | |
| 	// we need to run these tests in integration test because there are complex SQL queries
 | |
| 	assert.NoError(t, unittest.PrepareTestDatabase())
 | |
| 	t.Run("ByRepos", testTimesByRepos)
 | |
| 	t.Run("ByMilestones", testTimesByMilestones)
 | |
| 	t.Run("ByMembers", testTimesByMembers)
 | |
| }
 |