From f4920c9c7f5947d3b6476610f39bc3492ab4ef3b Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Fri, 24 Feb 2023 22:15:10 +0100 Subject: [PATCH] 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. --- models/activities/action.go | 16 +++++------ models/activities/action_test.go | 27 ++++++++++++------ models/activities/user_heatmap_test.go | 3 +- routers/web/feed/profile.go | 2 +- routers/web/feed/repo.go | 2 +- routers/web/user/home.go | 25 +++++++++++++++-- routers/web/user/profile.go | 34 +++++++++++++++++------ templates/user/dashboard/feeds.tmpl | 2 ++ web_src/js/components/ActivityHeatmap.vue | 2 ++ 9 files changed, 81 insertions(+), 32 deletions(-) diff --git a/models/activities/action.go b/models/activities/action.go index 2e845bf89..1412d2c05 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -380,14 +380,14 @@ type GetFeedsOptions struct { } // 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 { - 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) if err != nil { - return nil, err + return nil, 0, err } sess := db.GetEngine(ctx).Where(cond). @@ -398,16 +398,16 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, error) { sess = db.SetSessionPagination(sess, &opts) actions := make([]*Action, 0, opts.PageSize) - - if err := sess.Desc("`action`.created_unix").Find(&actions); err != nil { - return nil, fmt.Errorf("Find: %w", err) + count, err := sess.Desc("`action`.created_unix").FindAndCount(&actions) + if err != nil { + return nil, 0, fmt.Errorf("FindAndCount: %w", err) } 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 diff --git a/models/activities/action_test.go b/models/activities/action_test.go index f37e58f68..2fd86bb8f 100644 --- a/models/activities/action_test.go +++ b/models/activities/action_test.go @@ -44,7 +44,7 @@ func TestGetFeeds(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) 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, Actor: user, IncludePrivate: true, @@ -56,8 +56,9 @@ func TestGetFeeds(t *testing.T) { assert.EqualValues(t, 1, actions[0].ID) 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, Actor: user, IncludePrivate: false, @@ -65,6 +66,7 @@ func TestGetFeeds(t *testing.T) { }) assert.NoError(t, err) assert.Len(t, actions, 0) + assert.Equal(t, int64(0), count) } func TestGetFeedsForRepos(t *testing.T) { @@ -74,38 +76,42 @@ func TestGetFeedsForRepos(t *testing.T) { pubRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8}) // 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, IncludePrivate: true, }) assert.NoError(t, err) assert.Len(t, actions, 0) + assert.Equal(t, int64(0), count) // 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, IncludePrivate: true, }) assert.NoError(t, err) assert.Len(t, actions, 1) + assert.Equal(t, int64(1), count) // 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, IncludePrivate: true, Actor: user, }) assert.NoError(t, err) assert.Len(t, actions, 1) + assert.Equal(t, int64(1), count) // 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, IncludePrivate: true, Actor: user, }) assert.NoError(t, err) assert.Len(t, actions, 1) + assert.Equal(t, int64(1), count) } func TestGetFeeds2(t *testing.T) { @@ -114,7 +120,7 @@ func TestGetFeeds2(t *testing.T) { org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) 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, Actor: user, IncludePrivate: true, @@ -127,8 +133,9 @@ func TestGetFeeds2(t *testing.T) { assert.EqualValues(t, 2, actions[0].ID) 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, Actor: user, IncludePrivate: false, @@ -137,6 +144,7 @@ func TestGetFeeds2(t *testing.T) { }) assert.NoError(t, err) assert.Len(t, actions, 0) + assert.Equal(t, int64(0), count) } func TestActivityReadable(t *testing.T) { @@ -224,13 +232,14 @@ func TestGetFeedsCorrupted(t *testing.T) { 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, Actor: user, IncludePrivate: true, }) assert.NoError(t, err) assert.Len(t, actions, 0) + assert.Equal(t, int64(0), count) } func TestConsistencyUpdateAction(t *testing.T) { diff --git a/models/activities/user_heatmap_test.go b/models/activities/user_heatmap_test.go index 34715ab6d..98df7b38a 100644 --- a/models/activities/user_heatmap_test.go +++ b/models/activities/user_heatmap_test.go @@ -73,7 +73,7 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { } // 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, Actor: doer, IncludePrivate: true, @@ -90,6 +90,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, count, int64(contributions)) assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase '%s'", tc.desc)) // Test JSON rendering diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go index 764176919..b9dda2fc1 100644 --- a/routers/web/feed/profile.go +++ b/routers/web/feed/profile.go @@ -26,7 +26,7 @@ func ShowUserFeedAtom(ctx *context.Context) { func showUserFeed(ctx *context.Context, formatType string) { 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, Actor: ctx.Doer, IncludePrivate: includePrivate, diff --git a/routers/web/feed/repo.go b/routers/web/feed/repo.go index 1d24b5880..5fcad2677 100644 --- a/routers/web/feed/repo.go +++ b/routers/web/feed/repo.go @@ -15,7 +15,7 @@ import ( // ShowRepoFeed shows user activity on the repo as RSS / Atom feed 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, Actor: ctx.Doer, IncludePrivate: true, diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 4f45c1d5c..2593ab148 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -72,12 +72,23 @@ func Dashboard(ctx *context.Context) { 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["PageIsDashboard"] = true ctx.Data["PageIsNews"] = true cnt, _ := organization.GetOrganizationCount(ctx, ctxUser) ctx.Data["UserOrgsCount"] = cnt ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled + ctx.Data["Date"] = date var uid int64 if ctxUser != nil { @@ -98,8 +109,7 @@ func Dashboard(ctx *context.Context) { ctx.Data["HeatmapData"] = data } - var err error - ctx.Data["Feeds"], err = activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ + feeds, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ RequestedUser: ctxUser, RequestedTeam: ctx.Org.Team, Actor: ctx.Doer, @@ -107,13 +117,22 @@ func Dashboard(ctx *context.Context) { OnlyPerformedBy: false, IncludeDeleted: false, Date: ctx.FormString("date"), - ListOptions: db.ListOptions{PageSize: setting.UI.FeedPagingNum}, + ListOptions: db.ListOptions{ + Page: page, + PageSize: setting.UI.FeedPagingNum, + }, }) if err != nil { ctx.ServerError("GetFeeds", err) 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) } diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 4f0a81656..b44526045 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -119,6 +119,11 @@ func Profile(ctx *context.Context) { page = 1 } + pagingNum := setting.UI.User.RepoPagingNum + if tab == "activity" { + pagingNum = setting.UI.FeedPagingNum + } + topicOnly := ctx.FormBool("topic") var ( @@ -164,7 +169,7 @@ func Profile(ctx *context.Context) { switch tab { case "followers": items, count, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{ - PageSize: setting.UI.User.RepoPagingNum, + PageSize: pagingNum, Page: page, }) if err != nil { @@ -176,7 +181,7 @@ func Profile(ctx *context.Context) { total = int(count) case "following": items, count, err := user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{ - PageSize: setting.UI.User.RepoPagingNum, + PageSize: pagingNum, Page: page, }) if err != nil { @@ -187,24 +192,32 @@ func Profile(ctx *context.Context) { total = int(count) 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, Actor: ctx.Doer, IncludePrivate: showPrivate, OnlyPerformedBy: true, IncludeDeleted: false, - Date: ctx.FormString("date"), - ListOptions: db.ListOptions{PageSize: setting.UI.FeedPagingNum}, + Date: date, + ListOptions: db.ListOptions{ + PageSize: pagingNum, + Page: page, + }, }) if err != nil { ctx.ServerError("GetFeeds", err) return } + ctx.Data["Feeds"] = items + ctx.Data["Date"] = date + + total = int(count) case "stars": ctx.Data["PageIsProfileStarList"] = true repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ - PageSize: setting.UI.User.RepoPagingNum, + PageSize: pagingNum, Page: page, }, Actor: ctx.Doer, @@ -236,7 +249,7 @@ func Profile(ctx *context.Context) { case "watching": repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ - PageSize: setting.UI.User.RepoPagingNum, + PageSize: pagingNum, Page: page, }, Actor: ctx.Doer, @@ -258,7 +271,7 @@ func Profile(ctx *context.Context) { default: repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ - PageSize: setting.UI.User.RepoPagingNum, + PageSize: pagingNum, Page: page, }, Actor: ctx.Doer, @@ -281,12 +294,15 @@ func Profile(ctx *context.Context) { ctx.Data["Repos"] = repos 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.AddParam(ctx, "tab", "TabName") if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" { pager.AddParam(ctx, "language", "Language") } + if tab == "activity" { + pager.AddParam(ctx, "date", "Date") + } ctx.Data["Page"] = pager ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index 3f156249f..75a76db57 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -124,3 +124,5 @@
{{end}} + +{{template "base/paginate" .}} diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue index d0f81c586..98ffce44b 100644 --- a/web_src/js/components/ActivityHeatmap.vue +++ b/web_src/js/components/ActivityHeatmap.vue @@ -70,6 +70,8 @@ export default { params.set('date', clickedDate); } + params.delete('page'); + const newSearch = params.toString(); window.location.search = newSearch.length ? `?${newSearch}` : ''; }