Skip to content

[Feature] Private README.md for organization #32872

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

Merged
merged 24 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
784bedd
Use view as to get public and private profile repo
Dec 16, 2024
3682adb
make query string right
Dec 17, 2024
ef826a1
make sure the drop down appears only when criteria are met
Dec 17, 2024
6496b3c
Add hint for public and private profile repo name
changchaishi Dec 20, 2024
4b3a8ac
fix that drop down manu shows in repository tab
changchaishi Dec 20, 2024
d71cba7
add check icons and translation
changchaishi Dec 20, 2024
86e698d
add rollback to get private profile when view as is not present
changchaishi Dec 23, 2024
e95d716
Ignore view_as param when the profiles not both exist
changchaishi Dec 23, 2024
48b4be2
adding profile test case
changchaishi Dec 28, 2024
84ef527
use tw-hidden instead of display:none
changchaishi Dec 29, 2024
0c70525
Merge branch 'main' into private-readme
wxiaoguang Dec 30, 2024
a47cf32
temp
wxiaoguang Dec 30, 2024
586d304
temp
wxiaoguang Dec 30, 2024
0202a09
refactor git repo usage
wxiaoguang Dec 30, 2024
93e7a01
move view-as dropdown to sidebar
wxiaoguang Dec 30, 2024
ae3627f
clarify HasOrgProfileReadme vs HasUserProfileReadme, fix tests
wxiaoguang Dec 30, 2024
dc10881
fix comments
wxiaoguang Dec 30, 2024
db2c451
fix js
wxiaoguang Dec 30, 2024
b9b2701
fix mistake
wxiaoguang Dec 30, 2024
873dc8f
improve tests
wxiaoguang Dec 30, 2024
c1ae1ad
auto toggle "private" checkbox by repo name
wxiaoguang Dec 31, 2024
95312c7
make "view as" translatable
wxiaoguang Dec 31, 2024
bc5718d
fine tune "view as"
wxiaoguang Dec 31, 2024
2c0e19d
Merge branch 'main' into private-readme
wxiaoguang Dec 31, 2024
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
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,8 @@ new_repo_helper = A repository contains all project files, including revision hi
owner = Owner
owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit.
repo_name = Repository Name
repo_name_public_profile_hint=.profile is a special repository that you can use to add a README.md to your personal or organization publiv profile. Make sure it's public and initialize it with a README to get started.
repo_name_private_profile_hint=.profile-private is a special repository that you can use to add a README.md to your organization member profile. Make sure it's private and initialize it with a README to get started.
repo_name_helper = Good repository names use short, memorable and unique keywords.
repo_size = Repository Size
template = Template
Expand Down
49 changes: 39 additions & 10 deletions routers/web/org/home.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package org

import (
"fmt"
html_template "html/template"
"net/http"
"path"
"strings"
Expand Down Expand Up @@ -111,8 +112,38 @@ func home(ctx *context.Context, viewRepositories bool) {
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0

if !prepareOrgProfileReadme(ctx, viewRepositories) {
ctx.Data["PageIsViewRepositories"] = true
currentURL := ctx.Req.URL
queryParams := currentURL.Query()
queryParams.Set("view_as", "member")
ctx.Data["QueryForMember"] = html_template.URL(queryParams.Encode())
queryParams.Set("view_as", "public")
ctx.Data["QueryForPublic"] = html_template.URL(queryParams.Encode())

err = shared_user.RenderOrgHeader(ctx)
if err != nil {
ctx.ServerError("RenderOrgHeader", err)
return
}
isBothProfilesExist := ctx.Data["HasPublicProfileReadme"] == true && ctx.Data["HasPrivateProfileReadme"] == true

isViewerMember := ctx.FormString("view_as")
ctx.Data["IsViewerMember"] = isViewerMember == "member"

profileType := "Public"
if isViewerMember == "member" {
profileType = "Private"
}

if !isBothProfilesExist {
if !prepareOrgProfileReadme(ctx, viewRepositories, "Public") {
if !prepareOrgProfileReadme(ctx, viewRepositories, "Private") {
ctx.Data["PageIsViewRepositories"] = true
}
}
} else {
if !prepareOrgProfileReadme(ctx, viewRepositories, profileType) {
ctx.Data["PageIsViewRepositories"] = true
}
}

var (
Expand Down Expand Up @@ -168,28 +199,26 @@ func home(ctx *context.Context, viewRepositories bool) {
ctx.HTML(http.StatusOK, tplOrgHome)
}

func prepareOrgProfileReadme(ctx *context.Context, viewRepositories bool) bool {
profileDbRepo, profileGitRepo, profileReadme, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer)
func prepareOrgProfileReadme(ctx *context.Context, viewRepositories bool, profileType string) bool {
profileDbRepo, profileGitRepo, profileReadme, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer, profileType)
defer profileClose()
ctx.Data["HasProfileReadme"] = profileReadme != nil
ctx.Data[fmt.Sprintf("Has%sProfileReadme", profileType)] = profileReadme != nil

if profileGitRepo == nil || profileReadme == nil || viewRepositories {
return false
}

if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
log.Error("failed to GetBlobContent: %v", err)
log.Error("failed to GetBlobContent for %s profile readme: %v", profileType, err)
} else {
rctx := renderhelper.NewRenderContextRepoFile(ctx, profileDbRepo, renderhelper.RepoFileOptions{
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
})
if profileContent, err := markdown.RenderString(rctx, bytes); err != nil {
log.Error("failed to RenderString: %v", err)
log.Error("failed to RenderString for %s profile readme: %v", profileType, err)
} else {
ctx.Data["ProfileReadme"] = profileContent
ctx.Data[fmt.Sprintf("%sProfileReadme", profileType)] = profileContent
}
}

ctx.Data["PageIsViewOverview"] = true
return true
}
20 changes: 14 additions & 6 deletions routers/web/shared/user/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,12 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
}
}

