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:
+28
-12
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+27
-4
@@ -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() {
|
||||
|
||||
+27
-16
@@ -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>
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
ref: refs/heads/master
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
+1
@@ -0,0 +1 @@
|
||||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
+6
@@ -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]
|
||||
# *~
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
2a47ca4b614a9f5a43abbd5ad851a54a616ffee6
|
||||
+1
@@ -0,0 +1 @@
|
||||
d22b4d4daa5be07329fcef6ed458f00cf3392da0
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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: '[]'
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
-
|
||||
id: 1001
|
||||
org_id: 3
|
||||
team_id: 1001
|
||||
uid: 5
|
||||
|
||||
-
|
||||
id: 1002
|
||||
org_id: 23
|
||||
team_id: 1002
|
||||
uid: 5
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user