mirror of https://github.com/gogs/gogs.git
refactor(db): migrate `Follow` off `user.go` (#7203)
parent
8077360cf6
commit
b1fefcbe50
|
@ -55,6 +55,20 @@ Indexes:
|
||||||
"idx_action_user_id" (user_id)
|
"idx_action_user_id" (user_id)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Table "follow"
|
||||||
|
|
||||||
|
```
|
||||||
|
FIELD | COLUMN | POSTGRESQL | MYSQL | SQLITE3
|
||||||
|
-----------+-----------+-----------------+-----------------------+-------------------
|
||||||
|
ID | id | BIGSERIAL | BIGINT AUTO_INCREMENT | INTEGER
|
||||||
|
UserID | user_id | BIGINT NOT NULL | BIGINT NOT NULL | INTEGER NOT NULL
|
||||||
|
FollowID | follow_id | BIGINT NOT NULL | BIGINT NOT NULL | INTEGER NOT NULL
|
||||||
|
|
||||||
|
Primary keys: id
|
||||||
|
Indexes:
|
||||||
|
"follow_user_follow_unique" UNIQUE (user_id, follow_id)
|
||||||
|
```
|
||||||
|
|
||||||
# Table "lfs_object"
|
# Table "lfs_object"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -31,8 +31,8 @@ func TestDumpAndImport(t *testing.T) {
|
||||||
}
|
}
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
if len(Tables) != 5 {
|
if len(Tables) != 6 {
|
||||||
t.Fatalf("New table has added (want 5 got %d), please add new tests for the table and update this check", len(Tables))
|
t.Fatalf("New table has added (want 6 got %d), please add new tests for the table and update this check", len(Tables))
|
||||||
}
|
}
|
||||||
|
|
||||||
db := dbtest.NewDB(t, "dumpAndImport", Tables...)
|
db := dbtest.NewDB(t, "dumpAndImport", Tables...)
|
||||||
|
@ -131,6 +131,17 @@ func setupDBToDump(t *testing.T, db *gorm.DB) {
|
||||||
CreatedUnix: 1588568886,
|
CreatedUnix: 1588568886,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
&Follow{
|
||||||
|
ID: 1,
|
||||||
|
UserID: 1,
|
||||||
|
FollowID: 2,
|
||||||
|
},
|
||||||
|
&Follow{
|
||||||
|
ID: 2,
|
||||||
|
UserID: 2,
|
||||||
|
FollowID: 1,
|
||||||
|
},
|
||||||
|
|
||||||
&LFSObject{
|
&LFSObject{
|
||||||
RepoID: 1,
|
RepoID: 1,
|
||||||
OID: "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
|
OID: "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
|
||||||
|
|
|
@ -42,6 +42,7 @@ func newLogWriter() (logger.Writer, error) {
|
||||||
// NOTE: Lines are sorted in alphabetical order, each letter in its own line.
|
// NOTE: Lines are sorted in alphabetical order, each letter in its own line.
|
||||||
var Tables = []interface{}{
|
var Tables = []interface{}{
|
||||||
new(Access), new(AccessToken), new(Action),
|
new(Access), new(AccessToken), new(Action),
|
||||||
|
new(Follow),
|
||||||
new(LFSObject), new(LoginSource),
|
new(LFSObject), new(LoginSource),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +121,7 @@ func Init(w logger.Writer) (*gorm.DB, error) {
|
||||||
// Initialize stores, sorted in alphabetical order.
|
// Initialize stores, sorted in alphabetical order.
|
||||||
AccessTokens = &accessTokens{DB: db}
|
AccessTokens = &accessTokens{DB: db}
|
||||||
Actions = NewActionsStore(db)
|
Actions = NewActionsStore(db)
|
||||||
|
Follows = NewFollowsStore(db)
|
||||||
LoginSources = &loginSources{DB: db, files: sourceFiles}
|
LoginSources = &loginSources{DB: db, files: sourceFiles}
|
||||||
LFS = &lfs{DB: db}
|
LFS = &lfs{DB: db}
|
||||||
Perms = &perms{DB: db}
|
Perms = &perms{DB: db}
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
// Copyright 2022 The Gogs 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 db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FollowsStore is the persistent interface for follows.
|
||||||
|
//
|
||||||
|
// NOTE: All methods are sorted in alphabetical order.
|
||||||
|
type FollowsStore interface {
|
||||||
|
// Follow marks the user to follow the other user.
|
||||||
|
Follow(ctx context.Context, userID, followID int64) error
|
||||||
|
// IsFollowing returns true if the user is following the other user.
|
||||||
|
IsFollowing(ctx context.Context, userID, followID int64) bool
|
||||||
|
// Unfollow removes the mark the user to follow the other user.
|
||||||
|
Unfollow(ctx context.Context, userID, followID int64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var Follows FollowsStore
|
||||||
|
|
||||||
|
var _ FollowsStore = (*follows)(nil)
|
||||||
|
|
||||||
|
type follows struct {
|
||||||
|
*gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFollowsStore returns a persistent interface for follows with given
|
||||||
|
// database connection.
|
||||||
|
func NewFollowsStore(db *gorm.DB) FollowsStore {
|
||||||
|
return &follows{DB: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*follows) updateFollowingCount(tx *gorm.DB, userID, followID int64) error {
|
||||||
|
/*
|
||||||
|
Equivalent SQL for PostgreSQL:
|
||||||
|
|
||||||
|
UPDATE "user"
|
||||||
|
SET num_followers = (
|
||||||
|
SELECT COUNT(*) FROM follow WHERE follow_id = @followID
|
||||||
|
)
|
||||||
|
WHERE id = @followID
|
||||||
|
*/
|
||||||
|
err := tx.Model(&User{}).
|
||||||
|
Where("id = ?", followID).
|
||||||
|
Update(
|
||||||
|
"num_followers",
|
||||||
|
tx.Model(&Follow{}).Select("COUNT(*)").Where("follow_id = ?", followID),
|
||||||
|
).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, `update "num_followers"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Equivalent SQL for PostgreSQL:
|
||||||
|
|
||||||
|
UPDATE "user"
|
||||||
|
SET num_following = (
|
||||||
|
SELECT COUNT(*) FROM follow WHERE user_id = @userID
|
||||||
|
)
|
||||||
|
WHERE id = @userID
|
||||||
|
*/
|
||||||
|
err = tx.Model(&User{}).
|
||||||
|
Where("id = ?", userID).
|
||||||
|
Update(
|
||||||
|
"num_following",
|
||||||
|
tx.Model(&Follow{}).Select("COUNT(*)").Where("user_id = ?", userID),
|
||||||
|
).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, `update "num_following"`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *follows) Follow(ctx context.Context, userID, followID int64) error {
|
||||||
|
if userID == followID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
f := &Follow{
|
||||||
|
UserID: userID,
|
||||||
|
FollowID: followID,
|
||||||
|
}
|
||||||
|
result := tx.FirstOrCreate(f, f)
|
||||||
|
if result.Error != nil {
|
||||||
|
return errors.Wrap(result.Error, "upsert")
|
||||||
|
} else if result.RowsAffected <= 0 {
|
||||||
|
return nil // Relation already exists
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.updateFollowingCount(tx, userID, followID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *follows) IsFollowing(ctx context.Context, userID, followID int64) bool {
|
||||||
|
return db.WithContext(ctx).Where("user_id = ? AND follow_id = ?", userID, followID).First(&Follow{}).Error == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *follows) Unfollow(ctx context.Context, userID, followID int64) error {
|
||||||
|
if userID == followID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
err := tx.Where("user_id = ? AND follow_id = ?", userID, followID).Delete(&Follow{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "delete")
|
||||||
|
}
|
||||||
|
return db.updateFollowingCount(tx, userID, followID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow represents relations of users and their followers.
|
||||||
|
type Follow struct {
|
||||||
|
ID int64 `gorm:"primaryKey"`
|
||||||
|
UserID int64 `xorm:"UNIQUE(follow)" gorm:"uniqueIndex:follow_user_follow_unique;not null"`
|
||||||
|
FollowID int64 `xorm:"UNIQUE(follow)" gorm:"uniqueIndex:follow_user_follow_unique;not null"`
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
// Copyright 2022 The Gogs 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 db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"gogs.io/gogs/internal/dbtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFollows(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tables := []interface{}{new(User), new(EmailAddress), new(Follow)}
|
||||||
|
db := &follows{
|
||||||
|
DB: dbtest.NewDB(t, "follows", tables...),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
test func(*testing.T, *follows)
|
||||||
|
}{
|
||||||
|
{"Follow", followsFollow},
|
||||||
|
{"IsFollowing", followsIsFollowing},
|
||||||
|
{"Unfollow", followsUnfollow},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := clearTables(t, db.DB, tables...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
tc.test(t, db)
|
||||||
|
})
|
||||||
|
if t.Failed() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func followsFollow(t *testing.T, db *follows) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
usersStore := NewUsersStore(db.DB)
|
||||||
|
alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
bob, err := usersStore.Create(ctx, "bob", "bob@example.com", CreateUserOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = db.Follow(ctx, alice.ID, bob.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// It is OK to follow multiple times and just be noop.
|
||||||
|
err = db.Follow(ctx, alice.ID, bob.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
alice, err = usersStore.GetByID(ctx, alice.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, alice.NumFollowing)
|
||||||
|
|
||||||
|
bob, err = usersStore.GetByID(ctx, bob.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, bob.NumFollowers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func followsIsFollowing(t *testing.T, db *follows) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
usersStore := NewUsersStore(db.DB)
|
||||||
|
alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
bob, err := usersStore.Create(ctx, "bob", "bob@example.com", CreateUserOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got := db.IsFollowing(ctx, alice.ID, bob.ID)
|
||||||
|
assert.False(t, got)
|
||||||
|
|
||||||
|
err = db.Follow(ctx, alice.ID, bob.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
got = db.IsFollowing(ctx, alice.ID, bob.ID)
|
||||||
|
assert.True(t, got)
|
||||||
|
|
||||||
|
err = db.Unfollow(ctx, alice.ID, bob.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
got = db.IsFollowing(ctx, alice.ID, bob.ID)
|
||||||
|
assert.False(t, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func followsUnfollow(t *testing.T, db *follows) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
usersStore := NewUsersStore(db.DB)
|
||||||
|
alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
bob, err := usersStore.Create(ctx, "bob", "bob@example.com", CreateUserOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = db.Follow(ctx, alice.ID, bob.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// It is OK to unfollow multiple times and just be noop.
|
||||||
|
err = db.Unfollow(ctx, alice.ID, bob.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = db.Unfollow(ctx, alice.ID, bob.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
alice, err = usersStore.GetByID(ctx, alice.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, alice.NumFollowing)
|
||||||
|
|
||||||
|
bob, err = usersStore.GetByID(ctx, bob.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, bob.NumFollowers)
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import (
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
log "unknwon.dev/clog/v2"
|
log "unknwon.dev/clog/v2"
|
||||||
|
|
||||||
|
"gogs.io/gogs/internal/conf"
|
||||||
"gogs.io/gogs/internal/testutil"
|
"gogs.io/gogs/internal/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -37,6 +38,15 @@ func TestMain(m *testing.M) {
|
||||||
// NOTE: AutoMigrate does not respect logger passed in gorm.Config.
|
// NOTE: AutoMigrate does not respect logger passed in gorm.Config.
|
||||||
logger.Default = logger.Default.LogMode(level)
|
logger.Default = logger.Default.LogMode(level)
|
||||||
|
|
||||||
|
switch os.Getenv("GOGS_DATABASE_TYPE") {
|
||||||
|
case "mysql":
|
||||||
|
conf.UseMySQL = true
|
||||||
|
case "postgres":
|
||||||
|
conf.UsePostgreSQL = true
|
||||||
|
default:
|
||||||
|
conf.UseSQLite3 = true
|
||||||
|
}
|
||||||
|
|
||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ func init() {
|
||||||
legacyTables = append(legacyTables,
|
legacyTables = append(legacyTables,
|
||||||
new(User), new(PublicKey), new(TwoFactor), new(TwoFactorRecoveryCode),
|
new(User), new(PublicKey), new(TwoFactor), new(TwoFactorRecoveryCode),
|
||||||
new(Repository), new(DeployKey), new(Collaboration), new(Upload),
|
new(Repository), new(DeployKey), new(Collaboration), new(Upload),
|
||||||
new(Watch), new(Star), new(Follow),
|
new(Watch), new(Star),
|
||||||
new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser),
|
new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser),
|
||||||
new(Label), new(IssueLabel), new(Milestone),
|
new(Label), new(IssueLabel), new(Milestone),
|
||||||
new(Mirror), new(Release), new(Webhook), new(HookTask),
|
new(Mirror), new(Release), new(Webhook), new(HookTask),
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
{"ID":1,"UserID":1,"FollowID":2}
|
||||||
|
{"ID":2,"UserID":2,"FollowID":1}
|
|
@ -61,34 +61,6 @@ func (u *User) AfterSet(colName string, _ xorm.Cell) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// User.GetFollowers returns range of user's followers.
|
|
||||||
func (u *User) GetFollowers(page int) ([]*User, error) {
|
|
||||||
users := make([]*User, 0, ItemsPerPage)
|
|
||||||
sess := x.Limit(ItemsPerPage, (page-1)*ItemsPerPage).Where("follow.follow_id=?", u.ID)
|
|
||||||
if conf.UsePostgreSQL {
|
|
||||||
sess = sess.Join("LEFT", "follow", `"user".id=follow.user_id`)
|
|
||||||
} else {
|
|
||||||
sess = sess.Join("LEFT", "follow", "user.id=follow.user_id")
|
|
||||||
}
|
|
||||||
return users, sess.Find(&users)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) IsFollowing(followID int64) bool {
|
|
||||||
return IsFollowing(u.ID, followID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFollowing returns range of user's following.
|
|
||||||
func (u *User) GetFollowing(page int) ([]*User, error) {
|
|
||||||
users := make([]*User, 0, ItemsPerPage)
|
|
||||||
sess := x.Limit(ItemsPerPage, (page-1)*ItemsPerPage).Where("follow.user_id=?", u.ID)
|
|
||||||
if conf.UsePostgreSQL {
|
|
||||||
sess = sess.Join("LEFT", "follow", `"user".id=follow.follow_id`)
|
|
||||||
} else {
|
|
||||||
sess = sess.Join("LEFT", "follow", "user.id=follow.follow_id")
|
|
||||||
}
|
|
||||||
return users, sess.Find(&users)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGitSig generates and returns the signature of given user.
|
// NewGitSig generates and returns the signature of given user.
|
||||||
func (u *User) NewGitSig() *git.Signature {
|
func (u *User) NewGitSig() *git.Signature {
|
||||||
return &git.Signature{
|
return &git.Signature{
|
||||||
|
@ -887,77 +859,6 @@ func SearchUserByName(opts *SearchUserOptions) (users []*User, _ int64, _ error)
|
||||||
return users, count, sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).Find(&users)
|
return users, count, sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).Find(&users)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ___________ .__ .__
|
|
||||||
// \_ _____/___ | | | | ______ _ __
|
|
||||||
// | __)/ _ \| | | | / _ \ \/ \/ /
|
|
||||||
// | \( <_> ) |_| |_( <_> ) /
|
|
||||||
// \___ / \____/|____/____/\____/ \/\_/
|
|
||||||
// \/
|
|
||||||
|
|
||||||
// Follow represents relations of user and his/her followers.
|
|
||||||
type Follow struct {
|
|
||||||
ID int64
|
|
||||||
UserID int64 `xorm:"UNIQUE(follow)"`
|
|
||||||
FollowID int64 `xorm:"UNIQUE(follow)"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsFollowing(userID, followID int64) bool {
|
|
||||||
has, _ := x.Get(&Follow{UserID: userID, FollowID: followID})
|
|
||||||
return has
|
|
||||||
}
|
|
||||||
|
|
||||||
// FollowUser marks someone be another's follower.
|
|
||||||
func FollowUser(userID, followID int64) (err error) {
|
|
||||||
if userID == followID || IsFollowing(userID, followID) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sess := x.NewSession()
|
|
||||||
defer sess.Close()
|
|
||||||
if err = sess.Begin(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = sess.Insert(&Follow{UserID: userID, FollowID: followID}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = sess.Exec("UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", followID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = sess.Exec("UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", userID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return sess.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnfollowUser unmarks someone be another's follower.
|
|
||||||
func UnfollowUser(userID, followID int64) (err error) {
|
|
||||||
if userID == followID || !IsFollowing(userID, followID) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sess := x.NewSession()
|
|
||||||
defer sess.Close()
|
|
||||||
if err = sess.Begin(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = sess.Delete(&Follow{UserID: userID, FollowID: followID}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = sess.Exec("UPDATE `user` SET num_followers = num_followers - 1 WHERE id = ?", followID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = sess.Exec("UPDATE `user` SET num_following = num_following - 1 WHERE id = ?", userID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return sess.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepositoryAccesses finds all repositories with their access mode where a user has access but does not own.
|
// GetRepositoryAccesses finds all repositories with their access mode where a user has access but does not own.
|
||||||
func (u *User) GetRepositoryAccesses() (map[*Repository]AccessMode, error) {
|
func (u *User) GetRepositoryAccesses() (map[*Repository]AccessMode, error) {
|
||||||
accesses := make([]*Access, 0, 10)
|
accesses := make([]*Access, 0, 10)
|
||||||
|
|
|
@ -56,6 +56,14 @@ type UsersStore interface {
|
||||||
GetByUsername(ctx context.Context, username string) (*User, error)
|
GetByUsername(ctx context.Context, username string) (*User, error)
|
||||||
// HasForkedRepository returns true if the user has forked given repository.
|
// HasForkedRepository returns true if the user has forked given repository.
|
||||||
HasForkedRepository(ctx context.Context, userID, repoID int64) bool
|
HasForkedRepository(ctx context.Context, userID, repoID int64) bool
|
||||||
|
// ListFollowers returns a list of users that are following the given user.
|
||||||
|
// Results are paginated by given page and page size, and sorted by the time of
|
||||||
|
// follow in descending order.
|
||||||
|
ListFollowers(ctx context.Context, userID int64, page, pageSize int) ([]*User, error)
|
||||||
|
// ListFollowings returns a list of users that are followed by the given user.
|
||||||
|
// Results are paginated by given page and page size, and sorted by the time of
|
||||||
|
// follow in descending order.
|
||||||
|
ListFollowings(ctx context.Context, userID int64, page, pageSize int) ([]*User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var Users UsersStore
|
var Users UsersStore
|
||||||
|
@ -343,6 +351,52 @@ func (db *users) HasForkedRepository(ctx context.Context, userID, repoID int64)
|
||||||
return count > 0
|
return count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *users) ListFollowers(ctx context.Context, userID int64, page, pageSize int) ([]*User, error) {
|
||||||
|
/*
|
||||||
|
Equivalent SQL for PostgreSQL:
|
||||||
|
|
||||||
|
SELECT * FROM "user"
|
||||||
|
LEFT JOIN follow ON follow.user_id = "user".id
|
||||||
|
WHERE follow.follow_id = @userID
|
||||||
|
ORDER BY follow.id DESC
|
||||||
|
LIMIT @limit OFFSET @offset
|
||||||
|
*/
|
||||||
|
users := make([]*User, 0, pageSize)
|
||||||
|
tx := db.WithContext(ctx).
|
||||||
|
Where("follow.follow_id = ?", userID).
|
||||||
|
Limit(pageSize).Offset((page - 1) * pageSize).
|
||||||
|
Order("follow.id DESC")
|
||||||
|
if conf.UsePostgreSQL {
|
||||||
|
tx.Joins(`LEFT JOIN follow ON follow.user_id = "user".id`)
|
||||||
|
} else {
|
||||||
|
tx.Joins(`LEFT JOIN follow ON follow.user_id = user.id`)
|
||||||
|
}
|
||||||
|
return users, tx.Find(&users).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *users) ListFollowings(ctx context.Context, userID int64, page, pageSize int) ([]*User, error) {
|
||||||
|
/*
|
||||||
|
Equivalent SQL for PostgreSQL:
|
||||||
|
|
||||||
|
SELECT * FROM "user"
|
||||||
|
LEFT JOIN follow ON follow.user_id = "user".id
|
||||||
|
WHERE follow.user_id = @userID
|
||||||
|
ORDER BY follow.id DESC
|
||||||
|
LIMIT @limit OFFSET @offset
|
||||||
|
*/
|
||||||
|
users := make([]*User, 0, pageSize)
|
||||||
|
tx := db.WithContext(ctx).
|
||||||
|
Where("follow.user_id = ?", userID).
|
||||||
|
Limit(pageSize).Offset((page - 1) * pageSize).
|
||||||
|
Order("follow.id DESC")
|
||||||
|
if conf.UsePostgreSQL {
|
||||||
|
tx.Joins(`LEFT JOIN follow ON follow.follow_id = "user".id`)
|
||||||
|
} else {
|
||||||
|
tx.Joins(`LEFT JOIN follow ON follow.follow_id = user.id`)
|
||||||
|
}
|
||||||
|
return users, tx.Find(&users).Error
|
||||||
|
}
|
||||||
|
|
||||||
// UserType indicates the type of the user account.
|
// UserType indicates the type of the user account.
|
||||||
type UserType int
|
type UserType int
|
||||||
|
|
||||||
|
@ -530,3 +584,11 @@ func (u *User) AvatarURL() string {
|
||||||
}
|
}
|
||||||
return link
|
return link
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsFollowing returns true if the user is following the given user.
|
||||||
|
//
|
||||||
|
// TODO(unknwon): This is also used in templates, which should be fixed by
|
||||||
|
// having a dedicated type `template.User`.
|
||||||
|
func (u *User) IsFollowing(followID int64) bool {
|
||||||
|
return Follows.IsFollowing(context.TODO(), u.ID, followID)
|
||||||
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ func TestUsers(t *testing.T) {
|
||||||
}
|
}
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
tables := []interface{}{new(User), new(EmailAddress), new(Repository)}
|
tables := []interface{}{new(User), new(EmailAddress), new(Repository), new(Follow)}
|
||||||
db := &users{
|
db := &users{
|
||||||
DB: dbtest.NewDB(t, "users", tables...),
|
DB: dbtest.NewDB(t, "users", tables...),
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,8 @@ func TestUsers(t *testing.T) {
|
||||||
{"GetByID", usersGetByID},
|
{"GetByID", usersGetByID},
|
||||||
{"GetByUsername", usersGetByUsername},
|
{"GetByUsername", usersGetByUsername},
|
||||||
{"HasForkedRepository", usersHasForkedRepository},
|
{"HasForkedRepository", usersHasForkedRepository},
|
||||||
|
{"ListFollowers", usersListFollowers},
|
||||||
|
{"ListFollowings", usersListFollowings},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
|
@ -296,3 +298,71 @@ func usersHasForkedRepository(t *testing.T, db *users) {
|
||||||
has = db.HasForkedRepository(ctx, 1, 1)
|
has = db.HasForkedRepository(ctx, 1, 1)
|
||||||
assert.True(t, has)
|
assert.True(t, has)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func usersListFollowers(t *testing.T, db *users) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
john, err := db.Create(ctx, "john", "john@example.com", CreateUserOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := db.ListFollowers(ctx, john.ID, 1, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, got)
|
||||||
|
|
||||||
|
alice, err := db.Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
bob, err := db.Create(ctx, "bob", "bob@example.com", CreateUserOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
followsStore := NewFollowsStore(db.DB)
|
||||||
|
err = followsStore.Follow(ctx, alice.ID, john.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = followsStore.Follow(ctx, bob.ID, john.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// First page only has bob
|
||||||
|
got, err = db.ListFollowers(ctx, john.ID, 1, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, got, 1)
|
||||||
|
assert.Equal(t, bob.ID, got[0].ID)
|
||||||
|
|
||||||
|
// Second page only has alice
|
||||||
|
got, err = db.ListFollowers(ctx, john.ID, 2, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, got, 1)
|
||||||
|
assert.Equal(t, alice.ID, got[0].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func usersListFollowings(t *testing.T, db *users) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
john, err := db.Create(ctx, "john", "john@example.com", CreateUserOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := db.ListFollowers(ctx, john.ID, 1, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, got)
|
||||||
|
|
||||||
|
alice, err := db.Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
bob, err := db.Create(ctx, "bob", "bob@example.com", CreateUserOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
followsStore := NewFollowsStore(db.DB)
|
||||||
|
err = followsStore.Follow(ctx, john.ID, alice.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = followsStore.Follow(ctx, john.ID, bob.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// First page only has bob
|
||||||
|
got, err = db.ListFollowings(ctx, john.ID, 1, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, got, 1)
|
||||||
|
assert.Equal(t, bob.ID, got[0].ID)
|
||||||
|
|
||||||
|
// Second page only has alice
|
||||||
|
got, err = db.ListFollowings(ctx, john.ID, 2, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, got, 1)
|
||||||
|
assert.Equal(t, alice.ID, got[0].ID)
|
||||||
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ func responseApiUsers(c *context.APIContext, users []*db.User) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func listUserFollowers(c *context.APIContext, u *db.User) {
|
func listUserFollowers(c *context.APIContext, u *db.User) {
|
||||||
users, err := u.GetFollowers(c.QueryInt("page"))
|
users, err := db.Users.ListFollowers(c.Req.Context(), u.ID, c.QueryInt("page"), db.ItemsPerPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err, "get followers")
|
c.Error(err, "list followers")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
responseApiUsers(c, users)
|
responseApiUsers(c, users)
|
||||||
|
@ -41,9 +41,9 @@ func ListFollowers(c *context.APIContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func listUserFollowing(c *context.APIContext, u *db.User) {
|
func listUserFollowing(c *context.APIContext, u *db.User) {
|
||||||
users, err := u.GetFollowing(c.QueryInt("page"))
|
users, err := db.Users.ListFollowings(c.Req.Context(), u.ID, c.QueryInt("page"), db.ItemsPerPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err, "get following")
|
c.Error(err, "list followings")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
responseApiUsers(c, users)
|
responseApiUsers(c, users)
|
||||||
|
@ -62,7 +62,7 @@ func ListFollowing(c *context.APIContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkUserFollowing(c *context.APIContext, u *db.User, followID int64) {
|
func checkUserFollowing(c *context.APIContext, u *db.User, followID int64) {
|
||||||
if u.IsFollowing(followID) {
|
if db.Follows.IsFollowing(c.Req.Context(), u.ID, followID) {
|
||||||
c.NoContent()
|
c.NoContent()
|
||||||
} else {
|
} else {
|
||||||
c.NotFound()
|
c.NotFound()
|
||||||
|
@ -94,7 +94,7 @@ func Follow(c *context.APIContext) {
|
||||||
if c.Written() {
|
if c.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := db.FollowUser(c.User.ID, target.ID); err != nil {
|
if err := db.Follows.Follow(c.Req.Context(), c.User.ID, target.ID); err != nil {
|
||||||
c.Error(err, "follow user")
|
c.Error(err, "follow user")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -106,7 +106,7 @@ func Unfollow(c *context.APIContext) {
|
||||||
if c.Written() {
|
if c.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := db.UnfollowUser(c.User.ID, target.ID); err != nil {
|
if err := db.Follows.Unfollow(c.Req.Context(), c.User.ID, target.ID); err != nil {
|
||||||
c.Error(err, "unfollow user")
|
c.Error(err, "unfollow user")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -2311,6 +2311,12 @@ type MockUsersStore struct {
|
||||||
// HasForkedRepositoryFunc is an instance of a mock function object
|
// HasForkedRepositoryFunc is an instance of a mock function object
|
||||||
// controlling the behavior of the method HasForkedRepository.
|
// controlling the behavior of the method HasForkedRepository.
|
||||||
HasForkedRepositoryFunc *UsersStoreHasForkedRepositoryFunc
|
HasForkedRepositoryFunc *UsersStoreHasForkedRepositoryFunc
|
||||||
|
// ListFollowersFunc is an instance of a mock function object
|
||||||
|
// controlling the behavior of the method ListFollowers.
|
||||||
|
ListFollowersFunc *UsersStoreListFollowersFunc
|
||||||
|
// ListFollowingsFunc is an instance of a mock function object
|
||||||
|
// controlling the behavior of the method ListFollowings.
|
||||||
|
ListFollowingsFunc *UsersStoreListFollowingsFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMockUsersStore creates a new mock of the UsersStore interface. All
|
// NewMockUsersStore creates a new mock of the UsersStore interface. All
|
||||||
|
@ -2347,6 +2353,16 @@ func NewMockUsersStore() *MockUsersStore {
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ListFollowersFunc: &UsersStoreListFollowersFunc{
|
||||||
|
defaultHook: func(context.Context, int64, int, int) (r0 []*db.User, r1 error) {
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ListFollowingsFunc: &UsersStoreListFollowingsFunc{
|
||||||
|
defaultHook: func(context.Context, int64, int, int) (r0 []*db.User, r1 error) {
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2384,6 +2400,16 @@ func NewStrictMockUsersStore() *MockUsersStore {
|
||||||
panic("unexpected invocation of MockUsersStore.HasForkedRepository")
|
panic("unexpected invocation of MockUsersStore.HasForkedRepository")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ListFollowersFunc: &UsersStoreListFollowersFunc{
|
||||||
|
defaultHook: func(context.Context, int64, int, int) ([]*db.User, error) {
|
||||||
|
panic("unexpected invocation of MockUsersStore.ListFollowers")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ListFollowingsFunc: &UsersStoreListFollowingsFunc{
|
||||||
|
defaultHook: func(context.Context, int64, int, int) ([]*db.User, error) {
|
||||||
|
panic("unexpected invocation of MockUsersStore.ListFollowings")
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2409,6 +2435,12 @@ func NewMockUsersStoreFrom(i db.UsersStore) *MockUsersStore {
|
||||||
HasForkedRepositoryFunc: &UsersStoreHasForkedRepositoryFunc{
|
HasForkedRepositoryFunc: &UsersStoreHasForkedRepositoryFunc{
|
||||||
defaultHook: i.HasForkedRepository,
|
defaultHook: i.HasForkedRepository,
|
||||||
},
|
},
|
||||||
|
ListFollowersFunc: &UsersStoreListFollowersFunc{
|
||||||
|
defaultHook: i.ListFollowers,
|
||||||
|
},
|
||||||
|
ListFollowingsFunc: &UsersStoreListFollowingsFunc{
|
||||||
|
defaultHook: i.ListFollowings,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3072,3 +3104,231 @@ func (c UsersStoreHasForkedRepositoryFuncCall) Args() []interface{} {
|
||||||
func (c UsersStoreHasForkedRepositoryFuncCall) Results() []interface{} {
|
func (c UsersStoreHasForkedRepositoryFuncCall) Results() []interface{} {
|
||||||
return []interface{}{c.Result0}
|
return []interface{}{c.Result0}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UsersStoreListFollowersFunc describes the behavior when the ListFollowers
|
||||||
|
// method of the parent MockUsersStore instance is invoked.
|
||||||
|
type UsersStoreListFollowersFunc struct {
|
||||||
|
defaultHook func(context.Context, int64, int, int) ([]*db.User, error)
|
||||||
|
hooks []func(context.Context, int64, int, int) ([]*db.User, error)
|
||||||
|
history []UsersStoreListFollowersFuncCall
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFollowers delegates to the next hook function in the queue and stores
|
||||||
|
// the parameter and result values of this invocation.
|
||||||
|
func (m *MockUsersStore) ListFollowers(v0 context.Context, v1 int64, v2 int, v3 int) ([]*db.User, error) {
|
||||||
|
r0, r1 := m.ListFollowersFunc.nextHook()(v0, v1, v2, v3)
|
||||||
|
m.ListFollowersFunc.appendCall(UsersStoreListFollowersFuncCall{v0, v1, v2, v3, r0, r1})
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultHook sets function that is called when the ListFollowers method
|
||||||
|
// of the parent MockUsersStore instance is invoked and the hook queue is
|
||||||
|
// empty.
|
||||||
|
func (f *UsersStoreListFollowersFunc) SetDefaultHook(hook func(context.Context, int64, int, int) ([]*db.User, error)) {
|
||||||
|
f.defaultHook = hook
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||||
|
// ListFollowers method of the parent MockUsersStore instance invokes the
|
||||||
|
// hook at the front of the queue and discards it. After the queue is empty,
|
||||||
|
// the default hook function is invoked for any future action.
|
||||||
|
func (f *UsersStoreListFollowersFunc) PushHook(hook func(context.Context, int64, int, int) ([]*db.User, error)) {
|
||||||
|
f.mutex.Lock()
|
||||||
|
f.hooks = append(f.hooks, hook)
|
||||||
|
f.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||||
|
// given values.
|
||||||
|
func (f *UsersStoreListFollowersFunc) SetDefaultReturn(r0 []*db.User, r1 error) {
|
||||||
|
f.SetDefaultHook(func(context.Context, int64, int, int) ([]*db.User, error) {
|
||||||
|
return r0, r1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushReturn calls PushHook with a function that returns the given values.
|
||||||
|
func (f *UsersStoreListFollowersFunc) PushReturn(r0 []*db.User, r1 error) {
|
||||||
|
f.PushHook(func(context.Context, int64, int, int) ([]*db.User, error) {
|
||||||
|
return r0, r1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UsersStoreListFollowersFunc) nextHook() func(context.Context, int64, int, int) ([]*db.User, error) {
|
||||||
|
f.mutex.Lock()
|
||||||
|
defer f.mutex.Unlock()
|
||||||
|
|
||||||
|
if len(f.hooks) == 0 {
|
||||||
|
return f.defaultHook
|
||||||
|
}
|
||||||
|
|
||||||
|
hook := f.hooks[0]
|
||||||
|
f.hooks = f.hooks[1:]
|
||||||
|
return hook
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UsersStoreListFollowersFunc) appendCall(r0 UsersStoreListFollowersFuncCall) {
|
||||||
|
f.mutex.Lock()
|
||||||
|
f.history = append(f.history, r0)
|
||||||
|
f.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// History returns a sequence of UsersStoreListFollowersFuncCall objects
|
||||||
|
// describing the invocations of this function.
|
||||||
|
func (f *UsersStoreListFollowersFunc) History() []UsersStoreListFollowersFuncCall {
|
||||||
|
f.mutex.Lock()
|
||||||
|
history := make([]UsersStoreListFollowersFuncCall, len(f.history))
|
||||||
|
copy(history, f.history)
|
||||||
|
f.mutex.Unlock()
|
||||||
|
|
||||||
|
return history
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsersStoreListFollowersFuncCall is an object that describes an invocation
|
||||||
|
// of method ListFollowers on an instance of MockUsersStore.
|
||||||
|
type UsersStoreListFollowersFuncCall struct {
|
||||||
|
// Arg0 is the value of the 1st argument passed to this method
|
||||||
|
// invocation.
|
||||||
|
Arg0 context.Context
|
||||||
|
// Arg1 is the value of the 2nd argument passed to this method
|
||||||
|
// invocation.
|
||||||
|
Arg1 int64
|
||||||
|
// Arg2 is the value of the 3rd argument passed to this method
|
||||||
|
// invocation.
|
||||||
|
Arg2 int
|
||||||
|
// Arg3 is the value of the 4th argument passed to this method
|
||||||
|
// invocation.
|
||||||
|
Arg3 int
|
||||||
|
// Result0 is the value of the 1st result returned from this method
|
||||||
|
// invocation.
|
||||||
|
Result0 []*db.User
|
||||||
|
// Result1 is the value of the 2nd result returned from this method
|
||||||
|
// invocation.
|
||||||
|
Result1 error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Args returns an interface slice containing the arguments of this
|
||||||
|
// invocation.
|
||||||
|
func (c UsersStoreListFollowersFuncCall) Args() []interface{} {
|
||||||
|
return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Results returns an interface slice containing the results of this
|
||||||
|
// invocation.
|
||||||
|
func (c UsersStoreListFollowersFuncCall) Results() []interface{} {
|
||||||
|
return []interface{}{c.Result0, c.Result1}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsersStoreListFollowingsFunc describes the behavior when the
|
||||||
|
// ListFollowings method of the parent MockUsersStore instance is invoked.
|
||||||
|
type UsersStoreListFollowingsFunc struct {
|
||||||
|
defaultHook func(context.Context, int64, int, int) ([]*db.User, error)
|
||||||
|
hooks []func(context.Context, int64, int, int) ([]*db.User, error)
|
||||||
|
history []UsersStoreListFollowingsFuncCall
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFollowings delegates to the next hook function in the queue and
|
||||||
|
// stores the parameter and result values of this invocation.
|
||||||
|
func (m *MockUsersStore) ListFollowings(v0 context.Context, v1 int64, v2 int, v3 int) ([]*db.User, error) {
|
||||||
|
r0, r1 := m.ListFollowingsFunc.nextHook()(v0, v1, v2, v3)
|
||||||
|
m.ListFollowingsFunc.appendCall(UsersStoreListFollowingsFuncCall{v0, v1, v2, v3, r0, r1})
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultHook sets function that is called when the ListFollowings
|
||||||
|
// method of the parent MockUsersStore instance is invoked and the hook
|
||||||
|
// queue is empty.
|
||||||
|
func (f *UsersStoreListFollowingsFunc) SetDefaultHook(hook func(context.Context, int64, int, int) ([]*db.User, error)) {
|
||||||
|
f.defaultHook = hook
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||||
|
// ListFollowings method of the parent MockUsersStore instance invokes the
|
||||||
|
// hook at the front of the queue and discards it. After the queue is empty,
|
||||||
|
// the default hook function is invoked for any future action.
|
||||||
|
func (f *UsersStoreListFollowingsFunc) PushHook(hook func(context.Context, int64, int, int) ([]*db.User, error)) {
|
||||||
|
f.mutex.Lock()
|
||||||
|
f.hooks = append(f.hooks, hook)
|
||||||
|
f.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||||
|
// given values.
|
||||||
|
func (f *UsersStoreListFollowingsFunc) SetDefaultReturn(r0 []*db.User, r1 error) {
|
||||||
|
f.SetDefaultHook(func(context.Context, int64, int, int) ([]*db.User, error) {
|
||||||
|
return r0, r1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushReturn calls PushHook with a function that returns the given values.
|
||||||
|
func (f *UsersStoreListFollowingsFunc) PushReturn(r0 []*db.User, r1 error) {
|
||||||
|
f.PushHook(func(context.Context, int64, int, int) ([]*db.User, error) {
|
||||||
|
return r0, r1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UsersStoreListFollowingsFunc) nextHook() func(context.Context, int64, int, int) ([]*db.User, error) {
|
||||||
|
f.mutex.Lock()
|
||||||
|
defer f.mutex.Unlock()
|
||||||
|
|
||||||
|
if len(f.hooks) == 0 {
|
||||||
|
return f.defaultHook
|
||||||
|
}
|
||||||
|
|
||||||
|
hook := f.hooks[0]
|
||||||
|
f.hooks = f.hooks[1:]
|
||||||
|
return hook
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UsersStoreListFollowingsFunc) appendCall(r0 UsersStoreListFollowingsFuncCall) {
|
||||||
|
f.mutex.Lock()
|
||||||
|
f.history = append(f.history, r0)
|
||||||
|
f.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// History returns a sequence of UsersStoreListFollowingsFuncCall objects
|
||||||
|
// describing the invocations of this function.
|
||||||
|
func (f *UsersStoreListFollowingsFunc) History() []UsersStoreListFollowingsFuncCall {
|
||||||
|
f.mutex.Lock()
|
||||||
|
history := make([]UsersStoreListFollowingsFuncCall, len(f.history))
|
||||||
|
copy(history, f.history)
|
||||||
|
f.mutex.Unlock()
|
||||||
|
|
||||||
|
return history
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsersStoreListFollowingsFuncCall is an object that describes an
|
||||||
|
// invocation of method ListFollowings on an instance of MockUsersStore.
|
||||||
|
type UsersStoreListFollowingsFuncCall struct {
|
||||||
|
// Arg0 is the value of the 1st argument passed to this method
|
||||||
|
// invocation.
|
||||||
|
Arg0 context.Context
|
||||||
|
// Arg1 is the value of the 2nd argument passed to this method
|
||||||
|
// invocation.
|
||||||
|
Arg1 int64
|
||||||
|
// Arg2 is the value of the 3rd argument passed to this method
|
||||||
|
// invocation.
|
||||||
|
Arg2 int
|
||||||
|
// Arg3 is the value of the 4th argument passed to this method
|
||||||
|
// invocation.
|
||||||
|
Arg3 int
|
||||||
|
// Result0 is the value of the 1st result returned from this method
|
||||||
|
// invocation.
|
||||||
|
Result0 []*db.User
|
||||||
|
// Result1 is the value of the 2nd result returned from this method
|
||||||
|
// invocation.
|
||||||
|
Result1 error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Args returns an interface slice containing the arguments of this
|
||||||
|
// invocation.
|
||||||
|
func (c UsersStoreListFollowingsFuncCall) Args() []interface{} {
|
||||||
|
return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Results returns an interface slice containing the results of this
|
||||||
|
// invocation.
|
||||||
|
func (c UsersStoreListFollowingsFuncCall) Results() []interface{} {
|
||||||
|
return []interface{}{c.Result0, c.Result1}
|
||||||
|
}
|
||||||
|
|
|
@ -88,7 +88,14 @@ func Followers(c *context.Context, puser *context.ParamsUser) {
|
||||||
c.PageIs("Followers")
|
c.PageIs("Followers")
|
||||||
c.Data["CardsTitle"] = c.Tr("user.followers")
|
c.Data["CardsTitle"] = c.Tr("user.followers")
|
||||||
c.Data["Owner"] = puser
|
c.Data["Owner"] = puser
|
||||||
repo.RenderUserCards(c, puser.NumFollowers, puser.GetFollowers, FOLLOWERS)
|
repo.RenderUserCards(
|
||||||
|
c,
|
||||||
|
puser.NumFollowers,
|
||||||
|
func(page int) ([]*db.User, error) {
|
||||||
|
return db.Users.ListFollowers(c.Req.Context(), puser.ID, page, db.ItemsPerPage)
|
||||||
|
},
|
||||||
|
FOLLOWERS,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Following(c *context.Context, puser *context.ParamsUser) {
|
func Following(c *context.Context, puser *context.ParamsUser) {
|
||||||
|
@ -96,7 +103,14 @@ func Following(c *context.Context, puser *context.ParamsUser) {
|
||||||
c.PageIs("Following")
|
c.PageIs("Following")
|
||||||
c.Data["CardsTitle"] = c.Tr("user.following")
|
c.Data["CardsTitle"] = c.Tr("user.following")
|
||||||
c.Data["Owner"] = puser
|
c.Data["Owner"] = puser
|
||||||
repo.RenderUserCards(c, puser.NumFollowing, puser.GetFollowing, FOLLOWERS)
|
repo.RenderUserCards(
|
||||||
|
c,
|
||||||
|
puser.NumFollowing,
|
||||||
|
func(page int) ([]*db.User, error) {
|
||||||
|
return db.Users.ListFollowings(c.Req.Context(), puser.ID, page, db.ItemsPerPage)
|
||||||
|
},
|
||||||
|
FOLLOWERS,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Stars(_ *context.Context) {
|
func Stars(_ *context.Context) {
|
||||||
|
@ -106,9 +120,9 @@ func Action(c *context.Context, puser *context.ParamsUser) {
|
||||||
var err error
|
var err error
|
||||||
switch c.Params(":action") {
|
switch c.Params(":action") {
|
||||||
case "follow":
|
case "follow":
|
||||||
err = db.FollowUser(c.UserID(), puser.ID)
|
err = db.Follows.Follow(c.Req.Context(), c.UserID(), puser.ID)
|
||||||
case "unfollow":
|
case "unfollow":
|
||||||
err = db.UnfollowUser(c.UserID(), puser.ID)
|
err = db.Follows.Unfollow(c.Req.Context(), c.UserID(), puser.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in New Issue