From bb64be14a42c0903103777949048a979d372c323 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 24 Dec 2022 23:26:10 +0800 Subject: [PATCH 01/16] support org level projects --- models/issues/issue_project.go | 10 +- models/organization/team.go | 65 +- models/organization/team_list.go | 127 +++ models/organization/team_user.go | 19 - models/project/project.go | 71 +- modules/context/org.go | 28 + routers/web/org/projects.go | 744 ++++++++++++++++++ routers/web/org/projects_test.go | 27 + routers/web/repo/issue.go | 31 +- routers/web/repo/projects.go | 46 +- routers/web/web.go | 26 + services/context/user.go | 6 + templates/org/menu.tmpl | 3 + templates/org/projects/list.tmpl | 100 +++ templates/org/projects/new.tmpl | 69 ++ templates/org/projects/view.tmpl | 283 +++++++ .../repo/issue/view_content/sidebar.tmpl | 6 +- 17 files changed, 1511 insertions(+), 150 deletions(-) create mode 100644 models/organization/team_list.go create mode 100644 routers/web/org/projects.go create mode 100644 routers/web/org/projects_test.go create mode 100644 templates/org/projects/list.tmpl create mode 100644 templates/org/projects/new.tmpl create mode 100644 templates/org/projects/view.tmpl diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 8e559f13c92c..c9f4c9f5336b 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 55d3f1727665..0005f7e7c5bf 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,15 +280,7 @@ 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 { diff --git a/models/organization/team_list.go b/models/organization/team_list.go new file mode 100644 index 000000000000..acb18c4b8317 --- /dev/null +++ b/models/organization/team_list.go @@ -0,0 +1,127 @@ +// 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 7a024f1c6d9b..1c3b8c9b5cf3 100644 --- a/models/organization/team_user.go +++ b/models/organization/team_user.go @@ -72,25 +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) { diff --git a/models/project/project.go b/models/project/project.go index bcf1166408f8..f7c6d1b8ed21 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -9,6 +9,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" @@ -79,12 +82,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 @@ -95,6 +101,42 @@ 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 { + 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 init() { db.RegisterModel(new(Project)) } @@ -111,7 +153,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 @@ -120,6 +162,7 @@ func IsTypeValid(p Type) bool { // SearchOptions are options for GetProjects type SearchOptions struct { + OwnerID int64 RepoID int64 Page int IsClosed util.OptionalBool @@ -132,7 +175,10 @@ func GetProjects(ctx context.Context, opts SearchOptions) ([]*Project, int64, er e := db.GetEngine(ctx) projects := make([]*Project, 0, setting.UI.IssuePagingNum) - var cond builder.Cond = builder.Eq{"repo_id": opts.RepoID} + var cond builder.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}) @@ -143,6 +189,9 @@ 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}) + } count, err := e.Where(cond).Count(new(Project)) if err != nil { @@ -189,8 +238,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/modules/context/org.go b/modules/context/org.go index 39df29a86072..ff3a5ae7ec3f 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/projects.go b/routers/web/org/projects.go new file mode 100644 index 000000000000..f267175af2f5 --- /dev/null +++ b/routers/web/org/projects.go @@ -0,0 +1,744 @@ +// 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" + "code.gitea.io/gitea/models/organization" + project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/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" + "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 + } + + // ctx.Data["OpenCount"] = repo.NumOpenProjects + // ctx.Data["ClosedCount"] = repo.NumClosedProjects + + var total int + if !isShowClosed { + // total = repo.NumOpenProjects + } else { + // total = repo.NumClosedProjects + } + + projects, count, err := project_model.GetProjects(ctx, project_model.SearchOptions{ + OwnerID: ctx.ContextUser.ID, + Page: page, + IsClosed: util.OptionalBoolOf(isShowClosed), + SortType: sortType, + Type: project_model.TypeRepository, + }) + if err != nil { + ctx.ServerError("GetProjects", err) + return + } + + /*for i := range projects { + projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{ + URLPrefix: ctx.Repo.RepoLink, + Metas: ctx.Repo.Repository.ComposeMetas(), + GitRepo: ctx.Repo.GitRepo, + Ctx: ctx, + }, projects[i].Description) + if err != nil { + ctx.ServerError("RenderString", err) + return + } + }*/ + + ctx.Data["Projects"] = projects + + if isShowClosed { + ctx.Data["State"] = "closed" + } else { + ctx.Data["State"] = "open" + } + + numPages := 0 + if count > 0 { + numPages = (int(count) - 1/setting.UI.IssuePagingNum) + } + + pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages) + pager.AddParam(ctx, "state", "State") + ctx.Data["Page"] = pager + + ctx.Data["CanWriteProjects"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + ctx.Data["IsShowClosed"] = isShowClosed + ctx.Data["PageIsViewProjects"] = true + ctx.Data["SortType"] = sortType + + ctx.HTML(http.StatusOK, tplProjects) +} + +// 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"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() + 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 ctx.HasError() { + ctx.Data["CanWriteProjects"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + 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"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + + 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"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + + 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 + } + } + } + } + ctx.Data["LinkedPRs"] = linkedPrsMap + + /*project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ + URLPrefix: ctx.Repo.RepoLink, + Metas: ctx.Repo.Repository.ComposeMetas(), + GitRepo: ctx.Repo.GitRepo, + Ctx: ctx, + }, project.Description) + if err != nil { + ctx.ServerError("RenderString", err) + return + }*/ + + ctx.Data["PageIsViewProjects"] = true + ctx.Data["CanWriteProjects"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + ctx.Data["Project"] = project + ctx.Data["IssuesMap"] = issuesMap + ctx.Data["Boards"] = boards + + 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 + } + + /*if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized 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) + /*if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized 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 + } + + 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, + }) +} + +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 + } + + /*if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized 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, ctx.Repo.Repository.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 + } + + /*if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized 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 + } + + for _, issue := range movedIssues { + if issue.RepoID != project.RepoID { + 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, + }) +} + +func checkContextUser(ctx *context.Context, uid int64) *user_model.User { + orgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx.Doer.ID) + if err != nil { + ctx.ServerError("GetOrgsCanCreateRepoByUserID", err) + return nil + } + + if !ctx.Doer.IsAdmin { + orgsAvailable := []*organization.Organization{} + for i := 0; i < len(orgs); i++ { + if orgs[i].CanCreateRepo() { + orgsAvailable = append(orgsAvailable, orgs[i]) + } + } + ctx.Data["Orgs"] = orgsAvailable + } else { + ctx.Data["Orgs"] = orgs + } + + // Not equal means current user is an organization. + if uid == ctx.Doer.ID || uid == 0 { + return ctx.Doer + } + + org, err := user_model.GetUserByID(ctx, uid) + if user_model.IsErrUserNotExist(err) { + return ctx.Doer + } + + if err != nil { + ctx.ServerError("GetUserByID", fmt.Errorf("[%d]: %w", uid, err)) + return nil + } + + // Check ownership of organization. + if !org.IsOrganization() { + ctx.Error(http.StatusForbidden) + return nil + } + if !ctx.Doer.IsAdmin { + canCreate, err := organization.OrgFromUser(org).CanCreateOrgRepo(ctx.Doer.ID) + if err != nil { + ctx.ServerError("CanCreateOrgRepo", err) + return nil + } else if !canCreate { + ctx.Error(http.StatusForbidden) + return nil + } + } else { + ctx.Data["Orgs"] = orgs + } + return org +} diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go new file mode 100644 index 000000000000..7df5c43e141b --- /dev/null +++ b/routers/web/org/projects_test.go @@ -0,0 +1,27 @@ +// 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" + + "github.com/stretchr/testify/assert" +) + +func TestCheckProjectBoardChangePermissions(t *testing.T) { + unittest.PrepareTestEnv(t) + ctx := test.MockContext(t, "user2/repo1/projects/1/2") + test.LoadUser(t, ctx, 2) + test.LoadRepo(t, ctx, 1) + ctx.SetParams(":id", "1") + ctx.SetParams(":boardID", "2") + + project, board := 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 d315525dac87..d00414547353 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -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.GetProjects(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.GetProjects(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["OpenProjects"] = append(projects, projects2...) - ctx.Data["ClosedProjects"], _, err = project_model.GetProjects(ctx, project_model.SearchOptions{ + projects, _, err = project_model.GetProjects(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.GetProjects(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 @@ -987,7 +1010,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 75cd290b8f0c..98d59af94488 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -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/web.go b/routers/web/web.go index 31b3eb9baada..bbeb18e45cd0 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -835,6 +835,32 @@ 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() { + 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) + }, repo.MustEnableProjects, ) + m.Get("/code", user.CodeSearch) }, context_service.UserAssignmentWeb()) diff --git a/services/context/user.go b/services/context/user.go index 9dc84c3ac15e..9aad534fdb68 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,11 @@ func userAssignment(ctx *context.Context, errCb func(int, string, interface{})) } else { errCb(http.StatusInternalServerError, "GetUserByName", err) } + } else { + if ctx.ContextUser.IsOrganization() { + 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 87242b94d332..5f543424fce3 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 000000000000..4f30d9a18534 --- /dev/null +++ b/templates/org/projects/list.tmpl @@ -0,0 +1,100 @@ +{{template "base/head" .}} +
+ {{template "org/header" .}} +
+ +
+ {{template "base/alert" .}} + + + +
+ {{range .Projects}} +
  • + {{svg "octicon-project"}} {{.Title}} +
    + {{$closedDate:= TimeSinceUnix .ClosedDateUnix $.locale}} + {{if .IsClosed}} + {{svg "octicon-clock"}} {{$.locale.Tr "repo.milestones.closed" $closedDate|Str2html}} + {{end}} + + {{svg "octicon-issue-opened" 16 "mr-3"}} + {{JsPrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} + {{svg "octicon-check" 16 "mr-3"}} + {{JsPrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} + +
    + {{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}} + + {{end}} + {{if .Description}} +
    + {{.RenderedContent|Str2html}} +
    + {{end}} +
  • + {{end}} + + {{template "base/paginate" .}} +
    +
    +
    + +{{if or .CanWriteIssues .CanWritePulls}} + +{{end}} +{{template "base/footer" .}} diff --git a/templates/org/projects/new.tmpl b/templates/org/projects/new.tmpl new file mode 100644 index 000000000000..1cb6bb24a816 --- /dev/null +++ b/templates/org/projects/new.tmpl @@ -0,0 +1,69 @@ +{{template "base/head" .}} +
    + {{template "org/header" .}} +
    + +
    +

    + {{if .PageIsEditProjects}} + {{.locale.Tr "repo.projects.edit"}} +
    {{.locale.Tr "repo.projects.edit_subheader"}}
    + {{else}} + {{.locale.Tr "repo.projects.new"}} +
    {{.locale.Tr "repo.projects.new_subheader"}}
    + {{end}} +

    + {{template "base/alert" .}} +
    + {{.CsrfTokenHtml}} +
    +
    + + +
    +
    + + +
    + + {{if not .PageIsEditProjects}} + + + {{end}} +
    +
    +
    +
    + {{if .PageIsEditProjects}} + + {{.locale.Tr "repo.milestones.cancel"}} + + + {{else}} + + {{end}} +
    +
    + +
    +
    +
    +{{template "base/footer" .}} diff --git a/templates/org/projects/view.tmpl b/templates/org/projects/view.tmpl new file mode 100644 index 000000000000..01b727511bec --- /dev/null +++ b/templates/org/projects/view.tmpl @@ -0,0 +1,283 @@ +{{template "base/head" .}} +
    + {{template "org/header" .}} +
    +
    +
    +
    +
    + {{if .CanWriteProjects}} + {{.locale.Tr "new_project_board"}} + {{end}} + +
    +
    +
    + +
    +
    +
    + +
    + {{range $board := .Boards}} + +
    +
    +
    +
    + {{.NumIssues}} +
    + {{.Title}} +
    + {{if and $.CanWriteProjects (ne .ID 0)}} + + {{end}} +
    +
    + +
    + + {{range (index $.IssuesMap .ID)}} + + +
    +
    +
    + + {{if .IsPull}} + {{if .PullRequest.HasMerged}} + {{svg "octicon-git-merge" 16 "text purple"}} + {{else}} + {{if .IsClosed}} + {{svg "octicon-git-pull-request" 16 "text red"}} + {{else}} + {{svg "octicon-git-pull-request" 16 "text green"}} + {{end}} + {{end}} + {{else}} + {{if .IsClosed}} + {{svg "octicon-issue-closed" 16 "text red"}} + {{else}} + {{svg "octicon-issue-opened" 16 "text green"}} + {{end}} + {{end}} + + + {{.Title}} + +
    +
    + + #{{.Index}} + {{$timeStr := TimeSinceUnix .GetLastEventTimestamp $.locale}} + {{if .OriginalAuthor}} + {{$.locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape) | Safe}} + {{else if gt .Poster.ID 0}} + {{$.locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape) | Safe}} + {{else}} + {{$.locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape) | Safe}} + {{end}} + +
    + {{- if .MilestoneID}} + + {{- end}} + {{- range index $.LinkedPRs .ID}} + + {{- end}} +
    + + {{if or .Labels .Assignees}} +
    + {{range .Labels}} + {{.Name | RenderEmoji}} + {{end}} +
    + {{range .Assignees}} + {{avatar . 28 "mini mr-3"}} + {{end}} +
    +
    + {{end}} +
    + + + {{end}} +
    +
    + {{end}} +
    + +
    + +
    + +{{if or .CanWriteIssues .CanWritePulls}} + +{{end}} + +{{template "base/footer" .}} diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 63b99136a8ed..cefd76d9154f 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -219,7 +219,7 @@ {{.locale.Tr "repo.issues.new.open_projects"}} {{range .OpenProjects}} - + {{svg "octicon-project" 18 "mr-3"}} {{.Title}} @@ -231,7 +231,7 @@ {{.locale.Tr "repo.issues.new.closed_projects"}} {{range .ClosedProjects}} - + {{svg "octicon-project" 18 "mr-3"}} {{.Title}} @@ -243,7 +243,7 @@ {{.locale.Tr "repo.issues.new.no_projects"}}
    {{if .Issue.ProjectID}} - + {{svg "octicon-project" 18 "mr-3"}} {{.Issue.Project.Title}} From c5266e307b26a6e19af07dd9d8f6c6d4ee881400 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 24 Dec 2022 23:34:24 +0800 Subject: [PATCH 02/16] Fix move --- models/issues/issue.go | 2 +- routers/web/org/projects.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/models/issues/issue.go b/models/issues/issue.go index f45e635c0ece..04e919a0a9c2 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/routers/web/org/projects.go b/routers/web/org/projects.go index f267175af2f5..ff60b122ff44 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -672,8 +672,13 @@ func MoveIssues(ctx *context.Context) { return } + if _, err = movedIssues.LoadRepositories(ctx); err != nil { + ctx.ServerError("LoadRepositories", err) + return + } + for _, issue := range movedIssues { - if issue.RepoID != project.RepoID { + 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 } From 420c5cd136523fb7153bbf23808c0856a52987dd Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 24 Dec 2022 23:42:07 +0800 Subject: [PATCH 03/16] Change the repo ref --- templates/org/projects/view.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/org/projects/view.tmpl b/templates/org/projects/view.tmpl index 01b727511bec..fa570979be3e 100644 --- a/templates/org/projects/view.tmpl +++ b/templates/org/projects/view.tmpl @@ -204,7 +204,7 @@
    - #{{.Index}} + {{.Repo.FullName}}#{{.Index}} {{$timeStr := TimeSinceUnix .GetLastEventTimestamp $.locale}} {{if .OriginalAuthor}} {{$.locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape) | Safe}} From 0436072f868bb9c8474d2f777fc79481fc1f9e18 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 25 Dec 2022 00:03:33 +0800 Subject: [PATCH 04/16] improve FindProjects --- models/project/project.go | 19 +++- routers/web/org/projects.go | 152 ++++++------------------------- routers/web/org/projects_test.go | 2 +- routers/web/repo/issue.go | 10 +- routers/web/repo/projects.go | 2 +- routers/web/user/profile.go | 2 +- routers/web/web.go | 9 +- 7 files changed, 58 insertions(+), 138 deletions(-) diff --git a/models/project/project.go b/models/project/project.go index f7c6d1b8ed21..11ef6ebe884c 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -170,11 +170,7 @@ 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) - +func (opts *SearchOptions) toConds() builder.Cond { var cond builder.Cond = builder.NewCond() if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) @@ -192,6 +188,19 @@ func GetProjects(ctx context.Context, opts SearchOptions) ([]*Project, int64, er 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 { diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index ff60b122ff44..b024e3ba6c4a 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -12,10 +12,8 @@ import ( "strings" issues_model "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/organization" project_model "code.gitea.io/gitea/models/project" "code.gitea.io/gitea/models/unit" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" @@ -52,40 +50,35 @@ func Projects(ctx *context.Context) { page = 1 } - // ctx.Data["OpenCount"] = repo.NumOpenProjects - // ctx.Data["ClosedCount"] = repo.NumClosedProjects - - var total int - if !isShowClosed { - // total = repo.NumOpenProjects - } else { - // total = repo.NumClosedProjects - } - - projects, count, err := project_model.GetProjects(ctx, project_model.SearchOptions{ + 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.TypeRepository, + Type: project_model.TypeOrganization, }) if err != nil { - ctx.ServerError("GetProjects", err) + ctx.ServerError("FindProjects", err) return } - /*for i := range projects { - projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{ - URLPrefix: ctx.Repo.RepoLink, - Metas: ctx.Repo.Repository.ComposeMetas(), - GitRepo: ctx.Repo.GitRepo, - Ctx: ctx, - }, projects[i].Description) - if err != nil { - ctx.ServerError("RenderString", 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 @@ -95,12 +88,16 @@ func Projects(ctx *context.Context) { ctx.Data["State"] = "open" } + for _, project := range projects { + project.RenderedContent = project.Description + } + numPages := 0 - if count > 0 { - numPages = (int(count) - 1/setting.UI.IssuePagingNum) + if total > 0 { + numPages = (int(total) - 1/setting.UI.IssuePagingNum) } - pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages) + pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages) pager.AddParam(ctx, "state", "State") ctx.Data["Page"] = pager @@ -318,19 +315,9 @@ func ViewProject(ctx *context.Context) { } } } - ctx.Data["LinkedPRs"] = linkedPrsMap - - /*project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ - URLPrefix: ctx.Repo.RepoLink, - Metas: ctx.Repo.Repository.ComposeMetas(), - GitRepo: ctx.Repo.GitRepo, - Ctx: ctx, - }, project.Description) - if err != nil { - ctx.ServerError("RenderString", err) - return - }*/ + project.RenderedContent = project.Description + ctx.Data["LinkedPRs"] = linkedPrsMap ctx.Data["PageIsViewProjects"] = true ctx.Data["CanWriteProjects"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) ctx.Data["Project"] = project @@ -413,13 +400,6 @@ func DeleteProjectBoard(ctx *context.Context) { return } - /*if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "Only authorized 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) { @@ -462,12 +442,6 @@ func DeleteProjectBoard(ctx *context.Context) { // AddBoardToProjectPost allows a new board to be added to a project. func AddBoardToProjectPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.EditProjectBoardForm) - /*if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "Only authorized users are allowed to perform this action.", - }) - return - }*/ project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) if err != nil { @@ -502,13 +476,6 @@ func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr return nil, nil } - /*if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "Only authorized 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) { @@ -594,13 +561,6 @@ func MoveIssues(ctx *context.Context) { return } - /*if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "Only authorized 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) { @@ -693,57 +653,3 @@ func MoveIssues(ctx *context.Context) { "ok": true, }) } - -func checkContextUser(ctx *context.Context, uid int64) *user_model.User { - orgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx.Doer.ID) - if err != nil { - ctx.ServerError("GetOrgsCanCreateRepoByUserID", err) - return nil - } - - if !ctx.Doer.IsAdmin { - orgsAvailable := []*organization.Organization{} - for i := 0; i < len(orgs); i++ { - if orgs[i].CanCreateRepo() { - orgsAvailable = append(orgsAvailable, orgs[i]) - } - } - ctx.Data["Orgs"] = orgsAvailable - } else { - ctx.Data["Orgs"] = orgs - } - - // Not equal means current user is an organization. - if uid == ctx.Doer.ID || uid == 0 { - return ctx.Doer - } - - org, err := user_model.GetUserByID(ctx, uid) - if user_model.IsErrUserNotExist(err) { - return ctx.Doer - } - - if err != nil { - ctx.ServerError("GetUserByID", fmt.Errorf("[%d]: %w", uid, err)) - return nil - } - - // Check ownership of organization. - if !org.IsOrganization() { - ctx.Error(http.StatusForbidden) - return nil - } - if !ctx.Doer.IsAdmin { - canCreate, err := organization.OrgFromUser(org).CanCreateOrgRepo(ctx.Doer.ID) - if err != nil { - ctx.ServerError("CanCreateOrgRepo", err) - return nil - } else if !canCreate { - ctx.Error(http.StatusForbidden) - return nil - } - } else { - ctx.Data["Orgs"] = orgs - } - return org -} diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go index 7df5c43e141b..3445968bad49 100644 --- a/routers/web/org/projects_test.go +++ b/routers/web/org/projects_test.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package org_test +package org import ( "testing" diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index d00414547353..346716e11f3d 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,7 +474,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { var err error - projects, _, 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, @@ -484,7 +484,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { ctx.ServerError("GetProjects", err) return } - projects2, _, err := project_model.GetProjects(ctx, project_model.SearchOptions{ + projects2, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{ OwnerID: repo.OwnerID, Page: -1, IsClosed: util.OptionalBoolFalse, @@ -497,7 +497,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { ctx.Data["OpenProjects"] = append(projects, projects2...) - projects, _, err = project_model.GetProjects(ctx, project_model.SearchOptions{ + projects, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{ RepoID: repo.ID, Page: -1, IsClosed: util.OptionalBoolTrue, @@ -507,7 +507,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { ctx.ServerError("GetProjects", err) return } - projects2, _, err = project_model.GetProjects(ctx, project_model.SearchOptions{ + projects2, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{ OwnerID: repo.OwnerID, Page: -1, IsClosed: util.OptionalBoolTrue, diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 98d59af94488..3becf799c552 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), diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 0002d56de01b..0e342991d616 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 bbeb18e45cd0..46e799af87f9 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -858,8 +858,13 @@ func RegisterRoutes(m *web.Route) { m.Post("/move", org.MoveIssues) }) }) - }, reqSignIn) - }, repo.MustEnableProjects, ) + }, reqSignIn, func(ctx *context.Context) { + if !ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) { + ctx.NotFound("NewProject", nil) + return + } + }) + }, repo.MustEnableProjects) m.Get("/code", user.CodeSearch) }, context_service.UserAssignmentWeb()) From 4a5eb97cd4f1250a85d3f7e73fd52a545773821e Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 27 Dec 2022 14:20:06 +0800 Subject: [PATCH 05/16] Fix lint --- models/organization/team.go | 2 -- models/organization/team_user.go | 1 - models/project/project.go | 6 +++++- models/project/project_test.go | 4 ++-- routers/web/web.go | 4 ++-- templates/repo/issue/view_content/sidebar.tmpl | 6 +++--- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/models/organization/team.go b/models/organization/team.go index 0005f7e7c5bf..0c2577dab15a 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -280,8 +280,6 @@ func GetTeamNamesByID(teamIDs []int64) ([]string, error) { return teamNames, err } - - // 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_user.go b/models/organization/team_user.go index 1c3b8c9b5cf3..816daf3d34b3 100644 --- a/models/organization/team_user.go +++ b/models/organization/team_user.go @@ -72,7 +72,6 @@ func GetTeamMembers(ctx context.Context, opts *SearchMembersOptions) ([]*user_mo return members, nil } - // 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 11ef6ebe884c..b0982bfd8000 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -137,6 +137,10 @@ func (p *Project) Link() string { return "" } +func (p *Project) IsOrganizationProject() bool { + return p.Type == TypeOrganization +} + func init() { db.RegisterModel(new(Project)) } @@ -171,7 +175,7 @@ type SearchOptions struct { } func (opts *SearchOptions) toConds() builder.Cond { - var cond builder.Cond = builder.NewCond() + cond := builder.NewCond() if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } diff --git a/models/project/project_test.go b/models/project/project_test.go index 4fde0fc7ce3a..f639c6627bad 100644 --- a/models/project/project_test.go +++ b/models/project/project_test.go @@ -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/routers/web/web.go b/routers/web/web.go index 46e799af87f9..e560d8865a93 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -839,7 +839,7 @@ func RegisterRoutes(m *web.Route) { m.Group("/projects", func() { m.Get("", org.Projects) m.Get("/{id}", org.ViewProject) - m.Group("", func() { + m.Group("", func() { //nolint:dupl m.Get("/new", org.NewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) m.Group("/{id}", func() { @@ -1193,7 +1193,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/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index cefd76d9154f..2d75951786ad 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -221,7 +221,7 @@ {{range .OpenProjects}} {{svg "octicon-project" 18 "mr-3"}} - {{.Title}} + {{if .IsOrganizationProject}}org: {{end}}{{.Title}} {{end}} {{end}} @@ -233,7 +233,7 @@ {{range .ClosedProjects}} {{svg "octicon-project" 18 "mr-3"}} - {{.Title}} + {{if .IsOrganizationProject}}org: {{end}}{{.Title}} {{end}} {{end}} @@ -245,7 +245,7 @@ {{if .Issue.ProjectID}} {{svg "octicon-project" 18 "mr-3"}} - {{.Issue.Project.Title}} + {{if .Issue.Project.IsOrganizationProject}}org: {{end}}{{.Issue.Project.Title}} {{end}}
    From 68e6c142ddd58070523f2e1728fee3a4950116dc Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 27 Dec 2022 21:39:59 +0800 Subject: [PATCH 06/16] Fix lint --- models/organization/team_list.go | 1 + 1 file changed, 1 insertion(+) diff --git a/models/organization/team_list.go b/models/organization/team_list.go index acb18c4b8317..5d3bd555cc28 100644 --- a/models/organization/team_list.go +++ b/models/organization/team_list.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" + "xorm.io/builder" ) From c39fca773ef107fe8af1db8bc043edf08bd494c1 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 28 Dec 2022 12:30:28 +0800 Subject: [PATCH 07/16] improve menus --- routers/web/org/projects.go | 7 + routers/web/shared/user/header.go | 14 ++ routers/web/user/package.go | 17 +- templates/org/projects/list.tmpl | 100 +--------- templates/org/projects/new.tmpl | 69 +------ templates/org/projects/view.tmpl | 283 +--------------------------- templates/projects/list.tmpl | 98 ++++++++++ templates/projects/new.tmpl | 66 +++++++ templates/projects/view.tmpl | 279 +++++++++++++++++++++++++++ templates/user/overview/header.tmpl | 3 + 10 files changed, 485 insertions(+), 451 deletions(-) create mode 100644 routers/web/shared/user/header.go create mode 100644 templates/projects/list.tmpl create mode 100644 templates/projects/new.tmpl create mode 100644 templates/projects/view.tmpl diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index b024e3ba6c4a..8624d27ccb95 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -20,6 +20,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" ) @@ -81,6 +82,7 @@ func Projects(ctx *context.Context) { } ctx.Data["Projects"] = projects + shared_user.RenderUserHeader(ctx) if isShowClosed { ctx.Data["State"] = "closed" @@ -115,6 +117,7 @@ func NewProject(ctx *context.Context) { ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig() ctx.Data["CanWriteProjects"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() + shared_user.RenderUserHeader(ctx) ctx.HTML(http.StatusOK, tplProjectsNew) } @@ -122,6 +125,7 @@ func NewProject(ctx *context.Context) { 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"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) @@ -204,6 +208,7 @@ func EditProject(ctx *context.Context) { ctx.Data["PageIsEditProjects"] = true ctx.Data["PageIsViewProjects"] = true ctx.Data["CanWriteProjects"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + shared_user.RenderUserHeader(ctx) p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) if err != nil { @@ -232,6 +237,7 @@ func EditProjectPost(ctx *context.Context) { ctx.Data["PageIsEditProjects"] = true ctx.Data["PageIsViewProjects"] = true ctx.Data["CanWriteProjects"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + shared_user.RenderUserHeader(ctx) if ctx.HasError() { ctx.HTML(http.StatusOK, tplProjectsNew) @@ -323,6 +329,7 @@ func ViewProject(ctx *context.Context) { ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap ctx.Data["Boards"] = boards + shared_user.RenderUserHeader(ctx) ctx.HTML(http.StatusOK, tplProjectsView) } diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go new file mode 100644 index 000000000000..94e59e2a490f --- /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 3782f46b4242..d8ede2ecc833 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["HasPackages"] = hasPackages @@ -155,10 +156,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 ( @@ -234,10 +235,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, @@ -310,10 +311,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/templates/org/projects/list.tmpl b/templates/org/projects/list.tmpl index 4f30d9a18534..544ed387423c 100644 --- a/templates/org/projects/list.tmpl +++ b/templates/org/projects/list.tmpl @@ -1,100 +1,6 @@ {{template "base/head" .}} -
    - {{template "org/header" .}} -
    - -
    - {{template "base/alert" .}} - - - -
    - {{range .Projects}} -
  • - {{svg "octicon-project"}} {{.Title}} -
    - {{$closedDate:= TimeSinceUnix .ClosedDateUnix $.locale}} - {{if .IsClosed}} - {{svg "octicon-clock"}} {{$.locale.Tr "repo.milestones.closed" $closedDate|Str2html}} - {{end}} - - {{svg "octicon-issue-opened" 16 "mr-3"}} - {{JsPrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} - {{svg "octicon-check" 16 "mr-3"}} - {{JsPrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} - -
    - {{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}} - - {{end}} - {{if .Description}} -
    - {{.RenderedContent|Str2html}} -
    - {{end}} -
  • - {{end}} - - {{template "base/paginate" .}} -
    -
    +
    + {{template "user/overview/header" .}} + {{template "projects/list" .}}
    - -{{if or .CanWriteIssues .CanWritePulls}} - -{{end}} {{template "base/footer" .}} diff --git a/templates/org/projects/new.tmpl b/templates/org/projects/new.tmpl index 1cb6bb24a816..b3d6c6001e37 100644 --- a/templates/org/projects/new.tmpl +++ b/templates/org/projects/new.tmpl @@ -1,69 +1,6 @@ {{template "base/head" .}} -
    - {{template "org/header" .}} -
    - -
    -

    - {{if .PageIsEditProjects}} - {{.locale.Tr "repo.projects.edit"}} -
    {{.locale.Tr "repo.projects.edit_subheader"}}
    - {{else}} - {{.locale.Tr "repo.projects.new"}} -
    {{.locale.Tr "repo.projects.new_subheader"}}
    - {{end}} -

    - {{template "base/alert" .}} -
    - {{.CsrfTokenHtml}} -
    -
    - - -
    -
    - - -
    - - {{if not .PageIsEditProjects}} - - - {{end}} -
    -
    -
    -
    - {{if .PageIsEditProjects}} - - {{.locale.Tr "repo.milestones.cancel"}} - - - {{else}} - - {{end}} -
    -
    - -
    -
    +
    + {{template "user/overview/header" .}} + {{template "projects/new" .}}
    {{template "base/footer" .}} diff --git a/templates/org/projects/view.tmpl b/templates/org/projects/view.tmpl index fa570979be3e..03327e253001 100644 --- a/templates/org/projects/view.tmpl +++ b/templates/org/projects/view.tmpl @@ -1,283 +1,6 @@ {{template "base/head" .}} -
    - {{template "org/header" .}} -
    -
    -
    -
    -
    - {{if .CanWriteProjects}} - {{.locale.Tr "new_project_board"}} - {{end}} - -
    -
    -
    - -
    -
    -
    - -
    - {{range $board := .Boards}} - -
    -
    -
    -
    - {{.NumIssues}} -
    - {{.Title}} -
    - {{if and $.CanWriteProjects (ne .ID 0)}} - - {{end}} -
    -
    - -
    - - {{range (index $.IssuesMap .ID)}} - - -
    -
    -
    - - {{if .IsPull}} - {{if .PullRequest.HasMerged}} - {{svg "octicon-git-merge" 16 "text purple"}} - {{else}} - {{if .IsClosed}} - {{svg "octicon-git-pull-request" 16 "text red"}} - {{else}} - {{svg "octicon-git-pull-request" 16 "text green"}} - {{end}} - {{end}} - {{else}} - {{if .IsClosed}} - {{svg "octicon-issue-closed" 16 "text red"}} - {{else}} - {{svg "octicon-issue-opened" 16 "text green"}} - {{end}} - {{end}} - - - {{.Title}} - -
    -
    - - {{.Repo.FullName}}#{{.Index}} - {{$timeStr := TimeSinceUnix .GetLastEventTimestamp $.locale}} - {{if .OriginalAuthor}} - {{$.locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape) | Safe}} - {{else if gt .Poster.ID 0}} - {{$.locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape) | Safe}} - {{else}} - {{$.locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape) | Safe}} - {{end}} - -
    - {{- if .MilestoneID}} - - {{- end}} - {{- range index $.LinkedPRs .ID}} - - {{- end}} -
    - - {{if or .Labels .Assignees}} -
    - {{range .Labels}} - {{.Name | RenderEmoji}} - {{end}} -
    - {{range .Assignees}} - {{avatar . 28 "mini mr-3"}} - {{end}} -
    -
    - {{end}} -
    - - - {{end}} -
    -
    - {{end}} -
    - -
    - +
    + {{template "user/overview/header" .}} + {{template "projects/view" .}}
    - -{{if or .CanWriteIssues .CanWritePulls}} - -{{end}} - {{template "base/footer" .}} diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl new file mode 100644 index 000000000000..ae2eaec6eaa7 --- /dev/null +++ b/templates/projects/list.tmpl @@ -0,0 +1,98 @@ +
    +
    + {{if .CanWriteProjects}} + +
    + {{end}} + + {{template "base/alert" .}} + + + +
    + {{range .Projects}} +
  • + {{svg "octicon-project"}} {{.Title}} +
    + {{$closedDate:= TimeSinceUnix .ClosedDateUnix $.locale}} + {{if .IsClosed}} + {{svg "octicon-clock"}} {{$.locale.Tr "repo.milestones.closed" $closedDate|Str2html}} + {{end}} + + {{svg "octicon-issue-opened" 16 "mr-3"}} + {{JsPrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} + {{svg "octicon-check" 16 "mr-3"}} + {{JsPrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} + +
    + {{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}} + + {{end}} + {{if .Description}} +
    + {{.RenderedContent|Str2html}} +
    + {{end}} +
  • + {{end}} + + {{template "base/paginate" .}} +
    +
    +
    + +{{if or .CanWriteIssues .CanWritePulls}} + +{{end}} diff --git a/templates/projects/new.tmpl b/templates/projects/new.tmpl new file mode 100644 index 000000000000..1069102792b1 --- /dev/null +++ b/templates/projects/new.tmpl @@ -0,0 +1,66 @@ +
    +
    + +
    +

    + {{if .PageIsEditProjects}} + {{.locale.Tr "repo.projects.edit"}} +
    {{.locale.Tr "repo.projects.edit_subheader"}}
    + {{else}} + {{.locale.Tr "repo.projects.new"}} +
    {{.locale.Tr "repo.projects.new_subheader"}}
    + {{end}} +

    + {{template "base/alert" .}} +
    + {{.CsrfTokenHtml}} +
    +
    + + +
    +
    + + +
    + + {{if not .PageIsEditProjects}} + + + {{end}} +
    +
    +
    +
    + {{if .PageIsEditProjects}} + + {{.locale.Tr "repo.milestones.cancel"}} + + + {{else}} + + {{end}} +
    +
    + +
    +
    +
    diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl new file mode 100644 index 000000000000..ac72acb82b6a --- /dev/null +++ b/templates/projects/view.tmpl @@ -0,0 +1,279 @@ +
    +
    +
    +
    +
    +
    + {{if .CanWriteProjects}} + {{.locale.Tr "new_project_board"}} + {{end}} + +
    +
    +
    + +
    +
    +
    + +
    + {{range $board := .Boards}} + +
    +
    +
    +
    + {{.NumIssues}} +
    + {{.Title}} +
    + {{if and $.CanWriteProjects (ne .ID 0)}} + + {{end}} +
    +
    + +
    + + {{range (index $.IssuesMap .ID)}} + + +
    +
    +
    + + {{if .IsPull}} + {{if .PullRequest.HasMerged}} + {{svg "octicon-git-merge" 16 "text purple"}} + {{else}} + {{if .IsClosed}} + {{svg "octicon-git-pull-request" 16 "text red"}} + {{else}} + {{svg "octicon-git-pull-request" 16 "text green"}} + {{end}} + {{end}} + {{else}} + {{if .IsClosed}} + {{svg "octicon-issue-closed" 16 "text red"}} + {{else}} + {{svg "octicon-issue-opened" 16 "text green"}} + {{end}} + {{end}} + + + {{.Title}} + +
    +
    + + {{.Repo.FullName}}#{{.Index}} + {{$timeStr := TimeSinceUnix .GetLastEventTimestamp $.locale}} + {{if .OriginalAuthor}} + {{$.locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape) | Safe}} + {{else if gt .Poster.ID 0}} + {{$.locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape) | Safe}} + {{else}} + {{$.locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape) | Safe}} + {{end}} + +
    + {{- if .MilestoneID}} + + {{- end}} + {{- range index $.LinkedPRs .ID}} + + {{- end}} +
    + + {{if or .Labels .Assignees}} +
    + {{range .Labels}} + {{.Name | RenderEmoji}} + {{end}} +
    + {{range .Assignees}} + {{avatar . 28 "mini mr-3"}} + {{end}} +
    +
    + {{end}} +
    + + + {{end}} +
    +
    + {{end}} +
    + +
    + +
    + +{{if or .CanWriteIssues .CanWritePulls}} + +{{end}} diff --git a/templates/user/overview/header.tmpl b/templates/user/overview/header.tmpl index 61b19c60326f..8fb882718c97 100644 --- a/templates/user/overview/header.tmpl +++ b/templates/user/overview/header.tmpl @@ -22,6 +22,9 @@ {{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}} + + {{svg "octicon-project"}} {{.locale.Tr "user.projects"}} + {{if (not .UnitPackagesGlobalDisabled)}} {{svg "octicon-package"}} {{.locale.Tr "packages.title"}} From 2c40716c51e450b6ee9c399f355fd4d3715906bf Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 28 Dec 2022 13:11:27 +0800 Subject: [PATCH 08/16] Fix test --- models/project/project_test.go | 2 +- routers/web/org/projects_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/project/project_test.go b/models/project/project_test.go index f639c6627bad..c2d9005c43b8 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}, } diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go index 3445968bad49..f7d4c27ad926 100644 --- a/routers/web/org/projects_test.go +++ b/routers/web/org/projects_test.go @@ -14,7 +14,7 @@ import ( func TestCheckProjectBoardChangePermissions(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/projects/1/2") + ctx := test.MockContext(t, "user2/-/projects/1/2") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) ctx.SetParams(":id", "1") From 2c081a5ddbf02e87b804a50130e2bff768821670 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 28 Dec 2022 13:25:59 +0800 Subject: [PATCH 09/16] support user level projects --- routers/web/org/projects.go | 19 +++++++++++++------ routers/web/web.go | 11 ++++++++++- templates/user/profile.tmpl | 3 +++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 8624d27ccb95..b8271b344f51 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -103,7 +103,7 @@ func Projects(ctx *context.Context) { pager.AddParam(ctx, "state", "State") ctx.Data["Page"] = pager - ctx.Data["CanWriteProjects"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) ctx.Data["IsShowClosed"] = isShowClosed ctx.Data["PageIsViewProjects"] = true ctx.Data["SortType"] = sortType @@ -111,11 +111,18 @@ func Projects(ctx *context.Context) { 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"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() shared_user.RenderUserHeader(ctx) ctx.HTML(http.StatusOK, tplProjectsNew) @@ -128,7 +135,7 @@ func NewProjectPost(ctx *context.Context) { shared_user.RenderUserHeader(ctx) if ctx.HasError() { - ctx.Data["CanWriteProjects"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) ctx.Data["PageIsViewProjects"] = true ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig() ctx.HTML(http.StatusOK, tplProjectsNew) @@ -207,7 +214,7 @@ func EditProject(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.edit") ctx.Data["PageIsEditProjects"] = true ctx.Data["PageIsViewProjects"] = true - ctx.Data["CanWriteProjects"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) shared_user.RenderUserHeader(ctx) p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) @@ -236,7 +243,7 @@ func EditProjectPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.edit") ctx.Data["PageIsEditProjects"] = true ctx.Data["PageIsViewProjects"] = true - ctx.Data["CanWriteProjects"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) shared_user.RenderUserHeader(ctx) if ctx.HasError() { @@ -325,7 +332,7 @@ func ViewProject(ctx *context.Context) { project.RenderedContent = project.Description ctx.Data["LinkedPRs"] = linkedPrsMap ctx.Data["PageIsViewProjects"] = true - ctx.Data["CanWriteProjects"] = ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) + ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap ctx.Data["Boards"] = boards diff --git a/routers/web/web.go b/routers/web/web.go index e560d8865a93..f3d629103fd2 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -859,7 +859,16 @@ func RegisterRoutes(m *web.Route) { }) }) }, reqSignIn, func(ctx *context.Context) { - if !ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) { + 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 } diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 6c31723e0f3b..74211eb67b85 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -106,6 +106,9 @@ {{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}} + + {{svg "octicon-project"}} {{.locale.Tr "user.projects"}} + {{if .IsPackageEnabled}} {{svg "octicon-package"}} {{.locale.Tr "packages.title"}} From b2c2deba1919e9c14d115c7a25eca28c1de4c95e Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 29 Dec 2022 23:25:30 +0800 Subject: [PATCH 10/16] Fix test --- routers/web/org/main_test.go | 17 +++++++++++++++++ routers/web/org/projects.go | 7 ++++--- routers/web/org/projects_test.go | 5 +++-- 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 routers/web/org/main_test.go diff --git a/routers/web/org/main_test.go b/routers/web/org/main_test.go new file mode 100644 index 000000000000..41323a3601ea --- /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 index b8271b344f51..f4230b0532f3 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -482,7 +482,8 @@ func AddBoardToProjectPost(ctx *context.Context) { }) } -func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) { +// 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.", @@ -524,7 +525,7 @@ func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr // EditProjectBoard allows a project board's to be updated func EditProjectBoard(ctx *context.Context) { form := web.GetForm(ctx).(*forms.EditProjectBoardForm) - _, board := checkProjectBoardChangePermissions(ctx) + _, board := CheckProjectBoardChangePermissions(ctx) if ctx.Written() { return } @@ -551,7 +552,7 @@ func EditProjectBoard(ctx *context.Context) { // SetDefaultProjectBoard set default board for uncategorized issues/pulls func SetDefaultProjectBoard(ctx *context.Context) { - project, board := checkProjectBoardChangePermissions(ctx) + project, board := CheckProjectBoardChangePermissions(ctx) if ctx.Written() { return } diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go index f7d4c27ad926..f66d19ef2248 100644 --- a/routers/web/org/projects_test.go +++ b/routers/web/org/projects_test.go @@ -1,13 +1,14 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package org +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" ) @@ -20,7 +21,7 @@ func TestCheckProjectBoardChangePermissions(t *testing.T) { ctx.SetParams(":id", "1") ctx.SetParams(":boardID", "2") - project, board := checkProjectBoardChangePermissions(ctx) + project, board := org.CheckProjectBoardChangePermissions(ctx) assert.NotNil(t, project) assert.NotNil(t, board) assert.False(t, ctx.Written()) From 547c3092a8e75dcca4bdedeafb8a6205a3b969fd Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 29 Dec 2022 23:29:14 +0800 Subject: [PATCH 11/16] improvement --- models/project/project.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/project/project.go b/models/project/project.go index b0982bfd8000..8be420e868d8 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -110,7 +110,7 @@ func (p *Project) LoadOwner(ctx context.Context) (err error) { } func (p *Project) LoadRepo(ctx context.Context) (err error) { - if p.RepoID == 0 { + if p.RepoID == 0 || p.Repo != nil { return nil } p.Repo, err = repo_model.GetRepositoryByID(ctx, p.RepoID) From d0a368017ce0973dd0d0bcb2e1feb84da79ed892 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 31 Dec 2022 21:01:04 +0800 Subject: [PATCH 12/16] Fix test --- routers/web/org/projects_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go index f66d19ef2248..cc8bb141a130 100644 --- a/routers/web/org/projects_test.go +++ b/routers/web/org/projects_test.go @@ -20,6 +20,7 @@ func TestCheckProjectBoardChangePermissions(t *testing.T) { test.LoadRepo(t, ctx, 1) ctx.SetParams(":id", "1") ctx.SetParams(":boardID", "2") + ctx.ContextUser = ctx.Doer // user2 project, board := org.CheckProjectBoardChangePermissions(ctx) assert.NotNil(t, project) From ada588942d525b0cc3cf596699ff0de25b19d3a2 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 31 Dec 2022 21:10:52 +0800 Subject: [PATCH 13/16] Use project-symblink icon for org level project --- templates/repo/issue/view_content/sidebar.tmpl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 2d75951786ad..c87b61369cc6 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -220,8 +220,8 @@
    {{range .OpenProjects}} - {{svg "octicon-project" 18 "mr-3"}} - {{if .IsOrganizationProject}}org: {{end}}{{.Title}} + {{if .IsOrganizationProject}}{{svg "octicon-project-symlink" 18 "mr-3"}}{{else}}{{svg "octicon-project" 18 "mr-3"}}{{end}} + {{.Title}} {{end}} {{end}} @@ -232,8 +232,8 @@
    {{range .ClosedProjects}} - {{svg "octicon-project" 18 "mr-3"}} - {{if .IsOrganizationProject}}org: {{end}}{{.Title}} + {{if .IsOrganizationProject}}{{svg "octicon-project-symlink" 18 "mr-3"}}{{else}}{{svg "octicon-project" 18 "mr-3"}}{{end}} + {{.Title}} {{end}} {{end}} @@ -244,8 +244,8 @@ From a4ce734b1732bd518a3bc41a91b628c4a333b750 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 31 Dec 2022 23:43:15 +0800 Subject: [PATCH 14/16] Fix test --- models/fixtures/project.yml | 9 +++++++++ models/fixtures/project_board.yml | 8 ++++++++ routers/web/org/projects.go | 2 +- routers/web/org/projects_test.go | 7 +++---- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/models/fixtures/project.yml b/models/fixtures/project.yml index 3d42597c5e8a..f38b5344bba2 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 9e06e8c23960..dc4f9cf565d7 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/routers/web/org/projects.go b/routers/web/org/projects.go index f4230b0532f3..1ce44d4866d8 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -515,7 +515,7 @@ func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr 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, ctx.Repo.Repository.ID), + "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, project.ID), }) return nil, nil } diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go index cc8bb141a130..3450fa8e72db 100644 --- a/routers/web/org/projects_test.go +++ b/routers/web/org/projects_test.go @@ -15,12 +15,11 @@ import ( func TestCheckProjectBoardChangePermissions(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/-/projects/1/2") + ctx := test.MockContext(t, "user2/-/projects/4/4") test.LoadUser(t, ctx, 2) - test.LoadRepo(t, ctx, 1) - ctx.SetParams(":id", "1") - ctx.SetParams(":boardID", "2") ctx.ContextUser = ctx.Doer // user2 + ctx.SetParams(":id", "4") + ctx.SetParams(":boardID", "4") project, board := org.CheckProjectBoardChangePermissions(ctx) assert.NotNil(t, project) From ed072550f97349edd8e9b1de914a5729f963a7ed Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 2 Jan 2023 00:33:28 +0800 Subject: [PATCH 15/16] fix test --- services/context/user.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/context/user.go b/services/context/user.go index 9aad534fdb68..8ca8d36270ca 100644 --- a/services/context/user.go +++ b/services/context/user.go @@ -59,8 +59,7 @@ func userAssignment(ctx *context.Context, errCb func(int, string, interface{})) } } else { if ctx.ContextUser.IsOrganization() { - ctx.Org.Organization = (*org_model.Organization)(ctx.ContextUser) - ctx.Data["Org"] = ctx.Org.Organization + ctx.Data["Org"] = (*org_model.Organization)(ctx.ContextUser) } } } From a3322d2638f6099fe2c3f11c93cde48a70c6b380 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 2 Jan 2023 11:06:08 +0800 Subject: [PATCH 16/16] Fix bug --- services/context/user.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/context/user.go b/services/context/user.go index 8ca8d36270ca..7642cba4e1f0 100644 --- a/services/context/user.go +++ b/services/context/user.go @@ -59,7 +59,11 @@ func userAssignment(ctx *context.Context, errCb func(int, string, interface{})) } } else { if ctx.ContextUser.IsOrganization() { - ctx.Data["Org"] = (*org_model.Organization)(ctx.ContextUser) + if ctx.Org == nil { + ctx.Org = &context.Organization{} + } + ctx.Org.Organization = (*org_model.Organization)(ctx.ContextUser) + ctx.Data["Org"] = ctx.Org.Organization } } }