func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) {
profileDbRepo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ".profile")
func FindUserProfileReadme(ctx *context.Context, doer *user_model.User, profileType string) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) {
profileName := ".profile"
if profileType != "Public" {
profileName = ".profile-private"
}
profileDbRepo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, profileName)
if err == nil {
perm, err := access_model.GetUserRepoPermission(ctx, profileDbRepo, doer)
if err == nil && !profileDbRepo.IsEmpty && perm.CanRead(unit.TypeCode) {
Expand All @@ -130,9 +134,9 @@ func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profile
func RenderUserHeader(ctx *context.Context) {
prepareContextForCommonProfile(ctx)

_, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx, ctx.Doer)
_, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx, ctx.Doer, "Public")
defer profileClose()
ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil
ctx.Data["HasPublicProfileReadme"] = profileReadmeBlob != nil
}

func LoadHeaderCount(ctx *context.Context) error {
Expand Down Expand Up @@ -174,9 +178,13 @@ func RenderOrgHeader(ctx *context.Context) error {
return err
}

_, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx, ctx.Doer)
_, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx, ctx.Doer, "Public")
defer profileClose()
ctx.Data["HasPublicProfileReadme"] = profileReadmeBlob != nil

_, _, profileReadmeBlob, profileClose = FindUserProfileReadme(ctx, ctx.Doer, "Private")
defer profileClose()
ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil
ctx.Data["HasPrivateProfileReadme"] = profileReadmeBlob != nil

return nil
}
6 changes: 3 additions & 3 deletions routers/web/user/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func userProfile(ctx *context.Context) {
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
}

profileDbRepo, _ /*profileGitRepo*/, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer)
profileDbRepo, _ /*profileGitRepo*/, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer, "Public")
defer profileClose()

showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
Expand All @@ -96,7 +96,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
}
}
ctx.Data["TabName"] = tab
ctx.Data["HasProfileReadme"] = profileReadme != nil
ctx.Data["HasPublicProfileReadme"] = profileReadme != nil

