feat(package): gitea: add private heatmap opt-in
This commit is contained in:
@@ -82,6 +82,7 @@ type UserSettings struct {
|
||||
// Privacy
|
||||
HideEmail bool `json:"hide_email"`
|
||||
HideActivity bool `json:"hide_activity"`
|
||||
IncludePrivateContributions bool `json:"include_private_contributions"`
|
||||
}
|
||||
|
||||
// UserSettingsOptions represents options to change user settings
|
||||
@@ -97,6 +98,7 @@ type UserSettingsOptions struct {
|
||||
// Privacy
|
||||
HideEmail *bool `json:"hide_email"`
|
||||
HideActivity *bool `json:"hide_activity"`
|
||||
IncludePrivateContributions *bool `json:"include_private_contributions"`
|
||||
}
|
||||
|
||||
// RenameUserOption options when renaming a user
|
||||
|
||||
@@ -708,6 +708,8 @@
|
||||
"settings.privacy": "Privacy",
|
||||
"settings.keep_activity_private": "Hide Activity from profile page",
|
||||
"settings.keep_activity_private_popup": "Makes the activity visible only for you and the admins",
|
||||
"settings.include_private_contributions": "Show private contributions on profile heatmap",
|
||||
"settings.include_private_contributions_popup": "Publicly reveals only contribution dates and counts from non-public repositories, never repository or commit details.",
|
||||
"settings.lookup_avatar_by_mail": "Look Up Avatar by Email Address",
|
||||
"settings.federated_avatar_lookup": "Federated Avatar Lookup",
|
||||
"settings.enable_custom_avatar": "Use Custom Avatar",
|
||||
|
||||
@@ -24,7 +24,12 @@ func GetUserSettings(ctx *context.APIContext) {
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserSettings"
|
||||
ctx.JSON(http.StatusOK, convert.User2UserSettings(ctx.Doer))
|
||||
settings, err := convert.User2UserSettings(ctx, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, settings)
|
||||
}
|
||||
|
||||
// UpdateUserSettings returns user settings
|
||||
@@ -55,11 +60,17 @@ func UpdateUserSettings(ctx *context.APIContext) {
|
||||
DiffViewStyle: optional.FromPtr(form.DiffViewStyle),
|
||||
KeepEmailPrivate: optional.FromPtr(form.HideEmail),
|
||||
KeepActivityPrivate: optional.FromPtr(form.HideActivity),
|
||||
IncludePrivateContributions: optional.FromPtr(form.IncludePrivateContributions),
|
||||
}
|
||||
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.User2UserSettings(ctx.Doer))
|
||||
settings, err := convert.User2UserSettings(ctx, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, settings)
|
||||
}
|
||||
|
||||
@@ -48,16 +48,32 @@ func Profile(ctx *context.Context) {
|
||||
ctx.Data["PageIsSettingsProfile"] = true
|
||||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
||||
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
|
||||
if !loadProfilePrivateContributionSetting(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsProfile)
|
||||
}
|
||||
|
||||
func loadProfilePrivateContributionSetting(ctx *context.Context) bool {
|
||||
includePrivateContributions, err := user_model.GetIncludePrivateContributions(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetIncludePrivateContributions", err)
|
||||
return false
|
||||
}
|
||||
ctx.Data["IncludePrivateContributions"] = includePrivateContributions
|
||||
return true
|
||||
}
|
||||
|
||||
// ProfilePost response for change user's profile
|
||||
func ProfilePost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings_title")
|
||||
ctx.Data["PageIsSettingsProfile"] = true
|
||||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
||||
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
|
||||
if !loadProfilePrivateContributionSetting(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplSettingsProfile)
|
||||
@@ -100,6 +116,7 @@ func ProfilePost(ctx *context.Context) {
|
||||
Location: optional.Some(form.Location),
|
||||
Visibility: optional.Some(form.Visibility),
|
||||
KeepActivityPrivate: optional.Some(form.KeepActivityPrivate),
|
||||
IncludePrivateContributions: optional.Some(form.IncludePrivateContributions),
|
||||
}
|
||||
|
||||
if form.FullName != "" {
|
||||
|
||||
@@ -86,7 +86,11 @@ func toUser(ctx context.Context, user *user_model.User, signed, authed bool) *ap
|
||||
}
|
||||
|
||||
// User2UserSettings return UserSettings based on a user
|
||||
func User2UserSettings(user *user_model.User) api.UserSettings {
|
||||
func User2UserSettings(ctx context.Context, user *user_model.User) (api.UserSettings, error) {
|
||||
includePrivateContributions, err := user_model.GetIncludePrivateContributions(ctx, user.ID)
|
||||
if err != nil {
|
||||
return api.UserSettings{}, err
|
||||
}
|
||||
return api.UserSettings{
|
||||
FullName: user.FullName,
|
||||
Website: user.Website,
|
||||
@@ -96,8 +100,9 @@ func User2UserSettings(user *user_model.User) api.UserSettings {
|
||||
Theme: user.Theme,
|
||||
HideEmail: user.KeepEmailPrivate,
|
||||
HideActivity: user.KeepActivityPrivate,
|
||||
IncludePrivateContributions: includePrivateContributions,
|
||||
DiffViewStyle: user.DiffViewStyle,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ToUserAndPermission return User and its collaboration permission for a repository
|
||||
|
||||
@@ -216,6 +216,7 @@ type UpdateProfileForm struct {
|
||||
Description string `binding:"MaxSize(255)"`
|
||||
Visibility structs.VisibleType
|
||||
KeepActivityPrivate bool
|
||||
IncludePrivateContributions bool
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
password_module "code.gitea.io/gitea/modules/auth/password"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
@@ -47,6 +48,7 @@ type UpdateOptions struct {
|
||||
IsRestricted optional.Option[bool]
|
||||
Visibility optional.Option[structs.VisibleType]
|
||||
KeepActivityPrivate optional.Option[bool]
|
||||
IncludePrivateContributions optional.Option[bool]
|
||||
Language optional.Option[string]
|
||||
Theme optional.Option[string]
|
||||
DiffViewStyle optional.Option[string]
|
||||
@@ -182,7 +184,23 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
|
||||
cols = append(cols, "last_login_unix")
|
||||
}
|
||||
|
||||
return user_model.UpdateUserCols(ctx, u, cols...)
|
||||
if len(cols) > 0 || opts.IncludePrivateContributions.Has() {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if len(cols) > 0 {
|
||||
if err := user_model.UpdateUserCols(ctx, u, cols...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.IncludePrivateContributions.Has() {
|
||||
return user_model.SetIncludePrivateContributions(ctx, u.ID, opts.IncludePrivateContributions.Value())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UpdateAuthOptions struct {
|
||||
|
||||
@@ -44,6 +44,7 @@ func TestUpdateUser(t *testing.T) {
|
||||
IsAdmin: UpdateOptionFieldFromValue(true),
|
||||
Visibility: optional.Some(structs.VisibleTypePrivate),
|
||||
KeepActivityPrivate: optional.Some(true),
|
||||
IncludePrivateContributions: optional.Some(true),
|
||||
Language: optional.Some("lang"),
|
||||
Theme: optional.Some("theme"),
|
||||
DiffViewStyle: optional.Some("split"),
|
||||
@@ -66,6 +67,9 @@ func TestUpdateUser(t *testing.T) {
|
||||
assert.Equal(t, opts.IsAdmin.Value().FieldValue, user.IsAdmin)
|
||||
assert.Equal(t, opts.Visibility.Value(), user.Visibility)
|
||||
assert.Equal(t, opts.KeepActivityPrivate.Value(), user.KeepActivityPrivate)
|
||||
includePrivateContributions, err := user_model.GetIncludePrivateContributions(t.Context(), user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, opts.IncludePrivateContributions.Value(), includePrivateContributions)
|
||||
assert.Equal(t, opts.Language.Value(), user.Language)
|
||||
assert.Equal(t, opts.Theme.Value(), user.Theme)
|
||||
assert.Equal(t, opts.DiffViewStyle.Value(), user.DiffViewStyle)
|
||||
@@ -86,6 +90,9 @@ func TestUpdateUser(t *testing.T) {
|
||||
assert.Equal(t, opts.IsAdmin.Value().FieldValue, user.IsAdmin)
|
||||
assert.Equal(t, opts.Visibility.Value(), user.Visibility)
|
||||
assert.Equal(t, opts.KeepActivityPrivate.Value(), user.KeepActivityPrivate)
|
||||
includePrivateContributions, err = user_model.GetIncludePrivateContributions(t.Context(), user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, opts.IncludePrivateContributions.Value(), includePrivateContributions)
|
||||
assert.Equal(t, opts.Language.Value(), user.Language)
|
||||
assert.Equal(t, opts.Theme.Value(), user.Theme)
|
||||
assert.Equal(t, opts.DiffViewStyle.Value(), user.DiffViewStyle)
|
||||
|
||||
@@ -29655,6 +29655,10 @@
|
||||
"type": "boolean",
|
||||
"x-go-name": "HideEmail"
|
||||
},
|
||||
"include_private_contributions": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "IncludePrivateContributions"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"x-go-name": "Language"
|
||||
@@ -29699,6 +29703,10 @@
|
||||
"type": "boolean",
|
||||
"x-go-name": "HideEmail"
|
||||
},
|
||||
"include_private_contributions": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "IncludePrivateContributions"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"x-go-name": "Language"
|
||||
|
||||
@@ -88,6 +88,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="ui checkbox" id="include-private-contributions">
|
||||
<label data-tooltip-content="{{ctx.Locale.Tr "settings.include_private_contributions_popup"}}"><strong>{{ctx.Locale.Tr "settings.include_private_contributions"}}</strong></label>
|
||||
<input name="include_private_contributions" type="checkbox" {{if .IncludePrivateContributions}}checked{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="field">
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -56,6 +57,26 @@ func testPrivateActivityHelperEnablePrivateActivity(t *testing.T) {
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func testPrivateActivityHelperSetIncludePrivateContributionsViaWeb(t *testing.T, session *TestSession, username string, include bool) {
|
||||
values := map[string]string{
|
||||
"name": username,
|
||||
"email": username + "@example.com",
|
||||
"language": "en-US",
|
||||
}
|
||||
if include {
|
||||
values["include_private_contributions"] = "1"
|
||||
}
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", values)
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func testPrivateActivityHelperAssertIncludePrivateContributions(t *testing.T, username string, expected bool) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
|
||||
includePrivateContributions, err := user_model.GetIncludePrivateContributions(t.Context(), user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, includePrivateContributions)
|
||||
}
|
||||
|
||||
func testPrivateActivityHelperHasVisibleActivitiesInHTMLDoc(htmlDoc *HTMLDoc) bool {
|
||||
return htmlDoc.doc.Find("#activity-feed").Find(".flex-item").Length() > 0
|
||||
}
|
||||
@@ -136,6 +157,80 @@ func testPrivateActivityHelperHasHeatmapContentFromSession(t *testing.T, session
|
||||
return len(items) != 0
|
||||
}
|
||||
|
||||
// check private contribution opt-in settings persistence and ownership
|
||||
|
||||
func TestPrivateActivityIncludePrivateContributionsWebSettingsPersistence(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, privateActivityTestUser)
|
||||
testPrivateActivityHelperAssertIncludePrivateContributions(t, privateActivityTestUser, false)
|
||||
|
||||
testPrivateActivityHelperSetIncludePrivateContributionsViaWeb(t, session, privateActivityTestUser, true)
|
||||
testPrivateActivityHelperAssertIncludePrivateContributions(t, privateActivityTestUser, true)
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: privateActivityTestUser})
|
||||
assert.False(t, user.KeepActivityPrivate, "private contribution opt-in must not hide the whole activity heatmap")
|
||||
|
||||
req := NewRequest(t, "GET", "/user/settings")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
assert.Equal(t, 1, htmlDoc.doc.Find("#include-private-contributions input[name='include_private_contributions']:checked").Length())
|
||||
|
||||
testPrivateActivityHelperSetIncludePrivateContributionsViaWeb(t, session, privateActivityTestUser, false)
|
||||
testPrivateActivityHelperAssertIncludePrivateContributions(t, privateActivityTestUser, false)
|
||||
}
|
||||
|
||||
func TestPrivateActivityIncludePrivateContributionsAPISettingsPersistence(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, privateActivityTestUser)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
includePrivateContributions := true
|
||||
req := NewRequestWithJSON(t, "PATCH", "/api/v1/user/settings", &api.UserSettingsOptions{
|
||||
IncludePrivateContributions: &includePrivateContributions,
|
||||
}).AddTokenAuth(token)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
settings := DecodeJSON(t, resp, &api.UserSettings{})
|
||||
assert.True(t, settings.IncludePrivateContributions)
|
||||
testPrivateActivityHelperAssertIncludePrivateContributions(t, privateActivityTestUser, true)
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: privateActivityTestUser})
|
||||
assert.False(t, user.KeepActivityPrivate, "API opt-in must not set KeepActivityPrivate")
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/user/settings").AddTokenAuth(token)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
settings = DecodeJSON(t, resp, &api.UserSettings{})
|
||||
assert.True(t, settings.IncludePrivateContributions)
|
||||
|
||||
includePrivateContributions = false
|
||||
req = NewRequestWithJSON(t, "PATCH", "/api/v1/user/settings", &api.UserSettingsOptions{
|
||||
IncludePrivateContributions: &includePrivateContributions,
|
||||
}).AddTokenAuth(token)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
settings = DecodeJSON(t, resp, &api.UserSettings{})
|
||||
assert.False(t, settings.IncludePrivateContributions)
|
||||
testPrivateActivityHelperAssertIncludePrivateContributions(t, privateActivityTestUser, false)
|
||||
}
|
||||
|
||||
func TestPrivateActivityIncludePrivateContributionsWebSettingsAuthBoundary(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
testPrivateActivityHelperAssertIncludePrivateContributions(t, privateActivityTestUser, false)
|
||||
|
||||
otherSession := loginUser(t, privateActivityTestOtherUser)
|
||||
testPrivateActivityHelperSetIncludePrivateContributionsViaWeb(t, otherSession, privateActivityTestOtherUser, true)
|
||||
testPrivateActivityHelperAssertIncludePrivateContributions(t, privateActivityTestOtherUser, true)
|
||||
testPrivateActivityHelperAssertIncludePrivateContributions(t, privateActivityTestUser, false)
|
||||
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": privateActivityTestUser,
|
||||
"email": privateActivityTestUser + "@example.com",
|
||||
"language": "en-US",
|
||||
"include_private_contributions": "1",
|
||||
})
|
||||
MakeRequest(t, req, http.StatusSeeOther)
|
||||
testPrivateActivityHelperAssertIncludePrivateContributions(t, privateActivityTestUser, false)
|
||||
}
|
||||
|
||||
// check activity visibility if the visibility is enabled
|
||||
|
||||
func TestPrivateActivityNoVisibleForPublic(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user