summaryrefslogtreecommitdiffstats
path: root/routers/web/org
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-12-12 23:57:56 +0100
commite68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch)
tree97775d6c13b0f416af55314eb6a89ef792474615 /routers/web/org
parentInitial commit. (diff)
downloadforgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz
forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r--routers/web/org/home.go189
-rw-r--r--routers/web/org/main_test.go14
-rw-r--r--routers/web/org/members.go144
-rw-r--r--routers/web/org/org.go80
-rw-r--r--routers/web/org/org_labels.go116
-rw-r--r--routers/web/org/projects.go610
-rw-r--r--routers/web/org/projects_test.go28
-rw-r--r--routers/web/org/setting.go258
-rw-r--r--routers/web/org/setting/blocked_users.go85
-rw-r--r--routers/web/org/setting/runners.go12
-rw-r--r--routers/web/org/setting_oauth2.go102
-rw-r--r--routers/web/org/setting_packages.go131
-rw-r--r--routers/web/org/teams.go628
13 files changed, 2397 insertions, 0 deletions
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
new file mode 100644
index 0000000..92793d9
--- /dev/null
+++ b/routers/web/org/home.go
@@ -0,0 +1,189 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "fmt"
+ "net/http"
+ "path"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/organization"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
+)
+
+const (
+ tplOrgHome base.TplName = "org/home"
+)
+
+// Home show organization home page
+func Home(ctx *context.Context) {
+ uname := ctx.Params(":username")
+
+ if strings.HasSuffix(uname, ".keys") || strings.HasSuffix(uname, ".gpg") {
+ ctx.NotFound("", nil)
+ return
+ }
+
+ ctx.SetParams(":org", uname)
+ context.HandleOrgAssignment(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ 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
+
+ var (
+ repos []*repo_model.Repository
+ count int64
+ err error
+ )
+ 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
+ }
+
+ opts := &organization.FindOrgMembersOpts{
+ OrgID: org.ID,
+ PublicOnly: ctx.Org.PublicMemberOnly,
+ ListOptions: db.ListOptions{Page: 1, PageSize: 25},
+ }
+ members, _, err := organization.FindOrgMembers(ctx, opts)
+ if err != nil {
+ ctx.ServerError("FindOrgMembers", err)
+ return
+ }
+
+ ctx.Data["Repos"] = repos
+ ctx.Data["Total"] = count
+ ctx.Data["Members"] = members
+ ctx.Data["Teams"] = ctx.Org.Teams
+ ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
+ ctx.Data["PageIsViewRepositories"] = true
+
+ err = shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", err)
+ return
+ }
+
+ pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
+ pager.SetDefaultParams(ctx)
+ pager.AddParamString("language", language)
+ if archived.Has() {
+ pager.AddParamString("archived", fmt.Sprint(archived.Value()))
+ }
+ if fork.Has() {
+ pager.AddParamString("fork", fmt.Sprint(fork.Value()))
+ }
+ if mirror.Has() {
+ pager.AddParamString("mirror", fmt.Sprint(mirror.Value()))
+ }
+ if template.Has() {
+ pager.AddParamString("template", fmt.Sprint(template.Value()))
+ }
+ if private.Has() {
+ pager.AddParamString("private", fmt.Sprint(private.Value()))
+ }
+ ctx.Data["Page"] = pager
+
+ ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
+
+ profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer)
+ defer profileClose()
+ prepareOrgProfileReadme(ctx, profileGitRepo, profileDbRepo, profileReadmeBlob)
+
+ ctx.HTML(http.StatusOK, tplOrgHome)
+}
+
+func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repository, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) {
+ if profileGitRepo == nil || profileReadme == nil {
+ return
+ }
+
+ if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
+ log.Error("failed to GetBlobContent: %v", err)
+ } else {
+ if profileContent, err := markdown.RenderString(&markup.RenderContext{
+ Ctx: ctx,
+ GitRepo: profileGitRepo,
+ Links: markup.Links{
+ // Pass repo link to markdown render for the full link of media elements.
+ // The profile of default branch would be shown.
+ Base: profileDbRepo.Link(),
+ BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
+ },
+ Metas: map[string]string{"mode": "document"},
+ }, bytes); err != nil {
+ log.Error("failed to RenderString: %v", err)
+ } else {
+ ctx.Data["ProfileReadme"] = profileContent
+ }
+ }
+}
diff --git a/routers/web/org/main_test.go b/routers/web/org/main_test.go
new file mode 100644
index 0000000..92237d6
--- /dev/null
+++ b/routers/web/org/main_test.go
@@ -0,0 +1,14 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org_test
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+)
+
+func TestMain(m *testing.M) {
+ unittest.MainTest(m)
+}
diff --git a/routers/web/org/members.go b/routers/web/org/members.go
new file mode 100644
index 0000000..9a3d60e
--- /dev/null
+++ b/routers/web/org/members.go
@@ -0,0 +1,144 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2020 The Gitea Authors.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
+)
+
+const (
+ // tplMembers template for organization members page
+ tplMembers base.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 := ctx.FormInt("page")
+ if page <= 1 {
+ page = 1
+ }
+
+ opts := &organization.FindOrgMembersOpts{
+ OrgID: org.ID,
+ PublicOnly: true,
+ }
+
+ if ctx.Doer != nil {
+ isMember, err := ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "IsOrgMember")
+ return
+ }
+ opts.PublicOnly = !isMember && !ctx.Doer.IsAdmin
+ }
+ ctx.Data["PublicOnly"] = opts.PublicOnly
+
+ total, err := organization.CountOrgMembers(ctx, opts)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "CountOrgMembers")
+ return
+ }
+
+ err = shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", err)
+ return
+ }
+
+ pager := context.NewPagination(int(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) {
+ uid := ctx.FormInt64("uid")
+ if uid == 0 {
+ ctx.Redirect(ctx.Org.OrgLink + "/members")
+ return
+ }
+
+ org := ctx.Org.Organization
+ var err error
+ switch ctx.Params(":action") {
+ case "private":
+ if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, false)
+ case "public":
+ if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, true)
+ case "remove":
+ if !ctx.Org.IsOwner {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ err = models.RemoveOrgUser(ctx, org.ID, uid)
+ if organization.IsErrLastOrgOwner(err) {
+ ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
+ ctx.JSONRedirect(ctx.Org.OrgLink + "/members")
+ return
+ }
+ case "leave":
+ err = models.RemoveOrgUser(ctx, org.ID, ctx.Doer.ID)
+ if err == nil {
+ ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName()))
+ ctx.JSON(http.StatusOK, map[string]any{
+ "redirect": "", // keep the user stay on current page, in case they want to do other operations.
+ })
+ } else if organization.IsErrLastOrgOwner(err) {
+ ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
+ ctx.JSONRedirect(ctx.Org.OrgLink + "/members")
+ } else {
+ log.Error("RemoveOrgUser(%d,%d): %v", org.ID, ctx.Doer.ID, err)
+ }
+ return
+ }
+
+ if err != nil {
+ log.Error("Action(%s): %v", ctx.Params(":action"), err)
+ ctx.JSON(http.StatusOK, map[string]any{
+ "ok": false,
+ "err": err.Error(),
+ })
+ return
+ }
+
+ redirect := ctx.Org.OrgLink + "/members"
+ if ctx.Params(":action") == "leave" {
+ redirect = setting.AppSubURL + "/"
+ }
+
+ ctx.JSONRedirect(redirect)
+}
diff --git a/routers/web/org/org.go b/routers/web/org/org.go
new file mode 100644
index 0000000..dd3aab4
--- /dev/null
+++ b/routers/web/org/org.go
@@ -0,0 +1,80 @@
+// 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"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/organization"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+)
+
+const (
+ // tplCreateOrg template path for create organization
+ tplCreateOrg base.TplName = "org/create"
+)
+
+// Create render the page for create organization
+func Create(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("new_org.title")
+ ctx.Data["DefaultOrgVisibilityMode"] = setting.Service.DefaultOrgVisibilityMode
+ if !ctx.Doer.CanCreateOrganization() {
+ ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
+ return
+ }
+ 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.title")
+
+ 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.RenderWithErr(ctx.Tr("form.org_name_been_taken"), tplCreateOrg, &form)
+ case db.IsErrNameReserved(err):
+ ctx.RenderWithErr(ctx.Tr("org.form.name_reserved", err.(db.ErrNameReserved).Name), tplCreateOrg, &form)
+ case db.IsErrNamePatternNotAllowed(err):
+ ctx.RenderWithErr(ctx.Tr("org.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplCreateOrg, &form)
+ case organization.IsErrUserNotAllowedCreateOrg(err):
+ ctx.RenderWithErr(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())
+}
diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go
new file mode 100644
index 0000000..02eae80
--- /dev/null
+++ b/routers/web/org/org_labels.go
@@ -0,0 +1,116 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/modules/label"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/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 := web.GetForm(ctx).(*forms.CreateLabelForm)
+ ctx.Data["Title"] = ctx.Tr("repo.labels")
+ ctx.Data["PageIsLabels"] = true
+ ctx.Data["PageIsOrgSettings"] = true
+
+ if ctx.HasError() {
+ ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+ return
+ }
+
+ l := &issues_model.Label{
+ OrgID: ctx.Org.Organization.ID,
+ Name: form.Title,
+ Exclusive: form.Exclusive,
+ Description: form.Description,
+ Color: form.Color,
+ }
+ if err := issues_model.NewLabel(ctx, l); err != nil {
+ ctx.ServerError("NewLabel", err)
+ return
+ }
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+}
+
+// UpdateLabel update a label's name and color
+func UpdateLabel(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.CreateLabelForm)
+ l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, form.ID)
+ if err != nil {
+ switch {
+ case issues_model.IsErrOrgLabelNotExist(err):
+ ctx.Error(http.StatusNotFound)
+ default:
+ ctx.ServerError("UpdateLabel", err)
+ }
+ return
+ }
+
+ l.Name = form.Title
+ l.Exclusive = form.Exclusive
+ 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.Redirect(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")
+}
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
new file mode 100644
index 0000000..64d233f
--- /dev/null
+++ b/routers/web/org/projects.go
@@ -0,0 +1,610 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ project_model "code.gitea.io/gitea/models/project"
+ attachment_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/web"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+)
+
+const (
+ tplProjects base.TplName = "org/projects/list"
+ tplProjectsNew base.TplName = "org/projects/new"
+ tplProjectsView base.TplName = "org/projects/view"
+)
+
+// MustEnableProjects check if projects are enabled in settings
+func MustEnableProjects(ctx *context.Context) {
+ if unit.TypeProjects.UnitGlobalDisabled() {
+ ctx.NotFound("EnableProjects", nil)
+ return
+ }
+}
+
+// Projects renders the home page of projects
+func Projects(ctx *context.Context) {
+ shared_user.PrepareContextForProfileBigAvatar(ctx)
+ ctx.Data["Title"] = ctx.Tr("repo.projects")
+
+ sortType := ctx.FormTrim("sort")
+
+ isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed"
+ keyword := ctx.FormTrim("q")
+ page := ctx.FormInt("page")
+ if page <= 1 {
+ 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
+ }
+
+ 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
+ shared_user.RenderUserHeader(ctx)
+
+ if isShowClosed {
+ ctx.Data["State"] = "closed"
+ } else {
+ ctx.Data["State"] = "open"
+ }
+
+ for _, project := range projects {
+ project.RenderedContent = templates.RenderMarkdownToHtml(ctx, project.Description)
+ }
+
+ err = shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", err)
+ return
+ }
+
+ numPages := 0
+ if total > 0 {
+ numPages = (int(total) - 1/setting.UI.IssuePagingNum)
+ }
+
+ pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages)
+ pager.AddParam(ctx, "state", "State")
+ 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"
+ shared_user.RenderUserHeader(ctx)
+
+ err := shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", 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")
+ shared_user.RenderUserHeader(ctx)
+
+ 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.Params(":action") {
+ case "open":
+ toClose = false
+ case "close":
+ toClose = true
+ default:
+ ctx.JSONRedirect(ctx.ContextUser.HomeLink() + "/-/projects")
+ return
+ }
+ id := ctx.ParamsInt64(":id")
+
+ if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil {
+ ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
+ return
+ }
+ ctx.JSONRedirect(fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), id))
+}
+
+// DeleteProject delete a project
+func DeleteProject(ctx *context.Context) {
+ p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
+ return
+ }
+ if p.OwnerID != ctx.ContextUser.ID {
+ ctx.NotFound("", nil)
+ 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()
+
+ shared_user.RenderUserHeader(ctx)
+
+ p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
+ return
+ }
+ if p.OwnerID != ctx.ContextUser.ID {
+ ctx.NotFound("", nil)
+ 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"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), 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.ParamsInt64(":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"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), projectID)
+
+ shared_user.RenderUserHeader(ctx)
+
+ err := shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", err)
+ return
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplProjectsNew)
+ return
+ }
+
+ p, err := project_model.GetProjectByID(ctx, projectID)
+ if err != nil {
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
+ return
+ }
+ if p.OwnerID != ctx.ContextUser.ID {
+ ctx.NotFound("", nil)
+ 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.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
+ return
+ }
+ if project.OwnerID != ctx.ContextUser.ID {
+ ctx.NotFound("", nil)
+ return
+ }
+
+ columns, err := project.GetColumns(ctx)
+ if err != nil {
+ ctx.ServerError("GetProjectColumns", err)
+ return
+ }
+
+ issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns)
+ if err != nil {
+ ctx.ServerError("LoadIssuesOfColumns", err)
+ return
+ }
+
+ if project.CardType != project_model.CardTypeTextOnly {
+ issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
+ for _, issuesList := range issuesMap {
+ for _, issue := range issuesList {
+ if issueAttachment, err := attachment_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
+ }
+ }
+ }
+ }
+
+ project.RenderedContent = templates.RenderMarkdownToHtml(ctx, 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
+ shared_user.RenderUserHeader(ctx)
+
+ err = shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", 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.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
+ return
+ }
+
+ pb, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
+ if err != nil {
+ ctx.ServerError("GetProjectColumn", err)
+ return
+ }
+ if pb.ProjectID != ctx.ParamsInt64(":id") {
+ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+ "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID),
+ })
+ return
+ }
+
+ if project.OwnerID != ctx.ContextUser.ID {
+ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+ "message": fmt.Sprintf("ProjectColumn[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID),
+ })
+ return
+ }
+
+ if err := project_model.DeleteColumnByID(ctx, ctx.ParamsInt64(":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.GetProjectByID(ctx, ctx.ParamsInt64(":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.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
+ return nil, nil
+ }
+
+ column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
+ if err != nil {
+ ctx.ServerError("GetProjectColumn", err)
+ return nil, nil
+ }
+ if column.ProjectID != ctx.ParamsInt64(":id") {
+ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+ "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID),
+ })
+ return nil, nil
+ }
+
+ if project.OwnerID != ctx.ContextUser.ID {
+ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+ "message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", column.ID, project.ID),
+ })
+ 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.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
+ return
+ }
+ if project.OwnerID != ctx.ContextUser.ID {
+ ctx.NotFound("InvalidRepoID", nil)
+ return
+ }
+
+ column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err)
+ return
+ }
+
+ if column.ProjectID != project.ID {
+ ctx.NotFound("ColumnNotInProject", nil)
+ 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_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil {
+ ctx.ServerError("MoveIssuesOnProjectColumn", err)
+ return
+ }
+
+ ctx.JSONOK()
+}
diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go
new file mode 100644
index 0000000..ab419cc
--- /dev/null
+++ b/routers/web/org/projects_test.go
@@ -0,0 +1,28 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org_test
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/routers/web/org"
+ "code.gitea.io/gitea/services/contexttest"
+
+ "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.SetParams(":id", "4")
+ ctx.SetParams(":columnID", "4")
+
+ project, column := org.CheckProjectColumnChangePermissions(ctx)
+ assert.NotNil(t, project)
+ assert.NotNil(t, column)
+ assert.False(t, ctx.Written())
+}
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
new file mode 100644
index 0000000..0be734a
--- /dev/null
+++ b/routers/web/org/setting.go
@@ -0,0 +1,258 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "net/http"
+ "net/url"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/web"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ user_setting "code.gitea.io/gitea/routers/web/user/setting"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ org_service "code.gitea.io/gitea/services/org"
+ repo_service "code.gitea.io/gitea/services/repository"
+ user_service "code.gitea.io/gitea/services/user"
+ webhook_service "code.gitea.io/gitea/services/webhook"
+)
+
+const (
+ // tplSettingsOptions template path for render settings
+ tplSettingsOptions base.TplName = "org/settings/options"
+ // tplSettingsDelete template path for render delete repository
+ tplSettingsDelete base.TplName = "org/settings/delete"
+ // tplSettingsHooks template path for render hook settings
+ tplSettingsHooks base.TplName = "org/settings/hooks"
+ // tplSettingsLabels template path for render labels settings
+ tplSettingsLabels base.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
+
+ err := shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", 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 org.Name != form.Name {
+ if err := user_service.RenameUser(ctx, org.AsUser(), form.Name); err != nil {
+ if user_model.IsErrUserAlreadyExist(err) {
+ ctx.Data["Err_Name"] = true
+ ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
+ } else if db.IsErrNameReserved(err) {
+ ctx.Data["Err_Name"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
+ } else if db.IsErrNamePatternNotAllowed(err) {
+ ctx.Data["Err_Name"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
+ } else {
+ ctx.ServerError("RenameUser", err)
+ }
+ return
+ }
+
+ ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name)
+ }
+
+ if form.Email != "" {
+ if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
+ ctx.Data["Err_Email"] = true
+ ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form)
+ return
+ }
+ }
+
+ opts := &user_service.UpdateOptions{
+ FullName: optional.Some(form.FullName),
+ Description: optional.Some(form.Description),
+ Website: optional.Some(form.Website),
+ Location: optional.Some(form.Location),
+ Visibility: optional.Some(form.Visibility),
+ RepoAdminChangeTeamAccess: optional.Some(form.RepoAdminChangeTeamAccess),
+ }
+ if ctx.Doer.IsAdmin {
+ opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation)
+ }
+
+ visibilityChanged := org.Visibility != form.Visibility
+
+ if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
+ ctx.ServerError("UpdateUser", err)
+ return
+ }
+
+ // update forks visibility
+ if visibilityChanged {
+ repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
+ Actor: org.AsUser(), Private: true, ListOptions: db.ListOptions{Page: 1, PageSize: org.NumRepos},
+ })
+ if err != nil {
+ ctx.ServerError("GetRepositories", err)
+ return
+ }
+ for _, repo := range repos {
+ repo.OwnerName = org.Name
+ if err := repo_service.UpdateRepository(ctx, repo, true); err != nil {
+ ctx.ServerError("UpdateRepository", 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")
+}
+
+// SettingsDelete response for deleting an organization
+func SettingsDelete(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("org.settings")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsDelete"] = true
+
+ if ctx.Req.Method == "POST" {
+ if ctx.Org.Organization.Name != ctx.FormString("org_name") {
+ ctx.Data["Err_OrgName"] = true
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_org_name"), tplSettingsDelete, nil)
+ return
+ }
+
+ if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil {
+ if models.IsErrUserOwnRepos(err) {
+ ctx.Flash.Error(ctx.Tr("form.org_still_own_repo"))
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
+ } else if models.IsErrUserOwnPackages(err) {
+ ctx.Flash.Error(ctx.Tr("form.org_still_own_packages"))
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
+ } else {
+ ctx.ServerError("DeleteOrganization", err)
+ }
+ } else {
+ log.Trace("Organization deleted: %s", ctx.Org.Organization.Name)
+ ctx.Redirect(setting.AppSubURL + "/")
+ }
+ return
+ }
+
+ err := shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", err)
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplSettingsDelete)
+}
+
+// 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["WebhookList"] = webhook_service.List()
+ 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
+ }
+
+ err = shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", 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
+
+ err := shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", err)
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplSettingsLabels)
+}
diff --git a/routers/web/org/setting/blocked_users.go b/routers/web/org/setting/blocked_users.go
new file mode 100644
index 0000000..0c7f245
--- /dev/null
+++ b/routers/web/org/setting/blocked_users.go
@@ -0,0 +1,85 @@
+// Copyright 2023 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ user_model "code.gitea.io/gitea/models/user"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
+ user_service "code.gitea.io/gitea/services/user"
+)
+
+const tplBlockedUsers = "org/settings/blocked_users"
+
+// BlockedUsers renders the blocked users page.
+func BlockedUsers(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings.blocked_users")
+ ctx.Data["PageIsSettingsBlockedUsers"] = true
+
+ blockedUsers, err := user_model.ListBlockedUsers(ctx, ctx.Org.Organization.ID, db.ListOptions{})
+ if err != nil {
+ ctx.ServerError("ListBlockedUsers", err)
+ return
+ }
+
+ err = shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", err)
+ return
+ }
+
+ ctx.Data["BlockedUsers"] = blockedUsers
+
+ ctx.HTML(http.StatusOK, tplBlockedUsers)
+}
+
+// BlockedUsersBlock blocks a particular user from the organization.
+func BlockedUsersBlock(ctx *context.Context) {
+ uname := strings.ToLower(ctx.FormString("uname"))
+ u, err := user_model.GetUserByName(ctx, uname)
+ if err != nil {
+ ctx.ServerError("GetUserByName", err)
+ return
+ }
+
+ if u.IsOrganization() {
+ ctx.ServerError("IsOrganization", fmt.Errorf("%s is an organization not a user", u.Name))
+ return
+ }
+
+ if err := user_service.BlockUser(ctx, ctx.Org.Organization.ID, u.ID); err != nil {
+ ctx.ServerError("BlockUser", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("settings.user_block_success"))
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/blocked_users")
+}
+
+// BlockedUsersUnblock unblocks a particular user from the organization.
+func BlockedUsersUnblock(ctx *context.Context) {
+ u, err := user_model.GetUserByID(ctx, ctx.FormInt64("user_id"))
+ if err != nil {
+ ctx.ServerError("GetUserByID", err)
+ return
+ }
+
+ if u.IsOrganization() {
+ ctx.ServerError("IsOrganization", fmt.Errorf("%s is an organization not a user", u.Name))
+ return
+ }
+
+ if err := user_model.UnblockUser(ctx, ctx.Org.Organization.ID, u.ID); err != nil {
+ ctx.ServerError("UnblockUser", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("settings.user_unblock_success"))
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/blocked_users")
+}
diff --git a/routers/web/org/setting/runners.go b/routers/web/org/setting/runners.go
new file mode 100644
index 0000000..fe05709
--- /dev/null
+++ b/routers/web/org/setting/runners.go
@@ -0,0 +1,12 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+ "code.gitea.io/gitea/services/context"
+)
+
+func RedirectToDefaultSetting(ctx *context.Context) {
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/actions/runners")
+}
diff --git a/routers/web/org/setting_oauth2.go b/routers/web/org/setting_oauth2.go
new file mode 100644
index 0000000..7f85579
--- /dev/null
+++ b/routers/web/org/setting_oauth2.go
@@ -0,0 +1,102 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "fmt"
+ "net/http"
+
+ "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/setting"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ user_setting "code.gitea.io/gitea/routers/web/user/setting"
+ "code.gitea.io/gitea/services/context"
+)
+
+const (
+ tplSettingsApplications base.TplName = "org/settings/applications"
+ tplSettingsOAuthApplicationEdit base.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
+
+ err = shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", 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")
+ 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
diff --git a/routers/web/org/setting_packages.go b/routers/web/org/setting_packages.go
new file mode 100644
index 0000000..af9836e
--- /dev/null
+++ b/routers/web/org/setting_packages.go
@@ -0,0 +1,131 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "fmt"
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/setting"
+ shared "code.gitea.io/gitea/routers/web/shared/packages"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
+)
+
+const (
+ tplSettingsPackages base.TplName = "org/settings/packages"
+ tplSettingsPackagesRuleEdit base.TplName = "org/settings/packages_cleanup_rules_edit"
+ tplSettingsPackagesRulePreview base.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
+
+ err := shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", 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
+
+ err := shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", 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
+
+ err := shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", 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
+
+ err := shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", 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))
+}
diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go
new file mode 100644
index 0000000..45c3674
--- /dev/null
+++ b/routers/web/org/teams.go
@@ -0,0 +1,628 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "path"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ org_model "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/perm"
+ repo_model "code.gitea.io/gitea/models/repo"
+ unit_model "code.gitea.io/gitea/models/unit"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/web"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/convert"
+ "code.gitea.io/gitea/services/forms"
+ org_service "code.gitea.io/gitea/services/org"
+ repo_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+ // tplTeams template path for teams list page
+ tplTeams base.TplName = "org/team/teams"
+ // tplTeamNew template path for create new team page
+ tplTeamNew base.TplName = "org/team/new"
+ // tplTeamMembers template path for showing team members page
+ tplTeamMembers base.TplName = "org/team/members"
+ // tplTeamRepositories template path for showing team repositories page
+ tplTeamRepositories base.TplName = "org/team/repositories"
+ // tplTeamInvite template path for team invites page
+ tplTeamInvite base.TplName = "org/team/invite"
+)
+
+// Teams render teams list page
+func Teams(ctx *context.Context) {
+ org := ctx.Org.Organization
+ ctx.Data["Title"] = org.FullName
+ ctx.Data["PageIsOrgTeams"] = true
+
+ for _, t := range ctx.Org.Teams {
+ if err := t.LoadMembers(ctx); err != nil {
+ ctx.ServerError("GetMembers", err)
+ return
+ }
+ }
+ ctx.Data["Teams"] = ctx.Org.Teams
+
+ err := shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", err)
+ return
+ }
+
+ 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.Params(":action") {
+ case "join":
+ if !ctx.Org.IsOwner {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
+ case "leave":
+ err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
+ if err != nil {
+ if org_model.IsErrLastOrgOwner(err) {
+ ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
+ } else {
+ log.Error("Action(%s): %v", ctx.Params(":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.Error(http.StatusNotFound)
+ return
+ }
+
+ uid := ctx.FormInt64("uid")
+ if uid == 0 {
+ ctx.Redirect(ctx.Org.OrgLink + "/teams")
+ return
+ }
+
+ err = models.RemoveTeamMember(ctx, ctx.Org.Team, uid)
+ if err != nil {
+ if org_model.IsErrLastOrgOwner(err) {
+ ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
+ } else {
+ log.Error("Action(%s): %v", ctx.Params(":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.Error(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 = models.AddTeamMember(ctx, ctx.Org.Team, u.ID)
+ }
+
+ page = "team"
+ case "remove_invite":
+ if !ctx.Org.IsOwner {
+ ctx.Error(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.Params(":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 {
+ log.Error("Action(%s): %v", ctx.Params(":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 {
+ 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.Error(http.StatusNotFound)
+ return
+ }
+
+ var err error
+ action := ctx.Params(":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 = org_service.TeamAddRepository(ctx, ctx.Org.Team, repo)
+ case "remove":
+ err = repo_service.RemoveRepositoryFromTeam(ctx, ctx.Org.Team, ctx.FormInt64("repoid"))
+ case "addall":
+ err = models.AddAllRepositories(ctx, ctx.Org.Team)
+ case "removeall":
+ err = models.RemoveAllRepositories(ctx, ctx.Org.Team)
+ }
+
+ if err != nil {
+ log.Error("Action(%s): '%s' %v", ctx.Params(":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) {
+ 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
+ if err := shared_user.LoadHeaderCount(ctx); err != nil {
+ ctx.ServerError("LoadHeaderCount", err)
+ return
+ }
+ ctx.HTML(http.StatusOK, tplTeamNew)
+}
+
+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 accessmode 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"
+ p := perm.ParseAccessMode(form.Permission)
+ unitPerms := getUnitPerms(ctx.Req.Form, p)
+ if p < perm.AccessModeAdmin {
+ // if p is less than admin accessmode, then it should be general accessmode,
+ // so we should calculate the minial accessmode from units accessmodes.
+ p = unit_model.MinUnitAccessMode(unitPerms)
+ }
+
+ t := &org_model.Team{
+ OrgID: ctx.Org.Organization.ID,
+ Name: form.TeamName,
+ Description: form.Description,
+ AccessMode: p,
+ 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.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
+ return
+ }
+
+ if err := models.NewTeam(ctx, t); err != nil {
+ ctx.Data["Err_TeamName"] = true
+ switch {
+ case org_model.IsErrTeamAlreadyExist(err):
+ ctx.RenderWithErr(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) {
+ ctx.Data["Title"] = ctx.Org.Team.Name
+ ctx.Data["PageIsOrgTeams"] = true
+ ctx.Data["PageIsOrgTeamMembers"] = true
+
+ if err := shared_user.LoadHeaderCount(ctx); err != nil {
+ ctx.ServerError("LoadHeaderCount", err)
+ return
+ }
+
+ 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) {
+ ctx.Data["Title"] = ctx.Org.Team.Name
+ ctx.Data["PageIsOrgTeams"] = true
+ ctx.Data["PageIsOrgTeamRepos"] = true
+
+ if err := shared_user.LoadHeaderCount(ctx); err != nil {
+ ctx.ServerError("LoadHeaderCount", err)
+ return
+ }
+
+ if err := ctx.Org.Team.LoadRepositories(ctx); err != nil {
+ ctx.ServerError("GetRepositories", err)
+ return
+ }
+ ctx.Data["Units"] = unit_model.Units
+ 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) {
+ 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
+ }
+ if err := shared_user.LoadHeaderCount(ctx); err != nil {
+ ctx.ServerError("LoadHeaderCount", 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
+ newAccessMode := perm.ParseAccessMode(form.Permission)
+ unitPerms := getUnitPerms(ctx.Req.Form, newAccessMode)
+ if newAccessMode < perm.AccessModeAdmin {
+ // if newAccessMode is less than admin accessmode, then it should be general accessmode,
+ // so we should calculate the minial accessmode from units accessmodes.
+ newAccessMode = unit_model.MinUnitAccessMode(unitPerms)
+ }
+ 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 != newAccessMode {
+ isAuthChanged = true
+ t.AccessMode = newAccessMode
+ }
+
+ if t.IncludesAllRepositories != includesAllRepositories {
+ isIncludeAllChanged = true
+ t.IncludesAllRepositories = includesAllRepositories
+ }
+ t.CanCreateOrgRepo = form.CanCreateOrgRepo
+
+ 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
+ } else {
+ t.CanCreateOrgRepo = true
+ }
+
+ t.Description = form.Description
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplTeamNew)
+ return
+ }
+
+ if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 {
+ ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
+ return
+ }
+
+ if err := models.UpdateTeam(ctx, t, isAuthChanged, isIncludeAllChanged); err != nil {
+ ctx.Data["Err_TeamName"] = true
+ switch {
+ case org_model.IsErrTeamAlreadyExist(err):
+ ctx.RenderWithErr(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 := models.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)
+ if err != nil {
+ if org_model.IsErrTeamInviteNotFound(err) {
+ ctx.NotFound("ErrTeamInviteNotFound", 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("ErrTeamInviteNotFound", err)
+ } else {
+ ctx.ServerError("getTeamInviteFromContext", err)
+ }
+ return
+ }
+
+ if err := models.AddTeamMember(ctx, team, ctx.Doer.ID); 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.Params("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
+}