Add missing tabs to org projects page (#22705) (#23412)

Backport #22705 by @yp05327

Fixes https://github.com/go-gitea/gitea/issues/22676

Context Data `IsOrganizationMember` and `IsOrganizationOwner` is used to
control the visibility of `people` and `team` tab.

2871ea0809/templates/org/menu.tmpl (L19-L40)

And because of the reuse of user projects page, User Context is changed
to Organization Context. But the value of `IsOrganizationMember` and
`IsOrganizationOwner` are not being given.

I reused func `HandleOrgAssignment` to add them to the ctx, but may have
some unnecessary variables, idk whether it is ok.

I found there is a missing `PageIsViewProjects` at create project page.

Co-authored-by: yp05327 <576951401@qq.com>
This commit is contained in:
Giteabot 2023-03-10 11:08:28 -05:00 committed by GitHub
parent edb618c136
commit e259daeff8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 115 additions and 63 deletions

View file

@ -239,6 +239,32 @@ func (org *Organization) CustomAvatarRelativePath() string {
return org.Avatar return org.Avatar
} }
// UnitPermission returns unit permission
func (org *Organization) UnitPermission(ctx context.Context, doer *user_model.User, unitType unit.Type) perm.AccessMode {
if doer != nil {
teams, err := GetUserOrgTeams(ctx, org.ID, doer.ID)
if err != nil {
log.Error("GetUserOrgTeams: %v", err)
return perm.AccessModeNone
}
if err := teams.LoadUnits(ctx); err != nil {
log.Error("LoadUnits: %v", err)
return perm.AccessModeNone
}
if len(teams) > 0 {
return teams.UnitMaxAccess(unitType)
}
}
if org.Visibility.IsPublic() {
return perm.AccessModeRead
}
return perm.AccessModeNone
}
// CreateOrganization creates record of a new organization. // CreateOrganization creates record of a new organization.
func CreateOrganization(org *Organization, owner *user_model.User) (err error) { func CreateOrganization(org *Organization, owner *user_model.User) (err error) {
if !owner.CanCreateOrganization() { if !owner.CanCreateOrganization() {

View file

@ -393,6 +393,11 @@ func (u *User) IsOrganization() bool {
return u.Type == UserTypeOrganization return u.Type == UserTypeOrganization
} }
// IsIndividual returns true if user is actually a individual user.
func (u *User) IsIndividual() bool {
return u.Type == UserTypeIndividual
}
// DisplayName returns full name if it's not empty, // DisplayName returns full name if it's not empty,
// returns username otherwise. // returns username otherwise.
func (u *User) DisplayName() string { func (u *User) DisplayName() string {

View file

@ -11,7 +11,6 @@ import (
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
) )
@ -31,29 +30,34 @@ type Organization struct {
} }
func (org *Organization) CanWriteUnit(ctx *Context, unitType unit.Type) bool { func (org *Organization) CanWriteUnit(ctx *Context, unitType unit.Type) bool {
if ctx.Doer == nil { return org.Organization.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeWrite
return false
}
return org.UnitPermission(ctx, ctx.Doer.ID, unitType) >= perm.AccessModeWrite
} }
func (org *Organization) UnitPermission(ctx *Context, doerID int64, unitType unit.Type) perm.AccessMode { func (org *Organization) CanReadUnit(ctx *Context, unitType unit.Type) bool {
if doerID > 0 { return org.Organization.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeRead
teams, err := organization.GetUserOrgTeams(ctx, org.Organization.ID, doerID) }
func GetOrganizationByParams(ctx *Context) {
orgName := ctx.Params(":org")
var err error
ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName)
if err != nil { if err != nil {
log.Error("GetUserOrgTeams: %v", err) if organization.IsErrOrgNotExist(err) {
return perm.AccessModeNone redirectUserID, err := user_model.LookupUserRedirect(orgName)
if err == nil {
RedirectToUser(ctx, orgName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound("GetUserByName", err)
} else {
ctx.ServerError("LookupUserRedirect", err)
} }
if len(teams) > 0 { } else {
return teams.UnitMaxAccess(unitType) ctx.ServerError("GetUserByName", err)
} }
return
} }
if org.Organization.Visibility == structs.VisibleTypePublic {
return perm.AccessModeRead
}
return perm.AccessModeNone
} }
// HandleOrgAssignment handles organization assignment // HandleOrgAssignment handles organization assignment
@ -77,25 +81,26 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
requireTeamAdmin = args[3] requireTeamAdmin = args[3]
} }
orgName := ctx.Params(":org")
var err error var err error
ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName)
if err != nil { if ctx.ContextUser == nil {
if organization.IsErrOrgNotExist(err) { // if Organization is not defined, get it from params
redirectUserID, err := user_model.LookupUserRedirect(orgName) if ctx.Org.Organization == nil {
if err == nil { GetOrganizationByParams(ctx)
RedirectToUser(ctx, orgName, redirectUserID) if ctx.Written() {
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound("GetUserByName", err)
} else {
ctx.ServerError("LookupUserRedirect", err)
}
} else {
ctx.ServerError("GetUserByName", err)
}
return return
} }
}
} else if ctx.ContextUser.IsOrganization() {
if ctx.Org == nil {
ctx.Org = &Organization{}
}
ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser)
} else {
// ContextUser is an individual User
return
}
org := ctx.Org.Organization org := ctx.Org.Organization
// Handle Visibility // Handle Visibility
@ -156,6 +161,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
} }
ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
ctx.Data["IsProjectEnabled"] = true
ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
ctx.Data["IsPublicMember"] = func(uid int64) bool { ctx.Data["IsPublicMember"] = func(uid int64) bool {
@ -231,6 +237,10 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
return return
} }
} }
ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects)
ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages)
ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode)
} }
// OrgAssignment returns a middleware to handle organization assignment // OrgAssignment returns a middleware to handle organization assignment

