Add pagination for dashboard and user activity feeds (#22937)

Previously only the last few activities where available. This works for
all activity and for activity on a date chosen on the heatmap.
This commit is contained in:
Brecht Van Lommel 2023-02-24 22:15:10 +01:00 committed by GitHub
parent 740a5ecdd9
commit f4920c9c7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 81 additions and 32 deletions

View File

@ -380,14 +380,14 @@ type GetFeedsOptions struct {
} }
// GetFeeds returns actions according to the provided options // GetFeeds returns actions according to the provided options
func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, error) { func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) {
if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == 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") return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
} }
cond, err := activityQueryCondition(opts) cond, err := activityQueryCondition(opts)
if err != nil { if err != nil {
return nil, err return nil, 0, err
} }
sess := db.GetEngine(ctx).Where(cond). sess := db.GetEngine(ctx).Where(cond).
@ -398,16 +398,16 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, error) {
sess = db.SetSessionPagination(sess, &opts) sess = db.SetSessionPagination(sess, &opts)
actions := make([]*Action, 0, opts.PageSize) actions := make([]*Action, 0, opts.PageSize)
count, err := sess.Desc("`action`.created_unix").FindAndCount(&actions)
if err := sess.Desc("`action`.created_unix").Find(&actions); err != nil { if err != nil {
return nil, fmt.Errorf("Find: %w", err) return nil, 0, fmt.Errorf("FindAndCount: %w", err)
} }
if err := ActionList(actions).loadAttributes(ctx); err != nil { if err := ActionList(actions).loadAttributes(ctx); err != nil {
return nil, fmt.Errorf("LoadAttributes: %w", err) return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
} }
return actions, nil return actions, count, nil
} }
// ActivityReadable return whether doer can read activities of user // ActivityReadable return whether doer can read activities of user

View File

@ -44,7 +44,7 @@ func TestGetFeeds(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
actions, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedUser: user, RequestedUser: user,
Actor: user, Actor: user,
IncludePrivate: true, IncludePrivate: true,
@ -56,8 +56,9 @@ func TestGetFeeds(t *testing.T) {
assert.EqualValues(t, 1, actions[0].ID) assert.EqualValues(t, 1, actions[0].ID)
assert.EqualValues(t, user.ID, actions[0].UserID) assert.EqualValues(t, user.ID, actions[0].UserID)
} }
assert.Equal(t, int64(1), count)
actions, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedUser: user, RequestedUser: user,
Actor: user, Actor: user,
IncludePrivate: false, IncludePrivate: false,
@ -65,6 +66,7 @@ func TestGetFeeds(t *testing.T) {
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, actions, 0) assert.Len(t, actions, 0)
assert.Equal(t, int64(0), count)
} }
func TestGetFeedsForRepos(t *testing.T) { func TestGetFeedsForRepos(t *testing.T) {
@ -74,38 +76,42 @@ func TestGetFeedsForRepos(t *testing.T) {
pubRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8}) pubRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8})
// private repo & no login // private repo & no login
actions, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedRepo: privRepo, RequestedRepo: privRepo,
IncludePrivate: true, IncludePrivate: true,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, actions, 0) assert.Len(t, actions, 0)
assert.Equal(t, int64(0), count)
// public repo & no login // public repo & no login
actions, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedRepo: pubRepo, RequestedRepo: pubRepo,
IncludePrivate: true, IncludePrivate: true,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, actions, 1) assert.Len(t, actions, 1)
assert.Equal(t, int64(1), count)
// private repo and login // private repo and login
actions, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedRepo: privRepo, RequestedRepo: privRepo,
IncludePrivate: true, IncludePrivate: true,
Actor: user, Actor: user,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, actions, 1) assert.Len(t, actions, 1)
assert.Equal(t, int64(1), count)
// public repo & login // public repo & login
actions, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedRepo: pubRepo, RequestedRepo: pubRepo,
IncludePrivate: true, IncludePrivate: true,
Actor: user, Actor: user,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, actions, 1) assert.Len(t, actions, 1)
assert.Equal(t, int64(1), count)
} }
func TestGetFeeds2(t *testing.T) { func TestGetFeeds2(t *testing.T) {
@ -114,7 +120,7 @@ func TestGetFeeds2(t *testing.T) {
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
actions, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedUser: org, RequestedUser: org,
Actor: user, Actor: user,
IncludePrivate: true, IncludePrivate: true,
@ -127,8 +133,9 @@ func TestGetFeeds2(t *testing.T) {
assert.EqualValues(t, 2, actions[0].ID) assert.EqualValues(t, 2, actions[0].ID)
assert.EqualValues(t, org.ID, actions[0].UserID) assert.EqualValues(t, org.ID, actions[0].UserID)
} }
assert.Equal(t, int64(1), count)
actions, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedUser: org, RequestedUser: org,
Actor: user, Actor: user,
IncludePrivate: false, IncludePrivate: false,
@ -137,6 +144,7 @@ func TestGetFeeds2(t *testing.T) {
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, actions, 0) assert.Len(t, actions, 0)
assert.Equal(t, int64(0), count)
} }
func TestActivityReadable(t *testing.T) { func TestActivityReadable(t *testing.T) {
@ -224,13 +232,14 @@ func TestGetFeedsCorrupted(t *testing.T) {
RepoID: 1700, RepoID: 1700,
}) })
actions, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedUser: user, RequestedUser: user,
Actor: user, Actor: user,
IncludePrivate: true, IncludePrivate: true,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, actions, 0) assert.Len(t, actions, 0)
assert.Equal(t, int64(0), count)
} }
func TestConsistencyUpdateAction(t *testing.T) { func TestConsistencyUpdateAction(t *testing.T) {

View File

@ -73,7 +73,7 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
} }
// get the action for comparison // get the action for comparison
actions, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedUser: user, RequestedUser: user,
Actor: doer, Actor: doer,
IncludePrivate: true, IncludePrivate: true,
@ -90,6 +90,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, count, int64(contributions))
assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase '%s'", tc.desc)) assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase '%s'", tc.desc))
// Test JSON rendering // Test JSON rendering

