forked from mirrors/forgejo
fix(web): org projects assignment in issue view (#7999)
Allows user to assign organization projects to their new issues, using the project sidebar selector, even when repository's projects are disabled. Moreover, the project sidebar selector is now hidden if no projects (repository-wide + organization-wide) are available. Fixes forgejo/forgejo#5666 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7999 Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
parent
07a6b6ce82
commit
731334e973
33 changed files with 593 additions and 36 deletions
|
|
@ -604,7 +604,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
|||
repoOwnerType = project_model.TypeOrganization
|
||||
}
|
||||
var err error
|
||||
projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
repositoryProjects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
RepoID: repo.ID,
|
||||
IsClosed: optional.Some(false),
|
||||
|
|
@ -614,7 +614,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
|||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
}
|
||||
projects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ownerProjects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
OwnerID: repo.OwnerID,
|
||||
IsClosed: optional.Some(false),
|
||||
|
|
@ -625,9 +625,10 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
|||
return
|
||||
}
|
||||
|
||||
ctx.Data["OpenProjects"] = append(projects, projects2...)
|
||||
ownerHasOpenProjects := len(ownerProjects) > 0
|
||||
ctx.Data["OpenProjects"] = append(repositoryProjects, ownerProjects...)
|
||||
|
||||
projects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
repositoryProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
RepoID: repo.ID,
|
||||
IsClosed: optional.Some(true),
|
||||
|
|
@ -637,7 +638,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
|||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
}
|
||||
projects2, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ownerProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
OwnerID: repo.OwnerID,
|
||||
IsClosed: optional.Some(true),
|
||||
|
|
@ -648,7 +649,8 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
|
|||
return
|
||||
}
|
||||
|
||||
ctx.Data["ClosedProjects"] = append(projects, projects2...)
|
||||
ctx.Data["OwnerHasProjects"] = ownerHasOpenProjects || len(ownerProjects) > 0
|
||||
ctx.Data["ClosedProjects"] = append(repositoryProjects, ownerProjects...)
|
||||
}
|
||||
|
||||
// repoReviewerSelection items to bee shown
|
||||
|
|
@ -968,6 +970,14 @@ func NewIssue(ctx *context.Context) {
|
|||
|
||||
isProjectsEnabled := ctx.Repo.CanRead(unit.TypeProjects)
|
||||
ctx.Data["IsProjectsEnabled"] = isProjectsEnabled
|
||||
|
||||
// Individuals always have projects unit enabled
|
||||
isOwnerProjectsEnabled := true
|
||||
if ctx.Repo.Owner.IsOrganization() {
|
||||
isOwnerProjectsEnabled = ctx.Org.CanReadUnit(ctx, unit.TypeProjects)
|
||||
}
|
||||
ctx.Data["IsOwnerProjectsEnabled"] = isOwnerProjectsEnabled
|
||||
|
||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
|
||||
|
|
@ -983,7 +993,7 @@ func NewIssue(ctx *context.Context) {
|
|||
}
|
||||
|
||||
projectID := ctx.FormInt64("project")
|
||||
if projectID > 0 && isProjectsEnabled {
|
||||
if projectID > 0 && (isProjectsEnabled || isOwnerProjectsEnabled) {
|
||||
project, err := project_model.GetProjectByID(ctx, projectID)
|
||||
if err != nil {
|
||||
log.Error("GetProjectByID: %d: %v", projectID, err)
|
||||
|
|
@ -1276,9 +1286,9 @@ func NewIssuePost(ctx *context.Context) {
|
|||
}
|
||||
|
||||
if projectID > 0 {
|
||||
if !ctx.Repo.CanRead(unit.TypeProjects) {
|
||||
if !ctx.Repo.CanRead(unit.TypeProjects) || (ctx.ContextUser.IsOrganization() && !ctx.Org.CanReadUnit(ctx, unit.TypeProjects)) {
|
||||
// User must also be able to see the project.
|
||||
ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
|
||||
ctx.Error(http.StatusForbidden, "user doesn't have permissions to read projects")
|
||||
return
|
||||
}
|
||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil {
|
||||
|
|
@ -1477,7 +1487,8 @@ func ViewIssue(ctx *context.Context) {
|
|||
}
|
||||
|
||||
ctx.Data["IsModerationEnabled"] = setting.Moderation.Enabled
|
||||
ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects)
|
||||
isProjectsEnabled := ctx.Repo.CanRead(unit.TypeProjects)
|
||||
ctx.Data["IsProjectsEnabled"] = isProjectsEnabled
|
||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
|
||||
|
|
@ -1546,6 +1557,7 @@ func ViewIssue(ctx *context.Context) {
|
|||
}
|
||||
ctx.Data["Labels"] = labels
|
||||
|
||||
isOwnerProjectsEnabled := true
|
||||
if repo.Owner.IsOrganization() {
|
||||
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
|
||||
if err != nil {
|
||||
|
|
@ -1553,9 +1565,11 @@ func ViewIssue(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
ctx.Data["OrgLabels"] = orgLabels
|
||||
|
||||
labels = append(labels, orgLabels...)
|
||||
|
||||
isOwnerProjectsEnabled = ctx.Org.CanReadUnit(ctx, unit.TypeProjects)
|
||||
}
|
||||
ctx.Data["IsOwnerProjectsEnabled"] = isOwnerProjectsEnabled
|
||||
|
||||
hasSelected := false
|
||||
for i := range labels {
|
||||
|
|
@ -1569,7 +1583,9 @@ func ViewIssue(ctx *context.Context) {
|
|||
// Check milestone and assignee.
|
||||
if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
RetrieveRepoMilestonesAndAssignees(ctx, repo)
|
||||
retrieveProjects(ctx, repo)
|
||||
if isProjectsEnabled || isOwnerProjectsEnabled {
|
||||
retrieveProjects(ctx, repo)
|
||||
}
|
||||
|
||||
if ctx.Written() {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@ func TestNewIssueValidateProject(t *testing.T) {
|
|||
contexttest.LoadUser(t, ctx, testCase.userID)
|
||||
contexttest.LoadRepo(t, ctx, testCase.repoID)
|
||||
contexttest.LoadGitRepo(t, ctx)
|
||||
if ctx.Repo.Owner.IsOrganization() {
|
||||
contexttest.LoadOrganization(t, ctx, ctx.Repo.Owner.ID)
|
||||
}
|
||||
|
||||
NewIssue(ctx)
|
||||
|
||||
|
|
|
|||
|
|
@ -400,7 +400,7 @@ func UpdateIssueProject(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
if _, err := issues.LoadRepositories(ctx); err != nil {
|
||||
ctx.ServerError("LoadProjects", err)
|
||||
ctx.ServerError("LoadRepositories", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -906,6 +906,29 @@ func registerRoutes(m *web.Route) {
|
|||
}
|
||||
}
|
||||
|
||||
reqRepoOrOwnerProjectReader := func(ctx *context.Context) {
|
||||
unitType := unit.TypeProjects
|
||||
if ctx.ContextUser == nil || ctx.Doer == nil {
|
||||
ctx.NotFound(unitType.String(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case ctx.ContextUser.IsIndividual():
|
||||
if ctx.Doer.ID == ctx.ContextUser.ID || ctx.Doer.IsAdmin {
|
||||
return
|
||||
}
|
||||
case ctx.ContextUser.IsOrganization():
|
||||
if ctx.Org.Organization.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeRead {
|
||||
return
|
||||
}
|
||||
default:
|
||||
ctx.NotFound(unitType.String(), nil)
|
||||
return
|
||||
}
|
||||
reqRepoProjectsReader(ctx)
|
||||
}
|
||||
|
||||
individualPermsChecker := func(ctx *context.Context) {
|
||||
// org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked.
|
||||
if ctx.ContextUser.IsIndividual() {
|
||||
|
|
@ -1255,7 +1278,7 @@ func registerRoutes(m *web.Route) {
|
|||
}
|
||||
m.Group("/issues", func() {
|
||||
m.Group("/new", func() {
|
||||
m.Combo("").Get(context.RepoRef(), repo.NewIssue).
|
||||
m.Combo("", context.EnsureOrg()).Get(context.RepoRef(), repo.NewIssue).
|
||||
Post(web.Bind(forms.CreateIssueForm{}), repo.NewIssuePost)
|
||||
m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate)
|
||||
})
|
||||
|
|
@ -1301,7 +1324,7 @@ func registerRoutes(m *web.Route) {
|
|||
|
||||
m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel)
|
||||
m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone)
|
||||
m.Post("/projects", reqRepoIssuesOrPullsWriter, reqRepoProjectsReader, repo.UpdateIssueProject)
|
||||
m.Post("/projects", reqRepoIssuesOrPullsWriter, context.EnsureOrg(), reqRepoOrOwnerProjectReader, repo.UpdateIssueProject)
|
||||
m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
|
||||
m.Post("/request_review", reqRepoIssuesOrPullsReader, repo.UpdatePullReviewRequest)
|
||||
m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview)
|
||||
|
|
@ -1427,7 +1450,7 @@ func registerRoutes(m *web.Route) {
|
|||
m.Group("", func() {
|
||||
m.Get("/issues/posters", repo.IssuePosters) // it can't use {type:issues|pulls} because other routes like "/pulls/{index}" has higher priority
|
||||
m.Get("/{type:^(issues|pulls)$}", repo.Issues)
|
||||
m.Get("/{type:^(issues|pulls)$}/{index}", repo.ViewIssue)
|
||||
m.Get("/{type:^(issues|pulls)$}/{index}", context.EnsureOrg(), repo.ViewIssue)
|
||||
m.Group("/{type:^(issues|pulls)$}/{index}/content-history", func() {
|
||||
m.Get("/overview", repo.GetContentHistoryOverview)
|
||||
m.Get("/list", repo.GetContentHistoryList)
|
||||
|
|
@ -1600,7 +1623,7 @@ func registerRoutes(m *web.Route) {
|
|||
|
||||
m.Get("/pulls/posters", repo.PullPosters)
|
||||
m.Group("/pulls/{index}", func() {
|
||||
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue)
|
||||
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, context.EnsureOrg(), repo.ViewIssue)
|
||||
m.Get(".diff", repo.DownloadPullDiff)
|
||||
m.Get(".patch", repo.DownloadPullPatch)
|
||||
m.Group("/commits", func() {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,32 @@ func GetOrganizationByParams(ctx *Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func ensureIsOrg(ctx *Context) bool {
|
||||
switch {
|
||||
// Getting Organization from params
|
||||
case ctx.ContextUser == nil:
|
||||
if ctx.Org.Organization == nil {
|
||||
GetOrganizationByParams(ctx)
|
||||
}
|
||||
return !ctx.Written()
|
||||
// Getting Organization from ContextUser
|
||||
case ctx.ContextUser.IsOrganization():
|
||||
if ctx.Org == nil {
|
||||
ctx.Org = &Organization{}
|
||||
}
|
||||
ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser)
|
||||
return true
|
||||
}
|
||||
// ContextUser is an individual User
|
||||
return false
|
||||
}
|
||||
|
||||
func EnsureOrg() func(*Context) {
|
||||
return func(ctx *Context) {
|
||||
ensureIsOrg(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleOrgAssignment handles organization assignment
|
||||
func HandleOrgAssignment(ctx *Context, args ...bool) {
|
||||
var (
|
||||
|
|
@ -87,24 +113,9 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
|
|||
|
||||
var err error
|
||||
|
||||
if ctx.ContextUser == nil {
|
||||
// if Organization is not defined, get it from params
|
||||
if ctx.Org.Organization == nil {
|
||||
GetOrganizationByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if ctx.ContextUser.IsOrganization() {
|
||||
if ctx.Org == nil {
|
||||
ctx.Org = &Organization{}
|
||||
}
|
||||
ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser)
|
||||
} else {
|
||||
// ContextUser is an individual User
|
||||
if !ensureIsOrg(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
org := ctx.Org.Organization
|
||||
|
||||
// Handle Visibility
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{if .IsProjectsEnabled}}
|
||||
{{if or .IsProjectsEnabled (and .OwnerHasProjects .IsOwnerProjectsEnabled)}}
|
||||
<div class="divider"></div>
|
||||
|
||||
<input id="project_id" name="project_id" type="hidden" value="{{.project_id}}">
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@
|
|||
{{template "repo/issue/view_content/sidebar/milestones" .}}
|
||||
<div class="divider"></div>
|
||||
|
||||
{{template "repo/issue/view_content/sidebar/projects" .}}
|
||||
<div class="divider"></div>
|
||||
{{if or .IsProjectsEnabled (and .OwnerHasProjects .IsOwnerProjectsEnabled)}}
|
||||
{{template "repo/issue/view_content/sidebar/projects" .}}
|
||||
<div class="divider"></div>
|
||||
{{end}}
|
||||
|
||||
{{template "repo/issue/view_content/sidebar/assignees" dict "isExistingIssue" true "." .}}
|
||||
<div class="divider"></div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/master
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
|
|
@ -0,0 +1 @@
|
|||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
2a47ca4b614a9f5a43abbd5ad851a54a616ffee6
|
||||
|
|
@ -0,0 +1 @@
|
|||
d22b4d4daa5be07329fcef6ed458f00cf3392da0
|
||||
5
tests/integration/fixtures/TestAssignProject/access.yml
Normal file
5
tests/integration/fixtures/TestAssignProject/access.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-
|
||||
id: 1001
|
||||
user_id: 5
|
||||
repo_id: 1001
|
||||
mode: 2
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-
|
||||
id: 1001
|
||||
repo_id: 1001
|
||||
user_id: 5
|
||||
mode: 2
|
||||
51
tests/integration/fixtures/TestAssignProject/project.yml
Normal file
51
tests/integration/fixtures/TestAssignProject/project.yml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# team 1001 projects
|
||||
-
|
||||
id: 1001
|
||||
title: project1001
|
||||
owner_id: 3
|
||||
repo_id: 0
|
||||
is_closed: false
|
||||
board_type: 0
|
||||
type: 3
|
||||
creator_id: 5
|
||||
created_unix: 1688973030
|
||||
updated_unix: 1688973030
|
||||
|
||||
# team 1002 projects
|
||||
-
|
||||
id: 1002
|
||||
title: project1002
|
||||
owner_id: 0
|
||||
repo_id: 1001
|
||||
is_closed: false
|
||||
board_type: 0
|
||||
type: 2
|
||||
creator_id: 5
|
||||
created_unix: 1688973030
|
||||
updated_unix: 1688973030
|
||||
|
||||
# user 5 project
|
||||
-
|
||||
id: 1003
|
||||
title: project1003
|
||||
owner_id: 5
|
||||
repo_id: 0
|
||||
is_closed: false
|
||||
board_type: 0
|
||||
type: 1
|
||||
creator_id: 5
|
||||
created_unix: 1688973030
|
||||
updated_unix: 1688973030
|
||||
|
||||
# org 1001 disabled project
|
||||
-
|
||||
id: 1004
|
||||
title: project1004
|
||||
owner_id: 1001
|
||||
repo_id: 0
|
||||
is_closed: false
|
||||
board_type: 0
|
||||
type: 3
|
||||
creator_id: 5
|
||||
created_unix: 1688973030
|
||||
updated_unix: 1688973030
|
||||
35
tests/integration/fixtures/TestAssignProject/repo_unit.yml
Normal file
35
tests/integration/fixtures/TestAssignProject/repo_unit.yml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
-
|
||||
id: 1001
|
||||
repo_id: 3
|
||||
type: 8
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 1002
|
||||
repo_id: 3
|
||||
type: 2
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 1003
|
||||
repo_id: 3
|
||||
type: 3
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 1004
|
||||
repo_id: 1001
|
||||
type: 8
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 1005
|
||||
repo_id: 1001
|
||||
type: 2
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 1006
|
||||
repo_id: 1001
|
||||
type: 3
|
||||
created_unix: 946684810
|
||||
30
tests/integration/fixtures/TestAssignProject/repository.yml
Normal file
30
tests/integration/fixtures/TestAssignProject/repository.yml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
-
|
||||
id: 1001
|
||||
owner_id: 1001
|
||||
owner_name: test_assign_project_org
|
||||
lower_name: test_assign_project_repo
|
||||
name: test_assign_project_repo
|
||||
default_branch: master
|
||||
num_watches: 4
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
num_milestones: 3
|
||||
num_closed_milestones: 1
|
||||
num_projects: 1
|
||||
num_closed_projects: 0
|
||||
is_private: false
|
||||
is_empty: false
|
||||
is_archived: false
|
||||
is_mirror: false
|
||||
status: 0
|
||||
is_fork: false
|
||||
fork_id: 0
|
||||
is_template: false
|
||||
template_id: 0
|
||||
size: 7597
|
||||
is_fsck_enabled: true
|
||||
close_issues_via_commit_in_any_branch: false
|
||||
created_unix: 1731254961
|
||||
updated_unix: 1731254961
|
||||
topics: '[]'
|
||||
|
||||
32
tests/integration/fixtures/TestAssignProject/team.yml
Normal file
32
tests/integration/fixtures/TestAssignProject/team.yml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
-
|
||||
id: 1001
|
||||
org_id: 3
|
||||
lower_name: writers
|
||||
name: Writers
|
||||
authorize: 2 # writer
|
||||
num_repos: 3
|
||||
num_members: 1
|
||||
includes_all_repositories: false
|
||||
can_create_org_repo: true
|
||||
|
||||
-
|
||||
id: 1002
|
||||
org_id: 1001
|
||||
lower_name: writers
|
||||
name: Writers
|
||||
authorize: 2 # writer
|
||||
num_repos: 1
|
||||
num_members: 1
|
||||
includes_all_repositories: false
|
||||
can_create_org_repo: true
|
||||
|
||||
-
|
||||
id: 1003
|
||||
org_id: 1001
|
||||
lower_name: owners
|
||||
name: Owners
|
||||
authorize: 4
|
||||
num_repos: 0
|
||||
num_members: 1
|
||||
includes_all_repositories: false
|
||||
can_create_org_repo: true
|
||||
11
tests/integration/fixtures/TestAssignProject/team_repo.yml
Normal file
11
tests/integration/fixtures/TestAssignProject/team_repo.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
-
|
||||
id: 1001
|
||||
org_id: 3
|
||||
team_id: 1001
|
||||
repo_id: 3
|
||||
|
||||
-
|
||||
id: 1002
|
||||
org_id: 1001
|
||||
team_id: 1002
|
||||
repo_id: 1001
|
||||
35
tests/integration/fixtures/TestAssignProject/team_unit.yml
Normal file
35
tests/integration/fixtures/TestAssignProject/team_unit.yml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
-
|
||||
id: 1001
|
||||
team_id: 1001
|
||||
type: 8
|
||||
access_mode: 2
|
||||
|
||||
-
|
||||
id: 1002
|
||||
team_id: 1002
|
||||
type: 8
|
||||
access_mode: 1
|
||||
|
||||
-
|
||||
id: 1003
|
||||
team_id: 1001
|
||||
type: 2
|
||||
access_mode: 2
|
||||
|
||||
-
|
||||
id: 1004
|
||||
team_id: 1001
|
||||
type: 3
|
||||
access_mode: 2
|
||||
|
||||
-
|
||||
id: 1005
|
||||
team_id: 1002
|
||||
type: 2
|
||||
access_mode: 2
|
||||
|
||||
-
|
||||
id: 1006
|
||||
team_id: 1002
|
||||
type: 3
|
||||
access_mode: 2
|
||||
11
tests/integration/fixtures/TestAssignProject/team_user.yml
Normal file
11
tests/integration/fixtures/TestAssignProject/team_user.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
-
|
||||
id: 1001
|
||||
org_id: 3
|
||||
team_id: 1001
|
||||
uid: 5
|
||||
|
||||
-
|
||||
id: 1002
|
||||
org_id: 23
|
||||
team_id: 1002
|
||||
uid: 5
|
||||
37
tests/integration/fixtures/TestAssignProject/user.yml
Normal file
37
tests/integration/fixtures/TestAssignProject/user.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
-
|
||||
id: 1001
|
||||
lower_name: test_assign_project_org
|
||||
name: test_assign_project_org
|
||||
full_name: ' <<<< >> >> > >> > >>> >> '
|
||||
email: org1001@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: onmention
|
||||
passwd: ZogKvWdyEx:password
|
||||
passwd_hash_algo: dummy
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: test_assign_project_org
|
||||
type: 1
|
||||
salt: ZogKvWdyEx
|
||||
max_repo_creation: 1000
|
||||
is_active: false
|
||||
is_admin: false
|
||||
is_restricted: false
|
||||
allow_git_hook: false
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: ""
|
||||
avatar_email: org1001@example.com
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
num_repos: 1
|
||||
num_teams: 1
|
||||
num_members: 1
|
||||
visibility: 0
|
||||
repo_admin_change_team_access: false
|
||||
theme: ""
|
||||
keep_activity_private: false
|
||||
created_unix: 1672578020
|
||||
|
|
@ -19,6 +19,7 @@ import (
|
|||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/models/db"
|
||||
issues_model "forgejo.org/models/issues"
|
||||
org_model "forgejo.org/models/organization"
|
||||
project_model "forgejo.org/models/project"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
unit_model "forgejo.org/models/unit"
|
||||
|
|
@ -31,6 +32,7 @@ import (
|
|||
api "forgejo.org/modules/structs"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/modules/translation"
|
||||
repo_service "forgejo.org/services/repository"
|
||||
files_service "forgejo.org/services/repository/files"
|
||||
user_service "forgejo.org/services/user"
|
||||
"forgejo.org/tests"
|
||||
|
|
@ -1661,3 +1663,113 @@ func TestIssueUrlHandling(t *testing.T) {
|
|||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIssueProjectSidebarMissing(t *testing.T) {
|
||||
const (
|
||||
repoID = 4
|
||||
userID = 5
|
||||
)
|
||||
defer unittest.OverrideFixtures("tests/integration/fixtures/TestAssignProject/")()
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
session := loginUser(t, user.Name)
|
||||
|
||||
issueURL := testNewIssue(t, session, user.Name, repo.Name, "Hello", "World")
|
||||
t.Run("Sidebar showing - user project available", func(tt *testing.T) {
|
||||
defer tests.PrintCurrentTest(tt)()
|
||||
req := NewRequest(t, "GET", issueURL)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
htmlDoc.AssertElement(t, ".select-project.dropdown", true)
|
||||
})
|
||||
|
||||
// Enable repository's project unit
|
||||
projectUnit := repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: unit_model.TypeProjects,
|
||||
}
|
||||
require.NoError(t, repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{projectUnit}, nil))
|
||||
|
||||
t.Run("Sidebar showing - repository project unit on", func(tt *testing.T) {
|
||||
defer tests.PrintCurrentTest(tt)()
|
||||
req := NewRequest(t, "GET", issueURL)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
htmlDoc.AssertElement(t, ".select-project.dropdown", true)
|
||||
})
|
||||
|
||||
project_model.DeleteProjectByID(ctx, 1003)
|
||||
// Disable repository's project unit
|
||||
require.NoError(t, repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, nil, []unit_model.Type{unit_model.TypeProjects}))
|
||||
t.Run("Sidebar missing", func(tt *testing.T) {
|
||||
defer tests.PrintCurrentTest(tt)()
|
||||
req := NewRequest(t, "GET", issueURL)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
htmlDoc.AssertElement(t, ".select-project.dropdown", false)
|
||||
})
|
||||
|
||||
// Team with project available
|
||||
team := unittest.AssertExistsAndLoadBean(t, &org_model.Team{ID: 1001})
|
||||
require.NoError(t, team.LoadMembers(ctx))
|
||||
require.NoError(t, team.LoadRepositories(ctx))
|
||||
|
||||
user = team.Members[0]
|
||||
repo = team.Repos[0]
|
||||
org := team.GetOrg(ctx)
|
||||
session = loginUser(t, user.Name)
|
||||
|
||||
issueURL = testNewIssue(t, session, org.Name, repo.Name, "Hello", "World")
|
||||
t.Run("Sidebar showing - org on & repo on", func(tt *testing.T) {
|
||||
defer tests.PrintCurrentTest(tt)()
|
||||
req := NewRequest(t, "GET", issueURL)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
htmlDoc.AssertElement(t, ".select-project.dropdown", true)
|
||||
})
|
||||
|
||||
// Disable repository project unit
|
||||
require.NoError(t, repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeProjects}))
|
||||
t.Run("Sidebar showing - org on & repo off", func(tt *testing.T) {
|
||||
defer tests.PrintCurrentTest(tt)()
|
||||
req := NewRequest(t, "GET", issueURL)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
htmlDoc.AssertElement(t, ".select-project.dropdown", true)
|
||||
})
|
||||
|
||||
// Team with project disabled
|
||||
team = unittest.AssertExistsAndLoadBean(t, &org_model.Team{ID: 1002})
|
||||
require.NoError(t, team.LoadMembers(ctx))
|
||||
require.NoError(t, team.LoadRepositories(ctx))
|
||||
|
||||
user = team.Members[0]
|
||||
repo = team.Repos[0]
|
||||
org = team.GetOrg(ctx)
|
||||
session = loginUser(t, user.Name)
|
||||
|
||||
require.NoError(t, project_model.DeleteProjectByID(db.DefaultContext, 1004))
|
||||
|
||||
issueURL = testNewIssue(t, session, org.Name, repo.Name, "Hello", "World")
|
||||
t.Run("Sidebar showing - org off & repo on", func(tt *testing.T) {
|
||||
defer tests.PrintCurrentTest(tt)()
|
||||
req := NewRequest(t, "GET", issueURL)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
htmlDoc.AssertElement(t, ".select-project.dropdown", true)
|
||||
})
|
||||
|
||||
// Disable repository project unit
|
||||
require.NoError(t, repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeProjects}))
|
||||
t.Run("Sidebar missing - org off & repo off", func(tt *testing.T) {
|
||||
defer tests.PrintCurrentTest(tt)()
|
||||
req := NewRequest(t, "GET", issueURL)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
htmlDoc.AssertElement(t, ".select-project.dropdown", false)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,20 @@ package integration
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
issues_model "forgejo.org/models/issues"
|
||||
org_model "forgejo.org/models/organization"
|
||||
project_model "forgejo.org/models/project"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
unit_model "forgejo.org/models/unit"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
repo_service "forgejo.org/services/repository"
|
||||
"forgejo.org/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -160,3 +168,117 @@ func TestChangeStatusProject(t *testing.T) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAssignProject(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("tests/integration/fixtures/TestAssignProject/")()
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
newTestIssue := func(t *testing.T, session *TestSession, owner *user_model.User, repo *repo_model.Repository) (*issues_model.Issue, string, string) {
|
||||
t.Helper()
|
||||
|
||||
issueURL := testNewIssue(t, session, owner.Name, repo.Name, "Hello", "World")
|
||||
indexStr := issueURL[strings.LastIndexByte(issueURL, '/')+1:]
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
require.NoError(t, err, "Invalid issue href: %s", issueURL)
|
||||
|
||||
issue := &issues_model.Issue{RepoID: repo.ID, Index: int64(index)}
|
||||
unittest.AssertExistsAndLoadBean(t, issue)
|
||||
|
||||
issueID := strconv.FormatInt(issue.ID, 10)
|
||||
return issue, indexStr, issueID
|
||||
}
|
||||
|
||||
updateIssueProject := func(t *testing.T, session *TestSession, projectID, issueID, owner, repo string, expectedStatus int) {
|
||||
t.Helper()
|
||||
|
||||
req := NewRequestWithValues(t, "POST", path.Join(owner, repo, "issues", "projects"), map[string]string{
|
||||
"issue_ids": issueID,
|
||||
"id": projectID,
|
||||
})
|
||||
session.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
// User
|
||||
t.Run("UserProjectOn+RepoProjectOff", func(tt *testing.T) {
|
||||
defer tests.PrintCurrentTest(tt)()
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
|
||||
session := loginUser(tt, user.Name)
|
||||
issue, _, issueID := newTestIssue(tt, session, user, repo)
|
||||
|
||||
updateIssueProject(tt, session, "1003", issueID, user.Name, repo.Name, http.StatusOK)
|
||||
require.NoError(tt, issue.LoadProject(db.DefaultContext))
|
||||
require.NotNil(tt, issue.Project)
|
||||
require.Equal(tt, int64(1003), issue.Project.ID)
|
||||
})
|
||||
|
||||
// Team 1001 - enabled project unit
|
||||
team := unittest.AssertExistsAndLoadBean(t, &org_model.Team{ID: 1001})
|
||||
require.NoError(t, team.LoadMembers(ctx))
|
||||
require.NoError(t, team.LoadRepositories(ctx))
|
||||
|
||||
user := team.Members[0]
|
||||
repo := team.Repos[0]
|
||||
org := team.GetOrg(ctx)
|
||||
|
||||
session := loginUser(t, user.Name)
|
||||
|
||||
t.Run("OrgProjectOn+RepoProjectOn", func(tt *testing.T) {
|
||||
defer tests.PrintCurrentTest(tt)()
|
||||
issue, _, issueID := newTestIssue(tt, session, org.AsUser(), repo)
|
||||
|
||||
updateIssueProject(tt, session, "1001", issueID, org.Name, repo.Name, http.StatusOK)
|
||||
|
||||
require.NoError(tt, issue.LoadProject(db.DefaultContext))
|
||||
require.NotNil(tt, issue.Project)
|
||||
require.Equal(tt, int64(1001), issue.Project.ID)
|
||||
})
|
||||
|
||||
// Disable repository project unit
|
||||
require.NoError(t, repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeProjects}))
|
||||
t.Run("OrgProjectOn+RepoProjectOff", func(tt *testing.T) {
|
||||
defer tests.PrintCurrentTest(tt)()
|
||||
issue, _, issueID := newTestIssue(tt, session, org.AsUser(), repo)
|
||||
|
||||
updateIssueProject(tt, session, "1001", issueID, org.Name, repo.Name, http.StatusOK)
|
||||
require.NoError(tt, issue.LoadProject(db.DefaultContext))
|
||||
require.NotNil(tt, issue.Project)
|
||||
require.Equal(tt, int64(1001), issue.Project.ID)
|
||||
})
|
||||
|
||||
// Team 1002 - disabled project unit
|
||||
team = unittest.AssertExistsAndLoadBean(t, &org_model.Team{ID: 1002})
|
||||
require.NoError(t, team.LoadMembers(ctx))
|
||||
require.NoError(t, team.LoadRepositories(ctx))
|
||||
|
||||
user = team.Members[0]
|
||||
repo = team.Repos[0]
|
||||
org = team.GetOrg(ctx)
|
||||
|
||||
session = loginUser(t, user.Name)
|
||||
|
||||
t.Run("OrgProjectOff+RepoProjectOn", func(tt *testing.T) {
|
||||
defer tests.PrintCurrentTest(tt)()
|
||||
issue, _, issueID := newTestIssue(tt, session, org.AsUser(), repo)
|
||||
|
||||
updateIssueProject(tt, session, "1002", issueID, org.Name, repo.Name, http.StatusOK)
|
||||
require.NoError(tt, issue.LoadProject(db.DefaultContext))
|
||||
require.NotNil(tt, issue.Project)
|
||||
require.Equal(tt, int64(1002), issue.Project.ID)
|
||||
})
|
||||
|
||||
// Disable repository project unit
|
||||
require.NoError(t, repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeProjects}))
|
||||
t.Run("OrgProjectOff+RepoProjectOff", func(tt *testing.T) {
|
||||
defer tests.PrintCurrentTest(tt)()
|
||||
issue, _, issueID := newTestIssue(tt, session, org.AsUser(), repo)
|
||||
|
||||
updateIssueProject(tt, session, "1002", issueID, org.Name, repo.Name, http.StatusOK)
|
||||
require.NoError(tt, issue.LoadProject(db.DefaultContext))
|
||||
require.NotNil(tt, issue.Project)
|
||||
require.Equal(tt, int64(1002), issue.Project.ID)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue