forked from forgejo/forgejo
Support org/user level projects (#22235)
Fix #13405 <img width="1151" alt="image" src="https://user-images.githubusercontent.com/81045/209442911-7baa3924-c389-47b6-b63b-a740803e640e.png"> Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
parent
0c048e554b
commit
6fe3c8b398
30 changed files with 1556 additions and 176 deletions
|
@ -24,3 +24,12 @@
|
|||
creator_id: 5
|
||||
board_type: 1
|
||||
type: 2
|
||||
|
||||
-
|
||||
id: 4
|
||||
title: project on user2
|
||||
owner_id: 2
|
||||
is_closed: false
|
||||
creator_id: 2
|
||||
board_type: 1
|
||||
type: 2
|
||||
|
|
|
@ -21,3 +21,11 @@
|
|||
creator_id: 2
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
||||
-
|
||||
id: 4
|
||||
project_id: 4
|
||||
title: Done
|
||||
creator_id: 2
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
|
|
@ -1098,7 +1098,7 @@ func GetIssueWithAttrsByID(id int64) (*Issue, error) {
|
|||
}
|
||||
|
||||
// GetIssuesByIDs return issues with the given IDs.
|
||||
func GetIssuesByIDs(ctx context.Context, issueIDs []int64) ([]*Issue, error) {
|
||||
func GetIssuesByIDs(ctx context.Context, issueIDs []int64) (IssueList, error) {
|
||||
issues := make([]*Issue, 0, 10)
|
||||
return issues, db.GetEngine(ctx).In("id", issueIDs).Find(&issues)
|
||||
}
|
||||
|
|
|
@ -125,13 +125,17 @@ func ChangeProjectAssign(issue *Issue, doer *user_model.User, newProjectID int64
|
|||
func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
|
||||
oldProjectID := issue.projectID(ctx)
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only check if we add a new project and not remove it.
|
||||
if newProjectID > 0 {
|
||||
newProject, err := project_model.GetProjectByID(ctx, newProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if newProject.RepoID != issue.RepoID {
|
||||
if newProject.RepoID != issue.RepoID && newProject.OwnerID != issue.Repo.OwnerID {
|
||||
return fmt.Errorf("issue's repository is not the same as project's repository")
|
||||
}
|
||||
}
|
||||
|
@ -140,10 +144,6 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U
|
|||
return err
|
||||
}
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldProjectID > 0 || newProjectID > 0 {
|
||||
if _, err := CreateComment(ctx, &CreateCommentOptions{
|
||||
Type: CommentTypeProject,
|
||||
|
|
|
@ -16,8 +16,6 @@ import (
|
|||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ___________
|
||||
|
@ -96,59 +94,6 @@ func init() {
|
|||
db.RegisterModel(new(TeamInvite))
|
||||
}
|
||||
|
||||
// SearchTeamOptions holds the search options
|
||||
type SearchTeamOptions struct {
|
||||
db.ListOptions
|
||||
UserID int64
|
||||
Keyword string
|
||||
OrgID int64
|
||||
IncludeDesc bool
|
||||
}
|
||||
|
||||
func (opts *SearchTeamOptions) toCond() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
|
||||
if len(opts.Keyword) > 0 {
|
||||
lowerKeyword := strings.ToLower(opts.Keyword)
|
||||
var keywordCond builder.Cond = builder.Like{"lower_name", lowerKeyword}
|
||||
if opts.IncludeDesc {
|
||||
keywordCond = keywordCond.Or(builder.Like{"LOWER(description)", lowerKeyword})
|
||||
}
|
||||
cond = cond.And(keywordCond)
|
||||
}
|
||||
|
||||
if opts.OrgID > 0 {
|
||||
cond = cond.And(builder.Eq{"`team`.org_id": opts.OrgID})
|
||||
}
|
||||
|
||||
if opts.UserID > 0 {
|
||||
cond = cond.And(builder.Eq{"team_user.uid": opts.UserID})
|
||||
}
|
||||
|
||||
return cond
|
||||
}
|
||||
|
||||
// SearchTeam search for teams. Caller is responsible to check permissions.
|
||||
func SearchTeam(opts *SearchTeamOptions) ([]*Team, int64, error) {
|
||||
sess := db.GetEngine(db.DefaultContext)
|
||||
|
||||
opts.SetDefaultValues()
|
||||
cond := opts.toCond()
|
||||
|
||||
if opts.UserID > 0 {
|
||||
sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id")
|
||||
}
|
||||
sess = db.SetSessionPagination(sess, opts)
|
||||
|
||||
teams := make([]*Team, 0, opts.PageSize)
|
||||
count, err := sess.Where(cond).OrderBy("lower_name").FindAndCount(&teams)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return teams, count, nil
|
||||
}
|
||||
|
||||
// ColorFormat provides a basic color format for a Team
|
||||
func (t *Team) ColorFormat(s fmt.State) {
|
||||
if t == nil {
|
||||
|
@ -335,16 +280,6 @@ func GetTeamNamesByID(teamIDs []int64) ([]string, error) {
|
|||
return teamNames, err
|
||||
}
|
||||
|
||||
// GetRepoTeams gets the list of teams that has access to the repository
|
||||
func GetRepoTeams(ctx context.Context, repo *repo_model.Repository) (teams []*Team, err error) {
|
||||
return teams, db.GetEngine(ctx).
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
Where("team.org_id = ?", repo.OwnerID).
|
||||
And("team_repo.repo_id=?", repo.ID).
|
||||
OrderBy("CASE WHEN name LIKE '" + OwnerTeamName + "' THEN '' ELSE name END").
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
// IncrTeamRepoNum increases the number of repos for the given team by 1
|
||||
func IncrTeamRepoNum(ctx context.Context, teamID int64) error {
|
||||
_, err := db.GetEngine(ctx).Incr("num_repos").ID(teamID).Update(new(Team))
|
||||
|
|
128
models/organization/team_list.go
Normal file
128
models/organization/team_list.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type TeamList []*Team
|
||||
|
||||
func (t TeamList) LoadUnits(ctx context.Context) error {
|
||||
for _, team := range t {
|
||||
if err := team.getUnits(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TeamList) UnitMaxAccess(tp unit.Type) perm.AccessMode {
|
||||
maxAccess := perm.AccessModeNone
|
||||
for _, team := range t {
|
||||
if team.IsOwnerTeam() {
|
||||
return perm.AccessModeOwner
|
||||
}
|
||||
for _, teamUnit := range team.Units {
|
||||
if teamUnit.Type != tp {
|
||||
continue
|
||||
}
|
||||
if teamUnit.AccessMode > maxAccess {
|
||||
maxAccess = teamUnit.AccessMode
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxAccess
|
||||
}
|
||||
|
||||
// SearchTeamOptions holds the search options
|
||||
type SearchTeamOptions struct {
|
||||
db.ListOptions
|
||||
UserID int64
|
||||
Keyword string
|
||||
OrgID int64
|
||||
IncludeDesc bool
|
||||
}
|
||||
|
||||
func (opts *SearchTeamOptions) toCond() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
|
||||
if len(opts.Keyword) > 0 {
|
||||
lowerKeyword := strings.ToLower(opts.Keyword)
|
||||
var keywordCond builder.Cond = builder.Like{"lower_name", lowerKeyword}
|
||||
if opts.IncludeDesc {
|
||||
keywordCond = keywordCond.Or(builder.Like{"LOWER(description)", lowerKeyword})
|
||||
}
|
||||
cond = cond.And(keywordCond)
|
||||
}
|
||||
|
||||
if opts.OrgID > 0 {
|
||||
cond = cond.And(builder.Eq{"`team`.org_id": opts.OrgID})
|
||||
}
|
||||
|
||||
if opts.UserID > 0 {
|
||||
cond = cond.And(builder.Eq{"team_user.uid": opts.UserID})
|
||||
}
|
||||
|
||||
return cond
|
||||
}
|
||||
|
||||
// SearchTeam search for teams. Caller is responsible to check permissions.
|
||||
func SearchTeam(opts *SearchTeamOptions) (TeamList, int64, error) {
|
||||
sess := db.GetEngine(db.DefaultContext)
|
||||
|
||||
opts.SetDefaultValues()
|
||||
cond := opts.toCond()
|
||||
|
||||
if opts.UserID > 0 {
|
||||
sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id")
|
||||
}
|
||||
sess = db.SetSessionPagination(sess, opts)
|
||||
|
||||
teams := make([]*Team, 0, opts.PageSize)
|
||||
count, err := sess.Where(cond).OrderBy("lower_name").FindAndCount(&teams)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return teams, count, nil
|
||||
}
|
||||
|
||||
// GetRepoTeams gets the list of teams that has access to the repository
|
||||
func GetRepoTeams(ctx context.Context, repo *repo_model.Repository) (teams TeamList, err error) {
|
||||
return teams, db.GetEngine(ctx).
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
Where("team.org_id = ?", repo.OwnerID).
|
||||
And("team_repo.repo_id=?", repo.ID).
|
||||
OrderBy("CASE WHEN name LIKE '" + OwnerTeamName + "' THEN '' ELSE name END").
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
// GetUserOrgTeams returns all teams that user belongs to in given organization.
|
||||
func GetUserOrgTeams(ctx context.Context, orgID, userID int64) (teams TeamList, err error) {
|
||||
return teams, db.GetEngine(ctx).
|
||||
Join("INNER", "team_user", "team_user.team_id = team.id").
|
||||
Where("team.org_id = ?", orgID).
|
||||
And("team_user.uid=?", userID).
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
// GetUserRepoTeams returns user repo's teams
|
||||
func GetUserRepoTeams(ctx context.Context, orgID, userID, repoID int64) (teams TeamList, err error) {
|
||||
return teams, db.GetEngine(ctx).
|
||||
Join("INNER", "team_user", "team_user.team_id = team.id").
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
Where("team.org_id = ?", orgID).
|
||||
And("team_user.uid=?", userID).
|
||||
And("team_repo.repo_id=?", repoID).
|
||||
Find(&teams)
|
||||
}
|
|
@ -72,26 +72,6 @@ func GetTeamMembers(ctx context.Context, opts *SearchMembersOptions) ([]*user_mo
|
|||
return members, nil
|
||||
}
|
||||
|
||||
// GetUserOrgTeams returns all teams that user belongs to in given organization.
|
||||
func GetUserOrgTeams(ctx context.Context, orgID, userID int64) (teams []*Team, err error) {
|
||||
return teams, db.GetEngine(ctx).
|
||||
Join("INNER", "team_user", "team_user.team_id = team.id").
|
||||
Where("team.org_id = ?", orgID).
|
||||
And("team_user.uid=?", userID).
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
// GetUserRepoTeams returns user repo's teams
|
||||
func GetUserRepoTeams(ctx context.Context, orgID, userID, repoID int64) (teams []*Team, err error) {
|
||||
return teams, db.GetEngine(ctx).
|
||||
Join("INNER", "team_user", "team_user.team_id = team.id").
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
Where("team.org_id = ?", orgID).
|
||||
And("team_user.uid=?", userID).
|
||||
And("team_repo.repo_id=?", repoID).
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
// IsUserInTeams returns if a user in some teams
|
||||
func IsUserInTeams(ctx context.Context, userID int64, teamIDs []int64) (bool, error) {
|
||||
return db.GetEngine(ctx).Where("uid=?", userID).In("team_id", teamIDs).Exist(new(TeamUser))
|
||||
|
|
|
@ -8,6 +8,9 @@ import (
|
|||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
@ -78,12 +81,15 @@ func (err ErrProjectBoardNotExist) Unwrap() error {
|
|||
|
||||
// Project represents a project board
|
||||
type Project struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Title string `xorm:"INDEX NOT NULL"`
|
||||
Description string `xorm:"TEXT"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
CreatorID int64 `xorm:"NOT NULL"`
|
||||
IsClosed bool `xorm:"INDEX"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Title string `xorm:"INDEX NOT NULL"`
|
||||
Description string `xorm:"TEXT"`
|
||||
OwnerID int64 `xorm:"INDEX"`
|
||||
Owner *user_model.User `xorm:"-"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
CreatorID int64 `xorm:"NOT NULL"`
|
||||
IsClosed bool `xorm:"INDEX"`
|
||||
BoardType BoardType
|
||||
Type Type
|
||||
|
||||
|
@ -94,6 +100,46 @@ type Project struct {
|
|||
ClosedDateUnix timeutil.TimeStamp
|
||||
}
|
||||
|
||||
func (p *Project) LoadOwner(ctx context.Context) (err error) {
|
||||
if p.Owner != nil {
|
||||
return nil
|
||||
}
|
||||
p.Owner, err = user_model.GetUserByID(ctx, p.OwnerID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Project) LoadRepo(ctx context.Context) (err error) {
|
||||
if p.RepoID == 0 || p.Repo != nil {
|
||||
return nil
|
||||
}
|
||||
p.Repo, err = repo_model.GetRepositoryByID(ctx, p.RepoID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Project) Link() string {
|
||||
if p.OwnerID > 0 {
|
||||
err := p.LoadOwner(db.DefaultContext)
|
||||
if err != nil {
|
||||
log.Error("LoadOwner: %v", err)
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("/%s/-/projects/%d", p.Owner.Name, p.ID)
|
||||
}
|
||||
if p.RepoID > 0 {
|
||||
err := p.LoadRepo(db.DefaultContext)
|
||||
if err != nil {
|
||||
log.Error("LoadRepo: %v", err)
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("/%s/projects/%d", p.Repo.RepoPath(), p.ID)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *Project) IsOrganizationProject() bool {
|
||||
return p.Type == TypeOrganization
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Project))
|
||||
}
|
||||
|
@ -110,7 +156,7 @@ func GetProjectsConfig() []ProjectsConfig {
|
|||
// IsTypeValid checks if a project type is valid
|
||||
func IsTypeValid(p Type) bool {
|
||||
switch p {
|
||||
case TypeRepository:
|
||||
case TypeRepository, TypeOrganization:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
@ -119,6 +165,7 @@ func IsTypeValid(p Type) bool {
|
|||
|
||||
// SearchOptions are options for GetProjects
|
||||
type SearchOptions struct {
|
||||
OwnerID int64
|
||||
RepoID int64
|
||||
Page int
|
||||
IsClosed util.OptionalBool
|
||||
|
@ -126,12 +173,11 @@ type SearchOptions struct {
|
|||
Type Type
|
||||
}
|
||||
|
||||
// GetProjects returns a list of all projects that have been created in the repository
|
||||
func GetProjects(ctx context.Context, opts SearchOptions) ([]*Project, int64, error) {
|
||||
e := db.GetEngine(ctx)
|
||||
projects := make([]*Project, 0, setting.UI.IssuePagingNum)
|
||||
|
||||
var cond builder.Cond = builder.Eq{"repo_id": opts.RepoID}
|
||||
func (opts *SearchOptions) toConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
switch opts.IsClosed {
|
||||
case util.OptionalBoolTrue:
|
||||
cond = cond.And(builder.Eq{"is_closed": true})
|
||||
|
@ -142,6 +188,22 @@ func GetProjects(ctx context.Context, opts SearchOptions) ([]*Project, int64, er
|
|||
if opts.Type > 0 {
|
||||
cond = cond.And(builder.Eq{"type": opts.Type})
|
||||
}
|
||||
if opts.OwnerID > 0 {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// CountProjects counts projects
|
||||
func CountProjects(ctx context.Context, opts SearchOptions) (int64, error) {
|
||||
return db.GetEngine(ctx).Where(opts.toConds()).Count(new(Project))
|
||||
}
|
||||
|
||||
// FindProjects returns a list of all projects that have been created in the repository
|
||||
func FindProjects(ctx context.Context, opts SearchOptions) ([]*Project, int64, error) {
|
||||
e := db.GetEngine(ctx)
|
||||
projects := make([]*Project, 0, setting.UI.IssuePagingNum)
|
||||
cond := opts.toConds()
|
||||
|
||||
count, err := e.Where(cond).Count(new(Project))
|
||||
if err != nil {
|
||||
|
@ -188,8 +250,10 @@ func NewProject(p *Project) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
|
||||
return err
|
||||
if p.RepoID > 0 {
|
||||
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := createBoardsForProjectsType(ctx, p); err != nil {
|
||||
|
|
|
@ -22,7 +22,7 @@ func TestIsProjectTypeValid(t *testing.T) {
|
|||
}{
|
||||
{TypeIndividual, false},
|
||||
{TypeRepository, true},
|
||||
{TypeOrganization, false},
|
||||
{TypeOrganization, true},
|
||||
{UnknownType, false},
|
||||
}
|
||||
|
||||
|
@ -34,13 +34,13 @@ func TestIsProjectTypeValid(t *testing.T) {
|
|||
func TestGetProjects(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
projects, _, err := GetProjects(db.DefaultContext, SearchOptions{RepoID: 1})
|
||||
projects, _, err := FindProjects(db.DefaultContext, SearchOptions{RepoID: 1})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 1 value for this repo exists in the fixtures
|
||||
assert.Len(t, projects, 1)
|
||||
|
||||
projects, _, err = GetProjects(db.DefaultContext, SearchOptions{RepoID: 3})
|
||||
projects, _, err = FindProjects(db.DefaultContext, SearchOptions{RepoID: 3})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 1 value for this repo exists in the fixtures
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue