mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	User action heatmap (#5131)
* Added basic heatmap data * Added extra case for sqlite * Built basic heatmap into user profile * Get contribution data from api & styling * Fixed lint & added extra group by statements for all database types * generated swagger spec * generated swagger spec * generated swagger spec * fixed swagger spec * fmt * Added tests * Added setting to enable/disable user heatmap * Added locale for loading text * Removed UseTiDB * Updated librejs & moment.js * Fixed import order * Fixed heatmap in postgresql * Update docs/content/doc/advanced/config-cheat-sheet.en-us.md Co-Authored-By: kolaente <konrad@kola-entertainments.de> * Added copyright header * Fixed a bug to show the heatmap for the actual user instead of the currently logged in * Added integration test for heatmaps * Added a heatmap on the dashboard * Fixed timestamp parsing * Hide heatmap on mobile * optimized postgresql group by query * Improved sqlite group by statement
This commit is contained in:
		@@ -193,6 +193,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
 | 
				
			|||||||
- `RECAPTCHA_SECRET`: **""**: Go to https://www.google.com/recaptcha/admin to get a secret for recaptcha.
 | 
					- `RECAPTCHA_SECRET`: **""**: Go to https://www.google.com/recaptcha/admin to get a secret for recaptcha.
 | 
				
			||||||
- `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha.
 | 
					- `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha.
 | 
				
			||||||
- `DEFAULT_ENABLE_DEPENDENCIES`: **true** Enable this to have dependencies enabled by default.
 | 
					- `DEFAULT_ENABLE_DEPENDENCIES`: **true** Enable this to have dependencies enabled by default.
 | 
				
			||||||
 | 
					- `ENABLE_USER_HEATMAP`: **true** Enable this to display the heatmap on users profiles.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Webhook (`webhook`)
 | 
					## Webhook (`webhook`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										30
									
								
								integrations/api_user_heatmap_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								integrations/api_user_heatmap_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					// Copyright 2018 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.package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package integrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestUserHeatmap(t *testing.T) {
 | 
				
			||||||
 | 
						prepareTestEnv(t)
 | 
				
			||||||
 | 
						adminUsername := "user1"
 | 
				
			||||||
 | 
						normalUsername := "user2"
 | 
				
			||||||
 | 
						session := loginUser(t, adminUsername)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						urlStr := fmt.Sprintf("/api/v1/users/%s/heatmap", normalUsername)
 | 
				
			||||||
 | 
						req := NewRequest(t, "GET", urlStr)
 | 
				
			||||||
 | 
						resp := session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
						var heatmap []*models.UserHeatmapData
 | 
				
			||||||
 | 
						DecodeJSON(t, resp, &heatmap)
 | 
				
			||||||
 | 
						var dummyheatmap []*models.UserHeatmapData
 | 
				
			||||||
 | 
						dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1540080000, Contributions: 1})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, dummyheatmap, heatmap)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
  act_user_id: 2
 | 
					  act_user_id: 2
 | 
				
			||||||
  repo_id: 2
 | 
					  repo_id: 2
 | 
				
			||||||
  is_private: true
 | 
					  is_private: true
 | 
				
			||||||
 | 
					  created_unix: 1540139562
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
  id: 2
 | 
					  id: 2
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -48,6 +48,7 @@ func MainTest(m *testing.M, pathToGiteaRoot string) {
 | 
				
			|||||||
	setting.RunUser = "runuser"
 | 
						setting.RunUser = "runuser"
 | 
				
			||||||
	setting.SSH.Port = 3000
 | 
						setting.SSH.Port = 3000
 | 
				
			||||||
	setting.SSH.Domain = "try.gitea.io"
 | 
						setting.SSH.Domain = "try.gitea.io"
 | 
				
			||||||
 | 
						setting.UseSQLite3 = true
 | 
				
			||||||
	setting.RepoRootPath, err = ioutil.TempDir(os.TempDir(), "repos")
 | 
						setting.RepoRootPath, err = ioutil.TempDir(os.TempDir(), "repos")
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		fatalTestError("TempDir: %v\n", err)
 | 
							fatalTestError("TempDir: %v\n", err)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										40
									
								
								models/user_heatmap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								models/user_heatmap.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					// Copyright 2018 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.package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UserHeatmapData represents the data needed to create a heatmap
 | 
				
			||||||
 | 
					type UserHeatmapData struct {
 | 
				
			||||||
 | 
						Timestamp     util.TimeStamp `json:"timestamp"`
 | 
				
			||||||
 | 
						Contributions int64          `json:"contributions"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetUserHeatmapDataByUser returns an array of UserHeatmapData
 | 
				
			||||||
 | 
					func GetUserHeatmapDataByUser(user *User) (hdata []*UserHeatmapData, err error) {
 | 
				
			||||||
 | 
						var groupBy string
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case setting.UseSQLite3:
 | 
				
			||||||
 | 
							groupBy = "strftime('%s', strftime('%Y-%m-%d', created_unix, 'unixepoch'))"
 | 
				
			||||||
 | 
						case setting.UseMySQL:
 | 
				
			||||||
 | 
							groupBy = "UNIX_TIMESTAMP(DATE_FORMAT(FROM_UNIXTIME(created_unix), '%Y%m%d'))"
 | 
				
			||||||
 | 
						case setting.UsePostgreSQL:
 | 
				
			||||||
 | 
							groupBy = "extract(epoch from date_trunc('day', to_timestamp(created_unix)))"
 | 
				
			||||||
 | 
						case setting.UseMSSQL:
 | 
				
			||||||
 | 
							groupBy = "dateadd(DAY,0, datediff(day,0, dateadd(s, created_unix, '19700101')))"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = x.Select(groupBy+" as timestamp, count(user_id) as contributions").
 | 
				
			||||||
 | 
							Table("action").
 | 
				
			||||||
 | 
							Where("user_id = ?", user.ID).
 | 
				
			||||||
 | 
							And("created_unix > ?", (util.TimeStampNow() - 31536000)).
 | 
				
			||||||
 | 
							GroupBy("timestamp").
 | 
				
			||||||
 | 
							OrderBy("timestamp").
 | 
				
			||||||
 | 
							Find(&hdata)
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										33
									
								
								models/user_heatmap_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								models/user_heatmap_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					// Copyright 2018 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.package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetUserHeatmapDataByUser(t *testing.T) {
 | 
				
			||||||
 | 
						// Prepare
 | 
				
			||||||
 | 
						assert.NoError(t, PrepareTestDatabase())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Insert some action
 | 
				
			||||||
 | 
						user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// get the action for comparison
 | 
				
			||||||
 | 
						actions, err := GetFeeds(GetFeedsOptions{
 | 
				
			||||||
 | 
							RequestedUser:    user,
 | 
				
			||||||
 | 
							RequestingUserID: user.ID,
 | 
				
			||||||
 | 
							IncludePrivate:   true,
 | 
				
			||||||
 | 
							OnlyPerformedBy:  false,
 | 
				
			||||||
 | 
							IncludeDeleted:   true,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get the heatmap and compare
 | 
				
			||||||
 | 
						heatmap, err := GetUserHeatmapDataByUser(user)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.Equal(t, len(actions), len(heatmap))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1218,6 +1218,7 @@ var Service struct {
 | 
				
			|||||||
	DefaultEnableDependencies               bool
 | 
						DefaultEnableDependencies               bool
 | 
				
			||||||
	DefaultAllowOnlyContributorsToTrackTime bool
 | 
						DefaultAllowOnlyContributorsToTrackTime bool
 | 
				
			||||||
	NoReplyAddress                          string
 | 
						NoReplyAddress                          string
 | 
				
			||||||
 | 
						EnableUserHeatmap                       bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// OpenID settings
 | 
						// OpenID settings
 | 
				
			||||||
	EnableOpenIDSignIn bool
 | 
						EnableOpenIDSignIn bool
 | 
				
			||||||
@@ -1249,6 +1250,7 @@ func newService() {
 | 
				
			|||||||
	Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true)
 | 
						Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true)
 | 
				
			||||||
	Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
 | 
						Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
 | 
				
			||||||
	Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply.example.org")
 | 
						Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply.example.org")
 | 
				
			||||||
 | 
						Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	sec = Cfg.Section("openid")
 | 
						sec = Cfg.Section("openid")
 | 
				
			||||||
	Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock)
 | 
						Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -320,6 +320,7 @@ starred = Starred Repositories
 | 
				
			|||||||
following = Following
 | 
					following = Following
 | 
				
			||||||
follow = Follow
 | 
					follow = Follow
 | 
				
			||||||
unfollow = Unfollow
 | 
					unfollow = Unfollow
 | 
				
			||||||
 | 
					heatmap.loading = Loading Heatmap…
 | 
				
			||||||
 | 
					
 | 
				
			||||||
form.name_reserved = The username '%s' is reserved.
 | 
					form.name_reserved = The username '%s' is reserved.
 | 
				
			||||||
form.name_pattern_not_allowed = The pattern '%s' is not allowed in a username.
 | 
					form.name_pattern_not_allowed = The pattern '%s' is not allowed in a username.
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -588,3 +588,20 @@ footer {
 | 
				
			|||||||
    border-bottom-width: 0 !important;
 | 
					    border-bottom-width: 0 !important;
 | 
				
			||||||
    margin-bottom: 2px !important;
 | 
					    margin-bottom: 2px !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#user-heatmap{
 | 
				
			||||||
 | 
					    width: 107%; // Fixes newest contributions not showing
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    margin: 40px 0 30px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    svg:not(:root) {
 | 
				
			||||||
 | 
					        overflow: inherit;
 | 
				
			||||||
 | 
					        padding: 0 !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @media only screen and (max-width: 1200px) {
 | 
				
			||||||
 | 
					        & {
 | 
				
			||||||
 | 
					            display: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -58,6 +58,10 @@
 | 
				
			|||||||
        .ui.repository.list {
 | 
					        .ui.repository.list {
 | 
				
			||||||
            margin-top: 25px;
 | 
					            margin-top: 25px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        #loading-heatmap{
 | 
				
			||||||
 | 
					            margin-bottom: 1em;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &.followers {
 | 
					    &.followers {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										9
									
								
								public/vendor/VERSIONS
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								public/vendor/VERSIONS
									
									
									
									
										vendored
									
									
								
							@@ -58,3 +58,12 @@ Version: 4.3.0
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
File(s): /vendor/assets/swagger-ui/
 | 
					File(s): /vendor/assets/swagger-ui/
 | 
				
			||||||
Version: 3.0.4
 | 
					Version: 3.0.4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					File(s): /vendor/plugins/d3/
 | 
				
			||||||
 | 
					Version: 4.13.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					File(s): /vendor/plugins/calendar-heatmap/
 | 
				
			||||||
 | 
					Version: 337b431
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					File(s): /vendor/plugins/moment/
 | 
				
			||||||
 | 
					Version: 2.22.2
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								public/vendor/librejs.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								public/vendor/librejs.html
									
									
									
									
										vendored
									
									
								
							@@ -135,6 +135,21 @@
 | 
				
			|||||||
          <td><a href="https://github.com/swagger-api/swagger-ui/blob/master/LICENSE">Apache-2.0</a></td>
 | 
					          <td><a href="https://github.com/swagger-api/swagger-ui/blob/master/LICENSE">Apache-2.0</a></td>
 | 
				
			||||||
          <td><a href="https://github.com/swagger-api/swagger-ui/archive/v3.0.4.tar.gz">swagger-ui-v3.0.4.tar.gz</a></td>
 | 
					          <td><a href="https://github.com/swagger-api/swagger-ui/archive/v3.0.4.tar.gz">swagger-ui-v3.0.4.tar.gz</a></td>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					          <td><a href="./plugins/d3/">d3</a></td>
 | 
				
			||||||
 | 
					          <td><a href="https://github.com/d3/d3/blob/master/LICENSE">BSD 3-Clause</a></td>
 | 
				
			||||||
 | 
					          <td><a href="https://github.com/d3/d3/releases/download/v4.13.0/d3.zip">d3.zip</a></td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					          <td><a href="./plugins/calendar-heatmap/">calendar-heatmap</a></td>
 | 
				
			||||||
 | 
					          <td><a href="https://github.com/DKirwan/calendar-heatmap/blob/master/LICENSE">MIT</a></td>
 | 
				
			||||||
 | 
					          <td><a href="https://github.com/DKirwan/calendar-heatmap/archive/master.zip">337b431.zip</a></td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					          <td><a href="./plugins/moment/">moment.js</a></td>
 | 
				
			||||||
 | 
					          <td><a href="https://github.com/moment/moment/blob/develop/LICENSE">MIT</a></td>
 | 
				
			||||||
 | 
					          <td><a href="https://github.com/moment/moment/archive/2.22.2.tar.gz">0.4.1.tar.gz</a></td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
      </tbody>
 | 
					      </tbody>
 | 
				
			||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
  </body>
 | 
					  </body>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										27
									
								
								public/vendor/plugins/calendar-heatmap/calendar-heatmap.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								public/vendor/plugins/calendar-heatmap/calendar-heatmap.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					text.month-name,
 | 
				
			||||||
 | 
					text.calendar-heatmap-legend-text,
 | 
				
			||||||
 | 
					text.day-initial {
 | 
				
			||||||
 | 
					  font-size: 10px;
 | 
				
			||||||
 | 
					  fill: inherit;
 | 
				
			||||||
 | 
					  font-family: Helvetica, arial, 'Open Sans', sans-serif;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					rect.day-cell:hover {
 | 
				
			||||||
 | 
					  stroke: #555555;
 | 
				
			||||||
 | 
					  stroke-width: 1px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.day-cell-tooltip {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  z-index: 9999;
 | 
				
			||||||
 | 
					  padding: 5px 9px;
 | 
				
			||||||
 | 
					  color: #bbbbbb;
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  background: rgba(0, 0, 0, 0.85);
 | 
				
			||||||
 | 
					  border-radius: 3px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.day-cell-tooltip > span {
 | 
				
			||||||
 | 
					  font-family: Helvetica, arial, 'Open Sans', sans-serif
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.calendar-heatmap {
 | 
				
			||||||
 | 
					  box-sizing: initial;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										311
									
								
								public/vendor/plugins/calendar-heatmap/calendar-heatmap.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								public/vendor/plugins/calendar-heatmap/calendar-heatmap.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,311 @@
 | 
				
			|||||||
 | 
					// https://github.com/DKirwan/calendar-heatmap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function calendarHeatmap() {
 | 
				
			||||||
 | 
					  // defaults
 | 
				
			||||||
 | 
					  var width = 750;
 | 
				
			||||||
 | 
					  var height = 110;
 | 
				
			||||||
 | 
					  var legendWidth = 150;
 | 
				
			||||||
 | 
					  var selector = 'body';
 | 
				
			||||||
 | 
					  var SQUARE_LENGTH = 11;
 | 
				
			||||||
 | 
					  var SQUARE_PADDING = 2;
 | 
				
			||||||
 | 
					  var MONTH_LABEL_PADDING = 6;
 | 
				
			||||||
 | 
					  var now = moment().endOf('day').toDate();
 | 
				
			||||||
 | 
					  var yearAgo = moment().startOf('day').subtract(1, 'year').toDate();
 | 
				
			||||||
 | 
					  var startDate = null;
 | 
				
			||||||
 | 
					  var counterMap= {};
 | 
				
			||||||
 | 
					  var data = [];
 | 
				
			||||||
 | 
					  var max = null;
 | 
				
			||||||
 | 
					  var colorRange = ['#D8E6E7', '#218380'];
 | 
				
			||||||
 | 
					  var tooltipEnabled = true;
 | 
				
			||||||
 | 
					  var tooltipUnit = 'contribution';
 | 
				
			||||||
 | 
					  var legendEnabled = true;
 | 
				
			||||||
 | 
					  var onClick = null;
 | 
				
			||||||
 | 
					  var weekStart = 1; //0 for Sunday, 1 for Monday
 | 
				
			||||||
 | 
					  var locale = {
 | 
				
			||||||
 | 
					    months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
 | 
				
			||||||
 | 
					    days: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
 | 
				
			||||||
 | 
					    No: 'No',
 | 
				
			||||||
 | 
					    on: 'on',
 | 
				
			||||||
 | 
					    Less: 'Less',
 | 
				
			||||||
 | 
					    More: 'More'
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  var v = Number(d3.version.split('.')[0]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // setters and getters
 | 
				
			||||||
 | 
					  chart.data = function (value) {
 | 
				
			||||||
 | 
					    if (!arguments.length) { return data; }
 | 
				
			||||||
 | 
					    data = value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    counterMap= {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    data.forEach(function (element, index) {
 | 
				
			||||||
 | 
					        var key= moment(element.date).format( 'YYYY-MM-DD' );
 | 
				
			||||||
 | 
					        var counter= counterMap[key] || 0;
 | 
				
			||||||
 | 
					        counterMap[key]= counter + element.count;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return chart;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  chart.max = function (value) {
 | 
				
			||||||
 | 
					    if (!arguments.length) { return max; }
 | 
				
			||||||
 | 
					    max = value;
 | 
				
			||||||
 | 
					    return chart;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  chart.selector = function (value) {
 | 
				
			||||||
 | 
					    if (!arguments.length) { return selector; }
 | 
				
			||||||
 | 
					    selector = value;
 | 
				
			||||||
 | 
					    return chart;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  chart.startDate = function (value) {
 | 
				
			||||||
 | 
					    if (!arguments.length) { return startDate; }
 | 
				
			||||||
 | 
					    yearAgo = value;
 | 
				
			||||||
 | 
					    now = moment(value).endOf('day').add(1, 'year').toDate();
 | 
				
			||||||
 | 
					    return chart;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  chart.colorRange = function (value) {
 | 
				
			||||||
 | 
					    if (!arguments.length) { return colorRange; }
 | 
				
			||||||
 | 
					    colorRange = value;
 | 
				
			||||||
 | 
					    return chart;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  chart.tooltipEnabled = function (value) {
 | 
				
			||||||
 | 
					    if (!arguments.length) { return tooltipEnabled; }
 | 
				
			||||||
 | 
					    tooltipEnabled = value;
 | 
				
			||||||
 | 
					    return chart;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  chart.tooltipUnit = function (value) {
 | 
				
			||||||
 | 
					    if (!arguments.length) { return tooltipUnit; }
 | 
				
			||||||
 | 
					    tooltipUnit = value;
 | 
				
			||||||
 | 
					    return chart;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  chart.legendEnabled = function (value) {
 | 
				
			||||||
 | 
					    if (!arguments.length) { return legendEnabled; }
 | 
				
			||||||
 | 
					    legendEnabled = value;
 | 
				
			||||||
 | 
					    return chart;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  chart.onClick = function (value) {
 | 
				
			||||||
 | 
					    if (!arguments.length) { return onClick(); }
 | 
				
			||||||
 | 
					    onClick = value;
 | 
				
			||||||
 | 
					    return chart;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  chart.locale = function (value) {
 | 
				
			||||||
 | 
					    if (!arguments.length) { return locale; }
 | 
				
			||||||
 | 
					    locale = value;
 | 
				
			||||||
 | 
					    return chart;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function chart() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    d3.select(chart.selector()).selectAll('svg.calendar-heatmap').remove(); // remove the existing chart, if it exists
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var dateRange = ((d3.time && d3.time.days) || d3.timeDays)(yearAgo, now); // generates an array of date objects within the specified range
 | 
				
			||||||
 | 
					    var monthRange = ((d3.time && d3.time.months) || d3.timeMonths)(moment(yearAgo).startOf('month').toDate(), now); // it ignores the first month if the 1st date is after the start of the month
 | 
				
			||||||
 | 
					    var firstDate = moment(dateRange[0]);
 | 
				
			||||||
 | 
					    if (chart.data().length == 0) {
 | 
				
			||||||
 | 
					      max = 0;
 | 
				
			||||||
 | 
					    } else if (max === null) {
 | 
				
			||||||
 | 
					      max = d3.max(chart.data(), function (d) { return d.count; }); // max data value
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // color range
 | 
				
			||||||
 | 
					    var color = ((d3.scale && d3.scale.linear) || d3.scaleLinear)()
 | 
				
			||||||
 | 
					      .range(chart.colorRange())
 | 
				
			||||||
 | 
					      .domain([0, max]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var tooltip;
 | 
				
			||||||
 | 
					    var dayRects;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    drawChart();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function drawChart() {
 | 
				
			||||||
 | 
					      var svg = d3.select(chart.selector())
 | 
				
			||||||
 | 
					        .style('position', 'relative')
 | 
				
			||||||
 | 
					        .append('svg')
 | 
				
			||||||
 | 
					        .attr('width', width)
 | 
				
			||||||
 | 
					        .attr('class', 'calendar-heatmap')
 | 
				
			||||||
 | 
					        .attr('height', height)
 | 
				
			||||||
 | 
					        .style('padding', '36px');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      dayRects = svg.selectAll('.day-cell')
 | 
				
			||||||
 | 
					        .data(dateRange);  //  array of days for the last yr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var enterSelection = dayRects.enter().append('rect')
 | 
				
			||||||
 | 
					        .attr('class', 'day-cell')
 | 
				
			||||||
 | 
					        .attr('width', SQUARE_LENGTH)
 | 
				
			||||||
 | 
					        .attr('height', SQUARE_LENGTH)
 | 
				
			||||||
 | 
					        .attr('fill', function(d) { return color(countForDate(d)); })
 | 
				
			||||||
 | 
					        .attr('x', function (d, i) {
 | 
				
			||||||
 | 
					          var cellDate = moment(d);
 | 
				
			||||||
 | 
					          var result = cellDate.week() - firstDate.week() + (firstDate.weeksInYear() * (cellDate.weekYear() - firstDate.weekYear()));
 | 
				
			||||||
 | 
					          return result * (SQUARE_LENGTH + SQUARE_PADDING);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .attr('y', function (d, i) {
 | 
				
			||||||
 | 
					          return MONTH_LABEL_PADDING + formatWeekday(d.getDay()) * (SQUARE_LENGTH + SQUARE_PADDING);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (typeof onClick === 'function') {
 | 
				
			||||||
 | 
					        (v === 3 ? enterSelection : enterSelection.merge(dayRects)).on('click', function(d) {
 | 
				
			||||||
 | 
					          var count = countForDate(d);
 | 
				
			||||||
 | 
					          onClick({ date: d, count: count});
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (chart.tooltipEnabled()) {
 | 
				
			||||||
 | 
					        (v === 3 ? enterSelection : enterSelection.merge(dayRects)).on('mouseover', function(d, i) {
 | 
				
			||||||
 | 
					          tooltip = d3.select(chart.selector())
 | 
				
			||||||
 | 
					            .append('div')
 | 
				
			||||||
 | 
					            .attr('class', 'day-cell-tooltip')
 | 
				
			||||||
 | 
					            .html(tooltipHTMLForDate(d))
 | 
				
			||||||
 | 
					            .style('left', function () { return Math.floor(i / 7) * SQUARE_LENGTH + 'px'; })
 | 
				
			||||||
 | 
					            .style('top', function () {
 | 
				
			||||||
 | 
					              return formatWeekday(d.getDay()) * (SQUARE_LENGTH + SQUARE_PADDING) + MONTH_LABEL_PADDING * 2 + 'px';
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .on('mouseout', function (d, i) {
 | 
				
			||||||
 | 
					          tooltip.remove();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (chart.legendEnabled()) {
 | 
				
			||||||
 | 
					        var colorRange = [color(0)];
 | 
				
			||||||
 | 
					        for (var i = 3; i > 0; i--) {
 | 
				
			||||||
 | 
					          colorRange.push(color(max / i));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var legendGroup = svg.append('g');
 | 
				
			||||||
 | 
					        legendGroup.selectAll('.calendar-heatmap-legend')
 | 
				
			||||||
 | 
					            .data(colorRange)
 | 
				
			||||||
 | 
					            .enter()
 | 
				
			||||||
 | 
					          .append('rect')
 | 
				
			||||||
 | 
					            .attr('class', 'calendar-heatmap-legend')
 | 
				
			||||||
 | 
					            .attr('width', SQUARE_LENGTH)
 | 
				
			||||||
 | 
					            .attr('height', SQUARE_LENGTH)
 | 
				
			||||||
 | 
					            .attr('x', function (d, i) { return (width - legendWidth) + (i + 1) * 13; })
 | 
				
			||||||
 | 
					            .attr('y', height + SQUARE_PADDING)
 | 
				
			||||||
 | 
					            .attr('fill', function (d) { return d; });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        legendGroup.append('text')
 | 
				
			||||||
 | 
					          .attr('class', 'calendar-heatmap-legend-text calendar-heatmap-legend-text-less')
 | 
				
			||||||
 | 
					          .attr('x', width - legendWidth - 13)
 | 
				
			||||||
 | 
					          .attr('y', height + SQUARE_LENGTH)
 | 
				
			||||||
 | 
					          .text(locale.Less);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        legendGroup.append('text')
 | 
				
			||||||
 | 
					          .attr('class', 'calendar-heatmap-legend-text calendar-heatmap-legend-text-more')
 | 
				
			||||||
 | 
					          .attr('x', (width - legendWidth + SQUARE_PADDING) + (colorRange.length + 1) * 13)
 | 
				
			||||||
 | 
					          .attr('y', height + SQUARE_LENGTH)
 | 
				
			||||||
 | 
					          .text(locale.More);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      dayRects.exit().remove();
 | 
				
			||||||
 | 
					      var monthLabels = svg.selectAll('.month')
 | 
				
			||||||
 | 
					          .data(monthRange)
 | 
				
			||||||
 | 
					          .enter().append('text')
 | 
				
			||||||
 | 
					          .attr('class', 'month-name')
 | 
				
			||||||
 | 
					          .text(function (d) {
 | 
				
			||||||
 | 
					            return locale.months[d.getMonth()];
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          .attr('x', function (d, i) {
 | 
				
			||||||
 | 
					            var matchIndex = 0;
 | 
				
			||||||
 | 
					            dateRange.find(function (element, index) {
 | 
				
			||||||
 | 
					              matchIndex = index;
 | 
				
			||||||
 | 
					              return moment(d).isSame(element, 'month') && moment(d).isSame(element, 'year');
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return Math.floor(matchIndex / 7) * (SQUARE_LENGTH + SQUARE_PADDING);
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          .attr('y', 0);  // fix these to the top
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      locale.days.forEach(function (day, index) {
 | 
				
			||||||
 | 
					        index = formatWeekday(index);
 | 
				
			||||||
 | 
					        if (index % 2) {
 | 
				
			||||||
 | 
					          svg.append('text')
 | 
				
			||||||
 | 
					            .attr('class', 'day-initial')
 | 
				
			||||||
 | 
					            .attr('transform', 'translate(-8,' + (SQUARE_LENGTH + SQUARE_PADDING) * (index + 1) + ')')
 | 
				
			||||||
 | 
					            .style('text-anchor', 'middle')
 | 
				
			||||||
 | 
					            .attr('dy', '2')
 | 
				
			||||||
 | 
					            .text(day);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function pluralizedTooltipUnit (count) {
 | 
				
			||||||
 | 
					      if ('string' === typeof tooltipUnit) {
 | 
				
			||||||
 | 
					        return (tooltipUnit + (count === 1 ? '' : 's'));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      for (var i in tooltipUnit) {
 | 
				
			||||||
 | 
					        var _rule = tooltipUnit[i];
 | 
				
			||||||
 | 
					        var _min = _rule.min;
 | 
				
			||||||
 | 
					        var _max = _rule.max || _rule.min;
 | 
				
			||||||
 | 
					        _max = _max === 'Infinity' ? Infinity : _max;
 | 
				
			||||||
 | 
					        if (count >= _min && count <= _max) {
 | 
				
			||||||
 | 
					          return _rule.unit;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function tooltipHTMLForDate(d) {
 | 
				
			||||||
 | 
					      var dateStr = moment(d).format('ddd, MMM Do YYYY');
 | 
				
			||||||
 | 
					      var count = countForDate(d);
 | 
				
			||||||
 | 
					      return '<span><strong>' + (count ? count : locale.No) + ' ' + pluralizedTooltipUnit(count) + '</strong> ' + locale.on + ' ' + dateStr + '</span>';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function countForDate(d) {
 | 
				
			||||||
 | 
					        var key= moment(d).format( 'YYYY-MM-DD' );
 | 
				
			||||||
 | 
					        return counterMap[key] || 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function formatWeekday(weekDay) {
 | 
				
			||||||
 | 
					      if (weekStart === 1) {
 | 
				
			||||||
 | 
					        if (weekDay === 0) {
 | 
				
			||||||
 | 
					          return 6;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          return weekDay - 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return weekDay;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var daysOfChart = chart.data().map(function (day) {
 | 
				
			||||||
 | 
					      return day.date.toDateString();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return chart;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// polyfill for Array.find() method
 | 
				
			||||||
 | 
					/* jshint ignore:start */
 | 
				
			||||||
 | 
					if (!Array.prototype.find) {
 | 
				
			||||||
 | 
					  Array.prototype.find = function (predicate) {
 | 
				
			||||||
 | 
					    if (this === null) {
 | 
				
			||||||
 | 
					      throw new TypeError('Array.prototype.find called on null or undefined');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (typeof predicate !== 'function') {
 | 
				
			||||||
 | 
					      throw new TypeError('predicate must be a function');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var list = Object(this);
 | 
				
			||||||
 | 
					    var length = list.length >>> 0;
 | 
				
			||||||
 | 
					    var thisArg = arguments[1];
 | 
				
			||||||
 | 
					    var value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (var i = 0; i < length; i++) {
 | 
				
			||||||
 | 
					      value = list[i];
 | 
				
			||||||
 | 
					      if (predicate.call(thisArg, value, i, list)) {
 | 
				
			||||||
 | 
					        return value;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/* jshint ignore:end */
 | 
				
			||||||
							
								
								
									
										2
									
								
								public/vendor/plugins/d3/d3.v4.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								public/vendor/plugins/d3/d3.v4.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								public/vendor/plugins/moment/moment.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/vendor/plugins/moment/moment.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -324,6 +324,13 @@ func mustEnableIssuesOrPulls(ctx *context.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func mustEnableUserHeatmap(ctx *context.Context) {
 | 
				
			||||||
 | 
						if !setting.Service.EnableUserHeatmap {
 | 
				
			||||||
 | 
							ctx.Status(404)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RegisterRoutes registers all v1 APIs routes to web application.
 | 
					// RegisterRoutes registers all v1 APIs routes to web application.
 | 
				
			||||||
// FIXME: custom form error response
 | 
					// FIXME: custom form error response
 | 
				
			||||||
func RegisterRoutes(m *macaron.Macaron) {
 | 
					func RegisterRoutes(m *macaron.Macaron) {
 | 
				
			||||||
@@ -348,6 +355,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			m.Group("/:username", func() {
 | 
								m.Group("/:username", func() {
 | 
				
			||||||
				m.Get("", user.GetInfo)
 | 
									m.Get("", user.GetInfo)
 | 
				
			||||||
 | 
									m.Get("/heatmap", mustEnableUserHeatmap, user.GetUserHeatmapData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				m.Get("/repos", user.ListUserRepos)
 | 
									m.Get("/repos", user.ListUserRepos)
 | 
				
			||||||
				m.Group("/tokens", func() {
 | 
									m.Group("/tokens", func() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
package swagger
 | 
					package swagger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	api "code.gitea.io/sdk/gitea"
 | 
						api "code.gitea.io/sdk/gitea"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -34,3 +35,10 @@ type swaggerModelEditUserOption struct {
 | 
				
			|||||||
	// in:body
 | 
						// in:body
 | 
				
			||||||
	Options api.EditUserOption
 | 
						Options api.EditUserOption
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UserHeatmapData
 | 
				
			||||||
 | 
					// swagger:response UserHeatmapData
 | 
				
			||||||
 | 
					type swaggerResponseUserHeatmapData struct {
 | 
				
			||||||
 | 
						// in:body
 | 
				
			||||||
 | 
						Body []models.UserHeatmapData `json:"body"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
package user
 | 
					package user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
@@ -133,3 +134,41 @@ func GetAuthenticatedUser(ctx *context.APIContext) {
 | 
				
			|||||||
	//     "$ref": "#/responses/User"
 | 
						//     "$ref": "#/responses/User"
 | 
				
			||||||
	ctx.JSON(200, ctx.User.APIFormat())
 | 
						ctx.JSON(200, ctx.User.APIFormat())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetUserHeatmapData is the handler to get a users heatmap
 | 
				
			||||||
 | 
					func GetUserHeatmapData(ctx *context.APIContext) {
 | 
				
			||||||
 | 
						// swagger:operation GET /users/{username}/heatmap user userGetHeatmapData
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Get a user's heatmap
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: username
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: username of user to get
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "200":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/UserHeatmapData"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get the user to throw an error if it does not exist
 | 
				
			||||||
 | 
						user, err := models.GetUserByName(ctx.Params(":username"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if models.IsErrUserNotExist(err) {
 | 
				
			||||||
 | 
								ctx.Status(http.StatusNotFound)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						heatmap, err := models.GetUserHeatmapDataByUser(user)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "GetUserHeatmapDataByUser", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ctx.JSON(200, heatmap)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -99,6 +99,8 @@ func Dashboard(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["PageIsDashboard"] = true
 | 
						ctx.Data["PageIsDashboard"] = true
 | 
				
			||||||
	ctx.Data["PageIsNews"] = true
 | 
						ctx.Data["PageIsNews"] = true
 | 
				
			||||||
	ctx.Data["SearchLimit"] = setting.UI.User.RepoPagingNum
 | 
						ctx.Data["SearchLimit"] = setting.UI.User.RepoPagingNum
 | 
				
			||||||
 | 
						ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap
 | 
				
			||||||
 | 
						ctx.Data["HeatmapUser"] = ctxUser.Name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	var mirrors []*models.Repository
 | 
						var mirrors []*models.Repository
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -87,6 +87,8 @@ func Profile(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["PageIsUserProfile"] = true
 | 
						ctx.Data["PageIsUserProfile"] = true
 | 
				
			||||||
	ctx.Data["Owner"] = ctxUser
 | 
						ctx.Data["Owner"] = ctxUser
 | 
				
			||||||
	ctx.Data["OpenIDs"] = openIDs
 | 
						ctx.Data["OpenIDs"] = openIDs
 | 
				
			||||||
 | 
						ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap
 | 
				
			||||||
 | 
						ctx.Data["HeatmapUser"] = ctxUser.Name
 | 
				
			||||||
	showPrivate := ctx.IsSigned && (ctx.User.IsAdmin || ctx.User.ID == ctxUser.ID)
 | 
						showPrivate := ctx.IsSigned && (ctx.User.IsAdmin || ctx.User.ID == ctxUser.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	orgs, err := models.GetOrgsByUserID(ctxUser.ID, showPrivate)
 | 
						orgs, err := models.GetOrgsByUserID(ctxUser.ID, showPrivate)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,6 +49,28 @@
 | 
				
			|||||||
		<script src="https://www.google.com/recaptcha/api.js" async></script>
 | 
							<script src="https://www.google.com/recaptcha/api.js" async></script>
 | 
				
			||||||
	{{end}}
 | 
						{{end}}
 | 
				
			||||||
{{end}}
 | 
					{{end}}
 | 
				
			||||||
 | 
					{{if .EnableHeatmap}}
 | 
				
			||||||
 | 
						<script src="{{AppSubUrl}}/vendor/plugins/moment/moment.min.js" charset="utf-8"></script>
 | 
				
			||||||
 | 
						<script src="{{AppSubUrl}}/vendor/plugins/d3/d3.v4.min.js" charset="utf-8"></script>
 | 
				
			||||||
 | 
						<script src="{{AppSubUrl}}/vendor/plugins/calendar-heatmap/calendar-heatmap.js" charset="utf-8"></script>
 | 
				
			||||||
 | 
						<script type="text/javascript">
 | 
				
			||||||
 | 
							$.get( '{{AppSubUrl}}/api/v1/users/{{.HeatmapUser}}/heatmap', function( chartRawData ) {
 | 
				
			||||||
 | 
								var chartData = [];
 | 
				
			||||||
 | 
								for (var i = 0; i < chartRawData.length; i++) {
 | 
				
			||||||
 | 
									chartData[i] = {date: new Date(chartRawData[i].timestamp * 1000), count: chartRawData[i].contributions};
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								$('#loading-heatmap').removeClass('active');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var heatmap = calendarHeatmap()
 | 
				
			||||||
 | 
									.data(chartData)
 | 
				
			||||||
 | 
									.selector('#user-heatmap')
 | 
				
			||||||
 | 
									.colorRange(['#f4f4f4', '#459928'])
 | 
				
			||||||
 | 
									.tooltipEnabled(true);
 | 
				
			||||||
 | 
								heatmap();
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						</script>
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
{{if .RequireTribute}}
 | 
					{{if .RequireTribute}}
 | 
				
			||||||
	<script src="{{AppSubUrl}}/vendor/plugins/tribute/tribute.min.js"></script>
 | 
						<script src="{{AppSubUrl}}/vendor/plugins/tribute/tribute.min.js"></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -100,6 +100,9 @@
 | 
				
			|||||||
{{end}}
 | 
					{{end}}
 | 
				
			||||||
{{if .RequireDropzone}}
 | 
					{{if .RequireDropzone}}
 | 
				
			||||||
	<link rel="stylesheet" href="{{AppSubUrl}}/vendor/plugins/dropzone/dropzone.css">
 | 
						<link rel="stylesheet" href="{{AppSubUrl}}/vendor/plugins/dropzone/dropzone.css">
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
 | 
					{{if .EnableHeatmap}}
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="{{AppSubUrl}}/vendor/plugins/calendar-heatmap/calendar-heatmap.css">
 | 
				
			||||||
{{end}}
 | 
					{{end}}
 | 
				
			||||||
	<style class="list-search-style"></style>
 | 
						<style class="list-search-style"></style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5494,6 +5494,35 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "/users/{username}/heatmap": {
 | 
				
			||||||
 | 
					      "get": {
 | 
				
			||||||
 | 
					        "produces": [
 | 
				
			||||||
 | 
					          "application/json"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "user"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "summary": "Get a user's heatmap",
 | 
				
			||||||
 | 
					        "operationId": "userGetHeatmapData",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "description": "username of user to get",
 | 
				
			||||||
 | 
					            "name": "username",
 | 
				
			||||||
 | 
					            "in": "path",
 | 
				
			||||||
 | 
					            "required": true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "200": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/UserHeatmapData"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "404": {
 | 
				
			||||||
 | 
					            "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "/users/{username}/keys": {
 | 
					    "/users/{username}/keys": {
 | 
				
			||||||
      "get": {
 | 
					      "get": {
 | 
				
			||||||
        "produces": [
 | 
					        "produces": [
 | 
				
			||||||
@@ -7666,6 +7695,12 @@
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
 | 
					      "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "TimeStamp": {
 | 
				
			||||||
 | 
					      "description": "TimeStamp defines a timestamp",
 | 
				
			||||||
 | 
					      "type": "integer",
 | 
				
			||||||
 | 
					      "format": "int64",
 | 
				
			||||||
 | 
					      "x-go-package": "code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "TrackedTime": {
 | 
					    "TrackedTime": {
 | 
				
			||||||
      "description": "TrackedTime worked time for an issue / pr",
 | 
					      "description": "TrackedTime worked time for an issue / pr",
 | 
				
			||||||
      "type": "object",
 | 
					      "type": "object",
 | 
				
			||||||
@@ -7737,6 +7772,21 @@
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
 | 
					      "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "UserHeatmapData": {
 | 
				
			||||||
 | 
					      "description": "UserHeatmapData represents the data needed to create a heatmap",
 | 
				
			||||||
 | 
					      "type": "object",
 | 
				
			||||||
 | 
					      "properties": {
 | 
				
			||||||
 | 
					        "contributions": {
 | 
				
			||||||
 | 
					          "type": "integer",
 | 
				
			||||||
 | 
					          "format": "int64",
 | 
				
			||||||
 | 
					          "x-go-name": "Contributions"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "timestamp": {
 | 
				
			||||||
 | 
					          "$ref": "#/definitions/TimeStamp"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "x-go-package": "code.gitea.io/gitea/models"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "WatchInfo": {
 | 
					    "WatchInfo": {
 | 
				
			||||||
      "description": "WatchInfo represents an API watch status of one repository",
 | 
					      "description": "WatchInfo represents an API watch status of one repository",
 | 
				
			||||||
      "type": "object",
 | 
					      "type": "object",
 | 
				
			||||||
@@ -8083,6 +8133,15 @@
 | 
				
			|||||||
        "$ref": "#/definitions/User"
 | 
					        "$ref": "#/definitions/User"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "UserHeatmapData": {
 | 
				
			||||||
 | 
					      "description": "UserHeatmapData",
 | 
				
			||||||
 | 
					      "schema": {
 | 
				
			||||||
 | 
					        "type": "array",
 | 
				
			||||||
 | 
					        "items": {
 | 
				
			||||||
 | 
					          "$ref": "#/definitions/UserHeatmapData"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "UserList": {
 | 
					    "UserList": {
 | 
				
			||||||
      "description": "UserList",
 | 
					      "description": "UserList",
 | 
				
			||||||
      "schema": {
 | 
					      "schema": {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,11 @@
 | 
				
			|||||||
		{{template "base/alert" .}}
 | 
							{{template "base/alert" .}}
 | 
				
			||||||
		<div class="ui mobile reversed stackable grid">
 | 
							<div class="ui mobile reversed stackable grid">
 | 
				
			||||||
			<div class="ten wide column">
 | 
								<div class="ten wide column">
 | 
				
			||||||
 | 
									{{if .EnableHeatmap}}
 | 
				
			||||||
 | 
										<div class="ui active centered inline indeterminate text loader" id="loading-heatmap">{{.i18n.Tr "user.heatmap.loading"}}</div>
 | 
				
			||||||
 | 
										<div id="user-heatmap"></div>
 | 
				
			||||||
 | 
										<div class="ui divider"></div>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
				{{template "user/dashboard/feeds" .}}
 | 
									{{template "user/dashboard/feeds" .}}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div id="app" class="six wide column">
 | 
								<div id="app" class="six wide column">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -95,6 +95,11 @@
 | 
				
			|||||||
				</div>
 | 
									</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				{{if eq .TabName "activity"}}
 | 
									{{if eq .TabName "activity"}}
 | 
				
			||||||
 | 
										{{if .EnableHeatmap}}
 | 
				
			||||||
 | 
											<div class="ui active centered inline indeterminate text loader" id="loading-heatmap">{{.i18n.Tr "user.heatmap.loading"}}</div>
 | 
				
			||||||
 | 
											<div id="user-heatmap"></div>
 | 
				
			||||||
 | 
											<div class="ui divider"></div>
 | 
				
			||||||
 | 
										{{end}}
 | 
				
			||||||
					<div class="feeds">
 | 
										<div class="feeds">
 | 
				
			||||||
						{{template "user/dashboard/feeds" .}}
 | 
											{{template "user/dashboard/feeds" .}}
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user