From 803d01f1e4342b3653054c91612b0e180e8cd4f0 Mon Sep 17 00:00:00 2001 From: Sidd Weiker Date: Thu, 1 Apr 2021 23:53:08 -0400 Subject: [PATCH 1/5] Group heatmap actions by 15 minute intervals Signed-off-by: Sidd Weiker --- models/user_heatmap.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/models/user_heatmap.go b/models/user_heatmap.go index 74678459cfe4..cae0d9c138b1 100644 --- a/models/user_heatmap.go +++ b/models/user_heatmap.go @@ -32,17 +32,14 @@ func getUserHeatmapData(user *User, team *Team, doer *User) ([]*UserHeatmapData, return hdata, nil } - var groupBy string - groupByName := "timestamp" // We need this extra case because mssql doesn't allow grouping by alias + // 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. + var groupBy = "created_unix / 900 * 900" + var groupByName = "timestamp" // We need this extra case because mssql doesn't allow grouping by alias switch { - case setting.Database.UseSQLite3: - groupBy = "strftime('%s', strftime('%Y-%m-%d', created_unix, 'unixepoch'))" case setting.Database.UseMySQL: - groupBy = "UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(created_unix)))" - case setting.Database.UsePostgreSQL: - groupBy = "extract(epoch from date_trunc('day', to_timestamp(created_unix)))" + groupBy = "created_unix DIV 900 * 900" case setting.Database.UseMSSQL: - groupBy = "datediff(SECOND, '19700101', dateadd(DAY, 0, datediff(day, 0, dateadd(s, created_unix, '19700101'))))" groupByName = groupBy } From 4a19f8f955cb394a9b4a2d24def0ed713bff8241 Mon Sep 17 00:00:00 2001 From: Sidd Weiker Date: Thu, 1 Apr 2021 23:57:48 -0400 Subject: [PATCH 2/5] Add multi-contribution test for user heatmap Signed-off-by: Sidd Weiker --- models/fixtures/action.yml | 24 ++++++++++++++++++++++++ models/user_heatmap_test.go | 28 ++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/models/fixtures/action.yml b/models/fixtures/action.yml index 14cfd90423cf..e3f3d2a97126 100644 --- a/models/fixtures/action.yml +++ b/models/fixtures/action.yml @@ -32,3 +32,27 @@ repo_id: 22 is_private: true 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 diff --git a/models/user_heatmap_test.go b/models/user_heatmap_test.go index cc08ac155159..1da3af7d182e 100644 --- a/models/user_heatmap_test.go +++ b/models/user_heatmap_test.go @@ -19,12 +19,20 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { CountResult int JSONResult string }{ - {2, 2, 1, `[{"timestamp":1603152000,"contributions":1}]`}, // self looks at action in private repo - {2, 1, 1, `[{"timestamp":1603152000,"contributions":1}]`}, // admin looks at action in private repo - {2, 3, 0, `[]`}, // other user looks at action in private repo - {2, 0, 0, `[]`}, // nobody looks at action in private repo - {16, 15, 1, `[{"timestamp":1603238400,"contributions":1}]`}, // collaborator looks at action in private repo - {3, 3, 0, `[]`}, // no action action not performed by target user + // self looks at action in private repo + {2, 2, 1, `[{"timestamp":1603227600,"contributions":1}]`}, + // admin looks at action in private repo + {2, 1, 1, `[{"timestamp":1603227600,"contributions":1}]`}, + // other user looks at action in private repo + {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 assert.NoError(t, PrepareTestDatabase()) @@ -51,9 +59,13 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { // Get the heatmap and compare heatmap, err := GetUserHeatmapDataByUser(user, doer) + var contributions int + for _, hm := range heatmap { + contributions += int(hm.Contributions) + } assert.NoError(t, err) - assert.Equal(t, len(actions), len(heatmap), "invalid action count: did the test data became too old?") - assert.Equal(t, tc.CountResult, len(heatmap), fmt.Sprintf("testcase %d", i)) + assert.Equal(t, len(actions), contributions, "invalid action count: did the test data became too old?") + assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase %d", i)) // Test JSON rendering json := jsoniter.ConfigCompatibleWithStandardLibrary From 8974d9a3397edff722070892e8b171c33a32ac8f Mon Sep 17 00:00:00 2001 From: Sidd Weiker Date: Fri, 2 Apr 2021 01:29:18 -0400 Subject: [PATCH 3/5] Add timezone aware summation for activity heatmap Signed-off-by: Sidd Weiker --- web_src/js/features/heatmap.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/web_src/js/features/heatmap.js b/web_src/js/features/heatmap.js index d1cb43dde0ed..07ecaee46167 100644 --- a/web_src/js/features/heatmap.js +++ b/web_src/js/features/heatmap.js @@ -7,8 +7,15 @@ export default async function initHeatmap() { if (!el) return; try { - const values = JSON.parse(el.dataset.heatmapData).map(({contributions, timestamp}) => { - return {date: new Date(timestamp * 1000), count: contributions}; + const heatmap = {}; + 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({ From a90651a111d1ad68ee0ad1647e6e2b4887fb8cf1 Mon Sep 17 00:00:00 2001 From: Sidd Weiker Date: Fri, 2 Apr 2021 02:04:50 -0400 Subject: [PATCH 4/5] Fix api user heatmap test Signed-off-by: Sidd Weiker --- integrations/api_user_heatmap_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/api_user_heatmap_test.go b/integrations/api_user_heatmap_test.go index 105d39e9ae2d..a0f0552a1794 100644 --- a/integrations/api_user_heatmap_test.go +++ b/integrations/api_user_heatmap_test.go @@ -26,7 +26,7 @@ func TestUserHeatmap(t *testing.T) { var heatmap []*models.UserHeatmapData DecodeJSON(t, resp, &heatmap) 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) } From 1043a474d4849e9dea7ab97e1ee5da2ebfc1c496 Mon Sep 17 00:00:00 2001 From: Sidd Weiker Date: Thu, 8 Apr 2021 16:32:01 -0400 Subject: [PATCH 5/5] Update variable declaration style --- models/user_heatmap.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/user_heatmap.go b/models/user_heatmap.go index cae0d9c138b1..35bb5ee0b29a 100644 --- a/models/user_heatmap.go +++ b/models/user_heatmap.go @@ -34,8 +34,8 @@ func getUserHeatmapData(user *User, team *Team, doer *User) ([]*UserHeatmapData, // 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. - var groupBy = "created_unix / 900 * 900" - var groupByName = "timestamp" // We need this extra case because mssql doesn't allow grouping by alias + groupBy := "created_unix / 900 * 900" + groupByName := "timestamp" // We need this extra case because mssql doesn't allow grouping by alias switch { case setting.Database.UseMySQL: groupBy = "created_unix DIV 900 * 900"