[MODERATION] organization blocking a user (#802)
- Resolves #476 - Follow up for: #540 - Ensure that the doer and blocked person cannot follow each other. - Ensure that the block person cannot watch doer's repositories. - Add unblock button to the blocked user list. - Add blocked since information to the blocked user list. - Add extra testing to moderation code. - Blocked user will unwatch doer's owned repository upon blocking. - Add flash messages to let the user know the block/unblock action was successful. - Add "You haven't blocked any users" message. - Add organization blocking a user. Co-authored-by: Gusted <postmaster@gusted.xyz> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/802 (cherry picked from commit 0505a1042197bd9136b58bc70ec7400a23471585) (cherry picked from commit 37b4e6ef9b85e97d651cf350c9f3ea272ee8d76a) (cherry picked from commit 217475385a815298dcbd8029e0cc8cb2c5877bae) (cherry picked from commit f2c38ce5c2f6cf4008aa1929539063715b50562c) (cherry picked from commit 1edfb68137d8c322a7a9a7c7196fc8f01ff1a889) (cherry picked from commit 2cbc12dc740e6fefc196b7fea6ac8a0ffbbfbeef) (cherry picked from commit 79ff020f182327986dcfd874bc49d4fe32efc29a)
This commit is contained in:
parent
dc9499bdf9
commit
cdf6318f51
26 changed files with 375 additions and 18 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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{})
|
||||
}
|
||||
|
||||
|
|
|
@ -601,6 +601,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.
|
||||
|
@ -892,6 +893,7 @@ hooks.desc = Add webhooks which will be triggered for <strong>all repositories</
|
|||
|
||||
orgs_none = You are not a member of any organizations.
|
||||
repos_none = You do not own any repositories
|
||||
blocked_users_none = You haven't blocked any users.
|
||||
|
||||
delete_account = Delete Your Account
|
||||
delete_prompt = This operation will permanently delete your user account. It <strong>CANNOT</strong> be undone.
|
||||
|
@ -914,6 +916,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? <a href="%s">Migrate repository.</a>
|
||||
owner = Owner
|
||||
|
@ -2524,6 +2530,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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
61
routers/web/org/setting/blocked_users.go
Normal file
61
routers/web/org/setting/blocked_users.go
Normal file
|
@ -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")
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
@ -369,8 +370,16 @@ func Action(ctx *context.Context) {
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.FormString("action")), err)
|
||||
return
|
||||
if !errors.Is(err, user_model.ErrBlockedByUser) {
|
||||
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.FormString("action")), err)
|
||||
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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -517,7 +517,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() {
|
||||
|
@ -770,6 +773,12 @@ func registerRoutes(m *web.Route) {
|
|||
addSettingsSecretsRoutes()
|
||||
}, 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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
41
services/user/block_test.go
Normal file
41
services/user/block_test.go
Normal file
|
@ -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))
|
||||
}
|
|
@ -1,5 +1,10 @@
|
|||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization profile">
|
||||
{{if .Flash}}
|
||||
<div class="ui container gt-mb-5">
|
||||
{{template "base/alert" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="ui container gt-df">
|
||||
{{avatar $.Context .Org 140 "org-avatar"}}
|
||||
<div id="org-info">
|
||||
|
|
40
templates/org/settings/blocked_users.tmpl
Normal file
40
templates/org/settings/blocked_users.tmpl
Normal file
|
@ -0,0 +1,40 @@
|
|||
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings blocked-users")}}
|
||||
<div class="org-setting-content">
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form ignore-dirty" id="block-user-form" action="{{$.Link}}/block" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="uid" value="">
|
||||
<div class="inline field ui left">
|
||||
<div id="search-user-box" class="ui search">
|
||||
<div class="ui input">
|
||||
<input class="prompt" name="uname" placeholder="{{.locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="ui red button">{{.locale.Tr "user.block"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui bottom attached table segment blocked-users">
|
||||
{{range .BlockedUsers}}
|
||||
<div class="item gt-df gt-ac gt-fw">
|
||||
{{avatar $.Context . 48 "gt-mr-3 gt-mb-0"}}
|
||||
<div class="gt-df gt-fc">
|
||||
<a href="{{.HomeLink}}">{{.Name}}</a>
|
||||
<i class="gt-mt-2">{{$.locale.Tr "settings.blocked_since" (DateTime "short" .CreatedUnix) | Safe}}</i>
|
||||
</div>
|
||||
<div class="gt-ml-auto content">
|
||||
<form action="{{$.Link}}/unblock" method="post">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="user_id" value="{{.ID}}">
|
||||
<button class="ui red button">{{$.locale.Tr "user.unblock"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="item">
|
||||
<span class="text grey italic">{{$.locale.Tr "settings.blocked_users_none"}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "org/settings/layout_footer" .}}
|
|
@ -35,6 +35,9 @@
|
|||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users">
|
||||
{{.locale.Tr "settings.blocked_users"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsSettingsDelete}}active {{end}}item" href="{{.OrgLink}}/settings/delete">
|
||||
{{.locale.Tr "org.settings.delete"}}
|
||||
</a>
|
||||
|
|
3
templates/swagger/v1_json.tmpl
generated
3
templates/swagger/v1_json.tmpl
generated
|
@ -13966,6 +13966,9 @@
|
|||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/empty"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/responses/forbidden"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content user profile">
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<div class="ui stackable grid">
|
||||
<div class="ui four wide column">
|
||||
<div class="ui card">
|
||||
|
|
|
@ -6,8 +6,23 @@
|
|||
<div class="ui attached segment">
|
||||
<div class="ui blocked-user list gt-mt-0">
|
||||
{{range .BlockedUsers}}
|
||||
<div class="item gt-df gt-ac">
|
||||
{{avatar $.Context . 28 "gt-mr-3"}}
|
||||
<div class="gt-df gt-fc">
|
||||
<a href="{{.HomeLink}}">{{.Name}}</a>
|
||||
<i class="gt-mt-2">{{$.locale.Tr "settings.blocked_since" (DateTime "short" .CreatedUnix) | Safe}}</i>
|
||||
</div>
|
||||
<div class="gt-ml-auto content">
|
||||
<form action="{{$.Link}}/unblock" method="post">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="user_id" value="{{.ID}}">
|
||||
<button class="ui red button">{{$.locale.Tr "user.unblock"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="item">
|
||||
{{avatar $.Context . 28 "gt-mr-3"}}<a href="{{.HomeLink}}">{{.Name}}</a>
|
||||
<span class="text grey italic">{{$.locale.Tr "settings.blocked_users_none"}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
|
|
@ -191,30 +191,35 @@
|
|||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.organization.teams #add-repo-form input,
|
||||
.organization.teams #repo-multiple-form input,
|
||||
.organization.teams #add-member-form input {
|
||||
.organization.teams #add-member-form input,
|
||||
.organization.settings #block-user-form input {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.organization.teams #add-repo-form .ui.button,
|
||||
.organization.teams #repo-multiple-form .ui.button,
|
||||
.organization.teams #add-member-form .ui.button {
|
||||
.organization.teams #add-member-form .ui.button,
|
||||
.organization.settings #block-user-form .ui.button {
|
||||
margin-left: 5px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue