0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-04-04 03:35:05 +02:00

add group pages to web router

- `/{username}/groups/{group_id}` -> show a group's homepage with its repositories
- `/{org}/groups/new` -> create a new group in an org
- `/{org}/groups/{group_id}` -> show a group in an org
- `/{org}/groups/{group_id}/teams` -> show/edit teams with access to a group
- `/{org}/groups/{group_id}/teams/{team}/edit` -> edit a team's access to a group
- `/{org}/group/{group_id}/settings` -> show/edit a group's settings
- `/group/search` -> search for groups with parameters. return matching groups and repos contained within them
This commit is contained in:
☙◦ The Tablet ❀ GamerGirlandCo ◦❧ 2025-08-16 16:09:45 -04:00
parent 97576a41c4
commit efe8193237
No known key found for this signature in database
GPG Key ID: 924A5F6AF051E87C
7 changed files with 809 additions and 0 deletions

45
routers/web/group/edit.go Normal file
View File

@ -0,0 +1,45 @@
package group
import (
group_model "code.gitea.io/gitea/models/group"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
group_service "code.gitea.io/gitea/services/group"
)
func MoveGroupItem(ctx *context.Context) {
form := &forms.MovedGroupItemForm{}
if err := json.NewDecoder(ctx.Req.Body).Decode(form); err != nil {
ctx.ServerError("DecodeMovedGroupItemForm", err)
return
}
if form.IsGroup {
group, err := group_model.GetGroupByID(ctx, form.ItemID)
if err != nil {
ctx.ServerError("GetGroupByID", err)
return
}
if group.ParentGroupID != form.NewParent {
if err = group_model.MoveGroup(ctx, group, form.NewParent, form.NewPos); err != nil {
ctx.ServerError("MoveGroup", err)
return
}
if err = group_service.RecalculateGroupAccess(ctx, group, false); err != nil {
ctx.ServerError("RecalculateGroupAccess", err)
}
}
} else {
repo, err := repo_model.GetRepositoryByID(ctx, form.ItemID)
if err != nil {
ctx.ServerError("GetRepositoryByID", err)
}
if repo.GroupID != form.NewParent {
if err = group_service.MoveRepositoryToGroup(ctx, repo, form.NewParent, form.NewPos); err != nil {
ctx.ServerError("MoveRepositoryToGroup", err)
}
}
}
ctx.JSONOK()
}

117
routers/web/group/home.go Normal file
View File

@ -0,0 +1,117 @@
package group
import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
shared_group_model "code.gitea.io/gitea/models/shared/group"
"code.gitea.io/gitea/modules/setting"
shared_group "code.gitea.io/gitea/routers/web/shared/group"
"code.gitea.io/gitea/services/context"
"net/http"
)
const (
tplGroupHome = "group/home"
)
func Home(ctx *context.Context) {
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
}
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
err := shared_group.LoadHeaderCount(ctx)
if err != nil {
ctx.ServerError("LoadHeaderCount", err)
return
}
opts := &organization.FindOrgMembersOpts{
Doer: ctx.Doer,
OrgID: org.ID,
IsDoerMember: ctx.Org.IsMember,
ListOptions: db.ListOptions{Page: 1, PageSize: 25},
}
members, err := shared_group_model.FindGroupMembers(ctx, ctx.RepoGroup.Group.ID, opts)
if err != nil {
ctx.ServerError("FindOrgMembers", err)
return
}
ctx.Data["Members"] = members
ctx.Data["Teams"] = ctx.RepoGroup.Teams
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
ctx.Data["ShowMemberAndTeamTab"] = ctx.RepoGroup.IsMember || len(members) > 0
ctx.Data["PageIsViewRepositories"] = true
var (
repos []*repo_model.Repository
count int64
)
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,
GroupID: ctx.RepoGroup.Group.ID,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
return
}
ctx.Data["Repos"] = repos
ctx.Data["Total"] = count
pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplGroupHome)
}

89
routers/web/group/new.go Normal file
View File