View File

@ -26,7 +26,7 @@ func ShowUserFeedAtom(ctx *context.Context) {
func showUserFeed(ctx *context.Context, formatType string) { func showUserFeed(ctx *context.Context, formatType string) {
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
actions, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ actions, _, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctx.ContextUser, RequestedUser: ctx.ContextUser,
Actor: ctx.Doer, Actor: ctx.Doer,
IncludePrivate: includePrivate, IncludePrivate: includePrivate,

View File

@ -15,7 +15,7 @@ import (
// ShowRepoFeed shows user activity on the repo as RSS / Atom feed // ShowRepoFeed shows user activity on the repo as RSS / Atom feed
func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType string) { func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType string) {
actions, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ actions, _, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
RequestedRepo: repo, RequestedRepo: repo,
Actor: ctx.Doer, Actor: ctx.Doer,
IncludePrivate: true, IncludePrivate: true,

View File

@ -72,12 +72,23 @@ func Dashboard(ctx *context.Context) {
return return
} }
var (
date = ctx.FormString("date")
page = ctx.FormInt("page")
)
// Make sure page number is at least 1. Will be posted to ctx.Data.
if page <= 1 {
page = 1
}
ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard") ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard")
ctx.Data["PageIsDashboard"] = true ctx.Data["PageIsDashboard"] = true
ctx.Data["PageIsNews"] = true ctx.Data["PageIsNews"] = true
cnt, _ := organization.GetOrganizationCount(ctx, ctxUser) cnt, _ := organization.GetOrganizationCount(ctx, ctxUser)
ctx.Data["UserOrgsCount"] = cnt ctx.Data["UserOrgsCount"] = cnt
ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
ctx.Data["Date"] = date
var uid int64 var uid int64
if ctxUser != nil { if ctxUser != nil {
@ -98,8 +109,7 @@ func Dashboard(ctx *context.Context) {
ctx.Data["HeatmapData"] = data ctx.Data["HeatmapData"] = data
} }
var err error feeds, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
ctx.Data["Feeds"], err = activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctxUser, RequestedUser: ctxUser,
RequestedTeam: ctx.Org.Team, RequestedTeam: ctx.Org.Team,
Actor: ctx.Doer, Actor: ctx.Doer,
@ -107,13 +117,22 @@ func Dashboard(ctx *context.Context) {
OnlyPerformedBy: false, OnlyPerformedBy: false,
IncludeDeleted: false, IncludeDeleted: false,
Date: ctx.FormString("date"), Date: ctx.FormString("date"),
ListOptions: db.ListOptions{PageSize: setting.UI.FeedPagingNum}, ListOptions: db.ListOptions{
Page: page,
PageSize: setting.UI.FeedPagingNum,
},
}) })
if err != nil { if err != nil {
ctx.ServerError("GetFeeds", err) ctx.ServerError("GetFeeds", err)
return return
} }
ctx.Data["Feeds"] = feeds
pager := context.NewPagination(int(count), setting.UI.FeedPagingNum, page, 5)
pager.AddParam(ctx, "date", "Date")
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplDashboard) ctx.HTML(http.StatusOK, tplDashboard)
} }

