初始提交: Gitea 项目代码

This commit is contained in:
root
2026-05-30 22:47:36 +08:00
commit f288f76350
6116 changed files with 776822 additions and 0 deletions
+38
View File
@@ -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")
}
+194
View File
@@ -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
}
+14
View File
@@ -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)
}
+131
View File
@@ -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
}
+42
View File
@@ -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))
}
+83
View File
@@ -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())
}
+116
View File
@@ -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")
}
+662
View File
@@ -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()
}
+58
View File
@@ -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())
}
+259
View File
@@ -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")
}
+101
View File
@@ -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
+127
View File
@@ -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))
}
+678
View File
@@ -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
}
+82
View File
@@ -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)
}