@ -0,0 +1,89 @@
package group
import (
"code.gitea.io/gitea/models/perm"
"net/http"
"code.gitea.io/gitea/models/db"
group_model "code.gitea.io/gitea/models/group"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"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"
group_service "code.gitea.io/gitea/services/group"
)
const tplGroupNew = "group/create"
func NewGroup(ctx *context.Context) {
ctx.Data["Title"] = ctx.Org.Organization.FullName
ctx.Data["PageIsNewGroup"] = true
if ctx.RepoGroup.Group != nil {
ctx.Data["Group"] = &group_model.Group{ParentGroupID: ctx.RepoGroup.Group.ID}
} else {
ctx.Data["Group"] = &group_model.Group{}
}
ctx.Data["Units"] = unit_model.Units
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
ctx.ServerError("LoadHeaderCount", err)
return
}
opts := group_model.FindGroupsOptions{
ActorID: ctx.Doer.ID,
CanCreateIn: optional.Some(true),
OwnerID: ctx.Org.Organization.ID,
}
cond := group_model.AccessibleGroupCondition(ctx.Doer, unit_model.TypeInvalid, perm.AccessModeWrite)
cond = cond.And(opts.ToConds())
groups, err := group_model.FindGroupsByCond(ctx, &group_model.FindGroupsOptions{
ListOptions: db.ListOptions{
ListAll: true,
},
ParentGroupID: -1,
}, cond)
for _, g := range groups {
err = g.LoadAccessibleSubgroups(ctx, true, ctx.Doer)
if err != nil {
ctx.ServerError("LoadAccessibleSubgroups", err)
return
}
}
if err != nil {
ctx.ServerError("FindGroupsByCond", err)
return
}
ctx.Data["Groups"] = groups
ctx.HTML(http.StatusOK, tplGroupNew)
}
func NewGroupPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateGroupForm)
log.GetLogger(log.DEFAULT).Info("what? %+v", form)
g := &group_model.Group{
OwnerID: ctx.Org.Organization.ID,
Name: form.GroupName,
Description: form.Description,
OwnerName: ctx.Org.Organization.Name,
ParentGroupID: form.ParentGroupID,
}
ctx.Data["Title"] = ctx.Org.Organization.FullName
ctx.Data["PageIsGroupNew"] = true
ctx.Data["Units"] = unit_model.Units
ctx.Data["Group"] = g
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplGroupNew)
return
}
if err := group_service.NewGroup(ctx, g); err != nil {
ctx.Data["Err_GroupName"] = true
ctx.ServerError("NewGroup", err)
return
}
log.Trace("Group created: %s/%s", ctx.Org.Organization.Name, g.Name)
ctx.Redirect(g.GroupLink())
}

155
routers/web/group/search.go Normal file
View File

