RSS/Atom support for Orgs (#17714)

part of #569
This commit is contained in:
6543 2022-03-10 15:54:51 +01:00 committed by GitHub
parent 5fdd30423e
commit cc98737ca8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 156 additions and 87 deletions

View File

@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -315,8 +316,10 @@ func (a *Action) GetIssueContent() string {
// GetFeedsOptions options for retrieving feeds // GetFeedsOptions options for retrieving feeds
type GetFeedsOptions struct { type GetFeedsOptions struct {
db.ListOptions
RequestedUser *user_model.User // the user we want activity for RequestedUser *user_model.User // the user we want activity for
RequestedTeam *Team // the team we want activity for RequestedTeam *Team // the team we want activity for
RequestedRepo *repo_model.Repository // the repo we want activity for
Actor *user_model.User // the user viewing the activity Actor *user_model.User // the user viewing the activity
IncludePrivate bool // include private actions IncludePrivate bool // include private actions
OnlyPerformedBy bool // only actions performed by requested user OnlyPerformedBy bool // only actions performed by requested user
@ -326,8 +329,8 @@ type GetFeedsOptions struct {
// GetFeeds returns actions according to the provided options // GetFeeds returns actions according to the provided options
func GetFeeds(opts GetFeedsOptions) ([]*Action, error) { func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
if !activityReadable(opts.RequestedUser, opts.Actor) { if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil {
return make([]*Action, 0), nil return nil, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
} }
cond, err := activityQueryCondition(opts) cond, err := activityQueryCondition(opts)
@ -335,9 +338,14 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
return nil, err return nil, err
} }
actions := make([]*Action, 0, setting.UI.FeedPagingNum) sess := db.GetEngine(db.DefaultContext).Where(cond)
if err := db.GetEngine(db.DefaultContext).Limit(setting.UI.FeedPagingNum).Desc("created_unix").Where(cond).Find(&actions); err != nil { opts.SetDefaultValues()
sess = db.SetSessionPagination(sess, &opts)
actions := make([]*Action, 0, opts.PageSize)
if err := sess.Desc("created_unix").Find(&actions); err != nil {
return nil, fmt.Errorf("Find: %v", err) return nil, fmt.Errorf("Find: %v", err)
} }
@ -349,41 +357,44 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
} }
func activityReadable(user, doer *user_model.User) bool { func activityReadable(user, doer *user_model.User) bool {
var doerID int64 return !user.KeepActivityPrivate ||
if doer != nil { doer != nil && (doer.IsAdmin || user.ID == doer.ID)
doerID = doer.ID
}
if doer == nil || !doer.IsAdmin {
if user.KeepActivityPrivate && doerID != user.ID {
return false
}
}
return true
} }
func activityQueryCondition(opts GetFeedsOptions) (builder.Cond, error) { func activityQueryCondition(opts GetFeedsOptions) (builder.Cond, error) {
cond := builder.NewCond() cond := builder.NewCond()
var repoIDs []int64 if opts.RequestedTeam != nil && opts.RequestedUser == nil {
var actorID int64 org, err := user_model.GetUserByID(opts.RequestedTeam.OrgID)
if opts.Actor != nil { if err != nil {
actorID = opts.Actor.ID return nil, err
}
opts.RequestedUser = org
}
// check activity visibility for actor ( similar to activityReadable() )
if opts.Actor == nil {
cond = cond.And(builder.In("act_user_id",
builder.Select("`user`.id").Where(
builder.Eq{"keep_activity_private": false, "visibility": structs.VisibleTypePublic},
).From("`user`"),
))
} else if !opts.Actor.IsAdmin {
cond = cond.And(builder.In("act_user_id",
builder.Select("`user`.id").Where(
builder.Eq{"keep_activity_private": false}.
And(builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))).
Or(builder.Eq{"id": opts.Actor.ID}).From("`user`"),
))
} }
// check readable repositories by doer/actor // check readable repositories by doer/actor
if opts.Actor == nil || !opts.Actor.IsAdmin { if opts.Actor == nil || !opts.Actor.IsAdmin {
if opts.RequestedUser.IsOrganization() {
env, err := OrgFromUser(opts.RequestedUser).AccessibleReposEnv(actorID)
if err != nil {
return nil, fmt.Errorf("AccessibleReposEnv: %v", err)
}
if repoIDs, err = env.RepoIDs(1, opts.RequestedUser.NumRepos); err != nil {
return nil, fmt.Errorf("GetUserRepositories: %v", err)
}
cond = cond.And(builder.In("repo_id", repoIDs))
} else {
cond = cond.And(builder.In("repo_id", AccessibleRepoIDsQuery(opts.Actor))) cond = cond.And(builder.In("repo_id", AccessibleRepoIDsQuery(opts.Actor)))
} }
if opts.RequestedRepo != nil {
cond = cond.And(builder.Eq{"repo_id": opts.RequestedRepo.ID})
} }
if opts.RequestedTeam != nil { if opts.RequestedTeam != nil {
@ -395,11 +406,14 @@ func activityQueryCondition(opts GetFeedsOptions) (builder.Cond, error) {
cond = cond.And(builder.In("repo_id", teamRepoIDs)) cond = cond.And(builder.In("repo_id", teamRepoIDs))
} }
if opts.RequestedUser != nil {
cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID}) cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})
if opts.OnlyPerformedBy { if opts.OnlyPerformedBy {
cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID}) cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID})
} }
}
if !opts.IncludePrivate { if !opts.IncludePrivate {
cond = cond.And(builder.Eq{"is_private": false}) cond = cond.And(builder.Eq{"is_private": false})
} }

