Fix heatmap activity (#15252)
* Group heatmap actions by 15 minute intervals Signed-off-by: Sidd Weiker <siddweiker@gmail.com> * Add multi-contribution test for user heatmap Signed-off-by: Sidd Weiker <siddweiker@gmail.com> * Add timezone aware summation for activity heatmap Signed-off-by: Sidd Weiker <siddweiker@gmail.com> * Fix api user heatmap test Signed-off-by: Sidd Weiker <siddweiker@gmail.com> * Update variable declaration style Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
		
							parent
							
								
									3ef23d5411
								
							
						
					
					
						commit
						f573e93ed4
					
				
					 5 changed files with 58 additions and 18 deletions
				
			
		|  | @ -26,7 +26,7 @@ func TestUserHeatmap(t *testing.T) { | ||||||
| 	var heatmap []*models.UserHeatmapData | 	var heatmap []*models.UserHeatmapData | ||||||
| 	DecodeJSON(t, resp, &heatmap) | 	DecodeJSON(t, resp, &heatmap) | ||||||
| 	var dummyheatmap []*models.UserHeatmapData | 	var dummyheatmap []*models.UserHeatmapData | ||||||
| 	dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1603152000, Contributions: 1}) | 	dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1603227600, Contributions: 1}) | ||||||
| 
 | 
 | ||||||
| 	assert.Equal(t, dummyheatmap, heatmap) | 	assert.Equal(t, dummyheatmap, heatmap) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -32,3 +32,27 @@ | ||||||
|   repo_id: 22 |   repo_id: 22 | ||||||
|   is_private: true |   is_private: true | ||||||
|   created_unix: 1603267920 |   created_unix: 1603267920 | ||||||
|  | 
 | ||||||
|  | - id: 5 | ||||||
|  |   user_id: 10 | ||||||
|  |   op_type: 1 # create repo | ||||||
|  |   act_user_id: 10 | ||||||
|  |   repo_id: 6 | ||||||
|  |   is_private: true | ||||||
|  |   created_unix: 1603010100 | ||||||
|  | 
 | ||||||
|  | - id: 6 | ||||||
|  |   user_id: 10 | ||||||
|  |   op_type: 1 # create repo | ||||||
|  |   act_user_id: 10 | ||||||
|  |   repo_id: 7 | ||||||
|  |   is_private: true | ||||||
|  |   created_unix: 1603011300 | ||||||
|  | 
 | ||||||
|  | - id: 7 | ||||||
|  |   user_id: 10 | ||||||
|  |   op_type: 1 # create repo | ||||||
|  |   act_user_id: 10 | ||||||
|  |   repo_id: 8 | ||||||
|  |   is_private: false | ||||||
|  |   created_unix: 1603011540 # grouped with id:7 | ||||||
|  |  | ||||||
|  | @ -32,17 +32,14 @@ func getUserHeatmapData(user *User, team *Team, doer *User) ([]*UserHeatmapData, | ||||||
| 		return hdata, nil | 		return hdata, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var groupBy string | 	// Group by 15 minute intervals which will allow the client to accurately shift the timestamp to their timezone.
 | ||||||
|  | 	// The interval is based on the fact that there are timezones such as UTC +5:30 and UTC +12:45.
 | ||||||
|  | 	groupBy := "created_unix / 900 * 900" | ||||||
| 	groupByName := "timestamp" // We need this extra case because mssql doesn't allow grouping by alias
 | 	groupByName := "timestamp" // We need this extra case because mssql doesn't allow grouping by alias
 | ||||||
| 	switch { | 	switch { | ||||||
| 	case setting.Database.UseSQLite3: |  | ||||||
| 		groupBy = "strftime('%s', strftime('%Y-%m-%d', created_unix, 'unixepoch'))" |  | ||||||
| 	case setting.Database.UseMySQL: | 	case setting.Database.UseMySQL: | ||||||
| 		groupBy = "UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(created_unix)))" | 		groupBy = "created_unix DIV 900 * 900" | ||||||
| 	case setting.Database.UsePostgreSQL: |  | ||||||
| 		groupBy = "extract(epoch from date_trunc('day', to_timestamp(created_unix)))" |  | ||||||
| 	case setting.Database.UseMSSQL: | 	case setting.Database.UseMSSQL: | ||||||
| 		groupBy = "datediff(SECOND, '19700101', dateadd(DAY, 0, datediff(day, 0, dateadd(s, created_unix, '19700101'))))" |  | ||||||
| 		groupByName = groupBy | 		groupByName = groupBy | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,12 +19,20 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { | ||||||
| 		CountResult int | 		CountResult int | ||||||
| 		JSONResult  string | 		JSONResult  string | ||||||
| 	}{ | 	}{ | ||||||
| 		{2, 2, 1, `[{"timestamp":1603152000,"contributions":1}]`}, // self looks at action in private repo
 | 		// self looks at action in private repo
 | ||||||
| 		{2, 1, 1, `[{"timestamp":1603152000,"contributions":1}]`}, // admin looks at action in private repo
 | 		{2, 2, 1, `[{"timestamp":1603227600,"contributions":1}]`}, | ||||||
| 		{2, 3, 0, `[]`}, // other user looks at action in private repo
 | 		// admin looks at action in private repo
 | ||||||
| 		{2, 0, 0, `[]`}, // nobody looks at action in private repo
 | 		{2, 1, 1, `[{"timestamp":1603227600,"contributions":1}]`}, | ||||||
| 		{16, 15, 1, `[{"timestamp":1603238400,"contributions":1}]`}, // collaborator looks at action in private repo
 | 		// other user looks at action in private repo
 | ||||||
| 		{3, 3, 0, `[]`}, // no action action not performed by target user
 | 		{2, 3, 0, `[]`}, | ||||||
|  | 		// nobody looks at action in private repo
 | ||||||
|  | 		{2, 0, 0, `[]`}, | ||||||
|  | 		// collaborator looks at action in private repo
 | ||||||
|  | 		{16, 15, 1, `[{"timestamp":1603267200,"contributions":1}]`}, | ||||||
|  | 		// no action action not performed by target user
 | ||||||
|  | 		{3, 3, 0, `[]`}, | ||||||
|  | 		// multiple actions performed with two grouped together
 | ||||||
|  | 		{10, 10, 3, `[{"timestamp":1603009800,"contributions":1},{"timestamp":1603010700,"contributions":2}]`}, | ||||||
| 	} | 	} | ||||||
| 	// Prepare
 | 	// Prepare
 | ||||||
| 	assert.NoError(t, PrepareTestDatabase()) | 	assert.NoError(t, PrepareTestDatabase()) | ||||||
|  | @ -51,9 +59,13 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 		// Get the heatmap and compare
 | 		// Get the heatmap and compare
 | ||||||
| 		heatmap, err := GetUserHeatmapDataByUser(user, doer) | 		heatmap, err := GetUserHeatmapDataByUser(user, doer) | ||||||
|  | 		var contributions int | ||||||
|  | 		for _, hm := range heatmap { | ||||||
|  | 			contributions += int(hm.Contributions) | ||||||
|  | 		} | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.Len(t, heatmap, len(actions), "invalid action count: did the test data became too old?") | 		assert.Len(t, actions, contributions, "invalid action count: did the test data became too old?") | ||||||
| 		assert.Len(t, heatmap, tc.CountResult, fmt.Sprintf("testcase %d", i)) | 		assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase %d", i)) | ||||||
| 
 | 
 | ||||||
| 		// Test JSON rendering
 | 		// Test JSON rendering
 | ||||||
| 		json := jsoniter.ConfigCompatibleWithStandardLibrary | 		json := jsoniter.ConfigCompatibleWithStandardLibrary | ||||||
|  |  | ||||||
|  | @ -7,8 +7,15 @@ export default async function initHeatmap() { | ||||||
|   if (!el) return; |   if (!el) return; | ||||||
| 
 | 
 | ||||||
|   try { |   try { | ||||||
|     const values = JSON.parse(el.dataset.heatmapData).map(({contributions, timestamp}) => { |     const heatmap = {}; | ||||||
|       return {date: new Date(timestamp * 1000), count: contributions}; |     JSON.parse(el.dataset.heatmapData).forEach(({contributions, timestamp}) => { | ||||||
|  |       // Convert to user timezone and sum contributions by date
 | ||||||
|  |       const dateStr = new Date(timestamp * 1000).toDateString(); | ||||||
|  |       heatmap[dateStr] = (heatmap[dateStr] || 0) + contributions; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const values = Object.keys(heatmap).map((v) => { | ||||||
|  |       return {date: new Date(v), count: heatmap[v]}; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const View = Vue.extend({ |     const View = Vue.extend({ | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue