diff --git a/models/fixtures/project.yml b/models/fixtures/project.yml
index 3d42597c5e..f38b5344bb 100644
--- a/models/fixtures/project.yml
+++ b/models/fixtures/project.yml
@@ -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
diff --git a/models/fixtures/project_board.yml b/models/fixtures/project_board.yml
index 9e06e8c239..dc4f9cf565 100644
--- a/models/fixtures/project_board.yml
+++ b/models/fixtures/project_board.yml
@@ -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
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 4a8ab06824..dc9e5c5acd 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -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)
}
diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go
index 8e559f13c9..c9f4c9f533 100644
--- a/models/issues/issue_project.go
+++ b/models/issues/issue_project.go
@@ -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,
diff --git a/models/organization/team.go b/models/organization/team.go
index 55d3f17276..0c2577dab1 100644
--- a/models/organization/team.go
+++ b/models/organization/team.go
@@ -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))
diff --git a/models/organization/team_list.go b/models/organization/team_list.go
new file mode 100644
index 0000000000..5d3bd555cc
--- /dev/null
+++ b/models/organization/team_list.go
@@ -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)
+}
diff --git a/models/organization/team_user.go b/models/organization/team_user.go
index 7a024f1c6d..816daf3d34 100644
--- a/models/organization/team_user.go
+++ b/models/organization/team_user.go
@@ -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))
diff --git a/models/project/project.go b/models/project/project.go
index f432d0bc4c..8bac9115ba 100644
--- a/models/project/project.go
+++ b/models/project/project.go
@@ -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 {
diff --git a/models/project/project_test.go b/models/project/project_test.go
index 4fde0fc7ce..c2d9005c43 100644
--- a/models/project/project_test.go
+++ b/models/project/project_test.go
@@ -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
diff --git a/modules/context/org.go b/modules/context/org.go
index 39df29a860..ff3a5ae7ec 100644
--- a/modules/context/org.go
+++ b/modules/context/org.go
@@ -9,7 +9,9 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
+ "code.gitea.io/gitea/models/unit"
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/structs"
)
@@ -28,6 +30,32 @@ type Organization struct {
Teams []*organization.Team
}
+func (org *Organization) CanWriteUnit(ctx *Context, unitType unit.Type) bool {
+ if ctx.Doer == nil {
+ return false
+ }
+ return org.UnitPermission(ctx, ctx.Doer.ID, unitType) >= perm.AccessModeWrite
+}
+
+func (org *Organization) UnitPermission(ctx *Context, doerID int64, unitType unit.Type) perm.AccessMode {
+ if doerID > 0 {
+ teams, err := organization.GetUserOrgTeams(ctx, org.Organization.ID, doerID)
+ if err != nil {
+ log.Error("GetUserOrgTeams: %v", err)
+ return perm.AccessModeNone
+ }
+ if len(teams) > 0 {
+ return teams.UnitMaxAccess(unitType)
+ }
+ }
+
+ if org.Organization.Visibility == structs.VisibleTypePublic {
+ return perm.AccessModeRead
+ }
+
+ return perm.AccessModeNone
+}
+
// HandleOrgAssignment handles organization assignment
func HandleOrgAssignment(ctx *Context, args ...bool) {
var (
diff --git a/routers/web/org/main_test.go b/routers/web/org/main_test.go
new file mode 100644
index 0000000000..41323a3601
--- /dev/null
+++ b/routers/web/org/main_test.go
@@ -0,0 +1,17 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org_test
+
+import (
+ "path/filepath"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+)
+
+func TestMain(m *testing.M) {
+ unittest.MainTest(m, &unittest.TestOptions{
+ GiteaRootPath: filepath.Join("..", "..", ".."),
+ })
+}
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
new file mode 100644
index 0000000000..1ce44d4866
--- /dev/null
+++ b/routers/web/org/projects.go
@@ -0,0 +1,670 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ project_model "code.gitea.io/gitea/models/project"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/web"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/forms"
+)
+
+const (
+ tplProjects base.TplName = "org/projects/list"
+ tplProjectsNew base.TplName = "org/projects/new"
+ tplProjectsView base.TplName = "org/projects/view"
+ tplGenericProjectsNew base.TplName = "user/project"
+)
+
+// MustEnableProjects check if projects are enabled in settings
+func MustEnableProjects(ctx *context.Context) {
+ if unit.TypeProjects.UnitGlobalDisabled() {
+ ctx.NotFound("EnableKanbanBoard", nil)
+ return
+ }
+}
+
+// Projects renders the home page of projects
+func Projects(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.project_board")
+
+ sortType := ctx.FormTrim("sort")
+
+ isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed"
+ page := ctx.FormInt("page")
+ if page <= 1 {
+ page = 1
+ }
+
+ projects, total, err := project_model.FindProjects(ctx, project_model.SearchOptions{
+ OwnerID: ctx.ContextUser.ID,
+ Page: page,
+ IsClosed: util.OptionalBoolOf(isShowClosed),
+ SortType: sortType,
+ Type: project_model.TypeOrganization,
+ })
+ if err != nil {
+ ctx.ServerError("FindProjects", err)
+ return
+ }
+
+ opTotal, err := project_model.CountProjects(ctx, project_model.SearchOptions{
+ OwnerID: ctx.ContextUser.ID,
+ IsClosed: util.OptionalBoolOf(!isShowClosed),
+ Type: project_model.TypeOrganization,
+ })
+ if err != nil {
+ ctx.ServerError("CountProjects", err)
+ return
+ }
+
+ if isShowClosed {
+ ctx.Data["OpenCount"] = opTotal
+ ctx.Data["ClosedCount"] = total
+ } else {
+ ctx.Data["OpenCount"] = total
+ ctx.Data["ClosedCount"] = opTotal
+ }
+
+ ctx.Data["Projects"] = projects
+ shared_user.RenderUserHeader(ctx)
+
+ if isShowClosed {
+ ctx.Data["State"] = "closed"
+ } else {
+ ctx.Data["State"] = "open"
+ }
+
+ for _, project := range projects {
+ project.RenderedContent = project.Description
+ }
+
+ numPages := 0
+ if total > 0 {
+ numPages = (int(total) - 1/setting.UI.IssuePagingNum)
+ }
+
+ pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages)
+ pager.AddParam(ctx, "state", "State")
+ ctx.Data["Page"] = pager
+
+ ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
+ ctx.Data["IsShowClosed"] = isShowClosed
+ ctx.Data["PageIsViewProjects"] = true
+ ctx.Data["SortType"] = sortType
+
+ ctx.HTML(http.StatusOK, tplProjects)
+}
+
+func canWriteUnit(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
+}
+
+// NewProject render creating a project page
+func NewProject(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.projects.new")
+ ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
+ ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
+ ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
+ shared_user.RenderUserHeader(ctx)
+ ctx.HTML(http.StatusOK, tplProjectsNew)
+}
+
+// NewProjectPost creates a new project
+func NewProjectPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.CreateProjectForm)
+ ctx.Data["Title"] = ctx.Tr("repo.projects.new")
+ shared_user.RenderUserHeader(ctx)
+
+ if ctx.HasError() {
+ ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
+ ctx.Data["PageIsViewProjects"] = true
+ ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
+ ctx.HTML(http.StatusOK, tplProjectsNew)
+ return
+ }
+
+ if err := project_model.NewProject(&project_model.Project{
+ OwnerID: ctx.ContextUser.ID,
+ Title: form.Title,
+ Description: form.Content,
+ CreatorID: ctx.Doer.ID,
+ BoardType: form.BoardType,
+ Type: project_model.TypeOrganization,
+ }); 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) {
+ toClose := false
+ switch ctx.Params(":action") {
+ case "open":
+ toClose = false
+ case "close":
+ toClose = true
+ default:
+ ctx.Redirect(ctx.Repo.RepoLink + "/projects")
+ }
+ id := ctx.ParamsInt64(":id")
+
+ if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil {
+ if project_model.IsErrProjectNotExist(err) {
+ ctx.NotFound("", err)
+ } else {
+ ctx.ServerError("ChangeProjectStatusByIDAndRepoID", err)
+ }
+ return
+ }
+ ctx.Redirect(ctx.Repo.RepoLink + "/projects?state=" + url.QueryEscape(ctx.Params(":action")))
+}
+
+// DeleteProject delete a project
+func DeleteProject(ctx *context.Context) {
+ p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ if project_model.IsErrProjectNotExist(err) {
+ ctx.NotFound("", nil)
+ } else {
+ ctx.ServerError("GetProjectByID", err)
+ }
+ return
+ }
+ if p.RepoID != ctx.Repo.Repository.ID {
+ ctx.NotFound("", nil)
+ return
+ }
+
+ if err := project_model.DeleteProjectByID(ctx, p.ID); err != nil {
+ ctx.Flash.Error("DeleteProjectByID: " + err.Error())
+ } else {
+ ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": ctx.Repo.RepoLink + "/projects",
+ })
+}
+
+// EditProject allows a project to be edited
+func EditProject(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
+ ctx.Data["PageIsEditProjects"] = true
+ ctx.Data["PageIsViewProjects"] = true
+ ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
+ shared_user.RenderUserHeader(ctx)
+
+ p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ if project_model.IsErrProjectNotExist(err) {
+ ctx.NotFound("", nil)
+ } else {
+ ctx.ServerError("GetProjectByID", err)
+ }
+ return
+ }
+ if p.RepoID != ctx.Repo.Repository.ID {
+ ctx.NotFound("", nil)
+ return
+ }
+
+ ctx.Data["title"] = p.Title
+ ctx.Data["content"] = p.Description
+
+ ctx.HTML(http.StatusOK, tplProjectsNew)
+}
+
+// EditProjectPost response for editing a project
+func EditProjectPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.CreateProjectForm)
+ ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
+ ctx.Data["PageIsEditProjects"] = true
+ ctx.Data["PageIsViewProjects"] = true
+ ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
+ shared_user.RenderUserHeader(ctx)
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplProjectsNew)
+ return
+ }
+
+ p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ if project_model.IsErrProjectNotExist(err) {
+ ctx.NotFound("", nil)
+ } else {
+ ctx.ServerError("GetProjectByID", err)
+ }
+ return
+ }
+ if p.RepoID != ctx.Repo.Repository.ID {
+ ctx.NotFound("", nil)
+ return
+ }
+
+ p.Title = form.Title
+ p.Description = form.Content
+ 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))
+ ctx.Redirect(ctx.Repo.RepoLink + "/projects")
+}
+
+// ViewProject renders the project board for a project
+func ViewProject(ctx *context.Context) {
+ project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ if project_model.IsErrProjectNotExist(err) {
+ ctx.NotFound("", nil)
+ } else {
+ ctx.ServerError("GetProjectByID", err)
+ }
+ return
+ }
+ if project.OwnerID != ctx.ContextUser.ID {
+ ctx.NotFound("", nil)
+ return
+ }
+
+ boards, err := project_model.GetBoards(ctx, project.ID)
+ if err != nil {
+ ctx.ServerError("GetProjectBoards", err)
+ return
+ }
+
+ if boards[0].ID == 0 {
+ boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
+ }
+
+ issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
+ if err != nil {
+ ctx.ServerError("LoadIssuesOfBoards", err)
+ return
+ }
+
+ 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: util.OptionalBoolTrue,
+ }); err == nil {
+ linkedPrsMap[issue.ID] = linkedPrs
+ }
+ }
+ }
+ }
+
+ project.RenderedContent = project.Description
+ ctx.Data["LinkedPRs"] = linkedPrsMap
+ ctx.Data["PageIsViewProjects"] = true
+ ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
+ ctx.Data["Project"] = project
+ ctx.Data["IssuesMap"] = issuesMap
+ ctx.Data["Boards"] = boards
+ shared_user.RenderUserHeader(ctx)
+
+ ctx.HTML(http.StatusOK, tplProjectsView)
+}
+
+func getActionIssues(ctx *context.Context) []*issues_model.Issue {
+ commaSeparatedIssueIDs := ctx.FormString("issue_ids")
+ if len(commaSeparatedIssueIDs) == 0 {
+ return nil
+ }
+ issueIDs := make([]int64, 0, 10)
+ for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
+ issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
+ if err != nil {
+ ctx.ServerError("ParseInt", err)
+ return nil
+ }
+ issueIDs = append(issueIDs, issueID)
+ }
+ issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
+ if err != nil {
+ ctx.ServerError("GetIssuesByIDs", err)
+ return nil
+ }
+ // Check access rights for all issues
+ issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues)
+ prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests)
+ for _, issue := range issues {
+ if issue.RepoID != ctx.Repo.Repository.ID {
+ ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect"))
+ return nil
+ }
+ if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
+ ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
+ return nil
+ }
+ if err = issue.LoadAttributes(ctx); err != nil {
+ ctx.ServerError("LoadAttributes", err)
+ return nil
+ }
+ }
+ return issues
+}
+
+// UpdateIssueProject change an issue's project
+func UpdateIssueProject(ctx *context.Context) {
+ issues := getActionIssues(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ projectID := ctx.FormInt64("id")
+ for _, issue := range issues {
+ oldProjectID := issue.ProjectID()
+ if oldProjectID == projectID {
+ continue
+ }
+
+ if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil {
+ ctx.ServerError("ChangeProjectAssign", err)
+ return
+ }
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "ok": true,
+ })
+}
+
+// DeleteProjectBoard allows for the deletion of a project board
+func DeleteProjectBoard(ctx *context.Context) {
+ if ctx.Doer == nil {
+ ctx.JSON(http.StatusForbidden, map[string]string{
+ "message": "Only signed in users are allowed to perform this action.",
+ })
+ return
+ }
+
+ project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ if project_model.IsErrProjectNotExist(err) {
+ ctx.NotFound("", nil)
+ } else {
+ ctx.ServerError("GetProjectByID", err)
+ }
+ return
+ }
+
+ pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+ if err != nil {
+ ctx.ServerError("GetProjectBoard", err)
+ return
+ }
+ if pb.ProjectID != ctx.ParamsInt64(":id") {
+ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+ "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID),
+ })
+ return
+ }
+
+ if project.OwnerID != ctx.ContextUser.ID {
+ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+ "message": fmt.Sprintf("ProjectBoard[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID),
+ })
+ return
+ }
+
+ if err := project_model.DeleteBoardByID(ctx.ParamsInt64(":boardID")); err != nil {
+ ctx.ServerError("DeleteProjectBoardByID", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "ok": true,
+ })
+}
+
+// AddBoardToProjectPost allows a new board to be added to a project.
+func AddBoardToProjectPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
+
+ project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ if project_model.IsErrProjectNotExist(err) {
+ ctx.NotFound("", nil)
+ } else {
+ ctx.ServerError("GetProjectByID", err)
+ }
+ return
+ }
+
+ if err := project_model.NewBoard(&project_model.Board{
+ ProjectID: project.ID,
+ Title: form.Title,
+ Color: form.Color,
+ CreatorID: ctx.Doer.ID,
+ }); err != nil {
+ ctx.ServerError("NewProjectBoard", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "ok": true,
+ })
+}
+
+// CheckProjectBoardChangePermissions check permission
+func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) {
+ if ctx.Doer == nil {
+ ctx.JSON(http.StatusForbidden, map[string]string{
+ "message": "Only signed in users are allowed to perform this action.",
+ })
+ return nil, nil
+ }
+
+ project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ if project_model.IsErrProjectNotExist(err) {
+ ctx.NotFound("", nil)
+ } else {
+ ctx.ServerError("GetProjectByID", err)
+ }
+ return nil, nil
+ }
+
+ board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+ if err != nil {
+ ctx.ServerError("GetProjectBoard", err)
+ return nil, nil
+ }
+ if board.ProjectID != ctx.ParamsInt64(":id") {
+ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+ "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
+ })
+ return nil, nil
+ }
+
+ if project.OwnerID != ctx.ContextUser.ID {
+ ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
+ "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, project.ID),
+ })
+ return nil, nil
+ }
+ return project, board
+}
+
+// EditProjectBoard allows a project board's to be updated
+func EditProjectBoard(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
+ _, board := CheckProjectBoardChangePermissions(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ if form.Title != "" {
+ board.Title = form.Title
+ }
+
+ board.Color = form.Color
+
+ if form.Sorting != 0 {
+ board.Sorting = form.Sorting
+ }
+
+ if err := project_model.UpdateBoard(ctx, board); err != nil {
+ ctx.ServerError("UpdateProjectBoard", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "ok": true,
+ })
+}
+
+// SetDefaultProjectBoard set default board for uncategorized issues/pulls
+func SetDefaultProjectBoard(ctx *context.Context) {
+ project, board := CheckProjectBoardChangePermissions(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ if err := project_model.SetDefaultBoard(project.ID, board.ID); err != nil {
+ ctx.ServerError("SetDefaultBoard", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "ok": true,
+ })
+}
+
+// MoveIssues moves or keeps issues in a column and sorts them inside that column
+func MoveIssues(ctx *context.Context) {
+ if ctx.Doer == nil {
+ ctx.JSON(http.StatusForbidden, map[string]string{
+ "message": "Only signed in users are allowed to perform this action.",
+ })
+ return
+ }
+
+ project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
+ if err != nil {
+ if project_model.IsErrProjectNotExist(err) {
+ ctx.NotFound("ProjectNotExist", nil)
+ } else {
+ ctx.ServerError("GetProjectByID", err)
+ }
+ return
+ }
+ if project.OwnerID != ctx.ContextUser.ID {
+ ctx.NotFound("InvalidRepoID", nil)
+ return
+ }
+
+ var board *project_model.Board
+
+ if ctx.ParamsInt64(":boardID") == 0 {
+ board = &project_model.Board{
+ ID: 0,
+ ProjectID: project.ID,
+ Title: ctx.Tr("repo.projects.type.uncategorized"),
+ }
+ } else {
+ board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+ if err != nil {
+ if project_model.IsErrProjectBoardNotExist(err) {
+ ctx.NotFound("ProjectBoardNotExist", nil)
+ } else {
+ ctx.ServerError("GetProjectBoard", err)
+ }
+ return
+ }
+ if board.ProjectID != project.ID {
+ ctx.NotFound("BoardNotInProject", nil)
+ return
+ }
+ }
+
+ type movedIssuesForm struct {
+ Issues []struct {
+ IssueID int64 `json:"issueID"`
+ Sorting int64 `json:"sorting"`
+ } `json:"issues"`
+ }
+
+ form := &movedIssuesForm{}
+ if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
+ ctx.ServerError("DecodeMovedIssuesForm", err)
+ }
+
+ 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 {
+ if issues_model.IsErrIssueNotExist(err) {
+ ctx.NotFound("IssueNotExisting", nil)
+ } else {
+ ctx.ServerError("GetIssueByID", err)
+ }
+ return
+ }
+
+ if len(movedIssues) != len(form.Issues) {
+ ctx.ServerError("some issues do not exist", errors.New("some issues do not exist"))
+ return
+ }
+
+ if _, err = movedIssues.LoadRepositories(ctx); err != nil {
+ ctx.ServerError("LoadRepositories", err)
+ return
+ }
+
+ for _, issue := range movedIssues {
+ if issue.RepoID != project.RepoID && issue.Repo.OwnerID != project.OwnerID {
+ ctx.ServerError("Some issue's repoID is not equal to project's repoID", errors.New("Some issue's repoID is not equal to project's repoID"))
+ return
+ }
+ }
+
+ if err = project_model.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil {
+ ctx.ServerError("MoveIssuesOnProjectBoard", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "ok": true,
+ })
+}
diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go
new file mode 100644
index 0000000000..3450fa8e72
--- /dev/null
+++ b/routers/web/org/projects_test.go
@@ -0,0 +1,28 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org_test
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/routers/web/org"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCheckProjectBoardChangePermissions(t *testing.T) {
+ unittest.PrepareTestEnv(t)
+ ctx := test.MockContext(t, "user2/-/projects/4/4")
+ test.LoadUser(t, ctx, 2)
+ ctx.ContextUser = ctx.Doer // user2
+ ctx.SetParams(":id", "4")
+ ctx.SetParams(":boardID", "4")
+
+ project, board := org.CheckProjectBoardChangePermissions(ctx)
+ assert.NotNil(t, project)
+ assert.NotNil(t, board)
+ assert.False(t, ctx.Written())
+}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 59ab717a1d..44ac81f65d 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -363,7 +363,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
}
if ctx.Repo.CanWriteIssuesOrPulls(ctx.Params(":type") == "pulls") {
- projects, _, err := project_model.GetProjects(ctx, project_model.SearchOptions{
+ projects, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
RepoID: repo.ID,
Type: project_model.TypeRepository,
IsClosed: util.OptionalBoolOf(isShowClosed),
@@ -474,8 +474,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
var err error
-
- ctx.Data["OpenProjects"], _, err = project_model.GetProjects(ctx, project_model.SearchOptions{
+ projects, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
RepoID: repo.ID,
Page: -1,
IsClosed: util.OptionalBoolFalse,
@@ -485,8 +484,20 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
ctx.ServerError("GetProjects", err)
return
}
+ projects2, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
+ OwnerID: repo.OwnerID,
+ Page: -1,
+ IsClosed: util.OptionalBoolFalse,
+ Type: project_model.TypeOrganization,
+ })
+ if err != nil {
+ ctx.ServerError("GetProjects", err)
+ return
+ }
- ctx.Data["ClosedProjects"], _, err = project_model.GetProjects(ctx, project_model.SearchOptions{
+ ctx.Data["OpenProjects"] = append(projects, projects2...)
+
+ projects, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{
RepoID: repo.ID,
Page: -1,
IsClosed: util.OptionalBoolTrue,
@@ -496,6 +507,18 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
ctx.ServerError("GetProjects", err)
return
}
+ projects2, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{
+ OwnerID: repo.OwnerID,
+ Page: -1,
+ IsClosed: util.OptionalBoolTrue,
+ Type: project_model.TypeOrganization,
+ })
+ if err != nil {
+ ctx.ServerError("GetProjects", err)
+ return
+ }
+
+ ctx.Data["ClosedProjects"] = append(projects, projects2...)
}
// repoReviewerSelection items to bee shown
@@ -988,7 +1011,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
ctx.ServerError("GetProjectByID", err)
return nil, nil, 0, 0
}
- if p.RepoID != ctx.Repo.Repository.ID {
+ if p.RepoID != ctx.Repo.Repository.ID && p.OwnerID != ctx.Repo.Repository.OwnerID {
ctx.NotFound("", nil)
return nil, nil, 0, 0
}
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 75cd290b8f..3becf799c5 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -70,7 +70,7 @@ func Projects(ctx *context.Context) {
total = repo.NumClosedProjects
}
- projects, count, err := project_model.GetProjects(ctx, project_model.SearchOptions{
+ projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{
RepoID: repo.ID,
Page: page,
IsClosed: util.OptionalBoolOf(isShowClosed),
@@ -112,7 +112,7 @@ func Projects(ctx *context.Context) {
pager.AddParam(ctx, "state", "State")
ctx.Data["Page"] = pager
- ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
+ ctx.Data["CanWriteProjects"] = true
ctx.Data["IsShowClosed"] = isShowClosed
ctx.Data["IsProjectsPage"] = true
ctx.Data["SortType"] = sortType
@@ -653,47 +653,3 @@ func MoveIssues(ctx *context.Context) {
"ok": true,
})
}
-
-// CreateProject renders the generic project creation page
-func CreateProject(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.projects.new")
- ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
- ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
-
- ctx.HTML(http.StatusOK, tplGenericProjectsNew)
-}
-
-// CreateProjectPost creates an individual and/or organization project
-func CreateProjectPost(ctx *context.Context, form forms.UserCreateProjectForm) {
- user := checkContextUser(ctx, form.UID)
- if ctx.Written() {
- return
- }
-
- ctx.Data["ContextUser"] = user
-
- if ctx.HasError() {
- ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
- ctx.HTML(http.StatusOK, tplGenericProjectsNew)
- return
- }
-
- projectType := project_model.TypeIndividual
- if user.IsOrganization() {
- projectType = project_model.TypeOrganization
- }
-
- if err := project_model.NewProject(&project_model.Project{
- Title: form.Title,
- Description: form.Content,
- CreatorID: user.ID,
- BoardType: form.BoardType,
- Type: projectType,
- }); err != nil {
- ctx.ServerError("NewProject", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
- ctx.Redirect(setting.AppSubURL + "/")
-}
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
new file mode 100644
index 0000000000..94e59e2a49
--- /dev/null
+++ b/routers/web/shared/user/header.go
@@ -0,0 +1,14 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+func RenderUserHeader(ctx *context.Context) {
+ ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+ ctx.Data["ContextUser"] = ctx.ContextUser
+}
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index c0aba7583f..ed4f0dd797 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -19,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/forms"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -83,10 +84,10 @@ func ListPackages(ctx *context.Context) {
return
}
+ shared_user.RenderUserHeader(ctx)
+
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["IsPackagesPage"] = true
- ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
- ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["Query"] = query
ctx.Data["PackageType"] = packageType
ctx.Data["AvailableTypes"] = packages_model.TypeList
@@ -156,10 +157,10 @@ func RedirectToLastVersion(ctx *context.Context) {
func ViewPackageVersion(ctx *context.Context) {
pd := ctx.Package.Descriptor
+ shared_user.RenderUserHeader(ctx)
+
ctx.Data["Title"] = pd.Package.Name
ctx.Data["IsPackagesPage"] = true
- ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
- ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["PackageDescriptor"] = pd
var (
@@ -235,10 +236,10 @@ func ListPackageVersions(ctx *context.Context) {
query := ctx.FormTrim("q")
sort := ctx.FormTrim("sort")
+ shared_user.RenderUserHeader(ctx)
+
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["IsPackagesPage"] = true
- ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
- ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["PackageDescriptor"] = &packages_model.PackageDescriptor{
Package: p,
Owner: ctx.Package.Owner,
@@ -311,10 +312,10 @@ func ListPackageVersions(ctx *context.Context) {
func PackageSettings(ctx *context.Context) {
pd := ctx.Package.Descriptor
+ shared_user.RenderUserHeader(ctx)
+
ctx.Data["Title"] = pd.Package.Name
ctx.Data["IsPackagesPage"] = true
- ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
- ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["PackageDescriptor"] = pd
repos, _, _ := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 0002d56de0..0e342991d6 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -224,7 +224,7 @@ func Profile(ctx *context.Context) {
total = int(count)
case "projects":
- ctx.Data["OpenProjects"], _, err = project_model.GetProjects(ctx, project_model.SearchOptions{
+ ctx.Data["OpenProjects"], _, err = project_model.FindProjects(ctx, project_model.SearchOptions{
Page: -1,
IsClosed: util.OptionalBoolFalse,
Type: project_model.TypeIndividual,
diff --git a/routers/web/web.go b/routers/web/web.go
index f0fedd0715..d37d82820d 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -835,6 +835,46 @@ func RegisterRoutes(m *web.Route) {
})
}, ignSignIn, context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
}
+
+ m.Group("/projects", func() {
+ m.Get("", org.Projects)
+ m.Get("/{id}", org.ViewProject)
+ m.Group("", func() { //nolint:dupl
+ m.Get("/new", org.NewProject)
+ m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
+ m.Group("/{id}", func() {
+ m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost)
+ m.Post("/delete", org.DeleteProject)
+
+ m.Get("/edit", org.EditProject)
+ m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost)
+ m.Post("/{action:open|close}", org.ChangeProjectStatus)
+
+ m.Group("/{boardID}", func() {
+ m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard)
+ m.Delete("", org.DeleteProjectBoard)
+ m.Post("/default", org.SetDefaultProjectBoard)
+
+ m.Post("/move", org.MoveIssues)
+ })
+ })
+ }, reqSignIn, func(ctx *context.Context) {
+ if ctx.ContextUser == nil {
+ ctx.NotFound("NewProject", nil)
+ return
+ }
+ if ctx.ContextUser.IsOrganization() {
+ if !ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) {
+ ctx.NotFound("NewProject", nil)
+ return
+ }
+ } else if ctx.ContextUser.ID != ctx.Doer.ID {
+ ctx.NotFound("NewProject", nil)
+ return
+ }
+ })
+ }, repo.MustEnableProjects)
+
m.Get("/code", user.CodeSearch)
}, context_service.UserAssignmentWeb())
@@ -1168,7 +1208,7 @@ func RegisterRoutes(m *web.Route) {
m.Group("/projects", func() {
m.Get("", repo.Projects)
m.Get("/{id}", repo.ViewProject)
- m.Group("", func() {
+ m.Group("", func() { //nolint:dupl
m.Get("/new", repo.NewProject)
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
m.Group("/{id}", func() {
diff --git a/services/context/user.go b/services/context/user.go
index 9dc84c3ac1..7642cba4e1 100644
--- a/services/context/user.go
+++ b/services/context/user.go
@@ -8,6 +8,7 @@ import (
"net/http"
"strings"
+ org_model "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
)
@@ -56,6 +57,14 @@ func userAssignment(ctx *context.Context, errCb func(int, string, interface{}))
} else {
errCb(http.StatusInternalServerError, "GetUserByName", err)
}
+ } else {
+ if ctx.ContextUser.IsOrganization() {
+ if ctx.Org == nil {
+ ctx.Org = &context.Organization{}
+ }
+ ctx.Org.Organization = (*org_model.Organization)(ctx.ContextUser)
+ ctx.Data["Org"] = ctx.Org.Organization
+ }
}
}
}
diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl
index 87242b94d3..5f543424fc 100644
--- a/templates/org/menu.tmpl
+++ b/templates/org/menu.tmpl
@@ -3,6 +3,9 @@
{{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}}
+
+ {{svg "octicon-project"}} {{.locale.Tr "user.projects"}}
+
{{if .IsPackageEnabled}}
{{svg "octicon-package"}} {{.locale.Tr "packages.title"}}
diff --git a/templates/org/projects/list.tmpl b/templates/org/projects/list.tmpl
new file mode 100644
index 0000000000..544ed38742
--- /dev/null
+++ b/templates/org/projects/list.tmpl
@@ -0,0 +1,6 @@
+{{template "base/head" .}}
+ {{.locale.Tr "repo.projects.deletion_desc"}} {{.locale.Tr "repo.projects.deletion_desc"}}
+ {{if .PageIsEditProjects}}
+ {{.locale.Tr "repo.projects.edit"}}
+
+ {{template "base/alert" .}}
+
+ {{$.Project.Title}}
+