Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSS/Atom support for Orgs #17714

Merged
merged 25 commits into from
Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 54 additions & 40 deletions models/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"

Expand Down Expand Up @@ -315,29 +316,36 @@ func (a *Action) GetIssueContent() string {

// GetFeedsOptions options for retrieving feeds
type GetFeedsOptions struct {
RequestedUser *user_model.User // the user we want activity for
RequestedTeam *Team // the team we want activity for
Actor *user_model.User // the user viewing the activity
IncludePrivate bool // include private actions
OnlyPerformedBy bool // only actions performed by requested user
IncludeDeleted bool // include deleted actions
Date string // the day we want activity for: YYYY-MM-DD
db.ListOptions
RequestedUser *user_model.User // the user 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
IncludePrivate bool // include private actions
OnlyPerformedBy bool // only actions performed by requested user
IncludeDeleted bool // include deleted actions
Date string // the day we want activity for: YYYY-MM-DD
}

// GetFeeds returns actions according to the provided options
func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
if !activityReadable(opts.RequestedUser, opts.Actor) {
return make([]*Action, 0), nil
if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil {
return nil, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
}

cond, err := activityQueryCondition(opts)
if err != nil {
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)
}

Expand All @@ -349,41 +357,44 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
}

func activityReadable(user, doer *user_model.User) bool {
var doerID int64
if doer != nil {
doerID = doer.ID
}
if doer == nil || !doer.IsAdmin {
if user.KeepActivityPrivate && doerID != user.ID {
return false
}
}
return true
return !user.KeepActivityPrivate ||
doer != nil && (doer.IsAdmin || user.ID == doer.ID)
}

func activityQueryCondition(opts GetFeedsOptions) (builder.Cond, error) {
cond := builder.NewCond()

var repoIDs []int64
var actorID int64
if opts.Actor != nil {
actorID = opts.Actor.ID
if opts.RequestedTeam != nil && opts.RequestedUser == nil {
org, err := user_model.GetUserByID(opts.RequestedTeam.OrgID)
if err != nil {
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
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 {
Expand All @@ -395,11 +406,14 @@ func activityQueryCondition(opts GetFeedsOptions) (builder.Cond, error) {
cond = cond.And(builder.In("repo_id", teamRepoIDs))
}

cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})
if opts.RequestedUser != nil {
cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})

if opts.OnlyPerformedBy {
cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID})
if opts.OnlyPerformedBy {
cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID})
}
}

if !opts.IncludePrivate {
cond = cond.And(builder.Eq{"is_private": false})
}
Expand Down
40 changes: 40 additions & 0 deletions models/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,46 @@ func TestGetFeeds2(t *testing.T) {
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) {
assert.NoError(t, unittest.PrepareTestDatabase())

Expand Down
47 changes: 31 additions & 16 deletions models/user_heatmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,40 @@ import (

func TestGetUserHeatmapDataByUser(t *testing.T) {
testCases := []struct {
desc string
userID int64
doerID int64
CountResult int
JSONResult string
}{
// self looks at action in private repo
{2, 2, 1, `[{"timestamp":1603227600,"contributions":1}]`},
// admin looks at action in private repo
{2, 1, 1, `[{"timestamp":1603227600,"contributions":1}]`},
// other user looks at action in private repo
{2, 3, 0, `[]`},
// 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}]`},
{
"self looks at action in private repo",
2, 2, 1, `[{"timestamp":1603227600,"contributions":1}]`,
},
{
"admin looks at action in private repo",
2, 1, 1, `[{"timestamp":1603227600,"contributions":1}]`,
},
{
"other user looks at action in private repo",
2, 3, 0, `[]`,
},
{
"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
assert.NoError(t, unittest.PrepareTestDatabase())
Expand All @@ -46,7 +61,7 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
timeutil.Set(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC))
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)

doer := &user_model.User{ID: tc.doerID}
Expand Down Expand Up @@ -74,7 +89,7 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
}
assert.NoError(t, err)
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
jsonData, err := json.Marshal(heatmap)
Expand Down
45 changes: 24 additions & 21 deletions routers/web/feed/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,34 @@ func RetrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) []*mode
return nil
}

userCache := map[int64]*user_model.User{options.RequestedUser.ID: options.RequestedUser}
if ctx.User != nil {
userCache[ctx.User.ID] = ctx.User
}
for _, act := range actions {
if act.ActUser != nil {
userCache[act.ActUserID] = act.ActUser
// TODO: move load repoOwner of act.Repo into models.GetFeeds->loadAttributes()
{
zeripath marked this conversation as resolved.
Show resolved Hide resolved
userCache := map[int64]*user_model.User{options.RequestedUser.ID: options.RequestedUser}
if ctx.User != nil {
userCache[ctx.User.ID] = ctx.User
}
}

for _, act := range actions {
repoOwner, ok := userCache[act.Repo.OwnerID]
if !ok {
repoOwner, err = user_model.GetUserByID(act.Repo.OwnerID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
continue
for _, act := range actions {
if act.ActUser != nil {
userCache[act.ActUserID] = act.ActUser
}
}
for _, act := range actions {
repoOwner, ok := userCache[act.Repo.OwnerID]
if !ok {
repoOwner, err = user_model.GetUserByID(act.Repo.OwnerID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
continue
}
ctx.ServerError("GetUserByID", err)
return nil
}
ctx.ServerError("GetUserByID", err)
return nil
userCache[repoOwner.ID] = repoOwner
}
userCache[repoOwner.ID] = repoOwner
act.Repo.Owner = repoOwner
}
act.Repo.Owner = repoOwner
}

return actions
}

Expand All @@ -57,7 +60,7 @@ func ShowUserFeed(ctx *context.Context, ctxUser *user_model.User, formatType str
RequestedUser: ctxUser,
Actor: ctx.User,
IncludePrivate: false,
OnlyPerformedBy: true,
OnlyPerformedBy: !ctxUser.IsOrganization(),
IncludeDeleted: false,
Date: ctx.FormString("date"),
})
Expand Down
13 changes: 5 additions & 8 deletions routers/web/user/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,11 @@ func Profile(ctx *context.Context) {
}

if ctxUser.IsOrganization() {
/*
// TODO: enable after rss.RetrieveFeeds() do handle org correctly
// Show Org RSS feed
if len(showFeedType) != 0 {
rss.ShowUserFeed(ctx, ctxUser, showFeedType)
return
}
*/
// Show Org RSS feed
if len(showFeedType) != 0 {
feed.ShowUserFeed(ctx, ctxUser, showFeedType)
return
}

org.Home(ctx)
return
Expand Down