code.gitea.io/gitea@v1.21.7/routers/web/user/profile.go (about) 1 // Copyright 2015 The Gogs Authors. All rights reserved. 2 // Copyright 2019 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package user 6 7 import ( 8 "fmt" 9 "net/http" 10 "path" 11 "strings" 12 13 activities_model "code.gitea.io/gitea/models/activities" 14 "code.gitea.io/gitea/models/db" 15 repo_model "code.gitea.io/gitea/models/repo" 16 user_model "code.gitea.io/gitea/models/user" 17 "code.gitea.io/gitea/modules/context" 18 "code.gitea.io/gitea/modules/git" 19 "code.gitea.io/gitea/modules/log" 20 "code.gitea.io/gitea/modules/markup" 21 "code.gitea.io/gitea/modules/markup/markdown" 22 "code.gitea.io/gitea/modules/setting" 23 "code.gitea.io/gitea/modules/util" 24 "code.gitea.io/gitea/routers/web/feed" 25 "code.gitea.io/gitea/routers/web/org" 26 shared_user "code.gitea.io/gitea/routers/web/shared/user" 27 ) 28 29 // OwnerProfile render profile page for a user or a organization (aka, repo owner) 30 func OwnerProfile(ctx *context.Context) { 31 if strings.Contains(ctx.Req.Header.Get("Accept"), "application/rss+xml") { 32 feed.ShowUserFeedRSS(ctx) 33 return 34 } 35 if strings.Contains(ctx.Req.Header.Get("Accept"), "application/atom+xml") { 36 feed.ShowUserFeedAtom(ctx) 37 return 38 } 39 40 if ctx.ContextUser.IsOrganization() { 41 org.Home(ctx) 42 } else { 43 userProfile(ctx) 44 } 45 } 46 47 func userProfile(ctx *context.Context) { 48 // check view permissions 49 if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) { 50 ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name)) 51 return 52 } 53 54 ctx.Data["Title"] = ctx.ContextUser.DisplayName() 55 ctx.Data["PageIsUserProfile"] = true 56 57 // prepare heatmap data 58 if setting.Service.EnableUserHeatmap { 59 data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) 60 if err != nil { 61 ctx.ServerError("GetUserHeatmapDataByUser", err) 62 return 63 } 64 ctx.Data["HeatmapData"] = data 65 ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) 66 } 67 68 profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx) 69 defer profileClose() 70 71 showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) 72 prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileGitRepo, profileReadmeBlob) 73 // call PrepareContextForProfileBigAvatar later to avoid re-querying the NumFollowers & NumFollowing 74 shared_user.PrepareContextForProfileBigAvatar(ctx) 75 ctx.HTML(http.StatusOK, tplProfile) 76 } 77 78 func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadme *git.Blob) { 79 // if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page 80 // if there is not a profile readme, the overview tab should be treated as the repositories tab 81 tab := ctx.FormString("tab") 82 if tab == "" || tab == "overview" { 83 if profileReadme != nil { 84 tab = "overview" 85 } else { 86 tab = "repositories" 87 } 88 } 89 ctx.Data["TabName"] = tab 90 ctx.Data["HasProfileReadme"] = profileReadme != nil 91 92 page := ctx.FormInt("page") 93 if page <= 0 { 94 page = 1 95 } 96 97 pagingNum := setting.UI.User.RepoPagingNum 98 topicOnly := ctx.FormBool("topic") 99 var ( 100 repos []*repo_model.Repository 101 count int64 102 total int 103 orderBy db.SearchOrderBy 104 ) 105 106 ctx.Data["SortType"] = ctx.FormString("sort") 107 switch ctx.FormString("sort") { 108 case "newest": 109 orderBy = db.SearchOrderByNewest 110 case "oldest": 111 orderBy = db.SearchOrderByOldest 112 case "recentupdate": 113 orderBy = db.SearchOrderByRecentUpdated 114 case "leastupdate": 115 orderBy = db.SearchOrderByLeastUpdated 116 case "reversealphabetically": 117 orderBy = db.SearchOrderByAlphabeticallyReverse 118 case "alphabetically": 119 orderBy = db.SearchOrderByAlphabetically 120 case "moststars": 121 orderBy = db.SearchOrderByStarsReverse 122 case "feweststars": 123 orderBy = db.SearchOrderByStars 124 case "mostforks": 125 orderBy = db.SearchOrderByForksReverse 126 case "fewestforks": 127 orderBy = db.SearchOrderByForks 128 default: 129 ctx.Data["SortType"] = "recentupdate" 130 orderBy = db.SearchOrderByRecentUpdated 131 } 132 133 keyword := ctx.FormTrim("q") 134 ctx.Data["Keyword"] = keyword 135 136 language := ctx.FormTrim("language") 137 ctx.Data["Language"] = language 138 139 followers, numFollowers, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{ 140 PageSize: pagingNum, 141 Page: page, 142 }) 143 if err != nil { 144 ctx.ServerError("GetUserFollowers", err) 145 return 146 } 147 ctx.Data["NumFollowers"] = numFollowers 148 following, numFollowing, err := user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{ 149 PageSize: pagingNum, 150 Page: page, 151 }) 152 if err != nil { 153 ctx.ServerError("GetUserFollowing", err) 154 return 155 } 156 ctx.Data["NumFollowing"] = numFollowing 157 158 switch tab { 159 case "followers": 160 ctx.Data["Cards"] = followers 161 total = int(numFollowers) 162 case "following": 163 ctx.Data["Cards"] = following 164 total = int(numFollowing) 165 case "activity": 166 date := ctx.FormString("date") 167 pagingNum = setting.UI.FeedPagingNum 168 items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ 169 RequestedUser: ctx.ContextUser, 170 Actor: ctx.Doer, 171 IncludePrivate: showPrivate, 172 OnlyPerformedBy: true, 173 IncludeDeleted: false, 174 Date: date, 175 ListOptions: db.ListOptions{ 176 PageSize: pagingNum, 177 Page: page, 178 }, 179 }) 180 if err != nil { 181 ctx.ServerError("GetFeeds", err) 182 return 183 } 184 ctx.Data["Feeds"] = items 185 ctx.Data["Date"] = date 186 187 total = int(count) 188 case "stars": 189 ctx.Data["PageIsProfileStarList"] = true 190 repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ 191 ListOptions: db.ListOptions{ 192 PageSize: pagingNum, 193 Page: page, 194 }, 195 Actor: ctx.Doer, 196 Keyword: keyword, 197 OrderBy: orderBy, 198 Private: ctx.IsSigned, 199 StarredByID: ctx.ContextUser.ID, 200 Collaborate: util.OptionalBoolFalse, 201 TopicOnly: topicOnly, 202 Language: language, 203 IncludeDescription: setting.UI.SearchRepoDescription, 204 }) 205 if err != nil { 206 ctx.ServerError("SearchRepository", err) 207 return 208 } 209 210 total = int(count) 211 case "watching": 212 repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ 213 ListOptions: db.ListOptions{ 214 PageSize: pagingNum, 215 Page: page, 216 }, 217 Actor: ctx.Doer, 218 Keyword: keyword, 219 OrderBy: orderBy, 220 Private: ctx.IsSigned, 221 WatchedByID: ctx.ContextUser.ID, 222 Collaborate: util.OptionalBoolFalse, 223 TopicOnly: topicOnly, 224 Language: language, 225 IncludeDescription: setting.UI.SearchRepoDescription, 226 }) 227 if err != nil { 228 ctx.ServerError("SearchRepository", err) 229 return 230 } 231 232 total = int(count) 233 case "overview": 234 if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { 235 log.Error("failed to GetBlobContent: %v", err) 236 } else { 237 if profileContent, err := markdown.RenderString(&markup.RenderContext{ 238 Ctx: ctx, 239 GitRepo: profileGitRepo, 240 Links: markup.Links{ 241 // Give the repo link to the markdown render for the full link of media element. 242 // the media link usually be like /[user]/[repoName]/media/branch/[branchName], 243 // Eg. /Tom/.profile/media/branch/main 244 // The branch shown on the profile page is the default branch, this need to be in sync with doc, see: 245 // https://docs.gitea.com/usage/profile-readme 246 Base: profileDbRepo.Link(), 247 BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), 248 }, 249 Metas: map[string]string{"mode": "document"}, 250 }, bytes); err != nil { 251 log.Error("failed to RenderString: %v", err) 252 } else { 253 ctx.Data["ProfileReadme"] = profileContent 254 } 255 } 256 default: // default to "repositories" 257 repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ 258 ListOptions: db.ListOptions{ 259 PageSize: pagingNum, 260 Page: page, 261 }, 262 Actor: ctx.Doer, 263 Keyword: keyword, 264 OwnerID: ctx.ContextUser.ID, 265 OrderBy: orderBy, 266 Private: ctx.IsSigned, 267 Collaborate: util.OptionalBoolFalse, 268 TopicOnly: topicOnly, 269 Language: language, 270 IncludeDescription: setting.UI.SearchRepoDescription, 271 }) 272 if err != nil { 273 ctx.ServerError("SearchRepository", err) 274 return 275 } 276 277 total = int(count) 278 } 279 ctx.Data["Repos"] = repos 280 ctx.Data["Total"] = total 281 282 err = shared_user.LoadHeaderCount(ctx) 283 if err != nil { 284 ctx.ServerError("LoadHeaderCount", err) 285 return 286 } 287 288 pager := context.NewPagination(total, pagingNum, page, 5) 289 pager.SetDefaultParams(ctx) 290 pager.AddParam(ctx, "tab", "TabName") 291 if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" { 292 pager.AddParam(ctx, "language", "Language") 293 } 294 if tab == "activity" { 295 pager.AddParam(ctx, "date", "Date") 296 } 297 ctx.Data["Page"] = pager 298 } 299 300 // Action response for follow/unfollow user request 301 func Action(ctx *context.Context) { 302 var err error 303 switch ctx.FormString("action") { 304 case "follow": 305 err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID) 306 case "unfollow": 307 err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID) 308 } 309 310 if err != nil { 311 log.Error("Failed to apply action %q: %v", ctx.FormString("action"), err) 312 ctx.JSONError(fmt.Sprintf("Action %q failed", ctx.FormString("action"))) 313 return 314 } 315 ctx.JSONOK() 316 }