mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-03 00:02:43 +02:00
Merge branch 'main' into puni9869/ISSUE-36299
This commit is contained in:
23 files changed
+1220
-98
No files matched your search
@@ -64,8 +64,8 @@ func GetAssigneeIDsByIssue(ctx context.Context, issueID int64) ([]int64, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsUserAssignedToIssue returns true when the user is assigned to the issue
|
// IsUserAssignedToIssue returns true when the user is assigned to the issue
|
||||||
func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.User) (isAssigned bool, err error) {
|
func IsUserAssignedToIssue(ctx context.Context, issue *Issue, userID int64) (isAssigned bool, err error) {
|
||||||
return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": user.ID, "issue_id": issue.ID})
|
return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": userID, "issue_id": issue.ID})
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssignedIssuesOptions struct {
|
type AssignedIssuesOptions struct {
|
||||||
@@ -170,7 +170,7 @@ func toggleUserAssignee(ctx context.Context, issue *Issue, assigneeID int64) (re
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs
|
// MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs
|
||||||
func MakeIDsFromAPIAssigneesToAdd(ctx context.Context, oneAssignee string, multipleAssignees []string) (assigneeIDs []int64, err error) {
|
func MakeIDsFromAPIAssigneesToAdd(ctx context.Context, oneAssignee string, multipleAssignees []string) ([]int64, error) {
|
||||||
var requestAssignees []string
|
var requestAssignees []string
|
||||||
|
|
||||||
// Keeping the old assigning method for compatibility reasons
|
// Keeping the old assigning method for compatibility reasons
|
||||||
@@ -184,7 +184,5 @@ func MakeIDsFromAPIAssigneesToAdd(ctx context.Context, oneAssignee string, multi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the IDs of all assignees
|
// Get the IDs of all assignees
|
||||||
assigneeIDs, err = user_model.GetUserIDsByNames(ctx, requestAssignees, false)
|
return user_model.GetUserIDsByNames(ctx, requestAssignees, false)
|
||||||
|
|
||||||
return assigneeIDs, err
|
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ func TestUpdateAssignee(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Check if he got removed
|
// Check if he got removed
|
||||||
isAssigned, err := issues_model.IsUserAssignedToIssue(t.Context(), issue, user1)
|
isAssigned, err := issues_model.IsUserAssignedToIssue(t.Context(), issue, user1.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.False(t, isAssigned)
|
assert.False(t, isAssigned)
|
||||||
|
|
||||||
@@ -56,12 +56,12 @@ func TestUpdateAssignee(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user is assigned
|
// Check if the user is assigned
|
||||||
isAssigned, err = issues_model.IsUserAssignedToIssue(t.Context(), issue, user2)
|
isAssigned, err = issues_model.IsUserAssignedToIssue(t.Context(), issue, user2.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, isAssigned)
|
assert.True(t, isAssigned)
|
||||||
|
|
||||||
// This user should not be assigned
|
// This user should not be assigned
|
||||||
isAssigned, err = issues_model.IsUserAssignedToIssue(t.Context(), issue, &user_model.User{ID: 4})
|
isAssigned, err = issues_model.IsUserAssignedToIssue(t.Context(), issue, 4)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.False(t, isAssigned)
|
assert.False(t, isAssigned)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -567,9 +567,9 @@ func HasAccessUnit(ctx context.Context, user *user_model.User, repo *repo_model.
|
|||||||
|
|
||||||
// CanBeAssigned return true if user can be assigned to issue or pull requests in repo
|
// CanBeAssigned return true if user can be assigned to issue or pull requests in repo
|
||||||
// Currently any write access (code, issues or pr's) is assignable, to match assignee list in user interface.
|
// Currently any write access (code, issues or pr's) is assignable, to match assignee list in user interface.
|
||||||
func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository, _ bool) (bool, error) {
|
func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (bool, error) {
|
||||||
if user.IsOrganization() {
|
if user.IsOrganization() {
|
||||||
return false, fmt.Errorf("organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID)
|
return false, util.NewInvalidArgumentErrorf("organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID)
|
||||||
}
|
}
|
||||||
perm, err := GetIndividualUserRepoPermission(ctx, repo, user)
|
perm, err := GetIndividualUserRepoPermission(ctx, repo, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -125,6 +125,11 @@ type EditIssueOption struct {
|
|||||||
ContentVersion *int `json:"content_version"`
|
ContentVersion *int `json:"content_version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IssueAssigneesOption options for adding/removing issue assignees
|
||||||
|
type IssueAssigneesOption struct {
|
||||||
|
Assignees []string `json:"assignees"`
|
||||||
|
}
|
||||||
|
|
||||||
// EditDeadlineOption options for creating a deadline
|
// EditDeadlineOption options for creating a deadline
|
||||||
type EditDeadlineOption struct {
|
type EditDeadlineOption struct {
|
||||||
// required:true
|
// required:true
|
||||||
|
|||||||
+1
-1
@@ -106,7 +106,7 @@
|
|||||||
"eslint-plugin-vue-scoped-css": "3.1.1",
|
"eslint-plugin-vue-scoped-css": "3.1.1",
|
||||||
"eslint-plugin-wc": "3.1.0",
|
"eslint-plugin-wc": "3.1.0",
|
||||||
"globals": "17.6.0",
|
"globals": "17.6.0",
|
||||||
"happy-dom": "20.9.0",
|
"happy-dom": "20.10.1",
|
||||||
"jiti": "2.7.0",
|
"jiti": "2.7.0",
|
||||||
"markdownlint-cli": "0.48.0",
|
"markdownlint-cli": "0.48.0",
|
||||||
"material-icon-theme": "5.35.0",
|
"material-icon-theme": "5.35.0",
|
||||||
|
|||||||
Generated
+64
-29
@@ -257,7 +257,7 @@ importers:
|
|||||||
version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
|
version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
|
||||||
'@vitest/eslint-plugin':
|
'@vitest/eslint-plugin':
|
||||||
specifier: 1.6.19
|
specifier: 1.6.19
|
||||||
version: 1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)))
|
version: 1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)))
|
||||||
eslint:
|
eslint:
|
||||||
specifier: 10.4.1
|
specifier: 10.4.1
|
||||||
version: 10.4.1(jiti@2.7.0)
|
version: 10.4.1(jiti@2.7.0)
|
||||||
@@ -301,8 +301,8 @@ importers:
|
|||||||
specifier: 17.6.0
|
specifier: 17.6.0
|
||||||
version: 17.6.0
|
version: 17.6.0
|
||||||
happy-dom:
|
happy-dom:
|
||||||
specifier: 20.9.0
|
specifier: 20.10.1
|
||||||
version: 20.9.0
|
version: 20.10.1
|
||||||
jiti:
|
jiti:
|
||||||
specifier: 2.7.0
|
specifier: 2.7.0
|
||||||
version: 2.7.0
|
version: 2.7.0
|
||||||
@@ -347,7 +347,7 @@ importers:
|
|||||||
version: 17.17.3
|
version: 17.17.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: 4.1.8
|
specifier: 4.1.8
|
||||||
version: 4.1.8(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))
|
version: 4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))
|
||||||
vue-tsc:
|
vue-tsc:
|
||||||
specifier: 3.3.3
|
specifier: 3.3.3
|
||||||
version: 3.3.3(typescript@6.0.3)
|
version: 3.3.3(typescript@6.0.3)
|
||||||
@@ -1879,6 +1879,10 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
buffer-image-size@0.6.4:
|
||||||
|
resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
buffer@5.7.1:
|
buffer@5.7.1:
|
||||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
|
|
||||||
@@ -3006,8 +3010,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==}
|
resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==}
|
||||||
engines: {node: '>=0.8.0'}
|
engines: {node: '>=0.8.0'}
|
||||||
|
|
||||||
happy-dom@20.9.0:
|
happy-dom@20.10.1:
|
||||||
resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==}
|
resolution: {integrity: sha512-awPoqPjx8CgjapJllyDlgzgVHjBExcitKK5ZJkxwhQJyQpHFkyS2bEcqCm7IeW20cQvuCI0cz2Ifq79CJKqtiw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
har-schema@2.0.0:
|
har-schema@2.0.0:
|
||||||
@@ -6125,22 +6129,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)':
|
|
||||||
dependencies:
|
|
||||||
'@eslint-community/regexpp': 4.12.2
|
|
||||||
'@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
|
|
||||||
'@typescript-eslint/scope-manager': 8.60.1
|
|
||||||
'@typescript-eslint/type-utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
|
||||||
'@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
|
||||||
'@typescript-eslint/visitor-keys': 8.60.1
|
|
||||||
eslint: 10.4.1(jiti@2.7.0)
|
|
||||||
ignore: 7.0.5
|
|
||||||
natural-compare: 1.4.0
|
|
||||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
|
||||||
typescript: 5.9.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)':
|
'@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
@@ -6376,7 +6364,7 @@ snapshots:
|
|||||||
vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)
|
vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)
|
||||||
vue: 3.5.35(typescript@6.0.3)
|
vue: 3.5.35(typescript@6.0.3)
|
||||||
|
|
||||||
'@vitest/eslint-plugin@1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)))':
|
'@vitest/eslint-plugin@1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 8.60.1
|
'@typescript-eslint/scope-manager': 8.60.1
|
||||||
'@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
|
'@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
|
||||||
@@ -6384,7 +6372,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
|
'@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
vitest: 4.1.8(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))
|
vitest: 4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -6710,6 +6698,10 @@ snapshots:
|
|||||||
node-releases: 2.0.46
|
node-releases: 2.0.46
|
||||||
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
||||||
|
|
||||||
|
buffer-image-size@0.6.4:
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 25.9.1
|
||||||
|
|
||||||
buffer@5.7.1:
|
buffer@5.7.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
@@ -7459,6 +7451,17 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)):
|
||||||
|
dependencies:
|
||||||
|
debug: 3.2.7
|
||||||
|
optionalDependencies:
|
||||||
|
'@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
|
eslint-import-resolver-node: 0.3.10
|
||||||
|
eslint-import-resolver-typescript: 4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.1(jiti@2.7.0))
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)):
|
eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
@@ -7469,6 +7472,7 @@ snapshots:
|
|||||||
eslint-import-resolver-typescript: 4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.1(jiti@2.7.0))
|
eslint-import-resolver-typescript: 4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.1(jiti@2.7.0))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
eslint-plugin-array-func@5.1.1(eslint@10.4.1(jiti@2.7.0)):
|
eslint-plugin-array-func@5.1.1(eslint@10.4.1(jiti@2.7.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7503,7 +7507,7 @@ snapshots:
|
|||||||
'@eslint/eslintrc': 3.3.5
|
'@eslint/eslintrc': 3.3.5
|
||||||
'@eslint/js': 9.39.4
|
'@eslint/js': 9.39.4
|
||||||
'@github/browserslist-config': 1.0.0
|
'@github/browserslist-config': 1.0.0
|
||||||
'@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
'@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
aria-query: 5.3.2
|
aria-query: 5.3.2
|
||||||
eslint: 10.4.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
@@ -7512,7 +7516,7 @@ snapshots:
|
|||||||
eslint-plugin-eslint-comments: 3.2.0(eslint@10.4.1(jiti@2.7.0))
|
eslint-plugin-eslint-comments: 3.2.0(eslint@10.4.1(jiti@2.7.0))
|
||||||
eslint-plugin-filenames: 1.3.2(eslint@10.4.1(jiti@2.7.0))
|
eslint-plugin-filenames: 1.3.2(eslint@10.4.1(jiti@2.7.0))
|
||||||
eslint-plugin-i18n-text: 1.0.1(eslint@10.4.1(jiti@2.7.0))
|
eslint-plugin-i18n-text: 1.0.1(eslint@10.4.1(jiti@2.7.0))
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0))
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0))
|
||||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@10.4.1(jiti@2.7.0))
|
eslint-plugin-jsx-a11y: 6.10.2(eslint@10.4.1(jiti@2.7.0))
|
||||||
eslint-plugin-no-only-tests: 3.4.0
|
eslint-plugin-no-only-tests: 3.4.0
|
||||||
eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.7.0)))(eslint@10.4.1(jiti@2.7.0))(prettier@3.8.3)
|
eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.7.0)))(eslint@10.4.1(jiti@2.7.0))(prettier@3.8.3)
|
||||||
@@ -7552,6 +7556,35 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)):
|
||||||
|
dependencies:
|
||||||
|
'@rtsao/scc': 1.1.0
|
||||||
|
array-includes: 3.1.9
|
||||||
|
array.prototype.findlastindex: 1.2.6
|
||||||
|
array.prototype.flat: 1.3.3
|
||||||
|
array.prototype.flatmap: 1.3.3
|
||||||
|
debug: 3.2.7
|
||||||
|
doctrine: 2.1.0
|
||||||
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
|
eslint-import-resolver-node: 0.3.10
|
||||||
|
eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0))
|
||||||
|
hasown: 2.0.4
|
||||||
|
is-core-module: 2.16.2
|
||||||
|
is-glob: 4.0.3
|
||||||
|
minimatch: 3.1.5
|
||||||
|
object.fromentries: 2.0.8
|
||||||
|
object.groupby: 1.0.3
|
||||||
|
object.values: 1.2.1
|
||||||
|
semver: 6.3.1
|
||||||
|
string.prototype.trimend: 1.0.9
|
||||||
|
tsconfig-paths: 3.15.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- eslint-import-resolver-typescript
|
||||||
|
- eslint-import-resolver-webpack
|
||||||
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)):
|
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
@@ -7580,6 +7613,7 @@ snapshots:
|
|||||||
- eslint-import-resolver-typescript
|
- eslint-import-resolver-typescript
|
||||||
- eslint-import-resolver-webpack
|
- eslint-import-resolver-webpack
|
||||||
- supports-color
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
eslint-plugin-jsx-a11y@6.10.2(eslint@10.4.1(jiti@2.7.0)):
|
eslint-plugin-jsx-a11y@6.10.2(eslint@10.4.1(jiti@2.7.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8013,11 +8047,12 @@ snapshots:
|
|||||||
|
|
||||||
hammerjs@2.0.8: {}
|
hammerjs@2.0.8: {}
|
||||||
|
|
||||||
happy-dom@20.9.0:
|
happy-dom@20.10.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.9.1
|
'@types/node': 25.9.1
|
||||||
'@types/whatwg-mimetype': 3.0.2
|
'@types/whatwg-mimetype': 3.0.2
|
||||||
'@types/ws': 8.18.1
|
'@types/ws': 8.18.1
|
||||||
|
buffer-image-size: 0.6.4
|
||||||
entities: 7.0.1
|
entities: 7.0.1
|
||||||
whatwg-mimetype: 3.0.0
|
whatwg-mimetype: 3.0.0
|
||||||
ws: 8.21.0
|
ws: 8.21.0
|
||||||
@@ -10001,7 +10036,7 @@ snapshots:
|
|||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
|
|
||||||
vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)):
|
vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.8
|
'@vitest/expect': 4.1.8
|
||||||
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))
|
'@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))
|
||||||
@@ -10025,7 +10060,7 @@ snapshots:
|
|||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 25.9.1
|
'@types/node': 25.9.1
|
||||||
happy-dom: 20.9.0
|
happy-dom: 20.10.1
|
||||||
jsdom: 20.0.3
|
jsdom: 20.0.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- msw
|
- msw
|
||||||
|
|||||||
@@ -1226,6 +1226,7 @@ func Routes() *web.Router {
|
|||||||
})
|
})
|
||||||
}, reqToken())
|
}, reqToken())
|
||||||
m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees)
|
m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees)
|
||||||
|
m.Get("/assignees/{assignee}", reqToken(), reqAnyRepoReader(), repo.CheckRepoIssueAssignee)
|
||||||
m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
|
m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
|
||||||
m.Group("/teams", func() {
|
m.Group("/teams", func() {
|
||||||
m.Get("", reqAnyRepoReader(), repo.ListTeams)
|
m.Get("", reqAnyRepoReader(), repo.ListTeams)
|
||||||
@@ -1517,6 +1518,10 @@ func Routes() *web.Router {
|
|||||||
m.Combo("").Get(repo.GetIssue).
|
m.Combo("").Get(repo.GetIssue).
|
||||||
Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue).
|
Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue).
|
||||||
Delete(reqToken(), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
|
Delete(reqToken(), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
|
||||||
|
m.Combo("/assignees").
|
||||||
|
Post(reqToken(), mustNotBeArchived, bind(api.IssueAssigneesOption{}), repo.AddIssueAssignees).
|
||||||
|
Delete(reqToken(), mustNotBeArchived, bind(api.IssueAssigneesOption{}), repo.DeleteIssueAssignees)
|
||||||
|
m.Get("/assignees/{assignee}", repo.CheckIssueAssignee)
|
||||||
m.Group("/comments", func() {
|
m.Group("/comments", func() {
|
||||||
m.Combo("").Get(repo.ListIssueComments).
|
m.Combo("").Get(repo.ListIssueComments).
|
||||||
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
|
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
|
||||||
|
|||||||
@@ -367,5 +367,42 @@ func GetAssignees(ctx *context.APIContext) {
|
|||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, convert.ToUsers(ctx, ctx.Doer, assignees))
|
ctx.JSON(http.StatusOK, convert.ToUsers(ctx, ctx.Doer, assignees))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckRepoIssueAssignee check if a user can be assigned to issues in a repository
|
||||||
|
func CheckRepoIssueAssignee(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /repos/{owner}/{repo}/assignees/{assignee} repository repoCheckAssignee
|
||||||
|
// ---
|
||||||
|
// summary: Check if a user can be assigned to issues in a repository
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: assignee
|
||||||
|
// in: path
|
||||||
|
// description: username of the user to check for being an assignee
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
if checkAssignableUser(ctx, ctx.PathParam("assignee"), ctx.Repo.Repository) {
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -670,7 +670,7 @@ func CreateIssue(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
valid, err := access_model.CanBeAssigned(ctx, assignee, ctx.Repo.Repository, false)
|
valid, err := access_model.CanBeAssigned(ctx, assignee, ctx.Repo.Repository)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
issues_model "gitea.dev/models/issues"
|
||||||
|
access_model "gitea.dev/models/perm/access"
|
||||||
|
repo_model "gitea.dev/models/repo"
|
||||||
|
user_model "gitea.dev/models/user"
|
||||||
|
api "gitea.dev/modules/structs"
|
||||||
|
"gitea.dev/modules/web"
|
||||||
|
"gitea.dev/services/context"
|
||||||
|
"gitea.dev/services/convert"
|
||||||
|
issue_service "gitea.dev/services/issue"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddIssueAssignees add assignees to an issue
|
||||||
|
func AddIssueAssignees(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/assignees issue issueAddAssignees
|
||||||
|
// ---
|
||||||
|
// summary: Add assignees to an issue
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: index
|
||||||
|
// in: path
|
||||||
|
// description: index of the issue
|
||||||
|
// type: integer
|
||||||
|
// format: int64
|
||||||
|
// required: true
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// required: true
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/IssueAssigneesOption"
|
||||||
|
// responses:
|
||||||
|
// "201":
|
||||||
|
// "$ref": "#/responses/Issue"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
opts := web.GetForm(ctx).(*api.IssueAssigneesOption)
|
||||||
|
updateIssueAssignees(ctx, *opts, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteIssueAssignees remove assignees from an issue
|
||||||
|
func DeleteIssueAssignees(ctx *context.APIContext) {
|
||||||
|
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/assignees issue issueRemoveAssignees
|
||||||
|
// ---
|
||||||
|
// summary: Remove assignees from an issue
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: index
|
||||||
|
// in: path
|
||||||
|
// description: index of the issue
|
||||||
|
// type: integer
|
||||||
|
// format: int64
|
||||||
|
// required: true
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// required: true
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/IssueAssigneesOption"
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/Issue"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
opts := web.GetForm(ctx).(*api.IssueAssigneesOption)
|
||||||
|
updateIssueAssignees(ctx, *opts, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckIssueAssignee check if a user can be assigned to an issue
|
||||||
|
func CheckIssueAssignee(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assignees/{assignee} issue issueCheckAssignee
|
||||||
|
// ---
|
||||||
|
// summary: Check if a user can be assigned to an issue
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: index
|
||||||
|
// in: path
|
||||||
|
// description: index of the issue
|
||||||
|
// type: integer
|
||||||
|
// format: int64
|
||||||
|
// required: true
|
||||||
|
// - name: assignee
|
||||||
|
// in: path
|
||||||
|
// description: username of the user to check for being an assignee
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorAuto(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
||||||
|
ctx.APIErrorNotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkAssignableUser(ctx, ctx.PathParam("assignee"), ctx.Repo.Repository) {
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAssignableUser resolves assigneeName and verifies the user can be assigned to issues in repo.
|
||||||
|
// Returns true only when the user resolves AND is assignable; the caller is responsible for writing the 204.
|
||||||
|
// On any failure it writes the appropriate API response and returns false.
|
||||||
|
func checkAssignableUser(ctx *context.APIContext, assigneeName string, repo *repo_model.Repository) bool {
|
||||||
|
assignee, err := user_model.GetUserByName(ctx, assigneeName)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorAuto(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
canAssign, err := access_model.CanBeAssigned(ctx, assignee, repo)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorAuto(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canAssign {
|
||||||
|
ctx.APIErrorNotFound()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateIssueAssignees(ctx *context.APIContext, opts api.IssueAssigneesOption, isAdd bool) {
|
||||||
|
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorAuto(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||||
|
ctx.Status(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issue.LoadAttributes(ctx); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assigneeIDs, err := user_model.GetUserIDsByNames(ctx, opts.Assignees, false)
|
||||||
|
if err != nil {
|
||||||
|
if user_model.IsErrUserNotExist(err) {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.APIErrorAuto(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAdd {
|
||||||
|
err = issue_service.AddAssignees(ctx, issue, ctx.Doer, assigneeIDs)
|
||||||
|
} else {
|
||||||
|
err = issue_service.RemoveAssignees(ctx, issue, ctx.Doer, assigneeIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorAuto(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status := http.StatusOK
|
||||||
|
if isAdd {
|
||||||
|
status = http.StatusCreated
|
||||||
|
}
|
||||||
|
ctx.JSON(status, convert.ToAPIIssue(ctx, ctx.Doer, issue))
|
||||||
|
}
|
||||||
@@ -545,7 +545,7 @@ func CreatePullRequest(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
valid, err := access_model.CanBeAssigned(ctx, assignee, repo, true)
|
valid, err := access_model.CanBeAssigned(ctx, assignee, repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ type swaggerParameterBodies struct {
|
|||||||
EditIssueOption api.EditIssueOption
|
EditIssueOption api.EditIssueOption
|
||||||
// in:body
|
// in:body
|
||||||
EditDeadlineOption api.EditDeadlineOption
|
EditDeadlineOption api.EditDeadlineOption
|
||||||
|
// in:body
|
||||||
|
IssueAssigneesOption api.IssueAssigneesOption
|
||||||
|
|
||||||
// in:body
|
// in:body
|
||||||
CreateIssueCommentOption api.CreateIssueCommentOption
|
CreateIssueCommentOption api.CreateIssueCommentOption
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ func UpdateIssueAssignee(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull)
|
valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("canBeAssigned", err)
|
ctx.ServerError("canBeAssigned", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ func (r *Repository) CanUseTimetracker(ctx context.Context, issue *issues_model.
|
|||||||
// Checking for following:
|
// Checking for following:
|
||||||
// 1. Is timetracker enabled
|
// 1. Is timetracker enabled
|
||||||
// 2. Is the user a contributor, admin, poster or assignee and do the repository policies require this?
|
// 2. Is the user a contributor, admin, poster or assignee and do the repository policies require this?
|
||||||
isAssigned, _ := issues_model.IsUserAssignedToIssue(ctx, issue, user)
|
isAssigned, _ := issues_model.IsUserAssignedToIssue(ctx, issue, user.ID)
|
||||||
return r.Repository.IsTimetrackerEnabled(ctx) && (!r.Repository.AllowOnlyContributorsToTrackTime(ctx) ||
|
return r.Repository.IsTimetrackerEnabled(ctx) && (!r.Repository.AllowOnlyContributorsToTrackTime(ctx) ||
|
||||||
r.Permission.CanWriteIssuesOrPulls(issue.IsPull) || issue.IsPoster(user.ID) || isAssigned)
|
r.Permission.CanWriteIssuesOrPulls(issue.IsPull) || issue.IsPoster(user.ID) || isAssigned)
|
||||||
}
|
}
|
||||||
|
|||||||
+182
-40
@@ -6,6 +6,7 @@ package issue
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"gitea.dev/models/db"
|
||||||
issues_model "gitea.dev/models/issues"
|
issues_model "gitea.dev/models/issues"
|
||||||
access_model "gitea.dev/models/perm/access"
|
access_model "gitea.dev/models/perm/access"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
@@ -14,8 +15,7 @@ import (
|
|||||||
notify_service "gitea.dev/services/notify"
|
notify_service "gitea.dev/services/notify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array
|
func toBeRemovedAssignees(issue *issues_model.Issue, assignees []*user_model.User) (toBeRemovedAssignees []*user_model.User) {
|
||||||
func DeleteNotPassedAssignee(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assignees []*user_model.User) (err error) {
|
|
||||||
var found bool
|
var found bool
|
||||||
oriAssignees := make([]*user_model.User, len(issue.Assignees))
|
oriAssignees := make([]*user_model.User, len(issue.Assignees))
|
||||||
_ = copy(oriAssignees, issue.Assignees)
|
_ = copy(oriAssignees, issue.Assignees)
|
||||||
@@ -31,28 +31,54 @@ func DeleteNotPassedAssignee(ctx context.Context, issue *issues_model.Issue, doe
|
|||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
// This function also does comments and hooks, which is why we call it separately instead of directly removing the assignees here
|
// This function also does comments and hooks, which is why we call it separately instead of directly removing the assignees here
|
||||||
if _, _, err := ToggleAssigneeWithNotify(ctx, issue, doer, assignee.ID); err != nil {
|
toBeRemovedAssignees = append(toBeRemovedAssignees, assignee)
|
||||||
return err
|
}
|
||||||
}
|
}
|
||||||
|
return toBeRemovedAssignees
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array
|
||||||
|
func DeleteNotPassedAssignee(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assignees []*user_model.User) (err error) {
|
||||||
|
toBeRemoved := toBeRemovedAssignees(issue, assignees)
|
||||||
|
|
||||||
|
for _, assignee := range toBeRemoved {
|
||||||
|
// This function also does comments and hooks, which is why we call it separately instead of directly removing the assignees here
|
||||||
|
removed, comment, err := ToggleAssignee(ctx, issue, doer, assignee)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if removed {
|
||||||
|
notify_service.IssueChangeAssignee(ctx, doer, issue, assignee, true, comment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToggleAssigneeWithNoNotify changes a user between assigned and not assigned for this issue, and make issue comment for it.
|
// ToggleAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
|
||||||
func ToggleAssigneeWithNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *issues_model.Comment, err error) {
|
func ToggleAssignee(ctx context.Context, issue *issues_model.Issue, doer, assignee *user_model.User) (removed bool, comment *issues_model.Comment, err error) {
|
||||||
removed, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assigneeID)
|
removed, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assignee.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
issue.AssigneeID = assignee.ID
|
||||||
|
issue.Assignee = assignee
|
||||||
|
|
||||||
|
return removed, comment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToggleAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
|
||||||
|
func ToggleAssigneeWithNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *issues_model.Comment, err error) {
|
||||||
assignee, err := user_model.GetUserByID(ctx, assigneeID)
|
assignee, err := user_model.GetUserByID(ctx, assigneeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
issue.AssigneeID = assigneeID
|
|
||||||
issue.Assignee = assignee
|
removed, comment, err = ToggleAssignee(ctx, issue, doer, assignee)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
notify_service.IssueChangeAssignee(ctx, doer, issue, assignee, removed, comment)
|
notify_service.IssueChangeAssignee(ctx, doer, issue, assignee, removed, comment)
|
||||||
|
|
||||||
@@ -81,43 +107,85 @@ func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) {
|
if err := validateAssignee(ctx, issue, doer, assignee); err != nil {
|
||||||
return user_model.ErrBlockedUser
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
allNewAssignees = append(allNewAssignees, assignee)
|
allNewAssignees = append(allNewAssignees, assignee)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all old assignees not passed
|
assigneeCommentMap := make(map[int64]*issues_model.Comment)
|
||||||
if err = DeleteNotPassedAssignee(ctx, issue, doer, allNewAssignees); err != nil {
|
assigneeRemovedCommentMap := make(map[int64]*issues_model.Comment)
|
||||||
|
assigneeRemoved := make(map[int64]*user_model.User)
|
||||||
|
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
// Delete all old assignees not passed.
|
||||||
|
toBeRemoved := toBeRemovedAssignees(issue, allNewAssignees)
|
||||||
|
|
||||||
|
for _, assignee := range toBeRemoved {
|
||||||
|
// This function also does comments and hooks, which is why we call it separately instead of directly removing the assignees here
|
||||||
|
removed, comment, err := ToggleAssignee(ctx, issue, doer, assignee)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if removed {
|
||||||
|
assigneeRemoved[assignee.ID] = assignee
|
||||||
|
assigneeRemovedCommentMap[assignee.ID] = comment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all new assignees.
|
||||||
|
// Update the assignee. The function will check if the user exists, is already
|
||||||
|
// assigned (which he shouldn't as we deleted all assignees before) and
|
||||||
|
// has access to the repo.
|
||||||
|
for _, assignee := range allNewAssignees {
|
||||||
|
// Extra method to prevent double adding (which would result in removing).
|
||||||
|
comment, err := AddAssigneeIfNotAssigned(ctx, issue, doer, assignee)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
assigneeCommentMap[assignee.ID] = comment
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all new assignees
|
for _, assignee := range assigneeRemoved {
|
||||||
// Update the assignee. The function will check if the user exists, is already
|
notify_service.IssueChangeAssignee(ctx, doer, issue, assignee, true, assigneeRemovedCommentMap[assignee.ID])
|
||||||
// assigned (which he shouldn't as we deleted all assignees before) and
|
}
|
||||||
// has access to the repo.
|
|
||||||
for _, assignee := range allNewAssignees {
|
for _, assignee := range allNewAssignees {
|
||||||
// Extra method to prevent double adding (which would result in removing)
|
comment := assigneeCommentMap[assignee.ID]
|
||||||
_, err = AddAssigneeIfNotAssigned(ctx, issue, doer, assignee.ID, true)
|
if comment != nil {
|
||||||
if err != nil {
|
notify_service.IssueChangeAssignee(ctx, doer, issue, assignee, false, comment)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAssignee(ctx context.Context, issue *issues_model.Issue, doer, assignee *user_model.User) error {
|
||||||
|
if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) {
|
||||||
|
return user_model.ErrBlockedUser
|
||||||
|
}
|
||||||
|
|
||||||
|
valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
return repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assignee.ID, RepoName: issue.Repo.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue.
|
// AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue.
|
||||||
// Also checks for access of assigned user
|
// Also checks for access of assigned user
|
||||||
func AddAssigneeIfNotAssigned(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64, notify bool) (comment *issues_model.Comment, err error) {
|
func AddAssigneeIfNotAssigned(ctx context.Context, issue *issues_model.Issue, doer, assignee *user_model.User) (comment *issues_model.Comment, err error) {
|
||||||
assignee, err := user_model.GetUserByID(ctx, assigneeID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user is already assigned
|
// Check if the user is already assigned
|
||||||
isAssigned, err := issues_model.IsUserAssignedToIssue(ctx, issue, assignee)
|
isAssigned, err := issues_model.IsUserAssignedToIssue(ctx, issue, assignee.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -126,18 +194,92 @@ func AddAssigneeIfNotAssigned(ctx context.Context, issue *issues_model.Issue, do
|
|||||||
return nil, nil //nolint:nilnil // return nil because the user is already assigned
|
return nil, nil //nolint:nilnil // return nil because the user is already assigned
|
||||||
}
|
}
|
||||||
|
|
||||||
valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull)
|
if err := validateAssignee(ctx, issue, doer, assignee); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !valid {
|
|
||||||
return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name}
|
|
||||||
}
|
|
||||||
|
|
||||||
if notify {
|
_, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assignee.ID)
|
||||||
_, comment, err = ToggleAssigneeWithNotify(ctx, issue, doer, assigneeID)
|
|
||||||
return comment, err
|
|
||||||
}
|
|
||||||
_, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assigneeID)
|
|
||||||
return comment, err
|
return comment, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddAssignees adds multiple assignees to an issue atomically.
|
||||||
|
func AddAssignees(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeIDs []int64) error {
|
||||||
|
assigneeCommentMap := make(map[int64]*issues_model.Comment)
|
||||||
|
assignees := make(map[int64]*user_model.User)
|
||||||
|
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
for _, assigneeID := range assigneeIDs {
|
||||||
|
isAssigned, err := issues_model.IsUserAssignedToIssue(ctx, issue, assigneeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isAssigned {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
assignee, err := user_model.GetUserByID(ctx, assigneeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := validateAssignee(ctx, issue, doer, assignee); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
comment, err := AddAssigneeIfNotAssigned(ctx, issue, doer, assignee)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
assignees[assigneeID] = assignee
|
||||||
|
assigneeCommentMap[assigneeID] = comment
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(assignees) > 0 {
|
||||||
|
for assigneeID, assignee := range assignees {
|
||||||
|
notify_service.IssueChangeAssignee(ctx, doer, issue, assignee, false, assigneeCommentMap[assigneeID])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAssignees removes multiple assignees from an issue atomically.
|
||||||
|
func RemoveAssignees(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeIDs []int64) error {
|
||||||
|
assigneeCommentMap := make(map[int64]*issues_model.Comment)
|
||||||
|
assignees := make(map[int64]*user_model.User)
|
||||||
|
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
for _, assigneeID := range assigneeIDs {
|
||||||
|
isAssigned, err := issues_model.IsUserAssignedToIssue(ctx, issue, assigneeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !isAssigned {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
removed, comment, err := issues_model.ToggleIssueAssignee(ctx, issue, doer, assigneeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if removed {
|
||||||
|
assignee, err := user_model.GetUserByID(ctx, assigneeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
assignees[assigneeID] = assignee
|
||||||
|
assigneeCommentMap[assigneeID] = comment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(assignees) > 0 {
|
||||||
|
for assigneeID, assignee := range assignees {
|
||||||
|
notify_service.IssueChangeAssignee(ctx, doer, issue, assignee, true, assigneeCommentMap[assigneeID])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ package issue
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/models/db"
|
||||||
issues_model "gitea.dev/models/issues"
|
issues_model "gitea.dev/models/issues"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
@@ -29,7 +30,7 @@ func TestDeleteNotPassedAssignee(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Check if he got removed
|
// Check if he got removed
|
||||||
isAssigned, err := issues_model.IsUserAssignedToIssue(t.Context(), issue, user1)
|
isAssigned, err := issues_model.IsUserAssignedToIssue(t.Context(), issue, user1.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, isAssigned)
|
assert.True(t, isAssigned)
|
||||||
|
|
||||||
@@ -44,3 +45,55 @@ func TestDeleteNotPassedAssignee(t *testing.T) {
|
|||||||
assert.Empty(t, issue.Assignees)
|
assert.Empty(t, issue.Assignees)
|
||||||
assert.Empty(t, issue.Assignee)
|
assert.Empty(t, issue.Assignee)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddAssigneeIfNotAssignedBlocked(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
issue, err := issues_model.GetIssueByID(t.Context(), 1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, issue.LoadRepo(t.Context()))
|
||||||
|
|
||||||
|
doer, err := user_model.GetUserByID(t.Context(), 4)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assignee, err := user_model.GetUserByID(t.Context(), 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NoError(t, db.Insert(t.Context(), &user_model.Blocking{
|
||||||
|
BlockerID: assignee.ID,
|
||||||
|
BlockeeID: doer.ID,
|
||||||
|
}))
|
||||||
|
|
||||||
|
_, err = AddAssigneeIfNotAssigned(t.Context(), issue, doer, assignee)
|
||||||
|
assert.ErrorIs(t, err, user_model.ErrBlockedUser)
|
||||||
|
|
||||||
|
isAssigned, err := issues_model.IsUserAssignedToIssue(t.Context(), issue, assignee.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, isAssigned)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddAssigneesBlockedIsAtomic(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
issue, err := issues_model.GetIssueByID(t.Context(), 1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, issue.LoadAttributes(t.Context()))
|
||||||
|
|
||||||
|
doer, err := user_model.GetUserByID(t.Context(), 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
blockedAssignee, err := user_model.GetUserByID(t.Context(), 40)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NoError(t, db.Insert(t.Context(), &user_model.Blocking{
|
||||||
|
BlockerID: blockedAssignee.ID,
|
||||||
|
BlockeeID: doer.ID,
|
||||||
|
}))
|
||||||
|
|
||||||
|
err = AddAssignees(t.Context(), issue, doer, []int64{doer.ID, blockedAssignee.ID})
|
||||||
|
assert.ErrorIs(t, err, user_model.ErrBlockedUser)
|
||||||
|
|
||||||
|
assigneeIDs, err := issues_model.GetAssigneeIDsByIssue(t.Context(), issue.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.ElementsMatch(t, []int64{1}, assigneeIDs)
|
||||||
|
}
|
||||||
+17
-1
@@ -32,14 +32,24 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_mo
|
|||||||
return user_model.ErrBlockedUser
|
return user_model.ErrBlockedUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assigneeCommentMap := make(map[int64]*issues_model.Comment)
|
||||||
|
assignees := make(map[int64]*user_model.User)
|
||||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
|
if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, assigneeID := range assigneeIDs {
|
for _, assigneeID := range assigneeIDs {
|
||||||
if _, err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, true); err != nil {
|
assignee, err := user_model.GetUserByID(ctx, assigneeID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetUserByID: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assignees[assigneeID] = assignee
|
||||||
|
comment, err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assignee)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
assigneeCommentMap[assigneeID] = comment
|
||||||
}
|
}
|
||||||
if len(projectIDs) > 0 {
|
if len(projectIDs) > 0 {
|
||||||
err := issues_model.IssueAssignOrRemoveProject(ctx, issue, issue.Poster, projectIDs)
|
err := issues_model.IssueAssignOrRemoveProject(ctx, issue, issue.Poster, projectIDs)
|
||||||
@@ -65,6 +75,12 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_mo
|
|||||||
notify_service.IssueChangeMilestone(ctx, issue.Poster, issue, 0)
|
notify_service.IssueChangeMilestone(ctx, issue.Poster, issue, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(assigneeIDs) > 0 {
|
||||||
|
for _, assignee := range assignees {
|
||||||
|
notify_service.IssueChangeAssignee(ctx, issue.Poster, issue, assignee, false, assigneeCommentMap[assignee.ID])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-8
@@ -96,7 +96,7 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assigneeCommentMap := make(map[int64]*issues_model.Comment)
|
assigneeCommentMap := make(map[int64]*issues_model.Comment)
|
||||||
|
assignees := make(map[int64]*user_model.User)
|
||||||
var reviewNotifiers []*issue_service.ReviewRequestNotifier
|
var reviewNotifiers []*issue_service.ReviewRequestNotifier
|
||||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
if err := issues_model.NewPullRequest(ctx, repo, issue, labelIDs, uuids, pr); err != nil {
|
if err := issues_model.NewPullRequest(ctx, repo, issue, labelIDs, uuids, pr); err != nil {
|
||||||
@@ -104,10 +104,16 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, assigneeID := range assigneeIDs {
|
for _, assigneeID := range assigneeIDs {
|
||||||
comment, err := issue_service.AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, false)
|
assignee, err := user_model.GetUserByID(ctx, assigneeID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetUserByID: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
comment, err := issue_service.AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assignee)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
assignees[assigneeID] = assignee
|
||||||
assigneeCommentMap[assigneeID] = comment
|
assigneeCommentMap[assigneeID] = comment
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,12 +192,8 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
|
|||||||
if issue.Milestone != nil {
|
if issue.Milestone != nil {
|
||||||
notify_service.IssueChangeMilestone(ctx, issue.Poster, issue, 0)
|
notify_service.IssueChangeMilestone(ctx, issue.Poster, issue, 0)
|
||||||
}
|
}
|
||||||
for _, assigneeID := range assigneeIDs {
|
for _, assignee := range assignees {
|
||||||
assignee, err := user_model.GetUserByID(ctx, assigneeID)
|
notify_service.IssueChangeAssignee(ctx, issue.Poster, issue, assignee, false, assigneeCommentMap[assignee.ID])
|
||||||
if err != nil {
|
|
||||||
return ErrDependenciesLeft
|
|
||||||
}
|
|
||||||
notify_service.IssueChangeAssignee(ctx, issue.Poster, issue, assignee, false, assigneeCommentMap[assigneeID])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, colla
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
|
func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
|
||||||
if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned {
|
if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo); err != nil || canAssigned {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+237
@@ -6766,6 +6766,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/assignees/{assignee}": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Check if a user can be assigned to issues in a repository",
|
||||||
|
"operationId": "repoCheckAssignee",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "username of the user to check for being an assignee",
|
||||||
|
"name": "assignee",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"$ref": "#/responses/empty"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/responses/error"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/avatar": {
|
"/repos/{owner}/{repo}/avatar": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@@ -10962,6 +11008,183 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/issues/{index}/assignees": {
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"issue"
|
||||||
|
],
|
||||||
|
"summary": "Add assignees to an issue",
|
||||||
|
"operationId": "issueAddAssignees",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "index of the issue",
|
||||||
|
"name": "index",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/IssueAssigneesOption"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"$ref": "#/responses/Issue"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/responses/error"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/responses/forbidden"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/responses/validationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"issue"
|
||||||
|
],
|
||||||
|
"summary": "Remove assignees from an issue",
|
||||||
|
"operationId": "issueRemoveAssignees",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "index of the issue",
|
||||||
|
"name": "index",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/IssueAssigneesOption"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/Issue"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/responses/forbidden"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/responses/validationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/repos/{owner}/{repo}/issues/{index}/assignees/{assignee}": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"issue"
|
||||||
|
],
|
||||||
|
"summary": "Check if a user can be assigned to an issue",
|
||||||
|
"operationId": "issueCheckAssignee",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "index of the issue",
|
||||||
|
"name": "index",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "username of the user to check for being an assignee",
|
||||||
|
"name": "assignee",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"$ref": "#/responses/empty"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/responses/error"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/issues/{index}/blocks": {
|
"/repos/{owner}/{repo}/issues/{index}/blocks": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@@ -26838,6 +27061,20 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "gitea.dev/modules/structs"
|
"x-go-package": "gitea.dev/modules/structs"
|
||||||
},
|
},
|
||||||
|
"IssueAssigneesOption": {
|
||||||
|
"description": "IssueAssigneesOption options for adding/removing issue assignees",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"assignees": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"x-go-name": "Assignees"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "gitea.dev/modules/structs"
|
||||||
|
},
|
||||||
"IssueConfig": {
|
"IssueConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
Generated
+251
@@ -6992,6 +6992,20 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"x-go-package": "gitea.dev/modules/structs"
|
"x-go-package": "gitea.dev/modules/structs"
|
||||||
},
|
},
|
||||||
|
"IssueAssigneesOption": {
|
||||||
|
"description": "IssueAssigneesOption options for adding/removing issue assignees",
|
||||||
|
"properties": {
|
||||||
|
"assignees": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"x-go-name": "Assignees"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"x-go-package": "gitea.dev/modules/structs"
|
||||||
|
},
|
||||||
"IssueConfig": {
|
"IssueConfig": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"blank_issues_enabled": {
|
"blank_issues_enabled": {
|
||||||
@@ -17814,6 +17828,55 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/assignees/{assignee}": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "repoCheckAssignee",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"in": "path",
|
||||||
|
"name": "owner",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "name of the repo",
|
||||||
|
"in": "path",
|
||||||
|
"name": "repo",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "username of the user to check for being an assignee",
|
||||||
|
"in": "path",
|
||||||
|
"name": "assignee",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"$ref": "#/components/responses/empty"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/components/responses/error"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/notFound"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"summary": "Check if a user can be assigned to issues in a repository",
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/avatar": {
|
"/repos/{owner}/{repo}/avatar": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"operationId": "repoDeleteAvatar",
|
"operationId": "repoDeleteAvatar",
|
||||||
@@ -22371,6 +22434,194 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/issues/{index}/assignees": {
|
||||||
|
"delete": {
|
||||||
|
"operationId": "issueRemoveAssignees",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"in": "path",
|
||||||
|
"name": "owner",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "name of the repo",
|
||||||
|
"in": "path",
|
||||||
|
"name": "repo",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "index of the issue",
|
||||||
|
"in": "path",
|
||||||
|
"name": "index",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/IssueAssigneesOption"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"x-originalParamName": "body"
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/Issue"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/notFound"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/components/responses/validationError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"summary": "Remove assignees from an issue",
|
||||||
|
"tags": [
|
||||||
|
"issue"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"operationId": "issueAddAssignees",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"in": "path",
|
||||||
|
"name": "owner",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "name of the repo",
|
||||||
|
"in": "path",
|
||||||
|
"name": "repo",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "index of the issue",
|
||||||
|
"in": "path",
|
||||||
|
"name": "index",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/IssueAssigneesOption"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"x-originalParamName": "body"
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"$ref": "#/components/responses/Issue"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/components/responses/error"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/notFound"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/components/responses/validationError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"summary": "Add assignees to an issue",
|
||||||
|
"tags": [
|
||||||
|
"issue"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/repos/{owner}/{repo}/issues/{index}/assignees/{assignee}": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "issueCheckAssignee",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"in": "path",
|
||||||
|
"name": "owner",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "name of the repo",
|
||||||
|
"in": "path",
|
||||||
|
"name": "repo",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "index of the issue",
|
||||||
|
"in": "path",
|
||||||
|
"name": "index",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "username of the user to check for being an assignee",
|
||||||
|
"in": "path",
|
||||||
|
"name": "assignee",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"$ref": "#/components/responses/empty"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/components/responses/error"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/notFound"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"summary": "Check if a user can be assigned to an issue",
|
||||||
|
"tags": [
|
||||||
|
"issue"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/issues/{index}/blocks": {
|
"/repos/{owner}/{repo}/issues/{index}/blocks": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"operationId": "issueRemoveIssueBlocking",
|
"operationId": "issueRemoveIssueBlocking",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
auth_model "gitea.dev/models/auth"
|
auth_model "gitea.dev/models/auth"
|
||||||
|
"gitea.dev/models/db"
|
||||||
issues_model "gitea.dev/models/issues"
|
issues_model "gitea.dev/models/issues"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
@@ -35,6 +36,7 @@ func TestAPIIssue(t *testing.T) {
|
|||||||
t.Run("IssueContentVersion", testAPIIssueContentVersion)
|
t.Run("IssueContentVersion", testAPIIssueContentVersion)
|
||||||
t.Run("CreateIssue", testAPICreateIssue)
|
t.Run("CreateIssue", testAPICreateIssue)
|
||||||
t.Run("CreateIssueParallel", testAPICreateIssueParallel)
|
t.Run("CreateIssueParallel", testAPICreateIssueParallel)
|
||||||
|
t.Run("IssueAssignees", testAPIIssueAssignees)
|
||||||
t.Run("IssueProjects", testAPIIssueProjects)
|
t.Run("IssueProjects", testAPIIssueProjects)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,6 +497,91 @@ func testAPIIssueContentVersion(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAPIIssueAssignees(t *testing.T) {
|
||||||
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||||
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
|
||||||
|
session := loginUser(t, owner.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||||
|
|
||||||
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assignees", owner.Name, repo.Name, issue.Index)
|
||||||
|
getAssigneeIDs := func(issueID int64) []int64 {
|
||||||
|
assigneeIDs, err := issues_model.GetAssigneeIDsByIssue(t.Context(), issueID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
return assigneeIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("NonWriter", func(t *testing.T) {
|
||||||
|
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueAssigneesOption{Assignees: []string{"user40"}}).
|
||||||
|
AddTokenAuth(getUserToken(t, "user5", auth_model.AccessTokenScopeWriteIssue))
|
||||||
|
MakeRequest(t, req, http.StatusForbidden)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MissingIssue", func(t *testing.T) {
|
||||||
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/99999/assignees", owner.Name, repo.Name), &api.IssueAssigneesOption{Assignees: []string{"user40"}}).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UnknownAssignee", func(t *testing.T) {
|
||||||
|
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueAssigneesOption{Assignees: []string{"does-not-exist"}}).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
|
apiErr := DecodeJSON(t, resp, &api.APIError{})
|
||||||
|
assert.Equal(t, "user does not exist [uid: 0, name: does-not-exist]", apiErr.Message)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OrganizationAssignee", func(t *testing.T) {
|
||||||
|
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueAssigneesOption{Assignees: []string{"org3"}}).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusBadRequest)
|
||||||
|
|
||||||
|
checkReq := NewRequest(t, "GET", fmt.Sprintf("%s/%s", urlStr, "org3")).AddTokenAuth(token)
|
||||||
|
MakeRequest(t, checkReq, http.StatusBadRequest)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("BlockedAssigneeIsAtomic", func(t *testing.T) {
|
||||||
|
blockedIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 5})
|
||||||
|
blockedURL := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assignees", owner.Name, repo.Name, blockedIssue.Index)
|
||||||
|
blockedToken := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteIssue)
|
||||||
|
|
||||||
|
assert.Empty(t, getAssigneeIDs(blockedIssue.ID))
|
||||||
|
assert.NoError(t, db.Insert(t.Context(), &user_model.Blocking{
|
||||||
|
BlockerID: owner.ID,
|
||||||
|
BlockeeID: 40,
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", blockedURL, &api.IssueAssigneesOption{Assignees: []string{"user1", "user2"}}).
|
||||||
|
AddTokenAuth(blockedToken)
|
||||||
|
MakeRequest(t, req, http.StatusForbidden)
|
||||||
|
assert.Empty(t, getAssigneeIDs(blockedIssue.ID))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HappyPath", func(t *testing.T) {
|
||||||
|
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueAssigneesOption{Assignees: []string{"user40"}}).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusCreated)
|
||||||
|
apiIssue := DecodeJSON(t, resp, &api.Issue{})
|
||||||
|
assert.Len(t, apiIssue.Assignees, 2)
|
||||||
|
assert.ElementsMatch(t, []int64{1, 40}, []int64{apiIssue.Assignees[0].ID, apiIssue.Assignees[1].ID})
|
||||||
|
|
||||||
|
checkReq := NewRequest(t, "GET", fmt.Sprintf("%s/%s", urlStr, "user40")).AddTokenAuth(token)
|
||||||
|
MakeRequest(t, checkReq, http.StatusNoContent)
|
||||||
|
|
||||||
|
// This endpoint checks assignability, not current assignment membership.
|
||||||
|
checkReq = NewRequest(t, "GET", fmt.Sprintf("%s/%s", urlStr, "user5")).AddTokenAuth(token)
|
||||||
|
MakeRequest(t, checkReq, http.StatusNoContent)
|
||||||
|
|
||||||
|
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.IssueAssigneesOption{Assignees: []string{"user1"}}).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
apiIssue = DecodeJSON(t, resp, &api.Issue{})
|
||||||
|
assert.Len(t, apiIssue.Assignees, 1)
|
||||||
|
assert.Equal(t, int64(40), apiIssue.Assignees[0].ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func testAPIIssueProjects(t *testing.T) {
|
func testAPIIssueProjects(t *testing.T) {
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
|||||||
@@ -721,11 +721,25 @@ func TestAPIRepoGetAssignees(t *testing.T) {
|
|||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
session := loginUser(t, user.Name)
|
session := loginUser(t, user.Name)
|
||||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
|
|
||||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/assignees", user.Name, repo.Name).
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/assignees", user.Name, repo.Name).
|
||||||
AddTokenAuth(token)
|
AddTokenAuth(token)
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
assignees := DecodeJSON(t, resp, []*api.User{})
|
assignees := DecodeJSON(t, resp, []*api.User{})
|
||||||
assert.Len(t, assignees, 2)
|
assert.Len(t, assignees, 1)
|
||||||
|
|
||||||
|
assignee := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/assignees/%s", user.Name, repo.Name, assignee.Name).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
nonAssignee := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/assignees/%s", user.Name, repo.Name, nonAssignee.Name).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/assignees/%s", user.Name, repo.Name, "org3").
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user