View file

@ -156,6 +156,7 @@ func Home(ctx *context.Context) {
pager.SetDefaultParams(ctx) pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "language", "Language") pager.AddParam(ctx, "language", "Language")
ctx.Data["Page"] = pager ctx.Data["Page"] = pager
ctx.Data["ContextUser"] = ctx.ContextUser
ctx.HTML(http.StatusOK, tplOrgHome) ctx.HTML(http.StatusOK, tplOrgHome)
} }

View file

@ -123,6 +123,7 @@ func NewProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.new") ctx.Data["Title"] = ctx.Tr("repo.projects.new")
ctx.Data["BoardTypes"] = project_model.GetBoardConfig() ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
ctx.Data["PageIsViewProjects"] = true
ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
shared_user.RenderUserHeader(ctx) shared_user.RenderUserHeader(ctx)
ctx.HTML(http.StatusOK, tplProjectsNew) ctx.HTML(http.StatusOK, tplProjectsNew)

View file

@ -9,6 +9,8 @@ import (
) )
func RenderUserHeader(ctx *context.Context) { func RenderUserHeader(ctx *context.Context) {
ctx.Data["IsProjectEnabled"] = true
ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["ContextUser"] = ctx.ContextUser
} }

View file

@ -24,6 +24,7 @@ func CodeSearch(ctx *context.Context) {
return return
} }
ctx.Data["IsProjectEnabled"] = true
ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
ctx.Data["Title"] = ctx.Tr("explore.code") ctx.Data["Title"] = ctx.Tr("explore.code")

View file

@ -288,6 +288,7 @@ func Profile(ctx *context.Context) {
pager.AddParam(ctx, "language", "Language") pager.AddParam(ctx, "language", "Language")
} }
ctx.Data["Page"] = pager ctx.Data["Page"] = pager
ctx.Data["IsProjectEnabled"] = true
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

@ -690,6 +690,21 @@ func RegisterRoutes(m *web.Route) {
} }
} }
reqUnitAccess := func(unitType unit.Type, accessMode perm.AccessMode) func(ctx *context.Context) {
return func(ctx *context.Context) {
if ctx.ContextUser == nil {
ctx.NotFound(unitType.String(), nil)
return
}
if ctx.ContextUser.IsOrganization() {
if ctx.Org.Organization.UnitPermission(ctx, ctx.Doer, unitType) < accessMode {
ctx.NotFound(unitType.String(), nil)
return
}
}
}
}
// ***** START: Organization ***** // ***** START: Organization *****
m.Group("/org", func() { m.Group("/org", func() {
m.Group("/{org}", func() { m.Group("/{org}", func() {
@ -869,8 +884,10 @@ func RegisterRoutes(m *web.Route) {
} }
m.Group("/projects", func() { m.Group("/projects", func() {
m.Group("", func() {
m.Get("", org.Projects) m.Get("", org.Projects)
m.Get("/{id}", org.ViewProject) m.Get("/{id}", org.ViewProject)
}, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead))
m.Group("", func() { //nolint:dupl m.Group("", func() { //nolint:dupl
m.Get("/new", org.NewProject) m.Get("/new", org.NewProject)
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
@ -890,25 +907,18 @@ func RegisterRoutes(m *web.Route) {
m.Post("/move", org.MoveIssues) m.Post("/move", org.MoveIssues)
}) })
}) })
}, reqSignIn, func(ctx *context.Context) { }, reqSignIn, reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite), func(ctx *context.Context) {
if ctx.ContextUser == nil { if ctx.ContextUser.IsIndividual() && ctx.ContextUser.ID != ctx.Doer.ID {
ctx.NotFound("NewProject", nil)
return
}
if ctx.ContextUser.IsOrganization() {
if !ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) {
ctx.NotFound("NewProject", nil)
return
}
} else if ctx.ContextUser.ID != ctx.Doer.ID {
ctx.NotFound("NewProject", nil) ctx.NotFound("NewProject", nil)
return return
} }
}) })
}, repo.MustEnableProjects) }, repo.MustEnableProjects)
m.Group("", func() {
m.Get("/code", user.CodeSearch) m.Get("/code", user.CodeSearch)
}, context_service.UserAssignmentWeb()) }, reqUnitAccess(unit.TypeCode, perm.AccessModeRead))
}, context_service.UserAssignmentWeb(), context.OrgAssignment())
// ***** Release Attachment Download without Signin // ***** Release Attachment Download without Signin
m.Get("/{username}/{reponame}/releases/download/{vTag}/{fileName}", ignSignIn, context.RepoAssignment, repo.MustBeNotEmpty, repo.RedirectDownload) m.Get("/{username}/{reponame}/releases/download/{vTag}/{fileName}", ignSignIn, context.RepoAssignment, repo.MustBeNotEmpty, repo.RedirectDownload)

