diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 050a9e2d06..d50c89838d 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -37,7 +37,7 @@ lower_name: repo2 name: repo2 default_branch: master - num_watches: 0 + num_watches: 1 num_stars: 1 num_forks: 0 num_issues: 2 diff --git a/models/fixtures/watch.yml b/models/fixtures/watch.yml index c29f6bb65a..c6c9726cc8 100644 --- a/models/fixtures/watch.yml +++ b/models/fixtures/watch.yml @@ -26,4 +26,10 @@ id: 5 user_id: 11 repo_id: 1 - mode: 3 # auto + mode: 3 # auto + +- + id: 6 + user_id: 4 + repo_id: 2 + mode: 1 # normal diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index dd2ef62201..5d6e24e2a5 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -177,3 +177,16 @@ func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull boo Limit(30). Find(&users) } + +// GetWatchedRepoIDsOwnedBy returns the repos owned by a particular user watched by a particular user +func GetWatchedRepoIDsOwnedBy(ctx context.Context, userID, ownedByUserID int64) ([]int64, error) { + repoIDs := make([]int64, 0, 10) + err := db.GetEngine(ctx). + Table("repository"). + Select("`repository`.id"). + Join("LEFT", "watch", "`repository`.id=`watch`.repo_id"). + Where("`watch`.user_id=?", userID). + And("`watch`.mode<>?", WatchModeDont). + And("`repository`.owner_id=?", ownedByUserID).Find(&repoIDs) + return repoIDs, err +} diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go index 7816b0262a..ad794beb9b 100644 --- a/models/repo/user_repo_test.go +++ b/models/repo/user_repo_test.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" ) @@ -71,3 +72,15 @@ func TestRepoGetReviewers(t *testing.T) { assert.NoError(t, err) assert.Len(t, reviewers, 1) } + +func GetWatchedRepoIDsOwnedBy(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 9}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + repoIDs, err := repo_model.GetWatchedRepoIDsOwnedBy(db.DefaultContext, user1.ID, user2.ID) + assert.NoError(t, err) + assert.Len(t, repoIDs, 1) + assert.EqualValues(t, 1, repoIDs[0]) +} diff --git a/models/repo/watch.go b/models/repo/watch.go index 6ff3a3f7b3..53b35c0e51 100644 --- a/models/repo/watch.go +++ b/models/repo/watch.go @@ -201,3 +201,9 @@ func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error } return watchRepoMode(ctx, watch, WatchModeAuto) } + +// UnwatchRepos will unwatch the user from all given repositories. +func UnwatchRepos(ctx context.Context, userID int64, repoIDs []int64) error { + _, err := db.GetEngine(ctx).Where("user_id=?", userID).In("repo_id", repoIDs).Delete(&Watch{}) + return err +} diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go index b6ae2a0ef5..02d0d3b0dd 100644 --- a/models/repo/watch_test.go +++ b/models/repo/watch_test.go @@ -155,3 +155,16 @@ func TestWatchRepoMode(t *testing.T) { assert.NoError(t, repo_model.WatchRepoMode(12, 1, repo_model.WatchModeNone)) unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0) } + +func TestUnwatchRepos(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: 4, RepoID: 1}) + unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: 4, RepoID: 2}) + + err := repo_model.UnwatchRepos(db.DefaultContext, 4, []int64{1, 2}) + assert.NoError(t, err) + + unittest.AssertNotExistsBean(t, &repo_model.Watch{UserID: 4, RepoID: 1}) + unittest.AssertNotExistsBean(t, &repo_model.Watch{UserID: 4, RepoID: 2}) +} diff --git a/models/user/block.go b/models/user/block.go index 64dd93ed38..838bc7431e 100644 --- a/models/user/block.go +++ b/models/user/block.go @@ -53,10 +53,12 @@ func UnblockUser(ctx context.Context, userID, blockID int64) error { } // ListBlockedUsers returns the users that the user has blocked. +// The created_unix field of the user struct is overridden by the creation_unix +// field of blockeduser. func ListBlockedUsers(ctx context.Context, userID int64) ([]*User, error) { users := make([]*User, 0, 8) err := db.GetEngine(ctx). - Select("`user`.*"). + Select("`forgejo_blocked_user`.created_unix, `user`.*"). Join("INNER", "forgejo_blocked_user", "`user`.id=`forgejo_blocked_user`.block_id"). Where("`forgejo_blocked_user`.user_id=?", userID). Find(&users) diff --git a/models/user/follow.go b/models/user/follow.go index 936efbc164..5b3ff489ca 100644 --- a/models/user/follow.go +++ b/models/user/follow.go @@ -24,16 +24,25 @@ func init() { // IsFollowing returns true if user is following followID. func IsFollowing(userID, followID int64) bool { - has, _ := db.GetEngine(db.DefaultContext).Get(&Follow{UserID: userID, FollowID: followID}) + return IsFollowingCtx(db.DefaultContext, userID, followID) +} + +// IsFollowingCtx returns true if user is following followID. +func IsFollowingCtx(ctx context.Context, userID, followID int64) bool { + has, _ := db.GetEngine(ctx).Get(&Follow{UserID: userID, FollowID: followID}) return has } // FollowUser marks someone be another's follower. func FollowUser(ctx context.Context, userID, followID int64) (err error) { - if userID == followID || IsFollowing(userID, followID) { + if userID == followID || IsFollowingCtx(ctx, userID, followID) { return nil } + if IsBlocked(ctx, userID, followID) || IsBlocked(ctx, followID, userID) { + return ErrBlockedByUser + } + ctx, committer, err := db.TxContext(ctx) if err != nil { return err @@ -56,7 +65,7 @@ func FollowUser(ctx context.Context, userID, followID int64) (err error) { // UnfollowUser unmarks someone as another's follower. func UnfollowUser(ctx context.Context, userID, followID int64) (err error) { - if userID == followID || !IsFollowing(userID, followID) { + if userID == followID || !IsFollowingCtx(ctx, userID, followID) { return nil } diff --git a/models/user/user_test.go b/models/user/user_test.go index d5f0e80510..e21e9ad52e 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -457,6 +457,12 @@ func TestFollowUser(t *testing.T) { assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2)) + // Blocked user. + assert.ErrorIs(t, user_model.ErrBlockedByUser, user_model.FollowUser(db.DefaultContext, 1, 4)) + assert.ErrorIs(t, user_model.ErrBlockedByUser, user_model.FollowUser(db.DefaultContext, 4, 1)) + unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: 1, FollowID: 4}) + unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: 4, FollowID: 1}) + unittest.CheckConsistencyFor(t, &user_model.User{}) } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b5b4c38c62..a10b7aceaa 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -607,6 +607,7 @@ block_user = Block User block_user.detail = Please understand that if you block this user, other actions will be taken. Such as: block_user.detail_1 = You are being unfollowed from this user. block_user.detail_2 = This user cannot interact with your repositories, created issues and comments. +follow_blocked_user = You cannot follow this user because you have blocked this user or this user has blocked you. form.name_reserved = The username "%s" is reserved. form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username. @@ -898,6 +899,7 @@ hooks.desc = Add webhooks which will be triggered for all repositoriesCANNOT be undone. @@ -920,6 +922,10 @@ visibility.limited_tooltip = Visible to authenticated users only visibility.private = Private visibility.private_tooltip = Visible only to organization members +blocked_since = Blocked since %s +user_unblock_success = The user has been unblocked successfully. +user_block_success = The user has been blocked successfully. + [repo] new_repo_helper = A repository contains all project files, including revision history. Already have it elsewhere? Migrate repository. owner = Owner @@ -2531,6 +2537,7 @@ team_access_desc = Repository access team_permission_desc = Permission team_unit_desc = Allow Access to Repository Sections team_unit_disabled = (Disabled) +follow_blocked_user = You cannot follow this organisation because this organisation has blocked you. form.name_reserved = The organization name "%s" is reserved. form.name_pattern_not_allowed = The pattern "%s" is not allowed in an organization name. diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 364507711d..bc1e6724f7 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -5,6 +5,7 @@ package user import ( + "errors" "net/http" user_model "code.gitea.io/gitea/models/user" @@ -217,8 +218,14 @@ func Follow(ctx *context.APIContext) { // responses: // "204": // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil { + if errors.Is(err, user_model.ErrBlockedByUser) { + ctx.Error(http.StatusForbidden, "BlockedByUser", err) + return + } ctx.Error(http.StatusInternalServerError, "FollowUser", err) return } diff --git a/routers/web/org/setting/blocked_users.go b/routers/web/org/setting/blocked_users.go new file mode 100644 index 0000000000..eae6f81fa0 --- /dev/null +++ b/routers/web/org/setting/blocked_users.go @@ -0,0 +1,61 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "net/http" + "strings" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/routers/utils" + user_service "code.gitea.io/gitea/services/user" +) + +const tplBlockedUsers = "org/settings/blocked_users" + +// BlockedUsers renders the blocked users page. +func BlockedUsers(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings.blocked_users") + ctx.Data["PageIsSettingsBlockedUsers"] = true + + blockedUsers, err := user_model.ListBlockedUsers(ctx, ctx.Org.Organization.ID) + if err != nil { + ctx.ServerError("ListBlockedUsers", err) + return + } + + ctx.Data["BlockedUsers"] = blockedUsers + + ctx.HTML(http.StatusOK, tplBlockedUsers) +} + +// BlockedUsersBlock blocks a particular user from the organization. +func BlockedUsersBlock(ctx *context.Context) { + uname := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("uname"))) + u, err := user_model.GetUserByName(ctx, uname) + if err != nil { + ctx.ServerError("GetUserByName", err) + return + } + + if err := user_service.BlockUser(ctx, ctx.Org.Organization.ID, u.ID); err != nil { + ctx.ServerError("BlockUser", err) + return + } + + ctx.Flash.Success(ctx.Tr("settings.user_block_success")) + ctx.Redirect(ctx.Org.OrgLink + "/settings/blocked_users") +} + +// BlockedUsersUnblock unblocks a particular user from the organization. +func BlockedUsersUnblock(ctx *context.Context) { + if err := user_model.UnblockUser(ctx, ctx.Org.Organization.ID, ctx.FormInt64("user_id")); err != nil { + ctx.ServerError("BlockUser", err) + return + } + + ctx.Flash.Success(ctx.Tr("settings.user_unblock_success")) + ctx.Redirect(ctx.Org.OrgLink + "/settings/blocked_users") +} diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index b67a060db8..4c6fe605b7 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -5,6 +5,7 @@ package user import ( + "errors" "fmt" "net/http" "strings" @@ -294,9 +295,17 @@ func Action(ctx *context.Context) { } if err != nil { - log.Error("Failed to apply action %q: %v", ctx.FormString("action"), err) - ctx.JSONError(fmt.Sprintf("Action %q failed", ctx.FormString("action"))) - return + if !errors.Is(err, user_model.ErrBlockedByUser) { + log.Error("Failed to apply action %q: %v", ctx.FormString("action"), err) + ctx.JSONError(fmt.Sprintf("Action %q failed", ctx.FormString("action"))) + return + } + + if ctx.ContextUser.IsOrganization() { + ctx.Flash.Error(ctx.Tr("org.follow_blocked_user")) + } else { + ctx.Flash.Error(ctx.Tr("user.follow_blocked_user")) + } } if redirectViaJSON { diff --git a/routers/web/user/setting/blocked_users.go b/routers/web/user/setting/blocked_users.go index ea6ccf74d9..134becf969 100644 --- a/routers/web/user/setting/blocked_users.go +++ b/routers/web/user/setting/blocked_users.go @@ -32,3 +32,14 @@ func BlockedUsers(ctx *context.Context) { ctx.Data["BlockedUsers"] = blockedUsers ctx.HTML(http.StatusOK, tplSettingsBlockedUsers) } + +// UnblockUser unblocks a particular user for the doer. +func UnblockUser(ctx *context.Context) { + if err := user_model.UnblockUser(ctx, ctx.Doer.ID, ctx.FormInt64("user_id")); err != nil { + ctx.ServerError("UnblockUser", err) + return + } + + ctx.Flash.Success(ctx.Tr("settings.user_unblock_success")) + ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users") +} diff --git a/routers/web/web.go b/routers/web/web.go index d5d6c6615b..9246775d96 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -523,7 +523,10 @@ func registerRoutes(m *web.Route) { addWebhookEditRoutes() }, webhooksEnabled) - m.Get("/blocked_users", user_setting.BlockedUsers) + m.Group("/blocked_users", func() { + m.Get("", user_setting.BlockedUsers) + m.Post("/unblock", user_setting.UnblockUser) + }) }, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled)) m.Group("/user", func() { @@ -777,6 +780,12 @@ func registerRoutes(m *web.Route) { addSettingVariablesRoutes() }, actions.MustEnableActions) + m.Group("/blocked_users", func() { + m.Get("", org_setting.BlockedUsers) + m.Post("/block", org_setting.BlockedUsersBlock) + m.Post("/unblock", org_setting.BlockedUsersUnblock) + }) + m.RouteMethods("/delete", "GET,POST", org.SettingsDelete) m.Group("/packages", func() { diff --git a/services/user/block.go b/services/user/block.go index eff3242784..05f9a376a7 100644 --- a/services/user/block.go +++ b/services/user/block.go @@ -6,6 +6,7 @@ import ( "context" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" ) @@ -30,11 +31,28 @@ func BlockUser(ctx context.Context, userID, blockID int64) error { return err } - // Unfollow the user from block's perspective. + // Unfollow the user from the block's perspective. err = user_model.UnfollowUser(ctx, blockID, userID) if err != nil { return err } + // Unfollow the user from the doer's perspective. + err = user_model.UnfollowUser(ctx, userID, blockID) + if err != nil { + return err + } + + // Blocked user unwatch all repository owned by the doer. + repoIDs, err := repo_model.GetWatchedRepoIDsOwnedBy(ctx, blockID, userID) + if err != nil { + return err + } + + err = repo_model.UnwatchRepos(ctx, blockID, repoIDs) + if err != nil { + return err + } + return committer.Commit() } diff --git a/services/user/block_test.go b/services/user/block_test.go new file mode 100644 index 0000000000..8a0a3c4739 --- /dev/null +++ b/services/user/block_test.go @@ -0,0 +1,41 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +// TestBlockUser will ensure that when you block a user, certain actions have +// been taken, like unfollowing each other etc. +func TestBlockUser(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + // Follow each other. + assert.NoError(t, user_model.FollowUser(db.DefaultContext, doer.ID, blockedUser.ID)) + assert.NoError(t, user_model.FollowUser(db.DefaultContext, blockedUser.ID, doer.ID)) + + // Blocked user watch repository of doer. + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: doer.ID}) + assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, blockedUser.ID, repo.ID, true)) + + assert.NoError(t, BlockUser(db.DefaultContext, doer.ID, blockedUser.ID)) + + // Ensure they aren't following each other anymore. + assert.False(t, user_model.IsFollowing(doer.ID, blockedUser.ID)) + assert.False(t, user_model.IsFollowing(blockedUser.ID, doer.ID)) + + // Ensure blocked user isn't following doer's repository. + assert.False(t, repo_model.IsWatching(blockedUser.ID, repo.ID)) +} diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index 967b31e7a8..ec7a3efa8d 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -1,5 +1,10 @@ {{template "base/head" .}}
+ {{if .Flash}} +
+ {{template "base/alert" .}} +
+ {{end}}
{{avatar $.Context .Org 140 "org-avatar"}}
diff --git a/templates/org/settings/blocked_users.tmpl b/templates/org/settings/blocked_users.tmpl new file mode 100644 index 0000000000..904f6fd186 --- /dev/null +++ b/templates/org/settings/blocked_users.tmpl @@ -0,0 +1,40 @@ +{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings blocked-users")}} +
+
+
+ {{.CsrfTokenHtml}} + +
+ +
+ +
+
+
+ {{range .BlockedUsers}} +
+ {{avatar $.Context . 48 "gt-mr-3 gt-mb-0"}} +
+ {{.Name}} + {{$.locale.Tr "settings.blocked_since" (DateTime "short" .CreatedUnix) | Safe}} +
+
+
+ {{$.CsrfTokenHtml}} + + +
+
+
+ {{else}} +
+ {{$.locale.Tr "settings.blocked_users_none"}} +
+ {{end}} +
+
+{{template "org/settings/layout_footer" .}} diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl index e22d1b0f80..9528258826 100644 --- a/templates/org/settings/navbar.tmpl +++ b/templates/org/settings/navbar.tmpl @@ -38,6 +38,9 @@
{{end}} + + {{.locale.Tr "settings.blocked_users"}} + {{.locale.Tr "org.settings.delete"}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 4c6ee55f84..f1e56b3449 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -14135,6 +14135,9 @@ "responses": { "204": { "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" } } }, diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 30c06587fb..6a2f719a34 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -1,6 +1,7 @@ {{template "base/head" .}}
+ {{template "base/alert" .}}
{{template "shared/user/profile_big_avatar" .}} diff --git a/templates/user/settings/blocked_users.tmpl b/templates/user/settings/blocked_users.tmpl index fd0cb07883..dc90970ec2 100644 --- a/templates/user/settings/blocked_users.tmpl +++ b/templates/user/settings/blocked_users.tmpl @@ -6,8 +6,23 @@
{{range .BlockedUsers}} +
+ {{avatar $.Context . 28 "gt-mr-3"}} +
+ {{.Name}} + {{$.locale.Tr "settings.blocked_since" (DateTime "short" .CreatedUnix) | Safe}} +
+
+
+ {{$.CsrfTokenHtml}} + + +
+
+
+ {{else}}
- {{avatar $.Context . 28 "gt-mr-3"}}{{.Name}} + {{$.locale.Tr "settings.blocked_users_none"}}
{{end}}
diff --git a/tests/integration/api_user_follow_test.go b/tests/integration/api_user_follow_test.go index 62717af90e..bf6560b103 100644 --- a/tests/integration/api_user_follow_test.go +++ b/tests/integration/api_user_follow_test.go @@ -19,7 +19,7 @@ func TestAPIFollow(t *testing.T) { defer tests.PrepareTestEnv(t)() user1 := "user4" - user2 := "user1" + user2 := "user10" session1 := loginUser(t, user1) token1 := getTokenForLoggedInUser(t, session1, auth_model.AccessTokenScopeReadUser) diff --git a/tests/integration/block_test.go b/tests/integration/block_test.go index 03a5b14712..b8001f4968 100644 --- a/tests/integration/block_test.go +++ b/tests/integration/block_test.go @@ -41,7 +41,7 @@ func BlockUser(t *testing.T, doer, blockedUser *user_model.User) { var respBody redirect DecodeJSON(t, resp, &respBody) assert.EqualValues(t, "/"+blockedUser.Name, respBody.Redirect) - assert.EqualValues(t, true, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID})) + assert.True(t, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID})) } func TestBlockUser(t *testing.T) { @@ -156,3 +156,57 @@ func TestBlockCommentReaction(t *testing.T) { assert.EqualValues(t, true, respBody.Empty) } + +// TestBlockFollow ensures that the doer and blocked user cannot follow each other. +func TestBlockFollow(t *testing.T) { + defer tests.PrepareTestEnv(t)() + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + BlockUser(t, doer, blockedUser) + + // Doer cannot follow blocked user. + session := loginUser(t, doer.Name) + req := NewRequestWithValues(t, "POST", "/"+blockedUser.Name, map[string]string{ + "_csrf": GetCSRF(t, session, "/"+blockedUser.Name), + "action": "follow", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: doer.ID, FollowID: blockedUser.ID}) + + // Blocked user cannot follow doer. + session = loginUser(t, blockedUser.Name) + req = NewRequestWithValues(t, "POST", "/"+doer.Name, map[string]string{ + "_csrf": GetCSRF(t, session, "/"+doer.Name), + "action": "follow", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: blockedUser.ID, FollowID: doer.ID}) +} + +// TestBlockUserFromOrganization ensures that an organisation can block and unblock an user. +func TestBlockUserFromOrganization(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}) + blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17, Type: user_model.UserTypeOrganization}) + unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: org.ID}) + + session := loginUser(t, doer.Name) + req := NewRequestWithValues(t, "POST", org.OrganisationLink()+"/settings/blocked_users/block", map[string]string{ + "_csrf": GetCSRF(t, session, org.OrganisationLink()+"/settings/blocked_users"), + "uname": blockedUser.Name, + }) + session.MakeRequest(t, req, http.StatusSeeOther) + assert.True(t, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: org.ID})) + + req = NewRequestWithValues(t, "POST", org.OrganisationLink()+"/settings/blocked_users/unblock", map[string]string{ + "_csrf": GetCSRF(t, session, org.OrganisationLink()+"/settings/blocked_users"), + "user_id": strconv.FormatInt(blockedUser.ID, 10), + }) + session.MakeRequest(t, req, http.StatusSeeOther) + unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: org.ID}) +} diff --git a/web_src/css/org.css b/web_src/css/org.css index d579443ce1..4c6ecba94c 100644 --- a/web_src/css/org.css +++ b/web_src/css/org.css @@ -191,17 +191,20 @@ } .organization.teams .repositories .item, -.organization.teams .members .item { +.organization.teams .members .item, +.organization.settings .blocked-users .item { padding: 10px 19px; } .organization.teams .repositories .item:not(:last-child), -.organization.teams .members .item:not(:last-child) { +.organization.teams .members .item:not(:last-child), +.organization.settings .blocked-users .item:not(:last-child) { border-bottom: 1px solid var(--color-secondary); } .organization.teams .repositories .item .button, -.organization.teams .members .item .button { +.organization.teams .members .item .button, +.organization.settings .blocked-users .item button { padding: 9px 10px; margin: 0; }