page := ctx.FormInt("page")
if page <= 0 {
Expand Down Expand Up @@ -254,7 +254,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
if profileContent, err := markdown.RenderString(rctx, bytes); err != nil {
log.Error("failed to RenderString: %v", err)
} else {
ctx.Data["ProfileReadme"] = profileContent
ctx.Data["PublicProfileReadme"] = profileContent
}
}
case "organizations":
Expand Down
21 changes: 19 additions & 2 deletions templates/org/home.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,25 @@
<div class="ui container">
<div class="ui mobile reversed stackable grid">
<div class="ui {{if .ShowMemberAndTeamTab}}eleven wide{{end}} column">
{{if .ProfileReadme}}
<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
{{if or .PublicProfileReadme .PrivateProfileReadme}}
{{if and .ShowMemberAndTeamTab .HasPublicProfileReadme .HasPrivateProfileReadme}}
<div class="ui small secondary filter menu">
<div id="profile_view_as_dropdown" class="item ui small dropdown jump" style="padding-bottom: 1em;">
{{svg "octicon-eye" 14 "view as icon"}}<span class="text">View as: {{if not .IsViewerMember}}{{ctx.Locale.Tr "settings.visibility.public"}}{{else}}{{ctx.Locale.Tr "org.members.member"}}{{end}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<a href="{{$.Org.HomeLink}}?{{.QueryForPublic}}" class="{{if not .IsViewerMember}}active {{end}}item"><input hidden type="radio" {{if not .IsViewerMember}}checked{{end}}> {{ctx.Locale.Tr "settings.visibility.public"}} {{if not .IsViewerMember}}{{svg "octicon-check" 14 "check icon"}}{{end}}</a>
<a href="{{$.Org.HomeLink}}?{{.QueryForMember}}" class="{{if .IsViewerMember}}active {{end}}item"><input hidden type="radio" {{if .IsViewerMember}}checked{{end}}> {{ctx.Locale.Tr "org.members.member"}} {{if .IsViewerMember}}{{svg "octicon-check" 14 "check icon"}}{{end}}</a>
</div>
</div>
</div>
{{end}}
{{end}}
{{if .PrivateProfileReadme}}
<div id="readme_profile" class="markup">{{.PrivateProfileReadme}}</div>
{{end}}
{{if .PublicProfileReadme}}
<div id="readme_profile" class="markup">{{.PublicProfileReadme}}</div>
{{end}}
{{template "shared/repo_search" .}}
{{template "explore/repo_list" .}}
Expand Down
4 changes: 2 additions & 2 deletions templates/org/menu.tmpl
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<div class="ui container">
<overflow-menu class="ui secondary pointing tabular borderless menu tw-mb-4">
<div class="overflow-menu-items">
{{if .HasProfileReadme}}
{{if or .HasPublicProfileReadme .HasPrivateProfileReadme}}
<a class="{{if .PageIsViewOverview}}active {{end}}item" href="{{$.Org.HomeLink}}">
{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
</a>
{{end}}
<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}{{if .HasProfileReadme}}/-/repositories{{end}}">
<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}{{if or .HasPublicProfileReadme .HasPrivateProfileReadme}}/-/repositories{{end}}">
{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
{{if .RepoCount}}
<div class="ui small label">{{.RepoCount}}</div>
Expand Down
2 changes: 2 additions & 0 deletions templates/repo/create.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
<div class="inline required field {{if .Err_RepoName}}error{{end}}">
<label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input id="repo_name" name="repo_name" value="{{.repo_name}}" autofocus required maxlength="100">
<span id="repo_name_public_profile_hint" style="display:none" class="help">{{ctx.Locale.Tr "repo.repo_name_public_profile_hint"}}</span>
<span id="repo_name_private_profile_hint" style="display:none" class="help">{{ctx.Locale.Tr "repo.repo_name_private_profile_hint"}}</span>
<span class="help">{{ctx.Locale.Tr "repo.repo_name_helper"}}</span>
</div>
<div class="inline field">
Expand Down
2 changes: 1 addition & 1 deletion templates/user/overview/header.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<overflow-menu class="ui secondary pointing tabular borderless menu">
<div class="overflow-menu-items">
{{if and .HasProfileReadme .ContextUser.IsIndividual}}
{{if and .HasPublicProfileReadme .ContextUser.IsIndividual}}
<a class="{{if eq .TabName "overview"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=overview">
{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
</a>
Expand Down
2 changes: 1 addition & 1 deletion templates/user/profile.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
{{else if eq .TabName "followers"}}
{{template "repo/user_cards" .}}
{{else if eq .TabName "overview"}}
<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
<div id="readme_profile" class="markup">{{.PublicProfileReadme}}</div>
{{else if eq .TabName "organizations"}}
{{template "repo/user_cards" .}}
{{else}}
Expand Down
163 changes: 163 additions & 0 deletions tests/integration/org_profile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package integration

import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"testing"
"time"

auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"

"github.com/stretchr/testify/assert"
)

func getCreateProfileReadmeFileOptions(profileType string) api.CreateFileOptions {
content := fmt.Sprintf("# %s", profileType)
contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
return api.CreateFileOptions{
FileOptions: api.FileOptions{
BranchName: "main",
NewBranchName: "main",
Message: "create the profile README.md",
Dates: api.CommitDateOptions{
Author: time.Unix(946684810, 0),
Committer: time.Unix(978307190, 0),
},
},
ContentBase64: contentEncoded,
}
}

func createTestProfile(t *testing.T, orgName, profileType string) {
repoName := ".profile"
isPrivate := false
if profileType == "Private" {
repoName = ".profile-private"
isPrivate = true
}

ctx := NewAPITestContext(t, "user1", repoName, auth_model.AccessTokenScopeAll)
session := loginUser(t, "user1")
tokenAdmin := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)

// create repo
t.Run("CreateOrganization"+profileType+"ProfileRepo", doAPICreateOrganizationRepository(ctx, orgName, &api.CreateRepoOption{
Name: repoName,
Private: isPrivate,
}))

// create readme
createFileOptions := getCreateProfileReadmeFileOptions(profileType)
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", orgName, repoName, "README.md"), &createFileOptions).
AddTokenAuth(tokenAdmin)
MakeRequest(t, req, http.StatusCreated)
}