View File

@ -93,6 +93,46 @@ func TestGetFeeds2(t *testing.T) {
assert.Len(t, actions, 0) assert.Len(t, actions, 0)
} }
func TestActivityReadable(t *testing.T) {
tt := []struct {
desc string
user *user_model.User
doer *user_model.User
result bool
}{{
desc: "user should see own activity",
user: &user_model.User{ID: 1},
doer: &user_model.User{ID: 1},
result: true,
}, {
desc: "anon should see activity if public",
user: &user_model.User{ID: 1},
result: true,
}, {
desc: "anon should NOT see activity",
user: &user_model.User{ID: 1, KeepActivityPrivate: true},
result: false,
}, {
desc: "user should see own activity if private too",
user: &user_model.User{ID: 1, KeepActivityPrivate: true},
doer: &user_model.User{ID: 1},
result: true,
}, {
desc: "other user should NOT see activity",
user: &user_model.User{ID: 1, KeepActivityPrivate: true},
doer: &user_model.User{ID: 2},
result: false,
}, {
desc: "admin should see activity",
user: &user_model.User{ID: 1, KeepActivityPrivate: true},
doer: &user_model.User{ID: 2, IsAdmin: true},
result: true,
}}
for _, test := range tt {
assert.Equal(t, test.result, activityReadable(test.user, test.doer), test.desc)
}
}
func TestNotifyWatchers(t *testing.T) { func TestNotifyWatchers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())

View File

