diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index cc7ffb356a6c..891e23210827 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -8,8 +8,11 @@ import ( "fmt" "code.gitea.io/gitea/models/db" + "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/optional" ) // LoadProject load the project the issue was assigned to @@ -48,32 +51,36 @@ func (issue *Issue) ProjectBoardID(ctx context.Context) int64 { } // LoadIssuesFromBoard load issues assigned to this board -func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) { +func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board, doer *user_model.User, org *organization.Organization, isClosed optional.Option[bool]) (IssueList, error) { issueList := make(IssueList, 0, 10) + opts := &IssuesOptions{ + ProjectID: b.ProjectID, + SortType: "project-column-sorting", + IsClosed: isClosed, + } + + if doer != nil { + opts.User = doer + opts.Org = org + } else { + // non-login user can only access public repos + opts.AllPublic = true + } + if b.ID > 0 { - issues, err := Issues(ctx, &IssuesOptions{ - ProjectBoardID: b.ID, - ProjectID: b.ProjectID, - SortType: "project-column-sorting", - }) - if err != nil { - return nil, err - } - issueList = issues + opts.ProjectBoardID = b.ID + } else if b.Default { + opts.ProjectBoardID = db.NoConditionID + } else { + return issueList, nil } - if b.Default { - issues, err := Issues(ctx, &IssuesOptions{ - ProjectBoardID: db.NoConditionID, - ProjectID: b.ProjectID, - SortType: "project-column-sorting", - }) - if err != nil { - return nil, err - } - issueList = append(issueList, issues...) + issues, err := Issues(ctx, opts) + if err != nil { + return nil, err } + issueList = append(issueList, issues...) if err := issueList.LoadComments(ctx); err != nil { return nil, err @@ -83,18 +90,52 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList } // LoadIssuesFromBoardList load issues assigned to the boards -func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (map[int64]IssueList, error) { +func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList, doer *user_model.User, org *organization.Organization, isClosed optional.Option[bool]) (map[int64]IssueList, error) { + if unit.TypeIssues.UnitGlobalDisabled() { + return nil, nil + } issuesMap := make(map[int64]IssueList, len(bs)) - for i := range bs { - il, err := LoadIssuesFromBoard(ctx, bs[i]) + for _, b := range bs { + il, err := LoadIssuesFromBoard(ctx, b, doer, org, isClosed) if err != nil { return nil, err } - issuesMap[bs[i].ID] = il + issuesMap[b.ID] = il } return issuesMap, nil } +// NumIssuesInProjects returns counter of all issues assigned to a project list which doer can access +func NumIssuesInProjects(ctx context.Context, pl project_model.ProjectList, doer *user_model.User, org *organization.Organization, isClosed optional.Option[bool]) (map[int64]int, error) { + numMap := make(map[int64]int, len(pl)) + for _, p := range pl { + num, err := NumIssuesInProject(ctx, p, doer, org, isClosed) + if err != nil { + return nil, err + } + numMap[p.ID] = num + } + + return numMap, nil +} + +// NumIssuesInProject returns counter of all issues assigned to a project which doer can access +func NumIssuesInProject(ctx context.Context, p *project_model.Project, doer *user_model.User, org *organization.Organization, isClosed optional.Option[bool]) (int, error) { + numIssuesInProject := int(0) + bs, err := p.GetBoards(ctx) + if err != nil { + return 0, err + } + im, err := LoadIssuesFromBoardList(ctx, bs, doer, org, isClosed) + if err != nil { + return 0, err + } + for _, il := range im { + numIssuesInProject += len(il) + } + return numIssuesInProject, nil +} + // ChangeProjectAssign changes the project associated with an issue func ChangeProjectAssign(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { ctx, committer, err := db.TxContext(ctx) @@ -123,8 +164,11 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U if err != nil { return err } - if newProject.RepoID != issue.RepoID && newProject.OwnerID != issue.Repo.OwnerID { - return fmt.Errorf("issue's repository is not the same as project's repository") + + if canWriteByDoer, err := newProject.CanWriteByDoer(ctx, issue.Repo, doer); err != nil { + return err + } else if !canWriteByDoer { + return fmt.Errorf("doer have no write permission to project [id:%d]", newProjectID) } } diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index c5c9cecdb92d..836fba037156 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -202,7 +202,7 @@ func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session if opts.RepoCond == nil { opts.RepoCond = builder.NewCond() } - opts.RepoCond = opts.RepoCond.Or(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"is_private": false}))) + opts.RepoCond = opts.RepoCond.Or(repo_model.PublicRepoCond("issue.repo_id")) } if opts.RepoCond != nil { sess.And(opts.RepoCond) @@ -340,6 +340,7 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organizati } else { cond = cond.And( builder.Or( + repo_model.PublicRepoCond(repoIDstr), // all public repos repo_model.UserOwnedRepoCond(userID), // owned repos repo_model.UserAccessRepoCond(repoIDstr, userID), // user can access repo in a unit independent way repo_model.UserAssignedRepoCond(repoIDstr, userID), // user has been assigned accessible public repos diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 4175cb9b9266..3c6414e1ad2b 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -424,3 +424,17 @@ func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *u return perm.CanRead(unitType) } + +// CheckRepoUnitWriteUser check whether user could write the unit of this repository +func CheckRepoUnitWriteUser(ctx context.Context, repo *repo_model.Repository, user *user_model.User, unitType unit.Type) bool { + if user != nil && user.IsAdmin { + return true + } + perm, err := GetUserRepoPermission(ctx, repo, user) + if err != nil { + log.Error("GetUserRepoPermission: %w", err) + return false + } + + return perm.CanWrite(unitType) +} diff --git a/models/project/board.go b/models/project/board.go index 3e2d8e0472c5..8794b4b6cc61 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -59,6 +59,8 @@ type Board struct { ProjectID int64 `xorm:"INDEX NOT NULL"` CreatorID int64 `xorm:"NOT NULL"` + Project *Project `xorm:"-"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } diff --git a/models/project/project.go b/models/project/project.go index 8f9ee2a99e9c..b0de66bd6453 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -9,8 +9,13 @@ import ( "html/template" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" @@ -35,6 +40,9 @@ type ( // Type is used to identify the type of project in question and ownership Type uint8 + + // List is used to identify a list of projects + ProjectList []*Project //nolint ) const ( @@ -153,14 +161,155 @@ func (p *Project) IconName() string { return "octicon-project-symlink" } -func (p *Project) IsOrganizationProject() bool { - return p.Type == TypeOrganization +func (p *Project) IsIndividualProject() bool { + return p.Type == TypeIndividual } func (p *Project) IsRepositoryProject() bool { return p.Type == TypeRepository } +func (p *Project) IsOrganizationProject() bool { + return p.Type == TypeOrganization +} + +func (pl ProjectList) getOwnerIDs() []int64 { + ids := make(container.Set[int64], len(pl)) + for _, p := range pl { + if p.OwnerID == 0 { + continue + } + ids.Add(p.OwnerID) + } + return ids.Values() +} + +func (pl ProjectList) getRepoIDs() []int64 { + ids := make(container.Set[int64], len(pl)) + for _, p := range pl { + if p.RepoID == 0 { + continue + } + ids.Add(p.RepoID) + } + return ids.Values() +} + +func (pl ProjectList) LoadOwners(ctx context.Context) ([]*user_model.User, error) { + if len(pl) == 0 { + return nil, nil + } + + userIDs := pl.getOwnerIDs() + usersMap := make(map[int64]*user_model.User, len(userIDs)) + if err := db.GetEngine(ctx). + Where("id > 0"). + In("id", userIDs). + Find(&usersMap); err != nil { + return nil, fmt.Errorf("find users: %w", err) + } + for _, p := range pl { + if p.OwnerID > 0 && p.Owner == nil { + p.Owner = usersMap[p.OwnerID] + } + } + users := make([]*user_model.User, 0, len(usersMap)) + for _, u := range usersMap { + if u != nil { + users = append(users, u) + } + } + return users, nil +} + +func (pl ProjectList) LoadRepositories(ctx context.Context) (repo_model.RepositoryList, error) { + if len(pl) == 0 { + return nil, nil + } + + repoIDs := pl.getRepoIDs() + repos := make(map[int64]*repo_model.Repository, len(repoIDs)) + if err := db.GetEngine(ctx). + In("id", repoIDs). + Find(&repos); err != nil { + return nil, fmt.Errorf("find repository: %w", err) + } + for _, p := range pl { + if p.RepoID > 0 && p.Repo == nil { + p.Repo = repos[p.RepoID] + } + } + return repo_model.ValuesRepository(repos), nil +} + +func (pl ProjectList) FilterWritableByDoer(ctx context.Context, repo *repo_model.Repository, doer *user_model.User) (ProjectList, error) { + // non-login user have no write permission + if doer == nil { + return nil, nil + } + + // Check valid individual/organization projects + owners, err := pl.LoadOwners(ctx) + if err != nil { + return nil, err + } + validOwnerIDs := make(container.Set[int64], len(owners)) + for _, owner := range owners { + if CheckUserUnitWriteDoer(ctx, owner, doer, unit.TypeProjects) { + validOwnerIDs.Add(owner.ID) + } + } + + // Check valid repo projects + repos, err := pl.LoadRepositories(ctx) + if err != nil { + return nil, err + } + validRepoIDs := make(container.Set[int64], len(repos)) + for _, repo := range repos { + if access_model.CheckRepoUnitWriteUser(ctx, repo, doer, unit.TypeProjects) { + validRepoIDs.Add(repo.ID) + } + } + + projectList := pl[:0] + for _, p := range pl { + if (p.RepoID > 0 && p.RepoID == repo.ID && validRepoIDs.Contains(p.RepoID)) || + (p.OwnerID > 0 && p.OwnerID == repo.OwnerID && validOwnerIDs.Contains(p.OwnerID)) { + projectList = append(projectList, p) + } + } + return projectList, nil +} + +// CheckUserUnitWriteDoer return whether doer have write permission to the individual/organization project +func CheckUserUnitWriteDoer(ctx context.Context, user, doer *user_model.User, unitType unit.Type) bool { + if user == nil { + return false + } + if user.IsIndividual() && user.ID != doer.ID { + return false + } + if user.IsOrganization() && (*organization.Organization)(user).UnitPermission(ctx, doer, unitType) < perm.AccessModeWrite { + return false + } + return true +} + +// CanWriteByDoer return whether doer have write permission to the project +func (p Project) CanWriteByDoer(ctx context.Context, repo *repo_model.Repository, doer *user_model.User) (bool, error) { + var err error + projectList := ProjectList{&p} + projectList, err = projectList.FilterWritableByDoer(ctx, repo, doer) + if err != nil { + return false, err + } + if len(projectList) == 0 { + return false, nil + } + return true, nil +} + func init() { db.RegisterModel(new(Project)) } diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 6b452291eabb..405c028647b8 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -63,6 +63,34 @@ func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList { return RepositoryList(ValuesRepository(repoMap)) } +func (repos RepositoryList) GetOwnerIDs() []int64 { + ids := make(container.Set[int64], len(repos)) + for _, repo := range repos { + if repo.OwnerID == 0 { + continue + } + ids.Add(repo.OwnerID) + } + return ids.Values() +} + +func (repos RepositoryList) LoadOwners(ctx context.Context) error { + userIDs := repos.GetOwnerIDs() + users := make(map[int64]*user_model.User, len(userIDs)) + if err := db.GetEngine(ctx). + Where("id > 0"). + In("id", userIDs). + Find(&users); err != nil { + return fmt.Errorf("find users: %w", err) + } + for _, repo := range repos { + if repo.OwnerID > 0 && repo.Owner == nil { + repo.Owner = users[repo.OwnerID] + } + } + return nil +} + // LoadAttributes loads the attributes for the given RepositoryList func (repos RepositoryList) LoadAttributes(ctx context.Context) error { if len(repos) == 0 { @@ -77,15 +105,8 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error { } // Load owners. - users := make(map[int64]*user_model.User, len(set)) - if err := db.GetEngine(ctx). - Where("id > 0"). - In("id", set.Values()). - Find(&users); err != nil { - return fmt.Errorf("find users: %w", err) - } - for i := range repos { - repos[i].Owner = users[repos[i].OwnerID] + if err := repos.LoadOwners(ctx); err != nil { + return err } // Load primary language. @@ -194,6 +215,10 @@ const ( SearchOrderByForksReverse SearchOrderBy = "num_forks DESC" ) +func PublicRepoCond(id string) builder.Cond { + return builder.In(id, builder.Select("id").From("repository").Where(builder.Eq{"is_private": false})) +} + // UserOwnedRepoCond returns user ownered repositories func UserOwnedRepoCond(userID int64) builder.Cond { return builder.Eq{ diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index ad8bb90d9ea8..1351aa890483 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -128,6 +128,19 @@ func Projects(ctx *context.Context) { ctx.Data["PageIsViewProjects"] = true ctx.Data["SortType"] = sortType + numOpenIssues, err := issues_model.NumIssuesInProjects(ctx, projects, ctx.Doer, ctx.Org.Organization, optional.Some(false)) + if err != nil { + ctx.ServerError("NumIssuesInProjects", err) + return + } + numClosedIssues, err := issues_model.NumIssuesInProjects(ctx, projects, ctx.Doer, ctx.Org.Organization, optional.Some(true)) + if err != nil { + ctx.ServerError("NumIssuesInProjects", err) + return + } + ctx.Data["NumOpenIssuesInProjects"] = numOpenIssues + ctx.Data["NumClosedIssuesInProjects"] = numClosedIssues + ctx.HTML(http.StatusOK, tplProjects) } @@ -357,7 +370,7 @@ func ViewProject(ctx *context.Context) { boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized") } - issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) + issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards, ctx.Doer, ctx.Org.Organization, optional.None[bool]()) if err != nil { ctx.ServerError("LoadIssuesOfBoards", err) return diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 83a5b76bf15b..818e0379191b 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -591,8 +591,8 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { projectsUnit := repo.MustGetUnit(ctx, unit.TypeProjects) - var openProjects []*project_model.Project - var closedProjects []*project_model.Project + var openProjects project_model.ProjectList + var closedProjects project_model.ProjectList var err error if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) { @@ -629,6 +629,11 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { ctx.ServerError("GetProjects", err) return } + openProjects2, err = project_model.ProjectList(openProjects2).FilterWritableByDoer(ctx, repo, ctx.Doer) + if err != nil { + ctx.ServerError("FilterWritableByDoer", err) + return + } openProjects = append(openProjects, openProjects2...) closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ ListOptions: db.ListOptionsAll, @@ -640,6 +645,11 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { ctx.ServerError("GetProjects", err) return } + closedProjects2, err = project_model.ProjectList(closedProjects2).FilterWritableByDoer(ctx, repo, ctx.Doer) + if err != nil { + ctx.ServerError("FilterWritableByDoer", err) + return + } closedProjects = append(closedProjects, closedProjects2...) } @@ -980,15 +990,22 @@ func NewIssue(ctx *context.Context) { } projectID := ctx.FormInt64("project") - if projectID > 0 && isProjectsEnabled { + if projectID > 0 { project, err := project_model.GetProjectByID(ctx, projectID) if err != nil { log.Error("GetProjectByID: %d: %v", projectID, err) - } else if project.RepoID != ctx.Repo.Repository.ID { - log.Error("GetProjectByID: %d: %v", projectID, fmt.Errorf("project[%d] not in repo [%d]", project.ID, ctx.Repo.Repository.ID)) } else { - ctx.Data["project_id"] = projectID - ctx.Data["Project"] = project + canWriteByDoer, err := project.CanWriteByDoer(ctx, ctx.Repo.Repository, ctx.Doer) + if err != nil { + log.Error("CanWriteByDoer: %d: %v", projectID, err) + } else if !canWriteByDoer { + log.Error("CanWriteByDoer: %d: %v", projectID, fmt.Errorf("project[%d] not found", project.ID)) + } else if project.IsRepositoryProject() && !isProjectsEnabled { + log.Error("CanWriteByDoer: %d: %v", projectID, fmt.Errorf("projects is not enabled in repo[%d]", ctx.Repo.Repository.ID)) + } else { + ctx.Data["project_id"] = projectID + ctx.Data["Project"] = project + } } if len(ctx.Req.URL.Query().Get("project")) > 0 { @@ -1076,6 +1093,7 @@ func NewIssueChooseTemplate(ctx *context.Context) { ctx.Data["IssueConfigError"] = err // ctx.Flash.Err makes problems here ctx.Data["milestone"] = ctx.FormInt64("milestone") + // TODO: invalid projectid check? ctx.Data["project"] = ctx.FormInt64("project") ctx.HTML(http.StatusOK, tplIssueChoose) @@ -1102,7 +1120,7 @@ func DeleteIssue(ctx *context.Context) { } // ValidateRepoMetas check and returns repository's meta information -func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) { +func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64, string) { var ( repo = ctx.Repo.Repository err error @@ -1110,7 +1128,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull) if ctx.Written() { - return nil, nil, 0, 0 + return nil, nil, 0, 0, "" } var labelIDs []int64 @@ -1119,7 +1137,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull if len(form.LabelIDs) > 0 { labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ",")) if err != nil { - return nil, nil, 0, 0 + return nil, nil, 0, 0, "" } labelIDMark := make(container.Set[int64]) labelIDMark.AddMultiple(labelIDs...) @@ -1142,29 +1160,44 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID) if err != nil { ctx.ServerError("GetMilestoneByID", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, "" } if milestone.RepoID != repo.ID { ctx.ServerError("GetMilestoneByID", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, "" } ctx.Data["Milestone"] = milestone ctx.Data["milestone_id"] = milestoneID } + projectLink := "" + projectID := int64(0) if form.ProjectID > 0 { p, err := project_model.GetProjectByID(ctx, form.ProjectID) if err != nil { ctx.ServerError("GetProjectByID", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, "" } if p.RepoID != ctx.Repo.Repository.ID && p.OwnerID != ctx.Repo.Repository.OwnerID { ctx.NotFound("", nil) - return nil, nil, 0, 0 + return nil, nil, 0, 0, "" } - ctx.Data["Project"] = p - ctx.Data["project_id"] = form.ProjectID + // if projects unit of this repo is disabled in the repo, it will redirect to a 404 page + // if user have no access permission to the project, it will also redirect to a 404 page + // so we need to return empty projectLink here + if canWriteByDoer, err := p.CanWriteByDoer(ctx, ctx.Repo.Repository, ctx.Doer); err != nil { + ctx.ServerError("CanWriteByDoer", err) + return nil, nil, 0, 0, "" + } else if canWriteByDoer { + if !(p.IsRepositoryProject() && !ctx.Repo.CanRead(unit.TypeProjects)) { + ctx.Data["Project"] = p + ctx.Data["project_id"] = form.ProjectID + + projectID = form.ProjectID + projectLink = p.Link(ctx) + } + } } // Check assignees @@ -1172,7 +1205,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull if len(form.AssigneeIDs) > 0 { assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ",")) if err != nil { - return nil, nil, 0, 0 + return nil, nil, 0, 0, "" } // Check if the passed assignees actually exists and is assignable @@ -1180,18 +1213,18 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull assignee, err := user_model.GetUserByID(ctx, aID) if err != nil { ctx.ServerError("GetUserByID", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, "" } valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull) if err != nil { ctx.ServerError("CanBeAssigned", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, "" } if !valid { ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) - return nil, nil, 0, 0 + return nil, nil, 0, 0, "" } } } @@ -1201,7 +1234,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull assigneeIDs = append(assigneeIDs, form.AssigneeID) } - return labelIDs, assigneeIDs, milestoneID, form.ProjectID + return labelIDs, assigneeIDs, milestoneID, projectID, projectLink } // NewIssuePost response for creating new issue @@ -1219,7 +1252,7 @@ func NewIssuePost(ctx *context.Context) { attachments []string ) - labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false) + labelIDs, assigneeIDs, milestoneID, projectID, projectLink := ValidateRepoMetas(ctx, *form, false) if ctx.Written() { return } @@ -1276,8 +1309,8 @@ func NewIssuePost(ctx *context.Context) { } log.Trace("Issue created: %d/%d", repo.ID, issue.ID) - if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 { - ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10)) + if ctx.FormString("redirect_after_creation") == "project" && projectLink != "" { + ctx.Redirect(projectLink) } else { ctx.JSONRedirect(issue.Link()) } @@ -2639,6 +2672,7 @@ func SearchIssues(ctx *context.Context) { } } + // TODO:invalid projectid check? var projectID *int64 if v := ctx.FormInt64("project"); v > 0 { projectID = &v @@ -2796,6 +2830,7 @@ func ListIssues(ctx *context.Context) { } } + // TODO: invalid projectid check? var projectID *int64 if v := ctx.FormInt64("project"); v > 0 { projectID = &v diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 86909b5fd0f5..430fba0fabf9 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -126,6 +126,19 @@ func Projects(ctx *context.Context) { ctx.Data["IsProjectsPage"] = true ctx.Data["SortType"] = sortType + numOpenIssues, err := issues_model.NumIssuesInProjects(ctx, projects, ctx.Doer, ctx.Org.Organization, optional.Some(false)) + if err != nil { + ctx.ServerError("NumIssuesInProjects", err) + return + } + numClosedIssues, err := issues_model.NumIssuesInProjects(ctx, projects, ctx.Doer, ctx.Org.Organization, optional.Some(true)) + if err != nil { + ctx.ServerError("NumIssuesInProjects", err) + return + } + ctx.Data["NumOpenIssuesInProjects"] = numOpenIssues + ctx.Data["NumClosedIssuesInProjects"] = numClosedIssues + ctx.HTML(http.StatusOK, tplProjects) } @@ -184,7 +197,7 @@ func ChangeProjectStatus(ctx *context.Context) { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("", err) } else { - ctx.ServerError("ChangeProjectStatusByIDAndRepoID", err) + ctx.ServerError("ChangeProjectStatusByRepoIDAndID", err) } return } @@ -319,7 +332,7 @@ func ViewProject(ctx *context.Context) { boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized") } - issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) + issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards, ctx.Doer, ctx.Org.Organization, optional.None[bool]()) if err != nil { ctx.ServerError("LoadIssuesOfBoards", err) return diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index ed063715e50d..ccab0a1d0ba8 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1437,7 +1437,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { return } - labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, true) + labelIDs, assigneeIDs, milestoneID, projectID, _ := ValidateRepoMetas(ctx, *form, true) if ctx.Written() { return } diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index 54a41221bf31..b9659eae34d1 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -54,11 +54,11 @@
{{svg "octicon-issue-opened" 14}} - {{ctx.Locale.PrettyNumber (.NumOpenIssues ctx)}} {{ctx.Locale.Tr "repo.issues.open_title"}} + {{ctx.Locale.PrettyNumber (index $.NumOpenIssuesInProjects .ID)}} {{ctx.Locale.Tr "repo.issues.open_title"}}
{{svg "octicon-check" 14}} - {{ctx.Locale.PrettyNumber (.NumClosedIssues ctx)}} {{ctx.Locale.Tr "repo.issues.closed_title"}} + {{ctx.Locale.PrettyNumber (index $.NumClosedIssuesInProjects .ID)}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}} diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 1d03477a9f41..91613ef030a6 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -70,7 +70,7 @@
- {{.NumIssues ctx}} + {{len (index $.IssuesMap .ID)}}
{{.Title}}