@ -0,0 +1,155 @@
package group
import (
"errors"
"fmt"
"net/http"
"code.gitea.io/gitea/models/db"
group_model "code.gitea.io/gitea/models/group"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
group_service "code.gitea.io/gitea/services/group"
)
func toSearchRepoOptions(ctx *context.Context) (opts *repo_model.SearchRepoOptions) {
page := ctx.FormInt("page")
opts = &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
},
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
OwnerID: ctx.FormInt64("uid"),
PriorityOwnerID: ctx.FormInt64("priority_owner_id"),
TeamID: ctx.FormInt64("team_id"),
TopicOnly: ctx.FormBool("topic"),
Collaborate: optional.None[bool](),
Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
Template: optional.None[bool](),
StarredByID: ctx.FormInt64("starredBy"),
IncludeDescription: ctx.FormBool("includeDesc"),
}
if ctx.FormString("template") != "" {
opts.Template = optional.Some(ctx.FormBool("template"))
}
if ctx.FormBool("exclusive") {
opts.Collaborate = optional.Some(false)
}
mode := ctx.FormString("mode")
switch mode {
case "source":
opts.Fork = optional.Some(false)
opts.Mirror = optional.Some(false)
case "fork":
opts.Fork = optional.Some(true)
case "mirror":
opts.Mirror = optional.Some(true)
case "collaborative":
opts.Mirror = optional.Some(false)
opts.Collaborate = optional.Some(true)
case "":
default:
estr := fmt.Sprintf("Invalid search mode: \"%s\"", mode)
ctx.Status(http.StatusUnprocessableEntity)
ctx.ServerError("toSearchRepoOptions", errors.New(estr))
return nil
}
if ctx.FormString("archived") != "" {
opts.Archived = optional.Some(ctx.FormBool("archived"))
}
if ctx.FormString("is_private") != "" {
opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
}
sortMode := ctx.FormString("sort")
if len(sortMode) > 0 {
sortOrder := ctx.FormString("order")
if len(sortOrder) == 0 {
sortOrder = "asc"
}
if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok {
if orderBy, ok := searchModeMap[sortMode]; ok {
opts.OrderBy = orderBy
} else {
estr := fmt.Errorf("Invalid sort mode: \"%s\"", sortMode)
ctx.Status(http.StatusUnprocessableEntity)
ctx.ServerError("toSearchRepoOptions", estr)
return nil
}
} else {
estr := fmt.Errorf("Invalid sort order: \"%s\"", sortOrder)
ctx.Status(http.StatusUnprocessableEntity)
ctx.ServerError("toSearchRepoOptions", estr)
return nil
}
}
return
}
func SearchGroup(ctx *context.Context) {
gid := ctx.FormInt64("group_id")
var (
group *group_model.Group
err error
canSee bool = true
oid int64 = ctx.FormInt64("uid")
)
if gid > 0 {
group, err = group_model.GetGroupByID(ctx, gid)
if err != nil && !group_model.IsErrGroupNotExist(err) {
ctx.ServerError("GetGroupByID", err)
return
}
}
if group != nil {
canSee, err = group.CanAccess(ctx, ctx.Doer)
if err != nil {
ctx.ServerError("GroupCanAccess", err)
return
}
oid = group.OwnerID
}
if !canSee {
ctx.NotFound(nil)
return
}
subgroupOpts := &group_model.FindGroupsOptions{
ParentGroupID: gid,
ActorID: ctx.Doer.ID,
OwnerID: oid,
}
if gid == 0 {
page := ctx.FormInt("page")
if page <= 0 {
page = 1
}
subgroupOpts.ListOptions = db.ListOptions{
Page: page,
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
}
}
sro := toSearchRepoOptions(ctx)
rgw, err := group_service.SearchRepoGroupWeb(group, &group_service.WebSearchOptions{
OrgID: oid,
GroupOpts: subgroupOpts,
RepoOpts: *sro,
Actor: ctx.Doer,
Recurse: ctx.FormBool("recurse"),
Ctx: ctx,
Locale: ctx.Locale,
})
if err != nil {
ctx.ServerError("SearchRepoGroupWeb", err)
return
}
ctx.JSON(http.StatusOK, rgw)
}

View File

