初始提交: Gitea 项目代码
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/modules/templates"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplSettingsBlockedUsers templates.TplName = "org/settings/blocked_users"
|
||||
)
|
||||
|
||||
func BlockedUsers(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("user.block.list")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsBlockedUsers"] = true
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
shared_user.BlockedUsers(ctx, ctx.ContextUser)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
|
||||
}
|
||||
|
||||
func BlockedUsersPost(ctx *context.Context) {
|
||||
shared_user.BlockedUsersPost(ctx, ctx.ContextUser, ctx.ContextUser.OrganisationLink()+"/settings/blocked_users")
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/renderhelper"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const tplOrgHome templates.TplName = "org/home"
|
||||
|
||||
// Home show organization home page
|
||||
func Home(ctx *context.Context) {
|
||||
uname := ctx.PathParam("username")
|
||||
|
||||
if strings.HasSuffix(uname, ".keys") || strings.HasSuffix(uname, ".gpg") {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetPathParam("org", uname)
|
||||
context.OrgAssignment(context.OrgAssignmentOptions{})(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
home(ctx, false)
|
||||
}
|
||||
|
||||
func Repositories(ctx *context.Context) {
|
||||
home(ctx, true)
|
||||
}
|
||||
|
||||
func home(ctx *context.Context, viewRepositories bool) {
|
||||
org := ctx.Org.Organization
|
||||
|
||||
ctx.Data["PageIsUserProfile"] = true
|
||||
ctx.Data["Title"] = org.DisplayName()
|
||||
|
||||
var orderBy db.SearchOrderBy
|
||||
sortOrder := ctx.FormString("sort")
|
||||
if _, ok := repo_model.OrderByFlatMap[sortOrder]; !ok {
|
||||
sortOrder = setting.UI.ExploreDefaultSort // TODO: add new default sort order for org home?
|
||||
}
|
||||
ctx.Data["SortType"] = sortOrder
|
||||
orderBy = repo_model.OrderByFlatMap[sortOrder]
|
||||
|
||||
keyword := ctx.FormTrim("q")
|
||||
ctx.Data["Keyword"] = keyword
|
||||
|
||||
language := ctx.FormTrim("language")
|
||||
ctx.Data["Language"] = language
|
||||
|
||||
page := ctx.FormInt("page")
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
archived := ctx.FormOptionalBool("archived")
|
||||
ctx.Data["IsArchived"] = archived
|
||||
|
||||
fork := ctx.FormOptionalBool("fork")
|
||||
ctx.Data["IsFork"] = fork
|
||||
|
||||
mirror := ctx.FormOptionalBool("mirror")
|
||||
ctx.Data["IsMirror"] = mirror
|
||||
|
||||
template := ctx.FormOptionalBool("template")
|
||||
ctx.Data["IsTemplate"] = template
|
||||
|
||||
private := ctx.FormOptionalBool("private")
|
||||
ctx.Data["IsPrivate"] = private
|
||||
|
||||
opts := &organization.FindOrgMembersOpts{
|
||||
Doer: ctx.Doer,
|
||||
OrgID: org.ID,
|
||||
IsDoerMember: ctx.Org.IsMember,
|
||||
ListOptions: db.ListOptions{Page: 1, PageSize: 25},
|
||||
}
|
||||
|
||||
members, _, err := organization.FindOrgMembers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindOrgMembers", err)
|
||||
return
|
||||
}
|
||||
|
||||
const orgOverviewTeamsLimit = 5
|
||||
ctx.Data["OrgOverviewMembers"] = members
|
||||
ctx.Data["OrgOverviewTeams"] = ctx.Org.Teams[:min(len(ctx.Org.Teams), orgOverviewTeamsLimit)]
|
||||
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
|
||||
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
|
||||
|
||||
prepareResult, err := shared_user.RenderUserOrgHeader(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
// if no profile readme, it still means "view repositories"
|
||||
isViewOverview := !viewRepositories && prepareOrgProfileReadme(ctx, prepareResult)
|
||||
ctx.Data["PageIsViewRepositories"] = !isViewOverview
|
||||
ctx.Data["PageIsViewOverview"] = isViewOverview
|
||||
ctx.Data["ShowOrgProfileReadmeSelector"] = isViewOverview && prepareResult.ProfilePublicReadmeBlob != nil && prepareResult.ProfilePrivateReadmeBlob != nil
|
||||
|
||||
repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: setting.UI.User.RepoPagingNum,
|
||||
Page: page,
|
||||
},
|
||||
Keyword: keyword,
|
||||
OwnerID: org.ID,
|
||||
OrderBy: orderBy,
|
||||
Private: ctx.IsSigned,
|
||||
Actor: ctx.Doer,
|
||||
Language: language,
|
||||
IncludeDescription: setting.UI.SearchRepoDescription,
|
||||
Archived: archived,
|
||||
Fork: fork,
|
||||
Mirror: mirror,
|
||||
Template: template,
|
||||
IsPrivate: private,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchRepository", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Repos"] = repos
|
||||
ctx.Data["Total"] = count
|
||||
|
||||
pager := context.NewPagination(count, setting.UI.User.RepoPagingNum, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, tplOrgHome)
|
||||
}
|
||||
|
||||
func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.PrepareOwnerHeaderResult) bool {
|
||||
viewAs := ctx.FormString("view_as", util.Iif(ctx.Org.IsMember, "member", "public"))
|
||||
viewAsMember := viewAs == "member"
|
||||
|
||||
var profileRepo *repo_model.Repository
|
||||
var readmeBlob *git.Blob
|
||||
if viewAsMember {
|
||||
if prepareResult.ProfilePrivateReadmeBlob != nil {
|
||||
profileRepo, readmeBlob = prepareResult.ProfilePrivateRepo, prepareResult.ProfilePrivateReadmeBlob
|
||||
} else {
|
||||
profileRepo, readmeBlob = prepareResult.ProfilePublicRepo, prepareResult.ProfilePublicReadmeBlob
|
||||
viewAsMember = false
|
||||
}
|
||||
} else {
|
||||
if prepareResult.ProfilePublicReadmeBlob != nil {
|
||||
profileRepo, readmeBlob = prepareResult.ProfilePublicRepo, prepareResult.ProfilePublicReadmeBlob
|
||||
} else {
|
||||
profileRepo, readmeBlob = prepareResult.ProfilePrivateRepo, prepareResult.ProfilePrivateReadmeBlob
|
||||
viewAsMember = true
|
||||
}
|
||||
}
|
||||
if readmeBlob == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
readmeBytes, err := readmeBlob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err != nil {
|
||||
log.Error("failed to GetBlobContent for profile %q (view as %q) readme: %v", profileRepo.FullName(), viewAs, err)
|
||||
return false
|
||||
}
|
||||
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, profileRepo, renderhelper.RepoFileOptions{
|
||||
CurrentRefSubURL: path.Join("branch", util.PathEscapeSegments(profileRepo.DefaultBranch)),
|
||||
})
|
||||
ctx.Data["ProfileReadmeContent"], err = markdown.RenderString(rctx, readmeBytes)
|
||||
if err != nil {
|
||||
log.Error("failed to GetBlobContent for profile %q (view as %q) readme: %v", profileRepo.FullName(), viewAs, err)
|
||||
return false
|
||||
}
|
||||
ctx.Data["IsViewingOrgAsMember"] = viewAsMember
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/organization"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
org_service "gitea.dev/services/org"
|
||||
)
|
||||
|
||||
const (
|
||||
// tplMembers template for organization members page
|
||||
tplMembers templates.TplName = "org/member/members"
|
||||
)
|
||||
|
||||
// Members render organization users page
|
||||
func Members(ctx *context.Context) {
|
||||
org := ctx.Org.Organization
|
||||
ctx.Data["Title"] = org.FullName
|
||||
ctx.Data["PageIsOrgMembers"] = true
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
|
||||
opts := &organization.FindOrgMembersOpts{
|
||||
Doer: ctx.Doer,
|
||||
OrgID: org.ID,
|
||||
}
|
||||
|
||||
if ctx.Doer != nil {
|
||||
isMember, err := ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "IsOrgMember")
|
||||
return
|
||||
}
|
||||
opts.IsDoerMember = isMember
|
||||
}
|
||||
ctx.Data["PublicOnly"] = opts.PublicOnly()
|
||||
|
||||
total, err := organization.CountOrgMembers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "CountOrgMembers")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
pager := context.NewPagination(total, setting.UI.MembersPagingNum, page, 5)
|
||||
opts.ListOptions.Page = page
|
||||
opts.ListOptions.PageSize = setting.UI.MembersPagingNum
|
||||
members, membersIsPublic, err := organization.FindOrgMembers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMembers", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.Data["Members"] = members
|
||||
ctx.Data["MembersIsPublicMember"] = membersIsPublic
|
||||
ctx.Data["MembersIsUserOrgOwner"] = organization.IsUserOrgOwner(ctx, members, org.ID)
|
||||
ctx.Data["MembersTwoFaStatus"] = members.GetTwoFaStatus(ctx)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplMembers)
|
||||
}
|
||||
|
||||
// MembersAction response for operation to a member of organization
|
||||
func MembersAction(ctx *context.Context) {
|
||||
member, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
org := ctx.Org.Organization
|
||||
|
||||
switch ctx.PathParam("action") {
|
||||
case "private":
|
||||
if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, false)
|
||||
case "public":
|
||||
if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, true)
|
||||
case "remove":
|
||||
if !ctx.Org.IsOwner {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
err = org_service.RemoveOrgUser(ctx, org, member)
|
||||
case "leave":
|
||||
err = org_service.RemoveOrgUser(ctx, org, ctx.Doer)
|
||||
if err == nil {
|
||||
ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName()))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
ctx.JSONOK()
|
||||
return
|
||||
}
|
||||
|
||||
if organization.IsErrLastOrgOwner(err) {
|
||||
ctx.JSONError(ctx.Tr("form.last_org_owner"))
|
||||
return
|
||||
}
|
||||
|
||||
log.Error("Action(%s): %v", ctx.PathParam("action"), err)
|
||||
ctx.JSONError(err.Error()) // FIXME: legacy logic, errors are handled together, it's not right, need to distinguish between different errors
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/modules/util"
|
||||
shared_mention "gitea.dev/routers/web/shared/mention"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// GetMentionsInOwner returns JSON data for mention autocomplete on owner-level pages.
|
||||
func GetMentionsInOwner(ctx *context.Context) {
|
||||
// for individual users, we don't have a concept of "mentionable" users or teams, so just return an empty list
|
||||
if !ctx.ContextUser.IsOrganization() {
|
||||
ctx.JSON(http.StatusOK, []shared_mention.Mention{})
|
||||
return
|
||||
}
|
||||
|
||||
// for org, return members and teams
|
||||
c := shared_mention.NewCollector()
|
||||
org := organization.OrgFromUser(ctx.ContextUser)
|
||||
|
||||
// Get org members
|
||||
members, _, err := org.GetMembers(ctx, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMembers", err)
|
||||
return
|
||||
}
|
||||
c.AddUsers(ctx, members)
|
||||
|
||||
// Get mentionable teams
|
||||
if err := c.AddMentionableTeams(ctx, ctx.Doer, ctx.ContextUser); err != nil {
|
||||
ctx.ServerError("AddMentionableTeams", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, util.SliceNilAsEmpty(c.Result))
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
const (
|
||||
// tplCreateOrg template path for create organization
|
||||
tplCreateOrg templates.TplName = "org/create"
|
||||
)
|
||||
|
||||
// Create render the page for create organization
|
||||
func Create(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("new_org")
|
||||
if !ctx.Doer.CanCreateOrganization() {
|
||||
ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["visibility"] = setting.Service.DefaultOrgVisibilityMode
|
||||
ctx.Data["repo_admin_change_team_access"] = true
|
||||
|
||||
ctx.HTML(http.StatusOK, tplCreateOrg)
|
||||
}
|
||||
|
||||
// CreatePost response for create organization
|
||||
func CreatePost(ctx *context.Context) {
|
||||
form := *web.GetForm(ctx).(*forms.CreateOrgForm)
|
||||
ctx.Data["Title"] = ctx.Tr("new_org")
|
||||
|
||||
if !ctx.Doer.CanCreateOrganization() {
|
||||
ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplCreateOrg)
|
||||
return
|
||||
}
|
||||
|
||||
org := &organization.Organization{
|
||||
Name: form.OrgName,
|
||||
IsActive: true,
|
||||
Type: user_model.UserTypeOrganization,
|
||||
Visibility: form.Visibility,
|
||||
RepoAdminChangeTeamAccess: form.RepoAdminChangeTeamAccess,
|
||||
}
|
||||
|
||||
if err := organization.CreateOrganization(ctx, org, ctx.Doer); err != nil {
|
||||
ctx.Data["Err_OrgName"] = true
|
||||
switch {
|
||||
case user_model.IsErrUserAlreadyExist(err):
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.org_name_been_taken"), tplCreateOrg, &form)
|
||||
case db.IsErrNameReserved(err):
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("org.form.name_reserved", err.(db.ErrNameReserved).Name), tplCreateOrg, &form)
|
||||
case db.IsErrNamePatternNotAllowed(err):
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("org.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplCreateOrg, &form)
|
||||
case organization.IsErrUserNotAllowedCreateOrg(err):
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("org.form.create_org_not_allowed"), tplCreateOrg, &form)
|
||||
default:
|
||||
ctx.ServerError("CreateOrganization", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Organization created: %s", org.Name)
|
||||
|
||||
ctx.Redirect(org.AsUser().DashboardLink())
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/modules/label"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
shared_label "gitea.dev/routers/web/shared/label"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
// RetrieveLabels find all the labels of an organization
|
||||
func RetrieveLabels(ctx *context.Context) {
|
||||
labels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("RetrieveLabels.GetLabels", err)
|
||||
return
|
||||
}
|
||||
for _, l := range labels {
|
||||
l.CalOpenIssues()
|
||||
}
|
||||
ctx.Data["Labels"] = labels
|
||||
ctx.Data["NumLabels"] = len(labels)
|
||||
ctx.Data["SortType"] = ctx.FormString("sort")
|
||||
}
|
||||
|
||||
// NewLabel create new label for organization
|
||||
func NewLabel(ctx *context.Context) {
|
||||
form := shared_label.GetLabelEditForm(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
l := &issues_model.Label{
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
Name: form.Title,
|
||||
Exclusive: form.Exclusive,
|
||||
Description: form.Description,
|
||||
Color: form.Color,
|
||||
ExclusiveOrder: form.ExclusiveOrder,
|
||||
}
|
||||
if err := issues_model.NewLabel(ctx, l); err != nil {
|
||||
ctx.ServerError("NewLabel", err)
|
||||
return
|
||||
}
|
||||
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings/labels")
|
||||
}
|
||||
|
||||
// UpdateLabel update a label's name and color
|
||||
func UpdateLabel(ctx *context.Context) {
|
||||
form := shared_label.GetLabelEditForm(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, form.ID)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.JSONErrorNotFound()
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.ServerError("GetLabelInOrgByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
l.Name = form.Title
|
||||
l.Exclusive = form.Exclusive
|
||||
l.ExclusiveOrder = form.ExclusiveOrder
|
||||
l.Description = form.Description
|
||||
l.Color = form.Color
|
||||
l.SetArchived(form.IsArchived)
|
||||
if err := issues_model.UpdateLabel(ctx, l); err != nil {
|
||||
ctx.ServerError("UpdateLabel", err)
|
||||
return
|
||||
}
|
||||
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings/labels")
|
||||
}
|
||||
|
||||
// DeleteLabel delete a label
|
||||
func DeleteLabel(ctx *context.Context) {
|
||||
if err := issues_model.DeleteLabel(ctx, ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil {
|
||||
ctx.Flash.Error("DeleteLabel: " + err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings/labels")
|
||||
}
|
||||
|
||||
// InitializeLabels init labels for an organization
|
||||
func InitializeLabels(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.InitializeLabelsForm)
|
||||
if ctx.HasError() {
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/labels")
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_module.InitializeLabels(ctx, ctx.Org.Organization.ID, form.TemplateName, true); err != nil {
|
||||
if label.IsErrTemplateLoad(err) {
|
||||
originalErr := err.(label.ErrTemplateLoad).OriginalError
|
||||
ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
|
||||
return
|
||||
}
|
||||
ctx.ServerError("InitializeLabels", err)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
|
||||
}
|
||||
@@ -0,0 +1,662 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
project_model "gitea.dev/models/project"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/shared/issue"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
project_service "gitea.dev/services/projects"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
const (
|
||||
tplProjects templates.TplName = "org/projects/list"
|
||||
tplProjectsNew templates.TplName = "org/projects/new"
|
||||
tplProjectsView templates.TplName = "org/projects/view"
|
||||
)
|
||||
|
||||
// Projects renders the home page of projects
|
||||
func Projects(ctx *context.Context) {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Title"] = ctx.Tr("repo.projects")
|
||||
|
||||
sortType := ctx.FormTrim("sort")
|
||||
|
||||
isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed"
|
||||
keyword := ctx.FormTrim("q")
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
|
||||
var projectType project_model.Type
|
||||
if ctx.ContextUser.IsOrganization() {
|
||||
projectType = project_model.TypeOrganization
|
||||
} else {
|
||||
projectType = project_model.TypeIndividual
|
||||
}
|
||||
projects, total, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.UI.IssuePagingNum,
|
||||
},
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
IsClosed: optional.Some(isShowClosed),
|
||||
OrderBy: project_model.GetSearchOrderByBySortType(sortType),
|
||||
Type: projectType,
|
||||
Title: keyword,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindProjects", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
|
||||
ctx.ServerError("LoadIssueNumbersForProjects", err)
|
||||
return
|
||||
}
|
||||
|
||||
opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
IsClosed: optional.Some(!isShowClosed),
|
||||
Type: projectType,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("CountProjects", err)
|
||||
return
|
||||
}
|
||||
|
||||
if isShowClosed {
|
||||
ctx.Data["OpenCount"] = opTotal
|
||||
ctx.Data["ClosedCount"] = total
|
||||
} else {
|
||||
ctx.Data["OpenCount"] = total
|
||||
ctx.Data["ClosedCount"] = opTotal
|
||||
}
|
||||
|
||||
ctx.Data["Projects"] = projects
|
||||
|
||||
if isShowClosed {
|
||||
ctx.Data["State"] = "closed"
|
||||
} else {
|
||||
ctx.Data["State"] = "open"
|
||||
}
|
||||
renderUtils := templates.NewRenderUtils(ctx)
|
||||
for _, project := range projects {
|
||||
project.RenderedContent = renderUtils.MarkdownToHtml(project.Description)
|
||||
}
|
||||
|
||||
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
|
||||
ctx.Data["IsShowClosed"] = isShowClosed
|
||||
ctx.Data["PageIsViewProjects"] = true
|
||||
ctx.Data["SortType"] = sortType
|
||||
|
||||
ctx.HTML(http.StatusOK, tplProjects)
|
||||
}
|
||||
|
||||
func canWriteProjects(ctx *context.Context) bool {
|
||||
if ctx.ContextUser.IsOrganization() {
|
||||
return ctx.Org.CanWriteUnit(ctx, unit.TypeProjects)
|
||||
}
|
||||
return ctx.Doer != nil && ctx.ContextUser.ID == ctx.Doer.ID
|
||||
}
|
||||
|
||||
// RenderNewProject render creating a project page
|
||||
func RenderNewProject(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
|
||||
ctx.Data["TemplateConfigs"] = project_model.GetTemplateConfigs()
|
||||
ctx.Data["CardTypes"] = project_model.GetCardConfig()
|
||||
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
|
||||
ctx.Data["PageIsViewProjects"] = true
|
||||
ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
|
||||
ctx.Data["CancelLink"] = ctx.ContextUser.HomeLink() + "/-/projects"
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplProjectsNew)
|
||||
}
|
||||
|
||||
// NewProjectPost creates a new project
|
||||
func NewProjectPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.CreateProjectForm)
|
||||
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() {
|
||||
RenderNewProject(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
newProject := project_model.Project{
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
Title: form.Title,
|
||||
Description: form.Content,
|
||||
CreatorID: ctx.Doer.ID,
|
||||
TemplateType: form.TemplateType,
|
||||
CardType: form.CardType,
|
||||
}
|
||||
|
||||
if ctx.ContextUser.IsOrganization() {
|
||||
newProject.Type = project_model.TypeOrganization
|
||||
} else {
|
||||
newProject.Type = project_model.TypeIndividual
|
||||
}
|
||||
|
||||
if err := project_model.NewProject(ctx, &newProject); err != nil {
|
||||
ctx.ServerError("NewProject", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
|
||||
ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects")
|
||||
}
|
||||
|
||||
// ChangeProjectStatus updates the status of a project between "open" and "close"
|
||||
func ChangeProjectStatus(ctx *context.Context) {
|
||||
var toClose bool
|
||||
switch ctx.PathParam("action") {
|
||||
case "open":
|
||||
toClose = false
|
||||
case "close":
|
||||
toClose = true
|
||||
default:
|
||||
ctx.JSONRedirect(ctx.ContextUser.HomeLink() + "/-/projects")
|
||||
return
|
||||
}
|
||||
id := ctx.PathParamInt64("id")
|
||||
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, id, ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, project.ID, toClose); err != nil {
|
||||
ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, project.ID))
|
||||
}
|
||||
|
||||
// DeleteProject delete a project
|
||||
func DeleteProject(ctx *context.Context) {
|
||||
p, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.DeleteProjectByID(ctx, p.ID); err != nil {
|
||||
ctx.Flash.Error("DeleteProjectByID: " + err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(ctx.ContextUser.HomeLink() + "/-/projects")
|
||||
}
|
||||
|
||||
// RenderEditProject allows a project to be edited
|
||||
func RenderEditProject(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
|
||||
ctx.Data["PageIsEditProjects"] = true
|
||||
ctx.Data["PageIsViewProjects"] = true
|
||||
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
|
||||
ctx.Data["CardTypes"] = project_model.GetCardConfig()
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
p, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["projectID"] = p.ID
|
||||
ctx.Data["title"] = p.Title
|
||||
ctx.Data["content"] = p.Description
|
||||
ctx.Data["redirect"] = ctx.FormString("redirect")
|
||||
ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
|
||||
ctx.Data["card_type"] = p.CardType
|
||||
ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, p.ID)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplProjectsNew)
|
||||
}
|
||||
|
||||
// EditProjectPost response for editing a project
|
||||
func EditProjectPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.CreateProjectForm)
|
||||
projectID := ctx.PathParamInt64("id")
|
||||
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
|
||||
ctx.Data["PageIsEditProjects"] = true
|
||||
ctx.Data["PageIsViewProjects"] = true
|
||||
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
|
||||
ctx.Data["CardTypes"] = project_model.GetCardConfig()
|
||||
ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, projectID)
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplProjectsNew)
|
||||
return
|
||||
}
|
||||
|
||||
p, err := project_model.GetProjectByIDAndOwner(ctx, projectID, ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
p.Title = form.Title
|
||||
p.Description = form.Content
|
||||
p.CardType = form.CardType
|
||||
if err = project_model.UpdateProject(ctx, p); err != nil {
|
||||
ctx.ServerError("UpdateProjects", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title))
|
||||
if ctx.FormString("redirect") == "project" {
|
||||
ctx.Redirect(p.Link(ctx))
|
||||
} else {
|
||||
ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects")
|
||||
}
|
||||
}
|
||||
|
||||
// ViewProject renders the project with board view for a project
|
||||
func ViewProject(ctx *context.Context) {
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := project.LoadOwner(ctx); err != nil {
|
||||
ctx.ServerError("LoadOwner", err)
|
||||
return
|
||||
}
|
||||
|
||||
columns, err := project.GetColumns(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectColumns", err)
|
||||
return
|
||||
}
|
||||
|
||||
preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, project.RepoID, project.Owner)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
assigneeID := ctx.FormString("assignee")
|
||||
milestoneID := ctx.FormInt64("milestone")
|
||||
|
||||
// Prepare milestone IDs for filtering
|
||||
var milestoneIDs []int64
|
||||
if milestoneID > 0 {
|
||||
milestoneIDs = []int64{milestoneID}
|
||||
} else if milestoneID == db.NoConditionID {
|
||||
milestoneIDs = []int64{db.NoConditionID}
|
||||
}
|
||||
|
||||
opts := issues_model.IssuesOptions{
|
||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||
AssigneeID: assigneeID,
|
||||
MilestoneIDs: milestoneIDs,
|
||||
Owner: project.Owner,
|
||||
}
|
||||
if ctx.Doer != nil {
|
||||
opts.Doer = ctx.Doer
|
||||
} else {
|
||||
opts.AllPublic = true
|
||||
}
|
||||
|
||||
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||
return
|
||||
}
|
||||
for _, column := range columns {
|
||||
column.NumIssues = int64(len(issuesMap[column.ID]))
|
||||
}
|
||||
|
||||
if project.CardType != project_model.CardTypeTextOnly {
|
||||
issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
|
||||
for _, issuesList := range issuesMap {
|
||||
for _, issue := range issuesList {
|
||||
if issueAttachment, err := repo_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
|
||||
issuesAttachmentMap[issue.ID] = issueAttachment
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.Data["issuesAttachmentMap"] = issuesAttachmentMap
|
||||
}
|
||||
|
||||
linkedPrsMap := make(map[int64][]*issues_model.Issue)
|
||||
for _, issuesList := range issuesMap {
|
||||
for _, issue := range issuesList {
|
||||
var referencedIDs []int64
|
||||
for _, comment := range issue.Comments {
|
||||
if comment.RefIssueID != 0 && comment.RefIsPull {
|
||||
referencedIDs = append(referencedIDs, comment.RefIssueID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(referencedIDs) > 0 {
|
||||
if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
|
||||
IssueIDs: referencedIDs,
|
||||
IsPull: optional.Some(true),
|
||||
}); err == nil {
|
||||
linkedPrsMap[issue.ID] = linkedPrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add option to filter also by repository specific labels
|
||||
labels, err := issues_model.GetLabelsByOrgID(ctx, project.OwnerID, "", db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLabelsByOrgID", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the exclusive scope for every label ID
|
||||
labelExclusiveScopes := make([]string, 0, len(preparedLabelFilter.SelectedLabelIDs))
|
||||
for _, labelID := range preparedLabelFilter.SelectedLabelIDs {
|
||||
foundExclusiveScope := false
|
||||
for _, label := range labels {
|
||||
if label.ID == labelID || label.ID == -labelID {
|
||||
labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope())
|
||||
foundExclusiveScope = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundExclusiveScope {
|
||||
labelExclusiveScopes = append(labelExclusiveScopes, "")
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range labels {
|
||||
l.LoadSelectedLabelsAfterClick(preparedLabelFilter.SelectedLabelIDs, labelExclusiveScopes)
|
||||
}
|
||||
ctx.Data["Labels"] = labels
|
||||
ctx.Data["NumLabels"] = len(labels)
|
||||
|
||||
// Get milestones for filtering
|
||||
// For organization projects, we need to get milestones from all repos the user has access to
|
||||
var milestones issues_model.MilestoneList
|
||||
if project.RepoID > 0 {
|
||||
// Repo-specific project
|
||||
milestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
||||
RepoID: project.RepoID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepoMilestones", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Organization-wide project - get milestones from all organization repos
|
||||
// but only from repositories the current user can access.
|
||||
// Use RepoCond with a subquery to avoid materializing all repo IDs in memory
|
||||
// which can hit SQL parameter limits for orgs with many repos.
|
||||
accessCond := repo_model.AccessibleRepositoryCondition(ctx.Doer, unit.TypeIssues)
|
||||
repoCond := builder.And(
|
||||
builder.Eq{"owner_id": project.OwnerID},
|
||||
accessCond,
|
||||
)
|
||||
milestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
||||
RepoCond: repoCond,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOrgMilestones", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
openMilestones, closedMilestones := milestones.SplitByOpenClosed()
|
||||
ctx.Data["OpenMilestones"] = openMilestones
|
||||
ctx.Data["ClosedMilestones"] = closedMilestones
|
||||
ctx.Data["MilestoneID"] = milestoneID
|
||||
|
||||
// Get assignees.
|
||||
assigneeUsers, err := project_service.LoadIssuesAssigneesForProject(ctx, issuesMap)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadIssuesAssigneesForProject", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||
ctx.Data["AssigneeID"] = assigneeID
|
||||
|
||||
project.RenderedContent = templates.NewRenderUtils(ctx).MarkdownToHtml(project.Description)
|
||||
ctx.Data["LinkedPRs"] = linkedPrsMap
|
||||
ctx.Data["PageIsViewProjects"] = true
|
||||
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
|
||||
ctx.Data["Project"] = project
|
||||
ctx.Data["IssuesMap"] = issuesMap
|
||||
ctx.Data["Columns"] = columns
|
||||
ctx.Data["Title"] = fmt.Sprintf("%s - %s", project.Title, ctx.ContextUser.DisplayName())
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplProjectsView)
|
||||
}
|
||||
|
||||
// DeleteProjectColumn allows for the deletion of a project column
|
||||
func DeleteProjectColumn(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only signed in users are allowed to perform this action.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("columnID"), project.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetColumnByIDAndProjectID", project_model.IsErrProjectColumnNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.DeleteColumnByID(ctx, ctx.PathParamInt64("columnID")); err != nil {
|
||||
ctx.ServerError("DeleteProjectColumnByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// AddColumnToProjectPost allows a new column to be added to a project.
|
||||
func AddColumnToProjectPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
|
||||
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.NewColumn(ctx, &project_model.Column{
|
||||
ProjectID: project.ID,
|
||||
Title: form.Title,
|
||||
Color: form.Color,
|
||||
CreatorID: ctx.Doer.ID,
|
||||
}); err != nil {
|
||||
ctx.ServerError("NewProjectColumn", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// CheckProjectColumnChangePermissions check permission
|
||||
func CheckProjectColumnChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Column) {
|
||||
if ctx.Doer == nil {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only signed in users are allowed to perform this action.",
|
||||
})
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
column, err := project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("columnID"), project.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetColumnByIDAndProjectID", project_model.IsErrProjectColumnNotExist, err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return project, column
|
||||
}
|
||||
|
||||
// EditProjectColumn allows a project column's to be updated
|
||||
func EditProjectColumn(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
|
||||
_, column := CheckProjectColumnChangePermissions(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if form.Title != "" {
|
||||
column.Title = form.Title
|
||||
}
|
||||
column.Color = form.Color
|
||||
if form.Sorting != 0 {
|
||||
column.Sorting = form.Sorting
|
||||
}
|
||||
|
||||
if err := project_model.UpdateColumn(ctx, column); err != nil {
|
||||
ctx.ServerError("UpdateProjectColumn", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// SetDefaultProjectColumn set default column for uncategorized issues/pulls
|
||||
func SetDefaultProjectColumn(ctx *context.Context) {
|
||||
project, column := CheckProjectColumnChangePermissions(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.SetDefaultColumn(ctx, project.ID, column.ID); err != nil {
|
||||
ctx.ServerError("SetDefaultColumn", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// MoveIssues moves or keeps issues in a column and sorts them inside that column
|
||||
func MoveIssues(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only signed in users are allowed to perform this action.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
column, err := project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("columnID"), project.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetColumnByIDAndProjectID", project_model.IsErrProjectColumnNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
type movedIssuesForm struct {
|
||||
Issues []struct {
|
||||
IssueID int64 `json:"issueID"`
|
||||
Sorting int64 `json:"sorting"`
|
||||
} `json:"issues"`
|
||||
}
|
||||
|
||||
form := &movedIssuesForm{}
|
||||
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
|
||||
ctx.ServerError("DecodeMovedIssuesForm", err)
|
||||
return
|
||||
}
|
||||
|
||||
issueIDs := make([]int64, 0, len(form.Issues))
|
||||
sortedIssueIDs := make(map[int64]int64)
|
||||
for _, issue := range form.Issues {
|
||||
issueIDs = append(issueIDs, issue.IssueID)
|
||||
sortedIssueIDs[issue.Sorting] = issue.IssueID
|
||||
}
|
||||
movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(movedIssues) != len(form.Issues) {
|
||||
ctx.ServerError("some issues do not exist", errors.New("some issues do not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = movedIssues.LoadRepositories(ctx); err != nil {
|
||||
ctx.ServerError("LoadRepositories", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, issue := range movedIssues {
|
||||
if issue.RepoID != project.RepoID && issue.Repo.OwnerID != project.OwnerID {
|
||||
ctx.ServerError("Some issue's repoID is not equal to project's repoID", errors.New("Some issue's repoID is not equal to project's repoID"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = project_service.MoveIssuesOnProjectColumn(ctx, ctx.Doer, column, sortedIssueIDs); err != nil {
|
||||
ctx.ServerError("MoveIssuesOnProjectColumn", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/org"
|
||||
"gitea.dev/services/contexttest"
|
||||
"gitea.dev/services/forms"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckProjectColumnChangePermissions(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "user2/-/projects/4/4")
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
ctx.ContextUser = ctx.Doer // user2
|
||||
ctx.SetPathParam("id", "4")
|
||||
ctx.SetPathParam("columnID", "4")
|
||||
|
||||
project, column := org.CheckProjectColumnChangePermissions(ctx)
|
||||
assert.NotNil(t, project)
|
||||
assert.NotNil(t, column)
|
||||
assert.False(t, ctx.Written())
|
||||
}
|
||||
|
||||
func TestChangeProjectStatusRejectsForeignProjects(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
// project 4 is owned by user2 not user1
|
||||
ctx, _ := contexttest.MockContext(t, "user1/-/projects/4/close")
|
||||
contexttest.LoadUser(t, ctx, 1)
|
||||
ctx.ContextUser = ctx.Doer
|
||||
ctx.SetPathParam("action", "close")
|
||||
ctx.SetPathParam("id", "4")
|
||||
|
||||
org.ChangeProjectStatus(ctx)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
|
||||
}
|
||||
|
||||
func TestAddColumnToProjectPostRejectsForeignProjects(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "user1/-/projects/4/columns/new")
|
||||
contexttest.LoadUser(t, ctx, 1)
|
||||
ctx.ContextUser = ctx.Doer
|
||||
ctx.SetPathParam("id", "4")
|
||||
web.SetForm(ctx, &forms.EditProjectColumnForm{Title: "foreign"})
|
||||
|
||||
org.AddColumnToProjectPost(ctx)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
packages_model "gitea.dev/models/packages"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
user_setting "gitea.dev/routers/web/user/setting"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
org_service "gitea.dev/services/org"
|
||||
user_service "gitea.dev/services/user"
|
||||
)
|
||||
|
||||
const (
|
||||
// tplSettingsOptions template path for render settings
|
||||
tplSettingsOptions templates.TplName = "org/settings/options"
|
||||
// tplSettingsHooks template path for render hook settings
|
||||
tplSettingsHooks templates.TplName = "org/settings/hooks"
|
||||
// tplSettingsLabels template path for render labels settings
|
||||
tplSettingsLabels templates.TplName = "org/settings/labels"
|
||||
)
|
||||
|
||||
// Settings render the main settings page
|
||||
func Settings(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("org.settings")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsOptions"] = true
|
||||
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
|
||||
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
|
||||
ctx.Data["ContextUser"] = ctx.ContextUser
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsOptions)
|
||||
}
|
||||
|
||||
// SettingsPost response for settings change submitted
|
||||
func SettingsPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.UpdateOrgSettingForm)
|
||||
ctx.Data["Title"] = ctx.Tr("org.settings")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsOptions"] = true
|
||||
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplSettingsOptions)
|
||||
return
|
||||
}
|
||||
|
||||
org := ctx.Org.Organization
|
||||
if err := org_service.UpdateOrgEmailAddress(ctx, org, form.Email); err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("UpdateOrgEmailAddress", err)
|
||||
return
|
||||
}
|
||||
|
||||
opts := &user_service.UpdateOptions{
|
||||
FullName: optional.FromPtr(form.FullName),
|
||||
Description: optional.FromPtr(form.Description),
|
||||
Website: optional.FromPtr(form.Website),
|
||||
Location: optional.FromPtr(form.Location),
|
||||
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
|
||||
}
|
||||
if ctx.Doer.IsAdmin {
|
||||
opts.MaxRepoCreation = optional.FromPtr(form.MaxRepoCreation)
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Organization setting updated: %s", org.Name)
|
||||
ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings")
|
||||
}
|
||||
|
||||
// SettingsAvatar response for change avatar on settings page
|
||||
func SettingsAvatar(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.AvatarForm)
|
||||
form.Source = forms.AvatarLocal
|
||||
if err := user_setting.UpdateAvatarSetting(ctx, form, ctx.Org.Organization.AsUser()); err != nil {
|
||||
ctx.Flash.Error(err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("org.settings.update_avatar_success"))
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings")
|
||||
}
|
||||
|
||||
// SettingsDeleteAvatar response for delete avatar on settings page
|
||||
func SettingsDeleteAvatar(ctx *context.Context) {
|
||||
if err := user_service.DeleteAvatar(ctx, ctx.Org.Organization.AsUser()); err != nil {
|
||||
ctx.Flash.Error(err.Error())
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings")
|
||||
}
|
||||
|
||||
// SettingsDeleteOrgPost response for deleting an organization
|
||||
func SettingsDeleteOrgPost(ctx *context.Context) {
|
||||
if ctx.Org.Organization.Name != ctx.FormString("org_name") {
|
||||
ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false /* no purge */); err != nil {
|
||||
if repo_model.IsErrUserOwnRepos(err) {
|
||||
ctx.JSONError(ctx.Tr("form.org_still_own_repo"))
|
||||
} else if packages_model.IsErrUserOwnPackages(err) {
|
||||
ctx.JSONError(ctx.Tr("form.org_still_own_packages"))
|
||||
} else {
|
||||
log.Error("DeleteOrganization: %v", err)
|
||||
ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.delete_failed"))))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("org.settings.delete_successful", ctx.Org.Organization.Name))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/")
|
||||
}
|
||||
|
||||
// Webhooks render webhook list page
|
||||
func Webhooks(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("org.settings")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsHooks"] = true
|
||||
ctx.Data["BaseLink"] = ctx.Org.OrgLink + "/settings/hooks"
|
||||
ctx.Data["BaseLinkNew"] = ctx.Org.OrgLink + "/settings/hooks"
|
||||
ctx.Data["Description"] = ctx.Tr("org.settings.hooks_desc")
|
||||
|
||||
ws, err := db.Find[webhook.Webhook](ctx, webhook.ListWebhookOptions{OwnerID: ctx.Org.Organization.ID})
|
||||
if err != nil {
|
||||
ctx.ServerError("ListWebhooksByOpts", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Webhooks"] = ws
|
||||
ctx.HTML(http.StatusOK, tplSettingsHooks)
|
||||
}
|
||||
|
||||
// DeleteWebhook response for delete webhook
|
||||
func DeleteWebhook(ctx *context.Context) {
|
||||
if err := webhook.DeleteWebhookByOwnerID(ctx, ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil {
|
||||
ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings/hooks")
|
||||
}
|
||||
|
||||
// Labels render organization labels page
|
||||
func Labels(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.labels")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsOrgSettingsLabels"] = true
|
||||
ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsLabels)
|
||||
}
|
||||
|
||||
// SettingsRenamePost response for renaming organization
|
||||
func SettingsRenamePost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.RenameOrgForm)
|
||||
if ctx.HasError() {
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
oldOrgName, newOrgName := ctx.Org.Organization.Name, form.NewOrgName
|
||||
|
||||
if form.OrgName != oldOrgName {
|
||||
ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
|
||||
return
|
||||
}
|
||||
if newOrgName == oldOrgName {
|
||||
ctx.JSONError(ctx.Tr("org.settings.rename_no_change"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_service.RenameUser(ctx, ctx.Org.Organization.AsUser(), newOrgName, ctx.Doer); err != nil {
|
||||
if user_model.IsErrUserAlreadyExist(err) {
|
||||
ctx.JSONError(ctx.Tr("org.form.name_been_taken", newOrgName))
|
||||
} else if db.IsErrNameReserved(err) {
|
||||
ctx.JSONError(ctx.Tr("org.form.name_reserved", newOrgName))
|
||||
} else if db.IsErrNamePatternNotAllowed(err) {
|
||||
ctx.JSONError(ctx.Tr("org.form.name_pattern_not_allowed", newOrgName))
|
||||
} else {
|
||||
log.Error("RenameOrganization: %v", err)
|
||||
ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.rename_failed"))))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("org.settings.rename_success", oldOrgName, newOrgName))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/org/" + url.PathEscape(newOrgName) + "/settings")
|
||||
}
|
||||
|
||||
// SettingsChangeVisibilityPost response for change organization visibility
|
||||
func SettingsChangeVisibilityPost(ctx *context.Context) {
|
||||
visibility, ok := structs.VisibilityModes[ctx.FormString("visibility")]
|
||||
if !ok {
|
||||
ctx.Flash.Error(ctx.Tr("invalid_data", visibility))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/org/" + url.PathEscape(ctx.Org.Organization.Name) + "/settings")
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Org.Organization.Visibility == visibility {
|
||||
ctx.Flash.Info(ctx.Tr("nothing_has_been_changed"))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/org/" + url.PathEscape(ctx.Org.Organization.Name) + "/settings")
|
||||
return
|
||||
}
|
||||
|
||||
if err := org_service.ChangeOrganizationVisibility(ctx, ctx.Org.Organization, visibility); err != nil {
|
||||
log.Error("ChangeOrganizationVisibility: %v", err)
|
||||
ctx.JSONError(ctx.Tr("error.occurred"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("org.settings.change_visibility_success", ctx.Org.Organization.Name))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/org/" + url.PathEscape(ctx.Org.Organization.Name) + "/settings")
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
user_setting "gitea.dev/routers/web/user/setting"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplSettingsApplications templates.TplName = "org/settings/applications"
|
||||
tplSettingsOAuthApplicationEdit templates.TplName = "org/settings/applications_oauth2_edit"
|
||||
)
|
||||
|
||||
func newOAuth2CommonHandlers(org *context.Organization) *user_setting.OAuth2CommonHandlers {
|
||||
return &user_setting.OAuth2CommonHandlers{
|
||||
OwnerID: org.Organization.ID,
|
||||
BasePathList: fmt.Sprintf("%s/org/%s/settings/applications", setting.AppSubURL, org.Organization.Name),
|
||||
BasePathEditPrefix: fmt.Sprintf("%s/org/%s/settings/applications/oauth2", setting.AppSubURL, org.Organization.Name),
|
||||
TplAppEdit: tplSettingsOAuthApplicationEdit,
|
||||
}
|
||||
}
|
||||
|
||||
// Applications render org applications page (for org, at the moment, there are only OAuth2 applications)
|
||||
func Applications(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings.applications")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
|
||||
apps, err := db.Find[auth.OAuth2Application](ctx, auth.FindOAuth2ApplicationsOptions{
|
||||
OwnerID: ctx.Org.Organization.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Applications"] = apps
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsApplications)
|
||||
}
|
||||
|
||||
// OAuthApplicationsPost response for adding an oauth2 application
|
||||
func OAuthApplicationsPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings.applications")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
|
||||
oa := newOAuth2CommonHandlers(ctx.Org)
|
||||
oa.AddApp(ctx)
|
||||
}
|
||||
|
||||
// OAuth2ApplicationShow displays the given application
|
||||
func OAuth2ApplicationShow(ctx *context.Context) {
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
|
||||
oa := newOAuth2CommonHandlers(ctx.Org)
|
||||
oa.EditShow(ctx)
|
||||
}
|
||||
|
||||
// OAuth2ApplicationEdit response for editing oauth2 application
|
||||
func OAuth2ApplicationEdit(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings.applications")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
|
||||
oa := newOAuth2CommonHandlers(ctx.Org)
|
||||
oa.EditSave(ctx)
|
||||
}
|
||||
|
||||
// OAuthApplicationsRegenerateSecret handles the post request for regenerating the secret
|
||||
func OAuthApplicationsRegenerateSecret(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings_title")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
|
||||
oa := newOAuth2CommonHandlers(ctx.Org)
|
||||
oa.RegenerateSecret(ctx)
|
||||
}
|
||||
|
||||
// DeleteOAuth2Application deletes the given oauth2 application
|
||||
func DeleteOAuth2Application(ctx *context.Context) {
|
||||
oa := newOAuth2CommonHandlers(ctx.Org)
|
||||
oa.DeleteApp(ctx)
|
||||
}
|
||||
|
||||
// TODO: revokes the grant with the given id
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
shared "gitea.dev/routers/web/shared/packages"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplSettingsPackages templates.TplName = "org/settings/packages"
|
||||
tplSettingsPackagesRuleEdit templates.TplName = "org/settings/packages_cleanup_rules_edit"
|
||||
tplSettingsPackagesRulePreview templates.TplName = "org/settings/packages_cleanup_rules_preview"
|
||||
)
|
||||
|
||||
func Packages(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("packages.title")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsPackages"] = true
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
shared.SetPackagesContext(ctx, ctx.ContextUser)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsPackages)
|
||||
}
|
||||
|
||||
func PackagesRuleAdd(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("packages.title")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsPackages"] = true
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
shared.SetRuleAddContext(ctx)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
|
||||
}
|
||||
|
||||
func PackagesRuleEdit(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("packages.title")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsPackages"] = true
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
shared.SetRuleEditContext(ctx, ctx.ContextUser)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
|
||||
}
|
||||
|
||||
func PackagesRuleAddPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("packages.title")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsPackages"] = true
|
||||
|
||||
shared.PerformRuleAddPost(
|
||||
ctx,
|
||||
ctx.ContextUser,
|
||||
fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name),
|
||||
tplSettingsPackagesRuleEdit,
|
||||
)
|
||||
}
|
||||
|
||||
func PackagesRuleEditPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("packages.title")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsPackages"] = true
|
||||
|
||||
shared.PerformRuleEditPost(
|
||||
ctx,
|
||||
ctx.ContextUser,
|
||||
fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name),
|
||||
tplSettingsPackagesRuleEdit,
|
||||
)
|
||||
}
|
||||
|
||||
func PackagesRulePreview(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("packages.title")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsPackages"] = true
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
shared.SetRulePreviewContext(ctx, ctx.ContextUser)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsPackagesRulePreview)
|
||||
}
|
||||
|
||||
func InitializeCargoIndex(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("packages.title")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsPackages"] = true
|
||||
|
||||
shared.InitializeCargoIndex(ctx, ctx.ContextUser)
|
||||
|
||||
ctx.Redirect(fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name))
|
||||
}
|
||||
|
||||
func RebuildCargoIndex(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("packages.title")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsPackages"] = true
|
||||
|
||||
shared.RebuildCargoIndex(ctx, ctx.ContextUser)
|
||||
|
||||
ctx.Redirect(fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name))
|
||||
}
|
||||
@@ -0,0 +1,678 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
org_model "gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
unit_model "gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/services/forms"
|
||||
org_service "gitea.dev/services/org"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
// tplTeams template path for teams list page
|
||||
tplTeams templates.TplName = "org/team/teams"
|
||||
// tplTeamNew template path for create new team page
|
||||
tplTeamNew templates.TplName = "org/team/new"
|
||||
// tplTeamMembers template path for showing team members page
|
||||
tplTeamMembers templates.TplName = "org/team/members"
|
||||
// tplTeamRepositories template path for showing team repositories page
|
||||
tplTeamRepositories templates.TplName = "org/team/repositories"
|
||||
// tplTeamInvite template path for team invites page
|
||||
tplTeamInvite templates.TplName = "org/team/invite"
|
||||
)
|
||||
|
||||
// Teams render teams list page
|
||||
func Teams(ctx *context.Context) {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
org := ctx.Org.Organization
|
||||
ctx.Data["Title"] = org.FullName
|
||||
ctx.Data["PageIsOrgTeams"] = true
|
||||
|
||||
keyword := ctx.FormTrim("q")
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
pagingNum := setting.UI.MembersPagingNum
|
||||
|
||||
searchTeams := func() (teams []*org_model.Team, count int64, err error) {
|
||||
if keyword == "" {
|
||||
// fast path, use existing teams in context if no need to filter from database
|
||||
count = int64(len(ctx.Org.Teams))
|
||||
start := (page - 1) * pagingNum
|
||||
if start > len(ctx.Org.Teams) {
|
||||
return nil, count, nil
|
||||
}
|
||||
end := min(start+pagingNum, len(ctx.Org.Teams))
|
||||
return ctx.Org.Teams[start:end], count, nil
|
||||
}
|
||||
|
||||
shouldSeeAllOrgTeams, err := context.UserShouldSeeAllOrgTeams(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
opts := &org_model.SearchTeamOptions{
|
||||
OrgID: org.ID,
|
||||
UserID: util.Iif(shouldSeeAllOrgTeams, 0, ctx.Doer.ID),
|
||||
Keyword: keyword,
|
||||
IncludeDesc: true,
|
||||
ListOptions: db.ListOptions{Page: page, PageSize: pagingNum},
|
||||
}
|
||||
return org_model.SearchTeam(ctx, opts)
|
||||
}
|
||||
|
||||
teams, count, err := searchTeams()
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchTeam", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, t := range teams {
|
||||
if err := t.LoadMembers(ctx); err != nil {
|
||||
ctx.ServerError("GetMembers", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["OrgListTeams"] = teams
|
||||
ctx.Data["Keyword"] = keyword
|
||||
pager := context.NewPagination(count, setting.UI.MembersPagingNum, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, tplTeams)
|
||||
}
|
||||
|
||||
// TeamsAction response for join, leave, remove, add operations to team
|
||||
func TeamsAction(ctx *context.Context) {
|
||||
page := ctx.FormString("page")
|
||||
var err error
|
||||
switch ctx.PathParam("action") {
|
||||
case "join":
|
||||
if !ctx.Org.IsOwner {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
err = org_service.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer)
|
||||
case "leave":
|
||||
err = org_service.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer)
|
||||
if err != nil {
|
||||
if org_model.IsErrLastOrgOwner(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
|
||||
} else {
|
||||
log.Error("Action(%s): %v", ctx.PathParam("action"), err)
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"ok": false,
|
||||
"err": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
checkIsOrgMemberAndRedirect(ctx, ctx.Org.OrgLink+"/teams/")
|
||||
return
|
||||
case "remove":
|
||||
if !ctx.Org.IsOwner {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
user, _ := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
|
||||
if user == nil {
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/teams")
|
||||
return
|
||||
}
|
||||
|
||||
err = org_service.RemoveTeamMember(ctx, ctx.Org.Team, user)
|
||||
if err != nil {
|
||||
if org_model.IsErrLastOrgOwner(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
|
||||
} else {
|
||||
log.Error("Action(%s): %v", ctx.PathParam("action"), err)
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"ok": false,
|
||||
"err": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
checkIsOrgMemberAndRedirect(ctx, ctx.Org.OrgLink+"/teams/"+url.PathEscape(ctx.Org.Team.LowerName))
|
||||
return
|
||||
case "add":
|
||||
if !ctx.Org.IsOwner {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
uname := strings.ToLower(ctx.FormString("uname"))
|
||||
var u *user_model.User
|
||||
u, err = user_model.GetUserByName(ctx, uname)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
if setting.MailService != nil && user_model.ValidateEmail(uname) == nil {
|
||||
if err := org_service.CreateTeamInvite(ctx, ctx.Doer, ctx.Org.Team, uname); err != nil {
|
||||
if org_model.IsErrTeamInviteAlreadyExist(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.duplicate_invite_to_team"))
|
||||
} else if org_model.IsErrUserEmailAlreadyAdded(err) {
|
||||
ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
|
||||
} else {
|
||||
ctx.ServerError("CreateTeamInvite", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
|
||||
}
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName))
|
||||
} else {
|
||||
ctx.ServerError("GetUserByName", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if u.IsOrganization() {
|
||||
ctx.Flash.Error(ctx.Tr("form.cannot_add_org_to_team"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName))
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Org.Team.IsMember(ctx, u.ID) {
|
||||
ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
|
||||
} else {
|
||||
err = org_service.AddTeamMember(ctx, ctx.Org.Team, u)
|
||||
}
|
||||
|
||||
page = "team"
|
||||
case "remove_invite":
|
||||
if !ctx.Org.IsOwner {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
iid := ctx.FormInt64("iid")
|
||||
if iid == 0 {
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName))
|
||||
return
|
||||
}
|
||||
|
||||
if err := org_model.RemoveInviteByID(ctx, iid, ctx.Org.Team.ID); err != nil {
|
||||
log.Error("Action(%s): %v", ctx.PathParam("action"), err)
|
||||
ctx.ServerError("RemoveInviteByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
page = "team"
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if org_model.IsErrLastOrgOwner(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Flash.Error(ctx.Tr("org.teams.members.blocked_user"))
|
||||
} else {
|
||||
log.Error("Action(%s): %v", ctx.PathParam("action"), err)
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"ok": false,
|
||||
"err": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch page {
|
||||
case "team":
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName))
|
||||
case "home":
|
||||
ctx.Redirect(ctx.Org.Organization.AsUser().HomeLink())
|
||||
default:
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/teams")
|
||||
}
|
||||
}
|
||||
|
||||
func checkIsOrgMemberAndRedirect(ctx *context.Context, defaultRedirect string) {
|
||||
if isOrgMember, err := org_model.IsOrganizationMember(ctx, ctx.Org.Organization.ID, ctx.Doer.ID); err != nil {
|
||||
ctx.ServerError("IsOrganizationMember", err)
|
||||
return
|
||||
} else if !isOrgMember && !ctx.Doer.IsAdmin {
|
||||
if ctx.Org.Organization.Visibility.IsPrivate() {
|
||||
defaultRedirect = setting.AppSubURL + "/"
|
||||
} else {
|
||||
defaultRedirect = ctx.Org.Organization.HomeLink()
|
||||
}
|
||||
}
|
||||
ctx.JSONRedirect(defaultRedirect)
|
||||
}
|
||||
|
||||
// TeamsRepoAction operate team's repository
|
||||
func TeamsRepoAction(ctx *context.Context) {
|
||||
if !ctx.Org.IsOwner {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
action := ctx.PathParam("action")
|
||||
switch action {
|
||||
case "add":
|
||||
repoName := path.Base(ctx.FormString("repo_name"))
|
||||
var repo *repo_model.Repository
|
||||
repo, err = repo_model.GetRepositoryByName(ctx, ctx.Org.Organization.ID, repoName)
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
ctx.Flash.Error(ctx.Tr("org.teams.add_nonexistent_repo"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories")
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetRepositoryByName", err)
|
||||
return
|
||||
}
|
||||
err = repo_service.TeamAddRepository(ctx, ctx.Org.Team, repo)
|
||||
case "remove":
|
||||
err = repo_service.RemoveRepositoryFromTeam(ctx, ctx.Org.Team, ctx.FormInt64("repoid"))
|
||||
case "addall":
|
||||
err = repo_service.AddAllRepositoriesToTeam(ctx, ctx.Org.Team)
|
||||
case "removeall":
|
||||
err = repo_service.RemoveAllRepositoriesFromTeam(ctx, ctx.Org.Team)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error("Action(%s): '%s' %v", ctx.PathParam("action"), ctx.Org.Team.Name, err)
|
||||
ctx.ServerError("TeamsRepoAction", err)
|
||||
return
|
||||
}
|
||||
|
||||
if action == "addall" || action == "removeall" {
|
||||
ctx.JSONRedirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories")
|
||||
return
|
||||
}
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories")
|
||||
}
|
||||
|
||||
// NewTeam render create new team page
|
||||
func NewTeam(ctx *context.Context) {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Title"] = ctx.Org.Organization.FullName
|
||||
ctx.Data["PageIsOrgTeams"] = true
|
||||
ctx.Data["PageIsOrgTeamsNew"] = true
|
||||
ctx.Data["Team"] = &org_model.Team{}
|
||||
ctx.Data["Units"] = unit_model.Units
|
||||
ctx.HTML(http.StatusOK, tplTeamNew)
|
||||
}
|
||||
|
||||
// FIXME: TEAM-UNIT-PERMISSION: this design is not right, when a new unit is added in the future,
|
||||
// The existing teams won't inherit the correct admin permission for the new unit.
|
||||
// The full history is like this:
|
||||
// 1. There was only "team", no "team unit", so "team.authorize" was used to determine the team permission.
|
||||
// 2. Later, "team unit" was introduced, then the usage of "team.authorize" became inconsistent, and causes various bugs.
|
||||
// - Sometimes, "team.authorize" is used to determine the team permission, e.g. admin, owner
|
||||
// - Sometimes, "team unit" is used not really used and "team unit" is used.
|
||||
// - Some functions like `GetTeamsWithAccessToAnyRepoUnit` use both.
|
||||
//
|
||||
// 3. After introducing "team unit" and more unclear changes, it becomes difficult to maintain team permissions.
|
||||
// - Org owner need to click the permission for each unit, but can't just set a common "write" permission for all units.
|
||||
//
|
||||
// Ideally, "team.authorize=write" should mean the team has write access to all units including newly (future) added ones.
|
||||
func getUnitPerms(forms url.Values, teamPermission perm.AccessMode) map[unit_model.Type]perm.AccessMode {
|
||||
unitPerms := make(map[unit_model.Type]perm.AccessMode)
|
||||
for _, ut := range unit_model.AllRepoUnitTypes {
|
||||
// Default access mode is none
|
||||
unitPerms[ut] = perm.AccessModeNone
|
||||
|
||||
v, ok := forms[fmt.Sprintf("unit_%d", ut)]
|
||||
if ok {
|
||||
vv, _ := strconv.Atoi(v[0])
|
||||
if teamPermission >= perm.AccessModeAdmin {
|
||||
unitPerms[ut] = teamPermission
|
||||
// Don't allow `TypeExternal{Tracker,Wiki}` to influence this as they can only be set to READ perms.
|
||||
if ut == unit_model.TypeExternalTracker || ut == unit_model.TypeExternalWiki {
|
||||
unitPerms[ut] = perm.AccessModeRead
|
||||
}
|
||||
} else {
|
||||
unitPerms[ut] = perm.AccessMode(vv)
|
||||
if unitPerms[ut] >= perm.AccessModeAdmin {
|
||||
unitPerms[ut] = perm.AccessModeWrite
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return unitPerms
|
||||
}
|
||||
|
||||
// NewTeamPost response for create new team
|
||||
func NewTeamPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.CreateTeamForm)
|
||||
includesAllRepositories := form.RepoAccess == "all"
|
||||
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
|
||||
unitPerms := getUnitPerms(ctx.Req.Form, teamPermission)
|
||||
|
||||
t := &org_model.Team{
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
Name: form.TeamName,
|
||||
Description: form.Description,
|
||||
AccessMode: teamPermission,
|
||||
IncludesAllRepositories: includesAllRepositories,
|
||||
CanCreateOrgRepo: form.CanCreateOrgRepo,
|
||||
}
|
||||
|
||||
units := make([]*org_model.TeamUnit, 0, len(unitPerms))
|
||||
for tp, perm := range unitPerms {
|
||||
units = append(units, &org_model.TeamUnit{
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
Type: tp,
|
||||
AccessMode: perm,
|
||||
})
|
||||
}
|
||||
t.Units = units
|
||||
|
||||
ctx.Data["Title"] = ctx.Org.Organization.FullName
|
||||
ctx.Data["PageIsOrgTeams"] = true
|
||||
ctx.Data["PageIsOrgTeamsNew"] = true
|
||||
ctx.Data["Units"] = unit_model.Units
|
||||
ctx.Data["Team"] = t
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplTeamNew)
|
||||
return
|
||||
}
|
||||
|
||||
if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
|
||||
return
|
||||
}
|
||||
|
||||
if err := org_service.NewTeam(ctx, t); err != nil {
|
||||
ctx.Data["Err_TeamName"] = true
|
||||
switch {
|
||||
case org_model.IsErrTeamAlreadyExist(err):
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
|
||||
default:
|
||||
ctx.ServerError("NewTeam", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Team created: %s/%s", ctx.Org.Organization.Name, t.Name)
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(t.LowerName))
|
||||
}
|
||||
|
||||
// TeamMembers render team members page
|
||||
func TeamMembers(ctx *context.Context) {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = ctx.Org.Team.Name
|
||||
ctx.Data["PageIsOrgTeams"] = true
|
||||
ctx.Data["PageIsOrgTeamMembers"] = true
|
||||
|
||||
if err := ctx.Org.Team.LoadMembers(ctx); err != nil {
|
||||
ctx.ServerError("GetMembers", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Units"] = unit_model.Units
|
||||
|
||||
invites, err := org_model.GetInvitesByTeamID(ctx, ctx.Org.Team.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetInvitesByTeamID", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Invites"] = invites
|
||||
ctx.Data["IsEmailInviteEnabled"] = setting.MailService != nil
|
||||
|
||||
ctx.HTML(http.StatusOK, tplTeamMembers)
|
||||
}
|
||||
|
||||
// TeamRepositories show the repositories of team
|
||||
func TeamRepositories(ctx *context.Context) {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = ctx.Org.Team.Name
|
||||
ctx.Data["PageIsOrgTeams"] = true
|
||||
ctx.Data["PageIsOrgTeamRepos"] = true
|
||||
|
||||
repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{
|
||||
TeamID: ctx.Org.Team.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTeamRepositories", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Units"] = unit_model.Units
|
||||
ctx.Data["TeamRepos"] = repos
|
||||
ctx.HTML(http.StatusOK, tplTeamRepositories)
|
||||
}
|
||||
|
||||
// SearchTeam api for searching teams
|
||||
func SearchTeam(ctx *context.Context) {
|
||||
listOptions := db.ListOptions{
|
||||
Page: ctx.FormInt("page"),
|
||||
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
||||
}
|
||||
|
||||
opts := &org_model.SearchTeamOptions{
|
||||
// UserID is not set because the router already requires the doer to be an org admin. Thus, we don't need to restrict to teams that the user belongs in
|
||||
Keyword: ctx.FormTrim("q"),
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"),
|
||||
ListOptions: listOptions,
|
||||
}
|
||||
|
||||
teams, maxResults, err := org_model.SearchTeam(ctx, opts)
|
||||
if err != nil {
|
||||
log.Error("SearchTeam failed: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]any{
|
||||
"ok": false,
|
||||
"error": "SearchTeam internal failure",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
apiTeams, err := convert.ToTeams(ctx, teams, false)
|
||||
if err != nil {
|
||||
log.Error("convert ToTeams failed: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]any{
|
||||
"ok": false,
|
||||
"error": "SearchTeam failed to get units",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(maxResults)
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"ok": true,
|
||||
"data": apiTeams,
|
||||
})
|
||||
}
|
||||
|
||||
// EditTeam render team edit page
|
||||
func EditTeam(ctx *context.Context) {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Title"] = ctx.Org.Organization.FullName
|
||||
ctx.Data["PageIsOrgTeams"] = true
|
||||
if err := ctx.Org.Team.LoadUnits(ctx); err != nil {
|
||||
ctx.ServerError("LoadUnits", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Team"] = ctx.Org.Team
|
||||
ctx.Data["Units"] = unit_model.Units
|
||||
ctx.HTML(http.StatusOK, tplTeamNew)
|
||||
}
|
||||
|
||||
// EditTeamPost response for modify team information
|
||||
func EditTeamPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.CreateTeamForm)
|
||||
t := ctx.Org.Team
|
||||
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
|
||||
unitPerms := getUnitPerms(ctx.Req.Form, teamPermission)
|
||||
isAuthChanged := false
|
||||
isIncludeAllChanged := false
|
||||
includesAllRepositories := form.RepoAccess == "all"
|
||||
|
||||
ctx.Data["Title"] = ctx.Org.Organization.FullName
|
||||
ctx.Data["PageIsOrgTeams"] = true
|
||||
ctx.Data["Team"] = t
|
||||
ctx.Data["Units"] = unit_model.Units
|
||||
|
||||
if !t.IsOwnerTeam() {
|
||||
t.Name = form.TeamName
|
||||
if t.AccessMode != teamPermission {
|
||||
isAuthChanged = true
|
||||
t.AccessMode = teamPermission
|
||||
}
|
||||
|
||||
if t.IncludesAllRepositories != includesAllRepositories {
|
||||
isIncludeAllChanged = true
|
||||
t.IncludesAllRepositories = includesAllRepositories
|
||||
}
|
||||
t.CanCreateOrgRepo = form.CanCreateOrgRepo
|
||||
} else {
|
||||
t.CanCreateOrgRepo = true
|
||||
}
|
||||
|
||||
t.Description = form.Description
|
||||
units := make([]*org_model.TeamUnit, 0, len(unitPerms))
|
||||
for tp, perm := range unitPerms {
|
||||
units = append(units, &org_model.TeamUnit{
|
||||
OrgID: t.OrgID,
|
||||
TeamID: t.ID,
|
||||
Type: tp,
|
||||
AccessMode: perm,
|
||||
})
|
||||
}
|
||||
t.Units = units
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplTeamNew)
|
||||
return
|
||||
}
|
||||
|
||||
if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
|
||||
return
|
||||
}
|
||||
|
||||
if err := org_service.UpdateTeam(ctx, t, isAuthChanged, isIncludeAllChanged); err != nil {
|
||||
ctx.Data["Err_TeamName"] = true
|
||||
switch {
|
||||
case org_model.IsErrTeamAlreadyExist(err):
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
|
||||
default:
|
||||
ctx.ServerError("UpdateTeam", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(t.LowerName))
|
||||
}
|
||||
|
||||
// DeleteTeam response for the delete team request
|
||||
func DeleteTeam(ctx *context.Context) {
|
||||
if err := org_service.DeleteTeam(ctx, ctx.Org.Team); err != nil {
|
||||
ctx.Flash.Error("DeleteTeam: " + err.Error())
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("org.teams.delete_team_success"))
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(ctx.Org.OrgLink + "/teams")
|
||||
}
|
||||
|
||||
// TeamInvite renders the team invite page
|
||||
func TeamInvite(ctx *context.Context) {
|
||||
invite, org, team, inviter, err := getTeamInviteFromContext(ctx)
|
||||
// TODO: to quickly debug the UI, can uncomment this (don't worry, it won't pass CI lint)
|
||||
// invite, org, team, inviter, err = &org_model.TeamInvite{}, &org_model.Organization{}, &org_model.Team{}, ctx.Doer, nil
|
||||
if err != nil {
|
||||
if org_model.IsErrTeamInviteNotFound(err) {
|
||||
ctx.NotFound(err)
|
||||
} else {
|
||||
ctx.ServerError("getTeamInviteFromContext", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("org.teams.invite_team_member", team.Name)
|
||||
ctx.Data["Invite"] = invite
|
||||
ctx.Data["Organization"] = org
|
||||
ctx.Data["Team"] = team
|
||||
ctx.Data["Inviter"] = inviter
|
||||
|
||||
ctx.HTML(http.StatusOK, tplTeamInvite)
|
||||
}
|
||||
|
||||
// TeamInvitePost handles the team invitation
|
||||
func TeamInvitePost(ctx *context.Context) {
|
||||
invite, org, team, _, err := getTeamInviteFromContext(ctx)
|
||||
if err != nil {
|
||||
if org_model.IsErrTeamInviteNotFound(err) {
|
||||
ctx.NotFound(err)
|
||||
} else {
|
||||
ctx.ServerError("getTeamInviteFromContext", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := org_service.AddTeamMember(ctx, team, ctx.Doer); err != nil {
|
||||
ctx.ServerError("AddTeamMember", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := org_model.RemoveInviteByID(ctx, invite.ID, team.ID); err != nil {
|
||||
log.Error("RemoveInviteByID: %v", err)
|
||||
}
|
||||
|
||||
ctx.Redirect(org.OrganisationLink() + "/teams/" + url.PathEscape(team.LowerName))
|
||||
}
|
||||
|
||||
func getTeamInviteFromContext(ctx *context.Context) (*org_model.TeamInvite, *org_model.Organization, *org_model.Team, *user_model.User, error) {
|
||||
invite, err := org_model.GetInviteByToken(ctx, ctx.PathParam("token"))
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
inviter, err := user_model.GetUserByID(ctx, invite.InviterID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
team, err := org_model.GetTeamByID(ctx, invite.TeamID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
org, err := user_model.GetUserByID(ctx, team.OrgID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return invite, org_model.OrgFromUser(org), team, inviter, nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/modules/templates"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const tplByRepos templates.TplName = "org/worktime"
|
||||
|
||||
// parseOrgTimes contains functionality that is required in all these functions,
|
||||
// like parsing the date from the request, setting default dates, etc.
|
||||
func parseOrgTimes(ctx *context.Context) (unixFrom, unixTo int64) {
|
||||
rangeFrom := ctx.FormString("from")
|
||||
rangeTo := ctx.FormString("to")
|
||||
if rangeFrom == "" {
|
||||
rangeFrom = time.Now().Format("2006-01") + "-01" // defaults to start of current month
|
||||
}
|
||||
if rangeTo == "" {
|
||||
rangeTo = time.Now().Format("2006-01-02") // defaults to today
|
||||
}
|
||||
|
||||
ctx.Data["RangeFrom"] = rangeFrom
|
||||
ctx.Data["RangeTo"] = rangeTo
|
||||
|
||||
timeFrom, err := time.Parse("2006-01-02", rangeFrom)
|
||||
if err != nil {
|
||||
ctx.ServerError("time.Parse", err)
|
||||
}
|
||||
timeTo, err := time.Parse("2006-01-02", rangeTo)
|
||||
if err != nil {
|
||||
ctx.ServerError("time.Parse", err)
|
||||
}
|
||||
unixFrom = timeFrom.Unix()
|
||||
unixTo = timeTo.Add(1440*time.Minute - 1*time.Second).Unix() // humans expect that we include the ending day too
|
||||
return unixFrom, unixTo
|
||||
}
|
||||
|
||||
func Worktime(ctx *context.Context) {
|
||||
ctx.Data["PageIsOrgTimes"] = true
|
||||
|
||||
unixFrom, unixTo := parseOrgTimes(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
worktimeBy := ctx.FormString("by")
|
||||
ctx.Data["WorktimeBy"] = worktimeBy
|
||||
|
||||
var worktimeSumResult any
|
||||
var err error
|
||||
switch worktimeBy {
|
||||
case "milestones":
|
||||
worktimeSumResult, err = organization.GetWorktimeByMilestones(ctx, ctx.Org.Organization, unixFrom, unixTo)
|
||||
ctx.Data["WorktimeByMilestones"] = true
|
||||
case "members":
|
||||
worktimeSumResult, err = organization.GetWorktimeByMembers(ctx, ctx.Org.Organization, unixFrom, unixTo)
|
||||
ctx.Data["WorktimeByMembers"] = true
|
||||
default: /* by repos */
|
||||
worktimeSumResult, err = organization.GetWorktimeByRepos(ctx, ctx.Org.Organization, unixFrom, unixTo)
|
||||
ctx.Data["WorktimeByRepos"] = true
|
||||
}
|
||||
if err != nil {
|
||||
ctx.ServerError("GetWorktime", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["WorktimeSumResult"] = worktimeSumResult
|
||||
ctx.HTML(http.StatusOK, tplByRepos)
|
||||
}
|
||||
Reference in New Issue
Block a user