func TestOrgProfile(t *testing.T) {
onGiteaRun(t, testOrgProfile)
}

func testOrgProfile(t *testing.T, u *url.URL) {
// html #user-content-public (markdown title of public profile)
// html #user-content-private (markdown title of private profile)
// html #profile_view_as_dropdown (indicate whether the view as dropdown menu is present)

// PART 1: Test Both Private and Public
createTestProfile(t, "org3", "Public")
createTestProfile(t, "org3", "Private")

// Anonymous User
req := NewRequest(t, "GET", "org3")
resp := MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)

profileDivs := htmlDoc.doc.Find("#user-content-public")
assert.EqualValues(t, 1, profileDivs.Length())

dropDownDiv := htmlDoc.doc.Find("#profile_view_as_dropdown")
assert.EqualValues(t, 0, dropDownDiv.Length())

// Logged in but not member
session := loginUser(t, "user24")
req = NewRequest(t, "GET", "org3")
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)

profileDivs = htmlDoc.doc.Find("#user-content-public")
assert.EqualValues(t, 1, profileDivs.Length())

dropDownDiv = htmlDoc.doc.Find("#profile_view_as_dropdown")
assert.EqualValues(t, 0, dropDownDiv.Length())

// Site Admin
session = loginUser(t, "user1")
req = NewRequest(t, "GET", "org3")
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)

profileDivs = htmlDoc.doc.Find("#user-content-public")
assert.EqualValues(t, 1, profileDivs.Length())

dropDownDiv = htmlDoc.doc.Find("#profile_view_as_dropdown")
assert.EqualValues(t, 1, dropDownDiv.Length())

req = NewRequest(t, "GET", "/org3?view_as=member")
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)

profileDivs = htmlDoc.doc.Find("#user-content-private")
assert.EqualValues(t, 1, profileDivs.Length())

req = NewRequest(t, "GET", "/org3?view_as=public")
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)

profileDivs = htmlDoc.doc.Find("#user-content-public")
assert.EqualValues(t, 1, profileDivs.Length())

// PART 2: Each org has either one of private pr public profile
createTestProfile(t, "org41", "Public")
createTestProfile(t, "org42", "Private")

// Anonymous User
req = NewRequest(t, "GET", "/org41")
resp = MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
profileDivs = htmlDoc.doc.Find("#user-content-public")
assert.EqualValues(t, 1, profileDivs.Length())
dropDownDiv = htmlDoc.doc.Find("#profile_view_as_dropdown")
assert.EqualValues(t, 0, dropDownDiv.Length())

req = NewRequest(t, "GET", "/org42")
resp = MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
profileDivs = htmlDoc.doc.Find("#user-content-public")
assert.EqualValues(t, 0, profileDivs.Length())
profileDivs = htmlDoc.doc.Find("#user-content-public")
assert.EqualValues(t, 0, profileDivs.Length())
dropDownDiv = htmlDoc.doc.Find("#profile_view_as_dropdown")
assert.EqualValues(t, 0, dropDownDiv.Length())

// Site Admin
req = NewRequest(t, "GET", "/org41")
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
profileDivs = htmlDoc.doc.Find("#user-content-public")
assert.EqualValues(t, 1, profileDivs.Length())
dropDownDiv = htmlDoc.doc.Find("#profile_view_as_dropdown")
assert.EqualValues(t, 0, dropDownDiv.Length())

req = NewRequest(t, "GET", "/org42")
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
profileDivs = htmlDoc.doc.Find("#user-content-private")
assert.EqualValues(t, 1, profileDivs.Length())
dropDownDiv = htmlDoc.doc.Find("#profile_view_as_dropdown")
assert.EqualValues(t, 0, dropDownDiv.Length())
}
Loading
Loading