aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--integrations/privateactivity_test.go414
-rw-r--r--models/action.go6
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v141.go22
-rw-r--r--models/user.go5
-rw-r--r--models/user_heatmap.go5
-rw-r--r--modules/auth/user_form.go17
-rw-r--r--options/locale/locale_en-US.ini4
-rw-r--r--routers/user/home.go4
-rw-r--r--routers/user/profile.go4
-rw-r--r--routers/user/setting/profile.go1
-rw-r--r--templates/user/profile.tmpl13
-rw-r--r--templates/user/settings/profile.tmpl7
13 files changed, 488 insertions, 16 deletions
diff --git a/integrations/privateactivity_test.go b/integrations/privateactivity_test.go
new file mode 100644
index 0000000000..e9beb7c116
--- /dev/null
+++ b/integrations/privateactivity_test.go
@@ -0,0 +1,414 @@
+// Copyright 2020 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 integrations
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+const privateActivityTestAdmin = "user1"
+const privateActivityTestUser = "user2"
+
+// user3 is an organization so it is not usable here
+const privateActivityTestOtherUser = "user4"
+
+// activity helpers
+
+func testPrivateActivityDoSomethingForActionEntries(t *testing.T) {
+ repoBefore := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
+ owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repoBefore.OwnerID}).(*models.User)
+
+ session := loginUser(t, privateActivityTestUser)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all&token=%s", owner.Name, repoBefore.Name, token)
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
+ Body: "test",
+ Title: "test",
+ })
+ session.MakeRequest(t, req, http.StatusCreated)
+}
+
+// private activity helpers
+
+func testPrivateActivityHelperEnablePrivateActivity(t *testing.T) {
+ session := loginUser(t, privateActivityTestUser)
+ req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
+ "_csrf": GetCSRF(t, session, "/user/settings"),
+ "name": privateActivityTestUser,
+ "email": privateActivityTestUser + "@example.com",
+ "language": "en-us",
+ "keep_activity_private": "1",
+ })
+ session.MakeRequest(t, req, http.StatusFound)
+}
+
+func testPrivateActivityHelperHasVisibleActivitiesInHTMLDoc(htmlDoc *HTMLDoc) bool {
+ return htmlDoc.doc.Find(".feeds").Find(".news").Length() > 0
+}
+
+func testPrivateActivityHelperHasVisibleActivitiesFromSession(t *testing.T, session *TestSession) bool {
+ req := NewRequestf(t, "GET", "/%s?tab=activity", privateActivityTestUser)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ return testPrivateActivityHelperHasVisibleActivitiesInHTMLDoc(htmlDoc)
+}
+
+func testPrivateActivityHelperHasVisibleActivitiesFromPublic(t *testing.T) bool {
+ req := NewRequestf(t, "GET", "/%s?tab=activity", privateActivityTestUser)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ return testPrivateActivityHelperHasVisibleActivitiesInHTMLDoc(htmlDoc)
+}
+
+// heatmap UI helpers
+
+func testPrivateActivityHelperHasVisibleHeatmapInHTMLDoc(htmlDoc *HTMLDoc) bool {
+ return htmlDoc.doc.Find("#user-heatmap").Length() > 0
+}
+
+func testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t *testing.T, session *TestSession) bool {
+ req := NewRequestf(t, "GET", "/%s?tab=activity", privateActivityTestUser)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ return testPrivateActivityHelperHasVisibleHeatmapInHTMLDoc(htmlDoc)
+}
+
+func testPrivateActivityHelperHasVisibleDashboardHeatmapFromSession(t *testing.T, session *TestSession) bool {
+ req := NewRequest(t, "GET", "/")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ return testPrivateActivityHelperHasVisibleHeatmapInHTMLDoc(htmlDoc)
+}
+
+func testPrivateActivityHelperHasVisibleHeatmapFromPublic(t *testing.T) bool {
+ req := NewRequestf(t, "GET", "/%s?tab=activity", privateActivityTestUser)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ return testPrivateActivityHelperHasVisibleHeatmapInHTMLDoc(htmlDoc)
+}
+
+// heatmap API helpers
+
+func testPrivateActivityHelperHasHeatmapContentFromPublic(t *testing.T) bool {
+ req := NewRequestf(t, "GET", "/api/v1/users/%s/heatmap", privateActivityTestUser)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var items []*models.UserHeatmapData
+ DecodeJSON(t, resp, &items)
+
+ return len(items) != 0
+}
+
+func testPrivateActivityHelperHasHeatmapContentFromSession(t *testing.T, session *TestSession) bool {
+ token := getTokenForLoggedInUser(t, session)
+
+ req := NewRequestf(t, "GET", "/api/v1/users/%s/heatmap?token=%s", privateActivityTestUser, token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var items []*models.UserHeatmapData
+ DecodeJSON(t, resp, &items)
+
+ return len(items) != 0
+}
+
+// check activity visibility if the visibility is enabled
+
+func TestPrivateActivityNoVisibleForPublic(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromPublic(t)
+
+ assert.True(t, visible, "user should have visible activities")
+}
+
+func TestPrivateActivityNoVisibleForUserItself(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible activities")
+}
+
+func TestPrivateActivityNoVisibleForOtherUser(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestOtherUser)
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible activities")
+}
+
+func TestPrivateActivityNoVisibleForAdmin(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestAdmin)
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible activities")
+}
+
+// check activity visibility if the visibility is disabled
+
+func TestPrivateActivityYesInvisibleForPublic(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromPublic(t)
+
+ assert.False(t, visible, "user should have no visible activities")
+}
+
+func TestPrivateActivityYesVisibleForUserItself(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible activities")
+}
+
+func TestPrivateActivityYesInvisibleForOtherUser(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestOtherUser)
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+ assert.False(t, visible, "user should have no visible activities")
+}
+
+func TestPrivateActivityYesVisibleForAdmin(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestAdmin)
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible activities")
+}
+
+// check heatmap visibility if the visibility is enabled
+
+func TestPrivateActivityNoHeatmapVisibleForPublic(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ visible := testPrivateActivityHelperHasVisibleHeatmapFromPublic(t)
+
+ assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityNoHeatmapVisibleForUserItselfAtProfile(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityNoHeatmapVisibleForUserItselfAtDashboard(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ visible := testPrivateActivityHelperHasVisibleDashboardHeatmapFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityNoHeatmapVisibleForOtherUser(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestOtherUser)
+ visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityNoHeatmapVisibleForAdmin(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestAdmin)
+ visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible heatmap")
+}
+
+// check heatmap visibility if the visibility is disabled
+// this behavior, in special the one for the admin, is
+// due to the fact that the heatmap is the same for all viewers;
+// otherwise, there is no reason for it
+
+func TestPrivateActivityYesHeatmapInvisibleForPublic(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ visible := testPrivateActivityHelperHasVisibleHeatmapFromPublic(t)
+
+ assert.False(t, visible, "user should have no visible heatmap")
+}
+
+func TestPrivateActivityYesHeatmapInvisibleForUserItselfAtProfile(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+ assert.False(t, visible, "user should have no visible heatmap")
+}
+
+func TestPrivateActivityYesHeatmapInvisibleForUserItselfAtDashboard(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ visible := testPrivateActivityHelperHasVisibleDashboardHeatmapFromSession(t, session)
+
+ assert.False(t, visible, "user should have no visible heatmap")
+}
+
+func TestPrivateActivityYesHeatmapInvisibleForOtherUser(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestOtherUser)
+ visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+ assert.False(t, visible, "user should have no visible heatmap")
+}
+
+func TestPrivateActivityYesHeatmapInvsisibleForAdmin(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestAdmin)
+ visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+ assert.False(t, visible, "user should have no visible heatmap")
+}
+
+// check heatmap api provides content if the visibility is enabled
+
+func TestPrivateActivityNoHeatmapHasContentForPublic(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromPublic(t)
+
+ assert.True(t, hasContent, "user should have heatmap content")
+}
+
+func TestPrivateActivityNoHeatmapHasContentForUserItself(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+ assert.True(t, hasContent, "user should have heatmap content")
+}
+
+func TestPrivateActivityNoHeatmapHasContentForOtherUser(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestOtherUser)
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+ assert.True(t, hasContent, "user should have heatmap content")
+}
+
+func TestPrivateActivityNoHeatmapHasContentForAdmin(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestAdmin)
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+ assert.True(t, hasContent, "user should have heatmap content")
+}
+
+// check heatmap api provides no content if the visibility is disabled
+// this should be equal to the hidden heatmap at the UI
+
+func TestPrivateActivityYesHeatmapHasNoContentForPublic(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromPublic(t)
+
+ assert.False(t, hasContent, "user should have no heatmap content")
+}
+
+func TestPrivateActivityYesHeatmapHasNoContentForUserItself(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+ assert.False(t, hasContent, "user should have no heatmap content")
+}
+
+func TestPrivateActivityYesHeatmapHasNoContentForOtherUser(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestOtherUser)
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+ assert.False(t, hasContent, "user should have no heatmap content")
+}
+
+func TestPrivateActivityYesHeatmapHasNoContentForAdmin(t *testing.T) {
+ defer prepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestAdmin)
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+ assert.False(t, hasContent, "user should have no heatmap content")
+}
diff --git a/models/action.go b/models/action.go
index fd49c6d4ed..59ccdb2d4c 100644
--- a/models/action.go
+++ b/models/action.go
@@ -319,6 +319,12 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
cond = cond.And(builder.In("repo_id", AccessibleRepoIDsQuery(opts.Actor)))
}
+ if opts.Actor == nil || !opts.Actor.IsAdmin {
+ if opts.RequestedUser.KeepActivityPrivate && actorID != opts.RequestedUser.ID {
+ return make([]*Action, 0), nil
+ }
+ }
+
cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})
if opts.OnlyPerformedBy {
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 869661aee4..432bcffb1b 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -214,6 +214,8 @@ var migrations = []Migration{
NewMigration("prepend refs/heads/ to issue refs", prependRefsHeadsToIssueRefs),
// v140 -> v141
NewMigration("Save detected language file size to database instead of percent", fixLanguageStatsToSaveSize),
+ // v141 -> 142
+ NewMigration("Add KeepActivityPrivate to User table", addKeepActivityPrivateUserColumn),
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v141.go b/models/migrations/v141.go
new file mode 100644
index 0000000000..b5824ecd48
--- /dev/null
+++ b/models/migrations/v141.go
@@ -0,0 +1,22 @@
+// Copyright 2020 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 migrations
+
+import (
+ "fmt"
+
+ "xorm.io/xorm"
+)
+
+func addKeepActivityPrivateUserColumn(x *xorm.Engine) error {
+ type User struct {
+ KeepActivityPrivate bool
+ }
+
+ if err := x.Sync2(new(User)); err != nil {
+ return fmt.Errorf("Sync2: %v", err)
+ }
+ return nil
+}
diff --git a/models/user.go b/models/user.go
index 8875840db7..0ecb1b9a48 100644
--- a/models/user.go
+++ b/models/user.go
@@ -163,8 +163,9 @@ type User struct {
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
// Preferences
- DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
- Theme string `xorm:"NOT NULL DEFAULT ''"`
+ DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
+ Theme string `xorm:"NOT NULL DEFAULT ''"`
+ KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`
}
// SearchOrganizationsOptions options to filter organizations
diff --git a/models/user_heatmap.go b/models/user_heatmap.go
index 3d9e0683fc..ce3ec029ca 100644
--- a/models/user_heatmap.go
+++ b/models/user_heatmap.go
@@ -18,6 +18,11 @@ type UserHeatmapData struct {
// GetUserHeatmapDataByUser returns an array of UserHeatmapData
func GetUserHeatmapDataByUser(user *User) ([]*UserHeatmapData, error) {
hdata := make([]*UserHeatmapData, 0)
+
+ if user.KeepActivityPrivate {
+ return hdata, nil
+ }
+
var groupBy string
var groupByName = "timestamp" // We need this extra case because mssql doesn't allow grouping by alias
switch {
diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go
index 0c191fbc07..999d4cd74d 100644
--- a/modules/auth/user_form.go
+++ b/modules/auth/user_form.go
@@ -196,14 +196,15 @@ func (f *AccessTokenForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
// UpdateProfileForm form for updating profile
type UpdateProfileForm struct {
- Name string `binding:"AlphaDashDot;MaxSize(40)"`
- FullName string `binding:"MaxSize(100)"`
- Email string `binding:"Required;Email;MaxSize(254)"`
- KeepEmailPrivate bool
- Website string `binding:"ValidUrl;MaxSize(255)"`
- Location string `binding:"MaxSize(50)"`
- Language string `binding:"Size(5)"`
- Description string `binding:"MaxSize(255)"`
+ Name string `binding:"AlphaDashDot;MaxSize(40)"`
+ FullName string `binding:"MaxSize(100)"`
+ Email string `binding:"Required;Email;MaxSize(254)"`
+ KeepEmailPrivate bool
+ Website string `binding:"ValidUrl;MaxSize(255)"`
+ Location string `binding:"MaxSize(50)"`
+ Language string `binding:"Size(5)"`
+ Description string `binding:"MaxSize(255)"`
+ KeepActivityPrivate bool
}
// Validate validates the fields
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index f2e58b95b8..6227ceb2a2 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -392,6 +392,7 @@ follow = Follow
unfollow = Unfollow
heatmap.loading = Loading Heatmap…
user_bio = Biography
+disabled_public_activity = This user has disabled the public visibility of the activity.
form.name_reserved = The username '%s' is reserved.
form.name_pattern_not_allowed = The pattern '%s' is not allowed in a username.
@@ -430,6 +431,9 @@ continue = Continue
cancel = Cancel
language = Language
ui = Theme
+privacy = Privacy
+keep_activity_private = Hide the activity from the profile page
+keep_activity_private_popup = Makes the activity visible only for you and the admins
lookup_avatar_by_mail = Look Up Avatar by Email Address
federated_avatar_lookup = Federated Avatar Lookup
diff --git a/routers/user/home.go b/routers/user/home.go
index 2fc0c60aad..4e5fc3e4df 100644
--- a/routers/user/home.go
+++ b/routers/user/home.go
@@ -112,7 +112,9 @@ func Dashboard(ctx *context.Context) {
ctx.Data["PageIsDashboard"] = true
ctx.Data["PageIsNews"] = true
ctx.Data["SearchLimit"] = setting.UI.User.RepoPagingNum
- ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap
+ // no heatmap access for admins; GetUserHeatmapDataByUser ignores the calling user
+ // so everyone would get the same empty heatmap
+ ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap && !ctxUser.KeepActivityPrivate
ctx.Data["HeatmapUser"] = ctxUser.Name
var err error
diff --git a/routers/user/profile.go b/routers/user/profile.go
index 215dff0084..82fab4ad87 100644
--- a/routers/user/profile.go
+++ b/routers/user/profile.go
@@ -93,7 +93,9 @@ func Profile(ctx *context.Context) {
ctx.Data["PageIsUserProfile"] = true
ctx.Data["Owner"] = ctxUser
ctx.Data["OpenIDs"] = openIDs
- ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap
+ // no heatmap access for admins; GetUserHeatmapDataByUser ignores the calling user
+ // so everyone would get the same empty heatmap
+ ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap && !ctxUser.KeepActivityPrivate
ctx.Data["HeatmapUser"] = ctxUser.Name
showPrivate := ctx.IsSigned && (ctx.User.IsAdmin || ctx.User.ID == ctxUser.ID)
diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go
index d6f25f9135..ba9ba2b257 100644
--- a/routers/user/setting/profile.go
+++ b/routers/user/setting/profile.go
@@ -96,6 +96,7 @@ func ProfilePost(ctx *context.Context, form auth.UpdateProfileForm) {
ctx.User.Location = form.Location
ctx.User.Language = form.Language
ctx.User.Description = form.Description
+ ctx.User.KeepActivityPrivate = form.KeepActivityPrivate
if err := models.UpdateUserSetting(ctx.User); err != nil {
if _, ok := err.(models.ErrEmailAlreadyUsed); ok {
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl
index e07b4b0dd8..563bc78307 100644
--- a/templates/user/profile.tmpl
+++ b/templates/user/profile.tmpl
@@ -104,10 +104,15 @@
</div>
{{if eq .TabName "activity"}}
- {{if .EnableHeatmap}}
- {{template "user/dashboard/heatmap" .}}
- <div class="ui divider"></div>
- {{end}}
+ {{if .Owner.KeepActivityPrivate}}
+ <div class="ui info message">
+ <p>{{.i18n.Tr "user.disabled_public_activity"}}</p>
+ </div>
+ {{end}}
+ {{if .EnableHeatmap}}
+ {{template "user/dashboard/heatmap" .}}
+ <div class="ui divider"></div>
+ {{end}}
<div class="feeds">
{{template "user/dashboard/feeds" .}}
</div>
diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl
index 995bdfd638..b170c67579 100644
--- a/templates/user/settings/profile.tmpl
+++ b/templates/user/settings/profile.tmpl
@@ -59,6 +59,13 @@
</div>
<div class="field">
+ <label for="keep-activity-private">{{.i18n.Tr "settings.privacy"}}</label>
+ <div class="ui checkbox" id="keep-activity-private">
+ <label class="poping up" data-content="{{.i18n.Tr "settings.keep_activity_private_popup"}}"><strong>{{.i18n.Tr "settings.keep_activity_private"}}</strong></label>
+ <input name="keep_activity_private" type="checkbox" {{if .SignedUser.KeepActivityPrivate}}checked{{end}}>
+ </div>
+ </div>
+ <div class="field">
<button class="ui green button">{{$.i18n.Tr "settings.update_profile"}}</button>
</div>
</form>