@@ -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" | |||
@@ -315,19 +316,21 @@ 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) | |||
@@ -335,9 +338,14 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) { | |||
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) | |||
} | |||
@@ -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 { | |||
@@ -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}) | |||
} |
@@ -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()) | |||
@@ -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()) | |||
@@ -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} | |||
@@ -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) |
@@ -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() | |||
{ | |||
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 | |||
} | |||
@@ -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"), | |||
}) |
@@ -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 |