@ -19,25 +19,40 @@ import (
func TestGetUserHeatmapDataByUser(t *testing.T) { func TestGetUserHeatmapDataByUser(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string
userID int64 userID int64
doerID int64 doerID int64
CountResult int CountResult int
JSONResult string JSONResult string
}{ }{
// self looks at action in private repo {
{2, 2, 1, `[{"timestamp":1603227600,"contributions":1}]`}, "self looks at action in private repo",
// admin looks at action in private repo 2, 2, 1, `[{"timestamp":1603227600,"contributions":1}]`,
{2, 1, 1, `[{"timestamp":1603227600,"contributions":1}]`}, },
// other user looks at action in private repo {
{2, 3, 0, `[]`}, "admin looks at action in private repo",
// nobody looks at action in private repo 2, 1, 1, `[{"timestamp":1603227600,"contributions":1}]`,
{2, 0, 0, `[]`}, },
// collaborator looks at action in private repo {
{16, 15, 1, `[{"timestamp":1603267200,"contributions":1}]`}, "other user looks at action in private repo",
// no action action not performed by target user 2, 3, 0, `[]`,
{3, 3, 0, `[]`}, },
// multiple actions performed with two grouped together {
{10, 10, 3, `[{"timestamp":1603009800,"contributions":1},{"timestamp":1603010700,"contributions":2}]`}, "nobody looks at action in private repo",
2, 0, 0, `[]`,
},
{
"collaborator looks at action in private repo",
16, 15, 1, `[{"timestamp":1603267200,"contributions":1}]`,
},
{
"no action action not performed by target user",
3, 3, 0, `[]`,
},
{
"multiple actions performed with two grouped together",
10, 10, 3, `[{"timestamp":1603009800,"contributions":1},{"timestamp":1603010700,"contributions":2}]`,
},
} }
// Prepare // Prepare
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
@ -46,7 +61,7 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
timeutil.Set(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)) timeutil.Set(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC))
defer timeutil.Unset() defer timeutil.Unset()
for i, tc := range testCases { for _, tc := range testCases {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.userID}).(*user_model.User) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.userID}).(*user_model.User)
doer := &user_model.User{ID: tc.doerID} doer := &user_model.User{ID: tc.doerID}
@ -74,7 +89,7 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
} }
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, actions, contributions, "invalid action count: did the test data became too old?") assert.Len(t, actions, contributions, "invalid action count: did the test data became too old?")
assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase %d", i)) assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase '%s'", tc.desc))
// Test JSON rendering // Test JSON rendering
jsonData, err := json.Marshal(heatmap) jsonData, err := json.Marshal(heatmap)

View File

@ -23,6 +23,8 @@ func RetrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) []*mode
return nil return nil
} }
// TODO: move load repoOwner of act.Repo into models.GetFeeds->loadAttributes()
{
userCache := map[int64]*user_model.User{options.RequestedUser.ID: options.RequestedUser} userCache := map[int64]*user_model.User{options.RequestedUser.ID: options.RequestedUser}
if ctx.User != nil { if ctx.User != nil {
userCache[ctx.User.ID] = ctx.User userCache[ctx.User.ID] = ctx.User
@ -32,7 +34,6 @@ func RetrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) []*mode
userCache[act.ActUserID] = act.ActUser userCache[act.ActUserID] = act.ActUser
} }
} }
for _, act := range actions { for _, act := range actions {
repoOwner, ok := userCache[act.Repo.OwnerID] repoOwner, ok := userCache[act.Repo.OwnerID]
if !ok { if !ok {
@ -48,6 +49,8 @@ func RetrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) []*mode
} }
act.Repo.Owner = repoOwner act.Repo.Owner = repoOwner
} }
}
return actions return actions
} }
@ -57,7 +60,7 @@ func ShowUserFeed(ctx *context.Context, ctxUser *user_model.User, formatType str
RequestedUser: ctxUser, RequestedUser: ctxUser,
Actor: ctx.User, Actor: ctx.User,
IncludePrivate: false, IncludePrivate: false,
OnlyPerformedBy: true, OnlyPerformedBy: !ctxUser.IsOrganization(),
IncludeDeleted: false, IncludeDeleted: false,
Date: ctx.FormString("date"), Date: ctx.FormString("date"),
}) })

View File

@ -94,14 +94,11 @@ func Profile(ctx *context.Context) {
} }
if ctxUser.IsOrganization() { if ctxUser.IsOrganization() {
/*
// TODO: enable after rss.RetrieveFeeds() do handle org correctly
// Show Org RSS feed // Show Org RSS feed
if len(showFeedType) != 0 { if len(showFeedType) != 0 {
rss.ShowUserFeed(ctx, ctxUser, showFeedType) feed.ShowUserFeed(ctx, ctxUser, showFeedType)
return return
} }
*/
org.Home(ctx) org.Home(ctx)
return return