View file

@ -8,7 +8,6 @@ import (
"net/http" "net/http"
"strings" "strings"
org_model "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
) )
@ -57,14 +56,6 @@ func userAssignment(ctx *context.Context, errCb func(int, string, interface{}))
} else { } else {
errCb(http.StatusInternalServerError, "GetUserByName", err) errCb(http.StatusInternalServerError, "GetUserByName", err)
} }
} else {
if ctx.ContextUser.IsOrganization() {
if ctx.Org == nil {
ctx.Org = &context.Organization{}
}
ctx.Org.Organization = (*org_model.Organization)(ctx.ContextUser)
ctx.Data["Org"] = ctx.Org.Organization
}
} }
} }
} }

View file

@ -3,16 +3,18 @@
<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}"> <a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}">
{{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}} {{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}}
</a> </a>
{{if and .IsProjectEnabled .CanReadProjects}}
<a class="{{if .PageIsViewProjects}}active {{end}}item" href="{{$.Org.HomeLink}}/-/projects"> <a class="{{if .PageIsViewProjects}}active {{end}}item" href="{{$.Org.HomeLink}}/-/projects">
{{svg "octicon-project-symlink"}} {{.locale.Tr "user.projects"}} {{svg "octicon-project-symlink"}} {{.locale.Tr "user.projects"}}
</a> </a>
{{if .IsPackageEnabled}} {{end}}
{{if and .IsPackageEnabled .CanReadPackages}}
<a class="item" href="{{$.Org.HomeLink}}/-/packages"> <a class="item" href="{{$.Org.HomeLink}}/-/packages">
{{svg "octicon-package"}} {{.locale.Tr "packages.title"}} {{svg "octicon-package"}} {{.locale.Tr "packages.title"}}
</a> </a>
{{end}} {{end}}
{{if .IsRepoIndexerEnabled}} {{if and .IsRepoIndexerEnabled .CanReadCode}}
<a class="{{if $.PageIsOrgCode}}active {{end}}item" href="{{$.Org.HomeLink}}/-/code"> <a class="item" href="{{$.Org.HomeLink}}/-/code">
{{svg "octicon-code"}}&nbsp;{{$.locale.Tr "org.code"}} {{svg "octicon-code"}}&nbsp;{{$.locale.Tr "org.code"}}
</a> </a>
{{end}} {{end}}

View file

@ -22,15 +22,17 @@
<a class="item" href="{{.ContextUser.HomeLink}}"> <a class="item" href="{{.ContextUser.HomeLink}}">
{{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}} {{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}}
</a> </a>
{{if and .IsProjectEnabled (or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadProjects))}}
<a href="{{.ContextUser.HomeLink}}/-/projects" class="{{if .PageIsViewProjects}}active {{end}}item"> <a href="{{.ContextUser.HomeLink}}/-/projects" class="{{if .PageIsViewProjects}}active {{end}}item">
{{svg "octicon-project-symlink"}} {{.locale.Tr "user.projects"}} {{svg "octicon-project-symlink"}} {{.locale.Tr "user.projects"}}
</a> </a>
{{if (not .UnitPackagesGlobalDisabled)}} {{end}}
{{if and .IsPackageEnabled (or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadPackages))}}
<a href="{{.ContextUser.HomeLink}}/-/packages" class="{{if .IsPackagesPage}}active {{end}}item"> <a href="{{.ContextUser.HomeLink}}/-/packages" class="{{if .IsPackagesPage}}active {{end}}item">
{{svg "octicon-package"}} {{.locale.Tr "packages.title"}} {{svg "octicon-package"}} {{.locale.Tr "packages.title"}}
</a> </a>
{{end}} {{end}}
{{if .IsRepoIndexerEnabled}} {{if and .IsRepoIndexerEnabled (or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadCode))}}
<a href="{{.ContextUser.HomeLink}}/-/code" class="{{if .IsCodePage}}active {{end}}item"> <a href="{{.ContextUser.HomeLink}}/-/code" class="{{if .IsCodePage}}active {{end}}item">
{{svg "octicon-code"}} {{.locale.Tr "user.code"}} {{svg "octicon-code"}} {{.locale.Tr "user.code"}}
</a> </a>