@ -0,0 +1,143 @@
package group
import (
group_model "code.gitea.io/gitea/models/group"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/web"
shared_group "code.gitea.io/gitea/routers/web/shared/group"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
group_service "code.gitea.io/gitea/services/group"
repo_service "code.gitea.io/gitea/services/repository"
user_service "code.gitea.io/gitea/services/user"
"errors"
"fmt"
"io"
"net/http"
)
const (
tplSettingsOptions templates.TplName = "group/settings/options"
)
func RedirectToDefaultSetting(ctx *context.Context) {
ctx.Redirect(ctx.RepoGroup.OrgGroupLink + "/settings/actions/runners")
}
// Settings render the main settings page
func Settings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("group.settings")
ctx.Data["PageIsGroupSettings"] = true
ctx.Data["PageIsSettingsOptions"] = true
ctx.Data["CurrentVisibility"] = ctx.RepoGroup.Group.Visibility
ctx.Data["ContextUser"] = ctx.ContextUser
err := shared_group.LoadHeaderCount(ctx)
if err != nil {
ctx.ServerError("LoadHeaderCount", err)
return
}
ctx.HTML(http.StatusOK, tplSettingsOptions)
}
func SettingsPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UpdateGroupSettingForm)
ctx.Data["Title"] = ctx.Tr("group.settings")
ctx.Data["PageIsGroupSettings"] = true
ctx.Data["PageIsSettingsOptions"] = true
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplSettingsOptions)
return
}
group := ctx.RepoGroup.Group
opts := &group_service.UpdateOptions{
Description: optional.Some(form.Description),
Visibility: optional.Some(form.Visibility),
}
if form.Name != group.Name {
opts.Name = optional.Some(form.Name)
}
visibilityChanged := group.Visibility != form.Visibility
if err := group_service.UpdateGroup(ctx, group, opts); err != nil {
ctx.ServerError("UpdateGroup", err)
return
}
if visibilityChanged {
repos, _, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
Actor: ctx.ContextUser,
Private: true,
GroupID: group.ID,
})
if err != nil {
ctx.ServerError("SearchRepositories", err)
return
}
for _, repo := range repos {
if err = repo_service.UpdateRepository(ctx, repo, true); err != nil {
ctx.ServerError("UpdateRepository", err)
return
}
}
}
log.Trace("Group setting updated: '%s'", group.Name)
ctx.Flash.Success(ctx.Tr("group.settings.update_setting_success"))
ctx.Redirect(ctx.RepoGroup.OrgGroupLink + "/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 := updateAvatarSetting(ctx, form, ctx.RepoGroup.Group); err != nil {
ctx.Flash.Error(err.Error())
} else {
ctx.Flash.Success(ctx.Tr("group.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.RepoGroup.OrgGroupLink + "/settings")
}
func updateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, group *group_model.Group) error {
if form.Avatar != nil && form.Avatar.Filename != "" {
fr, err := form.Avatar.Open()
if err != nil {
return fmt.Errorf("Avatar.Open: %w", err)
}
defer fr.Close()
if form.Avatar.Size > setting.Avatar.MaxFileSize {
return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
}
data, err := io.ReadAll(fr)
if err != nil {
return fmt.Errorf("io.ReadAll: %w", err)
}
st := typesniffer.DetectContentType(data)
if !(st.IsImage() && !st.IsSvgImage()) {
return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image"))
}
if err = group_service.UploadAvatar(ctx, group, data); err != nil {
return fmt.Errorf("UploadAvatar: %w", err)
}
}
return nil
}

216
routers/web/group/team.go Normal file
View File

@ -0,0 +1,216 @@
package group
import (
"code.gitea.io/gitea/models/db"
group_model "code.gitea.io/gitea/models/group"
org_model "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
web_org "code.gitea.io/gitea/routers/web/org"
shared_group "code.gitea.io/gitea/routers/web/shared/group"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
group_service "code.gitea.io/gitea/services/group"
"net/http"
"strings"
)
const (
tplTeamEdit = "group/team/new"
tplTeams = "group/team/teams"
)
func MinUnitAccessMode(unitsMap map[unit_model.Type]perm.AccessMode) perm.AccessMode {
res := perm.AccessModeNone
for t, mode := range unitsMap {
// Don't allow `TypeExternal{Tracker,Wiki}` to influence this as they can only be set to READ perms.
if t == unit_model.TypeExternalTracker || t == unit_model.TypeExternalWiki {
continue
}
// get the minial permission great than AccessModeNone except all are AccessModeNone
if mode > perm.AccessModeNone && (res == perm.AccessModeNone || mode < res) {
res = mode
}
}
return res
}
func SearchTeamCandidates(ctx *context.Context) {
teams, _, err := org_model.SearchTeam(ctx, &org_model.SearchTeamOptions{
OrgID: ctx.Org.Organization.ID,
Keyword: ctx.FormTrim("q"),
ListOptions: db.ListOptions{
PageSize: setting.UI.MembersPagingNum,
},
})
if err != nil {
ctx.ServerError("Unable to search teams", err)
return
}
apiTeams, err := convert.ToTeams(ctx, teams, true)
if err != nil {
ctx.ServerError("Unable to search teams", err)
return
}
ctx.JSON(http.StatusOK, map[string]any{"data": apiTeams})
}
func Teams(ctx *context.Context) {
group := ctx.RepoGroup.Group
ctx.Data["Title"] = group.Name
ctx.Data["PageIsGroupTeams"] = true
for _, t := range ctx.RepoGroup.Teams {
if err := t.LoadMembers(ctx); err != nil {
ctx.ServerError("GetMembers", err)
return
}
}
ctx.Data["Teams"] = ctx.RepoGroup.Teams
err := shared_group.LoadHeaderCount(ctx)
if err != nil {
ctx.ServerError("RenderOrgHeader", err)
return
}
ctx.HTML(http.StatusOK, tplTeams)
}
func EditTeam(ctx *context.Context) {
ctx.Data["Title"] = ctx.RepoGroup.Group.Name
ctx.Data["PageIsGroupTeams"] = true
if err := ctx.RepoGroup.Team.LoadUnits(ctx); err != nil {
ctx.ServerError("LoadUnits", err)
return
}
if err := shared_group.LoadHeaderCount(ctx); err != nil {
ctx.ServerError("LoadHeaderCount", err)
return
}
ctx.Data["GroupTeam"] = ctx.RepoGroup.GroupTeam
ctx.Data["Team"] = ctx.RepoGroup.Team
ctx.Data["Units"] = unit_model.Units
ctx.HTML(http.StatusOK, tplTeamEdit)
}
func EditTeamPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateGroupTeamForm)
t := ctx.RepoGroup.Team
gt := ctx.RepoGroup.GroupTeam
newAccessMode := perm.ParseAccessMode(form.Permission)
unitPerms := web_org.GetUnitPerms(ctx.Req.Form, newAccessMode)
if newAccessMode < perm.AccessModeAdmin {
newAccessMode = MinUnitAccessMode(unitPerms)
}
ctx.Data["Title"] = ctx.RepoGroup.Group.Name
ctx.Data["PageIsGroupTeams"] = true
ctx.Data["Team"] = t
ctx.Data["GroupTeam"] = gt
ctx.Data["Units"] = unit_model.Units
if !t.IsOwnerTeam() {
if gt.AccessMode != newAccessMode {
gt.AccessMode = newAccessMode
}
if gt.CanCreateIn != form.CanCreateRepoOrSubGroup {
gt.CanCreateIn = form.CanCreateRepoOrSubGroup
}
} else {
gt.CanCreateIn = true
}
units := make([]*group_model.RepoGroupUnit, 0, len(unitPerms))
for tp, perm := range unitPerms {
units = append(units, &group_model.RepoGroupUnit{
GroupID: gt.GroupID,
TeamID: t.ID,
Type: tp,
AccessMode: perm,
})
}
gt.Units = units
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplTeamEdit)
return
}
if gt.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 {
ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamEdit, &form)
return
}
if err := group_service.UpdateGroupTeam(ctx, gt); err != nil {
ctx.ServerError("UpdateGroupTeam", err)
return
}
ctx.Redirect(ctx.Org.OrgLink + "/teams/")
}
func TeamAddPost(ctx *context.Context) {
if !ctx.RepoGroup.IsGroupAdmin || !ctx.RepoGroup.IsOwner {
ctx.NotFound(nil)
return
}
group := ctx.RepoGroup.Group
tname := strings.ToLower(ctx.FormTrim("tname"))
t, err := org_model.GetTeam(ctx, group.OwnerID, tname)
if err != nil {
if org_model.IsErrTeamNotExist(err) {
ctx.Flash.Error(ctx.Tr("form.team_not_exist"))
ctx.Redirect(ctx.RepoGroup.OrgGroupLink + "/teams")
} else {
ctx.ServerError("GetTeam", err)
}
return
}
has := group_model.HasTeamGroup(ctx, group.OwnerID, t.ID, group.ID)
if has {
ctx.Flash.Error(ctx.Tr("org.group.add_duplicate_team"))
} else {
parentGroup, err := group_model.FindGroupTeamByTeamID(ctx, group.ID, t.ID)
if err != nil {
ctx.ServerError("FindGroupTeamByTeamID", err)
return
}
mode := t.AccessMode
canCreateIn := t.CanCreateOrgRepo
if parentGroup != nil {
mode = max(t.AccessMode, parentGroup.AccessMode)
canCreateIn = parentGroup.CanCreateIn || t.CanCreateOrgRepo
}
if err = group.LoadParentGroup(ctx); err != nil {
ctx.ServerError("LoadParentGroup", err)
return
}
err = group_model.AddTeamGroup(ctx, ctx.RepoGroup.Group.OwnerID, t.ID, ctx.RepoGroup.Group.ID, mode, canCreateIn)
if err != nil {
ctx.ServerError("AddTeamGroup", err)
return
}
}
ctx.Redirect(group.OrgGroupLink() + "/teams")
}
func TeamRemove(ctx *context.Context) {
if !ctx.RepoGroup.IsGroupAdmin || !ctx.RepoGroup.IsOwner {
ctx.NotFound(nil)
return
}
org := ctx.Org.Organization.ID
group := ctx.RepoGroup.Group
team, err := org_model.GetTeam(ctx, org, ctx.PathParam("team"))
if err != nil {
if org_model.IsErrTeamNotExist(err) {
ctx.NotFound(err)
} else {
ctx.ServerError("GetTeam", err)
}
return
}
if err = group_model.RemoveTeamGroup(ctx, org, team.ID, group.ID); err != nil {
ctx.ServerError("RemoveTeamGroup", err)
return
}
ctx.JSONRedirect(ctx.RepoGroup.OrgGroupLink + "/teams")
}