View File

@ -119,6 +119,11 @@ func Profile(ctx *context.Context) {
page = 1 page = 1
} }
pagingNum := setting.UI.User.RepoPagingNum
if tab == "activity" {
pagingNum = setting.UI.FeedPagingNum
}
topicOnly := ctx.FormBool("topic") topicOnly := ctx.FormBool("topic")
var ( var (
@ -164,7 +169,7 @@ func Profile(ctx *context.Context) {
switch tab { switch tab {
case "followers": case "followers":
items, count, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{ items, count, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum, PageSize: pagingNum,
Page: page, Page: page,
}) })
if err != nil { if err != nil {
@ -176,7 +181,7 @@ func Profile(ctx *context.Context) {
total = int(count) total = int(count)
case "following": case "following":
items, count, err := user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{ items, count, err := user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum, PageSize: pagingNum,
Page: page, Page: page,
}) })
if err != nil { if err != nil {
@ -187,24 +192,32 @@ func Profile(ctx *context.Context) {
total = int(count) total = int(count)
case "activity": case "activity":
ctx.Data["Feeds"], err = activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ date := ctx.FormString("date")
items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctx.ContextUser, RequestedUser: ctx.ContextUser,
Actor: ctx.Doer, Actor: ctx.Doer,
IncludePrivate: showPrivate, IncludePrivate: showPrivate,
OnlyPerformedBy: true, OnlyPerformedBy: true,
IncludeDeleted: false, IncludeDeleted: false,
Date: ctx.FormString("date"), Date: date,
ListOptions: db.ListOptions{PageSize: setting.UI.FeedPagingNum}, ListOptions: db.ListOptions{
PageSize: pagingNum,
Page: page,
},
}) })
if err != nil { if err != nil {
ctx.ServerError("GetFeeds", err) ctx.ServerError("GetFeeds", err)
return return
} }
ctx.Data["Feeds"] = items
ctx.Data["Date"] = date
total = int(count)
case "stars": case "stars":
ctx.Data["PageIsProfileStarList"] = true ctx.Data["PageIsProfileStarList"] = true
repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{ ListOptions: db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum, PageSize: pagingNum,
Page: page, Page: page,
}, },
Actor: ctx.Doer, Actor: ctx.Doer,
@ -236,7 +249,7 @@ func Profile(ctx *context.Context) {
case "watching": case "watching":
repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{ ListOptions: db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum, PageSize: pagingNum,
Page: page, Page: page,
}, },
Actor: ctx.Doer, Actor: ctx.Doer,
@ -258,7 +271,7 @@ func Profile(ctx *context.Context) {
default: default:
repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{ ListOptions: db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum, PageSize: pagingNum,
Page: page, Page: page,
}, },
Actor: ctx.Doer, Actor: ctx.Doer,
@ -281,12 +294,15 @@ func Profile(ctx *context.Context) {
ctx.Data["Repos"] = repos ctx.Data["Repos"] = repos
ctx.Data["Total"] = total ctx.Data["Total"] = total
pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5) pager := context.NewPagination(total, pagingNum, page, 5)
pager.SetDefaultParams(ctx) pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "tab", "TabName") pager.AddParam(ctx, "tab", "TabName")
if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" { if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" {
pager.AddParam(ctx, "language", "Language") pager.AddParam(ctx, "language", "Language")
} }
if tab == "activity" {
pager.AddParam(ctx, "date", "Date")
}
ctx.Data["Page"] = pager ctx.Data["Page"] = pager
ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled

View File

@ -124,3 +124,5 @@
<div class="ui divider"></div> <div class="ui divider"></div>
</div> </div>
{{end}} {{end}}
{{template "base/paginate" .}}

View File

@ -70,6 +70,8 @@ export default {
params.set('date', clickedDate); params.set('date', clickedDate);
} }
params.delete('page');
const newSearch = params.toString(); const newSearch = params.toString();
window.location.search = newSearch.length ? `?${newSearch}` : ''; window.location.search = newSearch.length ? `?${newSearch}` : '';
} }