View File

@ -4,6 +4,7 @@
package web
import (
"code.gitea.io/gitea/routers/web/group"
"net/http"
"strings"
@ -922,6 +923,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Group("/org", func() {
m.Group("/{org}", func() {
m.Get("/-/search_team_candidates", optExploreSignIn, group.SearchTeamCandidates)
m.Get("/members", org.Members)
}, context.OrgAssignment(context.OrgAssignmentOptions{}))
}, optSignIn)
@ -951,6 +953,31 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones)
m.Post("/members/action/{action}", org.MembersAction)
m.Get("/teams", org.Teams)
m.Group("/groups", func() {
m.Combo("/new").
Get(group.NewGroup).
Post(web.Bind(forms.CreateGroupForm{}), group.NewGroupPost)
m.Group("/{group_id}", func() {
m.Get("/teams", group.Teams)
m.Post("/teams/add", group.TeamAddPost)
m.Combo("/teams/{team}/edit").
Get(group.EditTeam).
Post(web.Bind(forms.CreateGroupTeamForm{}), group.EditTeamPost)
m.Post("/teams/{team}/remove", group.TeamRemove)
m.Group("/settings", func() {
m.Combo("").
Get(group.Settings).
Post(web.Bind(forms.UpdateGroupSettingForm{}), group.SettingsPost)
m.Post("/avatar", web.Bind(forms.AvatarForm{}), group.SettingsAvatar)
m.Post("/avatar/delete", group.SettingsDeleteAvatar)
}, ctxDataSet("PageIsGroupSettings", true))
}, context.GroupAssignment(context.GroupAssignmentOptions{
RequireMember: true,
RequireOwner: false,
RequireGroupAdmin: true,
}))
})
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamMember: true}))
m.Group("/{org}", func() {
@ -1053,6 +1080,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
}, reqSignIn)
// end "/org": most org routes
m.Group("/group", func() {
m.Get("/search", group.SearchGroup)
}, reqSignIn)
// end "/group": search
m.Group("/repo", func() {
m.Get("/create", repo.Create)
m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost)
@ -1126,6 +1158,18 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
}, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false), individualPermsChecker)
}, optSignIn, context.UserAssignmentWeb(), context.OrgAssignment(context.OrgAssignmentOptions{}))
// end "/{username}/-": packages, projects, code
m.Group("/{username}/groups", func() {
m.Group("/{group_id}", func() {
m.Get("", group.Home)
}, context.GroupAssignment(context.GroupAssignmentOptions{}))
}, optSignIn, context.UserAssignmentWeb(), context.OrgAssignment(context.OrgAssignmentOptions{}))
m.Group("/{username}/groups", func() {
m.Post("/items/move", group.MoveGroupItem)
}, context.UserAssignmentWeb(), context.OrgAssignment(context.OrgAssignmentOptions{
RequireMember: true,
}))
// end "/{username}/groups"
repoDashFn := func() {
m.Group("/migrate", func() {