Multiple assignees (#3705)
parent
238a997ec0
commit
95f2e2b57b
|
@ -733,6 +733,22 @@ func (err ErrRepoFileAlreadyExist) Error() string {
|
||||||
return fmt.Sprintf("repository file already exists [file_name: %s]", err.FileName)
|
return fmt.Sprintf("repository file already exists [file_name: %s]", err.FileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo
|
||||||
|
type ErrUserDoesNotHaveAccessToRepo struct {
|
||||||
|
UserID int64
|
||||||
|
RepoName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExist.
|
||||||
|
func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
|
||||||
|
_, ok := err.(ErrUserDoesNotHaveAccessToRepo)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrUserDoesNotHaveAccessToRepo) Error() string {
|
||||||
|
return fmt.Sprintf("user doesn't have acces to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName)
|
||||||
|
}
|
||||||
|
|
||||||
// __________ .__
|
// __________ .__
|
||||||
// \______ \____________ ____ ____ | |__
|
// \______ \____________ ____ ____ | |__
|
||||||
// | | _/\_ __ \__ \ / \_/ ___\| | \
|
// | | _/\_ __ \__ \ / \_/ ___\| | \
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
repo_id: 1
|
repo_id: 1
|
||||||
index: 1
|
index: 1
|
||||||
poster_id: 1
|
poster_id: 1
|
||||||
assignee_id: 1
|
|
||||||
name: issue1
|
name: issue1
|
||||||
content: content for the first issue
|
content: content for the first issue
|
||||||
is_closed: false
|
is_closed: false
|
||||||
|
@ -67,7 +66,6 @@
|
||||||
repo_id: 3
|
repo_id: 3
|
||||||
index: 1
|
index: 1
|
||||||
poster_id: 1
|
poster_id: 1
|
||||||
assignee_id: 1
|
|
||||||
name: issue6
|
name: issue6
|
||||||
content: content6
|
content: content6
|
||||||
is_closed: false
|
is_closed: false
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
-
|
||||||
|
id: 1
|
||||||
|
assignee_id: 1
|
||||||
|
issue_id: 1
|
||||||
|
-
|
||||||
|
id: 2
|
||||||
|
assignee_id: 1
|
||||||
|
issue_id: 6
|
|
@ -3,7 +3,6 @@
|
||||||
uid: 1
|
uid: 1
|
||||||
issue_id: 1
|
issue_id: 1
|
||||||
is_read: true
|
is_read: true
|
||||||
is_assigned: true
|
|
||||||
is_mentioned: false
|
is_mentioned: false
|
||||||
|
|
||||||
-
|
-
|
||||||
|
@ -11,7 +10,6 @@
|
||||||
uid: 2
|
uid: 2
|
||||||
issue_id: 1
|
issue_id: 1
|
||||||
is_read: true
|
is_read: true
|
||||||
is_assigned: false
|
|
||||||
is_mentioned: false
|
is_mentioned: false
|
||||||
|
|
||||||
-
|
-
|
||||||
|
@ -19,5 +17,4 @@
|
||||||
uid: 4
|
uid: 4
|
||||||
issue_id: 1
|
issue_id: 1
|
||||||
is_read: false
|
is_read: false
|
||||||
is_assigned: false
|
|
||||||
is_mentioned: false
|
is_mentioned: false
|
||||||
|
|
151
models/issue.go
151
models/issue.go
|
@ -37,7 +37,7 @@ type Issue struct {
|
||||||
MilestoneID int64 `xorm:"INDEX"`
|
MilestoneID int64 `xorm:"INDEX"`
|
||||||
Milestone *Milestone `xorm:"-"`
|
Milestone *Milestone `xorm:"-"`
|
||||||
Priority int
|
Priority int
|
||||||
AssigneeID int64 `xorm:"INDEX"`
|
AssigneeID int64 `xorm:"-"`
|
||||||
Assignee *User `xorm:"-"`
|
Assignee *User `xorm:"-"`
|
||||||
IsClosed bool `xorm:"INDEX"`
|
IsClosed bool `xorm:"INDEX"`
|
||||||
IsRead bool `xorm:"-"`
|
IsRead bool `xorm:"-"`
|
||||||
|
@ -56,6 +56,7 @@ type Issue struct {
|
||||||
Comments []*Comment `xorm:"-"`
|
Comments []*Comment `xorm:"-"`
|
||||||
Reactions ReactionList `xorm:"-"`
|
Reactions ReactionList `xorm:"-"`
|
||||||
TotalTrackedTime int64 `xorm:"-"`
|
TotalTrackedTime int64 `xorm:"-"`
|
||||||
|
Assignees []*User `xorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -140,22 +141,6 @@ func (issue *Issue) loadPoster(e Engine) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issue *Issue) loadAssignee(e Engine) (err error) {
|
|
||||||
if issue.Assignee == nil && issue.AssigneeID > 0 {
|
|
||||||
issue.Assignee, err = getUserByID(e, issue.AssigneeID)
|
|
||||||
if err != nil {
|
|
||||||
issue.AssigneeID = -1
|
|
||||||
issue.Assignee = NewGhostUser()
|
|
||||||
if !IsErrUserNotExist(err) {
|
|
||||||
return fmt.Errorf("getUserByID.(assignee) [%d]: %v", issue.AssigneeID, err)
|
|
||||||
}
|
|
||||||
err = nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (issue *Issue) loadPullRequest(e Engine) (err error) {
|
func (issue *Issue) loadPullRequest(e Engine) (err error) {
|
||||||
if issue.IsPull && issue.PullRequest == nil {
|
if issue.IsPull && issue.PullRequest == nil {
|
||||||
issue.PullRequest, err = getPullRequestByIssueID(e, issue.ID)
|
issue.PullRequest, err = getPullRequestByIssueID(e, issue.ID)
|
||||||
|
@ -231,7 +216,7 @@ func (issue *Issue) loadAttributes(e Engine) (err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = issue.loadAssignee(e); err != nil {
|
if err = issue.loadAssignees(e); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,8 +328,11 @@ func (issue *Issue) APIFormat() *api.Issue {
|
||||||
if issue.Milestone != nil {
|
if issue.Milestone != nil {
|
||||||
apiIssue.Milestone = issue.Milestone.APIFormat()
|
apiIssue.Milestone = issue.Milestone.APIFormat()
|
||||||
}
|
}
|
||||||
if issue.Assignee != nil {
|
if len(issue.Assignees) > 0 {
|
||||||
apiIssue.Assignee = issue.Assignee.APIFormat()
|
for _, assignee := range issue.Assignees {
|
||||||
|
apiIssue.Assignees = append(apiIssue.Assignees, assignee.APIFormat())
|
||||||
|
}
|
||||||
|
apiIssue.Assignee = issue.Assignees[0].APIFormat() // For compatibility, we're keeping the first assignee as `apiIssue.Assignee`
|
||||||
}
|
}
|
||||||
if issue.IsPull {
|
if issue.IsPull {
|
||||||
apiIssue.PullRequest = &api.PullRequestMeta{
|
apiIssue.PullRequest = &api.PullRequestMeta{
|
||||||
|
@ -605,19 +593,6 @@ func (issue *Issue) ReplaceLabels(labels []*Label, doer *User) (err error) {
|
||||||
return sess.Commit()
|
return sess.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssignee sets the Assignee attribute of this issue.
|
|
||||||
func (issue *Issue) GetAssignee() (err error) {
|
|
||||||
if issue.AssigneeID == 0 || issue.Assignee != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
issue.Assignee, err = GetUserByID(issue.AssigneeID)
|
|
||||||
if IsErrUserNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBy sets issue to be read by given user.
|
// ReadBy sets issue to be read by given user.
|
||||||
func (issue *Issue) ReadBy(userID int64) error {
|
func (issue *Issue) ReadBy(userID int64) error {
|
||||||
if err := UpdateIssueUserByRead(userID, issue.ID); err != nil {
|
if err := UpdateIssueUserByRead(userID, issue.ID); err != nil {
|
||||||
|
@ -823,55 +798,6 @@ func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangeAssignee changes the Assignee field of this issue.
|
|
||||||
func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
|
|
||||||
var oldAssigneeID = issue.AssigneeID
|
|
||||||
issue.AssigneeID = assigneeID
|
|
||||||
if err = UpdateIssueUserByAssignee(issue); err != nil {
|
|
||||||
return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sess := x.NewSession()
|
|
||||||
defer sess.Close()
|
|
||||||
|
|
||||||
if err = issue.loadRepo(sess); err != nil {
|
|
||||||
return fmt.Errorf("loadRepo: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, oldAssigneeID, assigneeID); err != nil {
|
|
||||||
return fmt.Errorf("createAssigneeComment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
issue.Assignee, err = GetUserByID(issue.AssigneeID)
|
|
||||||
if err != nil && !IsErrUserNotExist(err) {
|
|
||||||
log.Error(4, "GetUserByID [assignee_id: %v]: %v", issue.AssigneeID, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error not nil here means user does not exist, which is remove assignee.
|
|
||||||
isRemoveAssignee := err != nil
|
|
||||||
if issue.IsPull {
|
|
||||||
issue.PullRequest.Issue = issue
|
|
||||||
apiPullRequest := &api.PullRequestPayload{
|
|
||||||
Index: issue.Index,
|
|
||||||
PullRequest: issue.PullRequest.APIFormat(),
|
|
||||||
Repository: issue.Repo.APIFormat(AccessModeNone),
|
|
||||||
Sender: doer.APIFormat(),
|
|
||||||
}
|
|
||||||
if isRemoveAssignee {
|
|
||||||
apiPullRequest.Action = api.HookIssueUnassigned
|
|
||||||
} else {
|
|
||||||
apiPullRequest.Action = api.HookIssueAssigned
|
|
||||||
}
|
|
||||||
if err := PrepareWebhooks(issue.Repo, HookEventPullRequest, apiPullRequest); err != nil {
|
|
||||||
log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, isRemoveAssignee, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go HookQueue.Add(issue.RepoID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTasks returns the amount of tasks in the issues content
|
// GetTasks returns the amount of tasks in the issues content
|
||||||
func (issue *Issue) GetTasks() int {
|
func (issue *Issue) GetTasks() int {
|
||||||
return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
|
return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
|
||||||
|
@ -887,6 +813,7 @@ type NewIssueOptions struct {
|
||||||
Repo *Repository
|
Repo *Repository
|
||||||
Issue *Issue
|
Issue *Issue
|
||||||
LabelIDs []int64
|
LabelIDs []int64
|
||||||
|
AssigneeIDs []int64
|
||||||
Attachments []string // In UUID format.
|
Attachments []string // In UUID format.
|
||||||
IsPull bool
|
IsPull bool
|
||||||
}
|
}
|
||||||
|
@ -909,14 +836,32 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if assigneeID := opts.Issue.AssigneeID; assigneeID > 0 {
|
// Keep the old assignee id thingy for compatibility reasons
|
||||||
|
if opts.Issue.AssigneeID > 0 {
|
||||||
|
isAdded := false
|
||||||
|
// Check if the user has already been passed to issue.AssigneeIDs, if not, add it
|
||||||
|
for _, aID := range opts.AssigneeIDs {
|
||||||
|
if aID == opts.Issue.AssigneeID {
|
||||||
|
isAdded = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAdded {
|
||||||
|
opts.AssigneeIDs = append(opts.AssigneeIDs, opts.Issue.AssigneeID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for and validate assignees
|
||||||
|
if len(opts.AssigneeIDs) > 0 {
|
||||||
|
for _, assigneeID := range opts.AssigneeIDs {
|
||||||
valid, err := hasAccess(e, assigneeID, opts.Repo, AccessModeWrite)
|
valid, err := hasAccess(e, assigneeID, opts.Repo, AccessModeWrite)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
|
return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
|
||||||
}
|
}
|
||||||
if !valid {
|
if !valid {
|
||||||
opts.Issue.AssigneeID = 0
|
return ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: opts.Repo.Name}
|
||||||
opts.Issue.Assignee = nil
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -931,11 +876,10 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Issue.AssigneeID > 0 {
|
// Insert the assignees
|
||||||
if err = opts.Issue.loadRepo(e); err != nil {
|
for _, assigneeID := range opts.AssigneeIDs {
|
||||||
return err
|
err = opts.Issue.changeAssignee(e, doer, assigneeID)
|
||||||
}
|
if err != nil {
|
||||||
if _, err = createAssigneeComment(e, doer, opts.Issue.Repo, opts.Issue, -1, opts.Issue.AssigneeID); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -995,7 +939,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIssue creates new issue with labels for repository.
|
// NewIssue creates new issue with labels for repository.
|
||||||
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
|
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) {
|
||||||
sess := x.NewSession()
|
sess := x.NewSession()
|
||||||
defer sess.Close()
|
defer sess.Close()
|
||||||
if err = sess.Begin(); err != nil {
|
if err = sess.Begin(); err != nil {
|
||||||
|
@ -1007,7 +951,11 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
|
||||||
Issue: issue,
|
Issue: issue,
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
Attachments: uuids,
|
Attachments: uuids,
|
||||||
|
AssigneeIDs: assigneeIDs,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
if IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return fmt.Errorf("newIssue: %v", err)
|
return fmt.Errorf("newIssue: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1150,7 +1098,8 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.AssigneeID > 0 {
|
if opts.AssigneeID > 0 {
|
||||||
sess.And("issue.assignee_id=?", opts.AssigneeID)
|
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||||
|
And("issue_assignees.assignee_id = ?", opts.AssigneeID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.PosterID > 0 {
|
if opts.PosterID > 0 {
|
||||||
|
@ -1372,7 +1321,8 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.AssigneeID > 0 {
|
if opts.AssigneeID > 0 {
|
||||||
sess.And("issue.assignee_id = ?", opts.AssigneeID)
|
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||||
|
And("issue_assignees.assignee_id = ?", opts.AssigneeID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.PosterID > 0 {
|
if opts.PosterID > 0 {
|
||||||
|
@ -1438,13 +1388,15 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
|
||||||
}
|
}
|
||||||
case FilterModeAssign:
|
case FilterModeAssign:
|
||||||
stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
|
stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
|
||||||
And("assignee_id = ?", opts.UserID).
|
Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||||
|
And("issue_assignees.assignee_id = ?", opts.UserID).
|
||||||
Count(new(Issue))
|
Count(new(Issue))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
|
stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
|
||||||
And("assignee_id = ?", opts.UserID).
|
Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||||
|
And("issue_assignees.assignee_id = ?", opts.UserID).
|
||||||
Count(new(Issue))
|
Count(new(Issue))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1466,7 +1418,8 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
|
||||||
|
|
||||||
cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed})
|
cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed})
|
||||||
stats.AssignCount, err = x.Where(cond).
|
stats.AssignCount, err = x.Where(cond).
|
||||||
And("assignee_id = ?", opts.UserID).
|
Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||||
|
And("issue_assignees.assignee_id = ?", opts.UserID).
|
||||||
Count(new(Issue))
|
Count(new(Issue))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1505,8 +1458,10 @@ func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen
|
||||||
|
|
||||||
switch filterMode {
|
switch filterMode {
|
||||||
case FilterModeAssign:
|
case FilterModeAssign:
|
||||||
openCountSession.And("assignee_id = ?", uid)
|
openCountSession.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||||
closedCountSession.And("assignee_id = ?", uid)
|
And("issue_assignees.assignee_id = ?", uid)
|
||||||
|
closedCountSession.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||||
|
And("issue_assignees.assignee_id = ?", uid)
|
||||||
case FilterModeCreate:
|
case FilterModeCreate:
|
||||||
openCountSession.And("poster_id = ?", uid)
|
openCountSession.And("poster_id = ?", uid)
|
||||||
closedCountSession.And("poster_id = ?", uid)
|
closedCountSession.And("poster_id = ?", uid)
|
||||||
|
|
|
@ -0,0 +1,263 @@
|
||||||
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
||||||
|
api "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IssueAssignees saves all issue assignees
|
||||||
|
type IssueAssignees struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
AssigneeID int64 `xorm:"INDEX"`
|
||||||
|
IssueID int64 `xorm:"INDEX"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// This loads all assignees of an issue
|
||||||
|
func (issue *Issue) loadAssignees(e Engine) (err error) {
|
||||||
|
// Reset maybe preexisting assignees
|
||||||
|
issue.Assignees = []*User{}
|
||||||
|
|
||||||
|
err = e.Table("`user`").
|
||||||
|
Join("INNER", "issue_assignees", "assignee_id = `user`.id").
|
||||||
|
Where("issue_assignees.issue_id = ?", issue.ID).
|
||||||
|
Find(&issue.Assignees)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have at least one assignee and if yes put it in as `Assignee`
|
||||||
|
if len(issue.Assignees) > 0 {
|
||||||
|
issue.Assignee = issue.Assignees[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAssigneesByIssue returns everyone assigned to that issue
|
||||||
|
func GetAssigneesByIssue(issue *Issue) (assignees []*User, err error) {
|
||||||
|
err = issue.loadAssignees(x)
|
||||||
|
if err != nil {
|
||||||
|
return assignees, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return issue.Assignees, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUserAssignedToIssue returns true when the user is assigned to the issue
|
||||||
|
func IsUserAssignedToIssue(issue *Issue, user *User) (isAssigned bool, err error) {
|
||||||
|
isAssigned, err = x.Exist(&IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array
|
||||||
|
func DeleteNotPassedAssignee(issue *Issue, doer *User, assignees []*User) (err error) {
|
||||||
|
var found bool
|
||||||
|
|
||||||
|
for _, assignee := range issue.Assignees {
|
||||||
|
|
||||||
|
found = false
|
||||||
|
for _, alreadyAssignee := range assignees {
|
||||||
|
if assignee.ID == alreadyAssignee.ID {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
// This function also does comments and hooks, which is why we call it seperatly instead of directly removing the assignees here
|
||||||
|
if err := UpdateAssignee(issue, doer, assignee.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeAssigneeList concats a string with all names of the assignees. Useful for logs.
|
||||||
|
func MakeAssigneeList(issue *Issue) (assigneeList string, err error) {
|
||||||
|
err = issue.loadAssignees(x)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for in, assignee := range issue.Assignees {
|
||||||
|
assigneeList += assignee.Name
|
||||||
|
|
||||||
|
if len(issue.Assignees) > (in + 1) {
|
||||||
|
assigneeList += ", "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAssigneeByUserID deletes all assignments of an user
|
||||||
|
func clearAssigneeByUserID(sess *xorm.Session, userID int64) (err error) {
|
||||||
|
_, err = sess.Delete(&IssueAssignees{AssigneeID: userID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAssigneeIfNotAssigned adds an assignee only if he isn't aleady assigned to the issue
|
||||||
|
func AddAssigneeIfNotAssigned(issue *Issue, doer *User, assigneeID int64) (err error) {
|
||||||
|
// Check if the user is already assigned
|
||||||
|
isAssigned, err := IsUserAssignedToIssue(issue, &User{ID: assigneeID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAssigned {
|
||||||
|
return issue.ChangeAssignee(doer, assigneeID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAssignee deletes or adds an assignee to an issue
|
||||||
|
func UpdateAssignee(issue *Issue, doer *User, assigneeID int64) (err error) {
|
||||||
|
return issue.ChangeAssignee(doer, assigneeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeAssignee changes the Assignee of this issue.
|
||||||
|
func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
|
||||||
|
sess := x.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
|
||||||
|
if err := sess.Begin(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issue.changeAssignee(sess, doer, assigneeID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sess.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID int64) (err error) {
|
||||||
|
|
||||||
|
// Update the assignee
|
||||||
|
removed, err := updateIssueAssignee(sess, issue, assigneeID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repo infos
|
||||||
|
if err = issue.loadRepo(sess); err != nil {
|
||||||
|
return fmt.Errorf("loadRepo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment
|
||||||
|
if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed); err != nil {
|
||||||
|
return fmt.Errorf("createAssigneeComment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.IsPull {
|
||||||
|
issue.PullRequest = &PullRequest{Issue: issue}
|
||||||
|
apiPullRequest := &api.PullRequestPayload{
|
||||||
|
Index: issue.Index,
|
||||||
|
PullRequest: issue.PullRequest.APIFormat(),
|
||||||
|
Repository: issue.Repo.APIFormat(AccessModeNone),
|
||||||
|
Sender: doer.APIFormat(),
|
||||||
|
}
|
||||||
|
if removed {
|
||||||
|
apiPullRequest.Action = api.HookIssueUnassigned
|
||||||
|
} else {
|
||||||
|
apiPullRequest.Action = api.HookIssueAssigned
|
||||||
|
}
|
||||||
|
if err := PrepareWebhooks(issue.Repo, HookEventPullRequest, apiPullRequest); err != nil {
|
||||||
|
log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
go HookQueue.Add(issue.RepoID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAPIAssignee is a helper function to add or delete one or multiple issue assignee(s)
|
||||||
|
// Deleting is done the Github way (quote from their api documentation):
|
||||||
|
// https://developer.github.com/v3/issues/#edit-an-issue
|
||||||
|
// "assignees" (array): Logins for Users to assign to this issue.
|
||||||
|
// Pass one or more user logins to replace the set of assignees on this Issue.
|
||||||
|
// Send an empty array ([]) to clear all assignees from the Issue.
|
||||||
|
func UpdateAPIAssignee(issue *Issue, oneAssignee string, multipleAssignees []string, doer *User) (err error) {
|
||||||
|
var allNewAssignees []*User
|
||||||
|
|
||||||
|
// Keep the old assignee thingy for compatibility reasons
|
||||||
|
if oneAssignee != "" {
|
||||||
|
// Prevent double adding assignees
|
||||||
|
var isDouble bool
|
||||||
|
for _, assignee := range multipleAssignees {
|
||||||
|
if assignee == oneAssignee {
|
||||||
|
isDouble = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isDouble {
|
||||||
|
multipleAssignees = append(multipleAssignees, oneAssignee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through all assignees to add them
|
||||||
|
for _, assigneeName := range multipleAssignees {
|
||||||
|
assignee, err := GetUserByName(assigneeName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewAssignees = append(allNewAssignees, assignee)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all old assignees not passed
|
||||||
|
if err = DeleteNotPassedAssignee(issue, doer, allNewAssignees); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
err = AddAssigneeIfNotAssigned(issue, doer, assignee.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs
|
||||||
|
func MakeIDsFromAPIAssigneesToAdd(oneAssignee string, multipleAssignees []string) (assigneeIDs []int64, err error) {
|
||||||
|
|
||||||
|
// Keeping the old assigning method for compatibility reasons
|
||||||
|
if oneAssignee != "" {
|
||||||
|
|
||||||
|
// Prevent double adding assignees
|
||||||
|
var isDouble bool
|
||||||
|
for _, assignee := range multipleAssignees {
|
||||||
|
if assignee == oneAssignee {
|
||||||
|
isDouble = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isDouble {
|
||||||
|
multipleAssignees = append(multipleAssignees, oneAssignee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the IDs of all assignees
|
||||||
|
assigneeIDs = GetUserIDsByNames(multipleAssignees)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdateAssignee(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
|
||||||
|
// Fake issue with assignees
|
||||||
|
issue, err := GetIssueByID(1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Assign multiple users
|
||||||
|
user2, err := GetUserByID(2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = UpdateAssignee(issue, &User{ID: 1}, user2.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
user3, err := GetUserByID(3)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = UpdateAssignee(issue, &User{ID: 1}, user3.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
user1, err := GetUserByID(1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = UpdateAssignee(issue, &User{ID: 1}, user1.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check if he got removed
|
||||||
|
isAssigned, err := IsUserAssignedToIssue(issue, user1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, isAssigned)
|
||||||
|
|
||||||
|
// Check if they're all there
|
||||||
|
assignees, err := GetAssigneesByIssue(issue)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var expectedAssignees []*User
|
||||||
|
expectedAssignees = append(expectedAssignees, user2)
|
||||||
|
expectedAssignees = append(expectedAssignees, user3)
|
||||||
|
|
||||||
|
for in, assignee := range assignees {
|
||||||
|
assert.Equal(t, assignee.ID, expectedAssignees[in].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user is assigned
|
||||||
|
isAssigned, err = IsUserAssignedToIssue(issue, user2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, isAssigned)
|
||||||
|
|
||||||
|
// This user should not be assigned
|
||||||
|
isAssigned, err = IsUserAssignedToIssue(issue, &User{ID: 4})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, isAssigned)
|
||||||
|
|
||||||
|
// Clean everyone
|
||||||
|
err = DeleteNotPassedAssignee(issue, user1, []*User{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check they're gone
|
||||||
|
assignees, err = GetAssigneesByIssue(issue)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, len(assignees))
|
||||||
|
}
|
|
@ -92,10 +92,9 @@ type Comment struct {
|
||||||
MilestoneID int64
|
MilestoneID int64
|
||||||
OldMilestone *Milestone `xorm:"-"`
|
OldMilestone *Milestone `xorm:"-"`
|
||||||
Milestone *Milestone `xorm:"-"`
|
Milestone *Milestone `xorm:"-"`
|
||||||
OldAssigneeID int64
|
|
||||||
AssigneeID int64
|
AssigneeID int64
|
||||||
|
RemovedAssignee bool
|
||||||
Assignee *User `xorm:"-"`
|
Assignee *User `xorm:"-"`
|
||||||
OldAssignee *User `xorm:"-"`
|
|
||||||
OldTitle string
|
OldTitle string
|
||||||
NewTitle string
|
NewTitle string
|
||||||
|
|
||||||
|
@ -247,18 +246,9 @@ func (c *Comment) LoadMilestone() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAssignees if comment.Type is CommentTypeAssignees, then load assignees
|
// LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees
|
||||||
func (c *Comment) LoadAssignees() error {
|
func (c *Comment) LoadAssigneeUser() error {
|
||||||
var err error
|
var err error
|
||||||
if c.OldAssigneeID > 0 {
|
|
||||||
c.OldAssignee, err = getUserByID(x, c.OldAssigneeID)
|
|
||||||
if err != nil {
|
|
||||||
if !IsErrUserNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.OldAssignee = NewGhostUser()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.AssigneeID > 0 {
|
if c.AssigneeID > 0 {
|
||||||
c.Assignee, err = getUserByID(x, c.AssigneeID)
|
c.Assignee, err = getUserByID(x, c.AssigneeID)
|
||||||
|
@ -331,7 +321,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||||
LabelID: LabelID,
|
LabelID: LabelID,
|
||||||
OldMilestoneID: opts.OldMilestoneID,
|
OldMilestoneID: opts.OldMilestoneID,
|
||||||
MilestoneID: opts.MilestoneID,
|
MilestoneID: opts.MilestoneID,
|
||||||
OldAssigneeID: opts.OldAssigneeID,
|
RemovedAssignee: opts.RemovedAssignee,
|
||||||
AssigneeID: opts.AssigneeID,
|
AssigneeID: opts.AssigneeID,
|
||||||
CommitID: opts.CommitID,
|
CommitID: opts.CommitID,
|
||||||
CommitSHA: opts.CommitSHA,
|
CommitSHA: opts.CommitSHA,
|
||||||
|
@ -480,13 +470,13 @@ func createMilestoneComment(e *xorm.Session, doer *User, repo *Repository, issue
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func createAssigneeComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldAssigneeID, assigneeID int64) (*Comment, error) {
|
func createAssigneeComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, assigneeID int64, removedAssignee bool) (*Comment, error) {
|
||||||
return createComment(e, &CreateCommentOptions{
|
return createComment(e, &CreateCommentOptions{
|
||||||
Type: CommentTypeAssignees,
|
Type: CommentTypeAssignees,
|
||||||
Doer: doer,
|
Doer: doer,
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
Issue: issue,
|
Issue: issue,
|
||||||
OldAssigneeID: oldAssigneeID,
|
RemovedAssignee: removedAssignee,
|
||||||
AssigneeID: assigneeID,
|
AssigneeID: assigneeID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -550,8 +540,8 @@ type CreateCommentOptions struct {
|
||||||
|
|
||||||
OldMilestoneID int64
|
OldMilestoneID int64
|
||||||
MilestoneID int64
|
MilestoneID int64
|
||||||
OldAssigneeID int64
|
|
||||||
AssigneeID int64
|
AssigneeID int64
|
||||||
|
RemovedAssignee bool
|
||||||
OldTitle string
|
OldTitle string
|
||||||
NewTitle string
|
NewTitle string
|
||||||
CommitID int64
|
CommitID int64
|
||||||
|
|
|
@ -154,38 +154,38 @@ func (issues IssueList) loadMilestones(e Engine) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issues IssueList) getAssigneeIDs() []int64 {
|
|
||||||
var ids = make(map[int64]struct{}, len(issues))
|
|
||||||
for _, issue := range issues {
|
|
||||||
if _, ok := ids[issue.AssigneeID]; !ok {
|
|
||||||
ids[issue.AssigneeID] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return keysInt64(ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (issues IssueList) loadAssignees(e Engine) error {
|
func (issues IssueList) loadAssignees(e Engine) error {
|
||||||
assigneeIDs := issues.getAssigneeIDs()
|
if len(issues) == 0 {
|
||||||
if len(assigneeIDs) == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
assigneeMaps := make(map[int64]*User, len(assigneeIDs))
|
type AssigneeIssue struct {
|
||||||
err := e.
|
IssueAssignee *IssueAssignees `xorm:"extends"`
|
||||||
In("id", assigneeIDs).
|
Assignee *User `xorm:"extends"`
|
||||||
Find(&assigneeMaps)
|
}
|
||||||
|
|
||||||
|
var assignees = make(map[int64][]*User, len(issues))
|
||||||
|
rows, err := e.Table("issue_assignees").
|
||||||
|
Join("INNER", "user", "`user`.id = `issue_assignees`.assignee_id").
|
||||||
|
In("`issue_assignees`.issue_id", issues.getIssueIDs()).
|
||||||
|
Rows(new(AssigneeIssue))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var assigneeIssue AssigneeIssue
|
||||||
|
err = rows.Scan(&assigneeIssue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee)
|
||||||
|
}
|
||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
if issue.AssigneeID <= 0 {
|
issue.Assignees = assignees[issue.ID]
|
||||||
continue
|
|
||||||
}
|
|
||||||
var ok bool
|
|
||||||
if issue.Assignee, ok = assigneeMaps[issue.AssigneeID]; !ok {
|
|
||||||
issue.Assignee = NewGhostUser()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,9 +46,16 @@ func mailIssueCommentToParticipants(e Engine, issue *Issue, doer *User, content
|
||||||
participants = append(participants, issue.Poster)
|
participants = append(participants, issue.Poster)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assignee must receive any communications
|
// Assignees must receive any communications
|
||||||
if issue.Assignee != nil && issue.AssigneeID > 0 && issue.AssigneeID != doer.ID {
|
assignees, err := GetAssigneesByIssue(issue)
|
||||||
participants = append(participants, issue.Assignee)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, assignee := range assignees {
|
||||||
|
if assignee.ID != doer.ID {
|
||||||
|
participants = append(participants, assignee)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tos := make([]string, 0, len(watchers)) // List of email addresses.
|
tos := make([]string, 0, len(watchers)) // List of email addresses.
|
||||||
|
|
|
@ -6,6 +6,8 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IssueUser represents an issue-user relation.
|
// IssueUser represents an issue-user relation.
|
||||||
|
@ -14,7 +16,6 @@ type IssueUser struct {
|
||||||
UID int64 `xorm:"INDEX"` // User ID.
|
UID int64 `xorm:"INDEX"` // User ID.
|
||||||
IssueID int64
|
IssueID int64
|
||||||
IsRead bool
|
IsRead bool
|
||||||
IsAssigned bool
|
|
||||||
IsMentioned bool
|
IsMentioned bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +35,6 @@ func newIssueUsers(e Engine, repo *Repository, issue *Issue) error {
|
||||||
issueUsers = append(issueUsers, &IssueUser{
|
issueUsers = append(issueUsers, &IssueUser{
|
||||||
IssueID: issue.ID,
|
IssueID: issue.ID,
|
||||||
UID: assignee.ID,
|
UID: assignee.ID,
|
||||||
IsAssigned: assignee.ID == issue.AssigneeID,
|
|
||||||
})
|
})
|
||||||
isPosterAssignee = isPosterAssignee || assignee.ID == issue.PosterID
|
isPosterAssignee = isPosterAssignee || assignee.ID == issue.PosterID
|
||||||
}
|
}
|
||||||
|
@ -51,34 +51,38 @@ func newIssueUsers(e Engine, repo *Repository, issue *Issue) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateIssueUserByAssignee(e Engine, issue *Issue) (err error) {
|
func updateIssueAssignee(e *xorm.Session, issue *Issue, assigneeID int64) (removed bool, err error) {
|
||||||
if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?", false, issue.ID); err != nil {
|
|
||||||
return err
|
// Check if the user exists
|
||||||
|
_, err = GetUserByID(assigneeID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assignee ID equals to 0 means clear assignee.
|
// Check if the submitted user is already assigne, if yes delete him otherwise add him
|
||||||
if issue.AssigneeID > 0 {
|
var toBeDeleted bool
|
||||||
if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE uid = ? AND issue_id = ?", true, issue.AssigneeID, issue.ID); err != nil {
|
for _, assignee := range issue.Assignees {
|
||||||
return err
|
if assignee.ID == assigneeID {
|
||||||
|
toBeDeleted = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateIssue(e, issue)
|
assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID}
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateIssueUserByAssignee updates issue-user relation for assignee.
|
if toBeDeleted {
|
||||||
func UpdateIssueUserByAssignee(issue *Issue) (err error) {
|
_, err = e.Delete(assigneeIn)
|
||||||
sess := x.NewSession()
|
if err != nil {
|
||||||
defer sess.Close()
|
return toBeDeleted, err
|
||||||
if err = sess.Begin(); err != nil {
|
}
|
||||||
return err
|
} else {
|
||||||
|
_, err = e.Insert(assigneeIn)
|
||||||
|
if err != nil {
|
||||||
|
return toBeDeleted, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = updateIssueUserByAssignee(sess, issue); err != nil {
|
return toBeDeleted, nil
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return sess.Commit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateIssueUserByRead updates issue-user relation for reading.
|
// UpdateIssueUserByRead updates issue-user relation for reading.
|
||||||
|
|
|
@ -32,23 +32,6 @@ func Test_newIssueUsers(t *testing.T) {
|
||||||
AssertExistsAndLoadBean(t, &IssueUser{IssueID: newIssue.ID, UID: repo.OwnerID})
|
AssertExistsAndLoadBean(t, &IssueUser{IssueID: newIssue.ID, UID: repo.OwnerID})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateIssueUserByAssignee(t *testing.T) {
|
|
||||||
assert.NoError(t, PrepareTestDatabase())
|
|
||||||
issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
|
|
||||||
|
|
||||||
// artificially change assignee in issue_user table
|
|
||||||
AssertSuccessfulInsert(t, &IssueUser{IssueID: issue.ID, UID: 5, IsAssigned: true})
|
|
||||||
_, err := x.Cols("is_assigned").
|
|
||||||
Update(&IssueUser{IsAssigned: false}, &IssueUser{IssueID: issue.ID, UID: issue.AssigneeID})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.NoError(t, UpdateIssueUserByAssignee(issue))
|
|
||||||
|
|
||||||
// issue_user table should now be correct again
|
|
||||||
AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: issue.AssigneeID}, "is_assigned=1")
|
|
||||||
AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: 5}, "is_assigned=0")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateIssueUserByRead(t *testing.T) {
|
func TestUpdateIssueUserByRead(t *testing.T) {
|
||||||
assert.NoError(t, PrepareTestDatabase())
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
|
issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
|
||||||
|
|
|
@ -180,6 +180,8 @@ var migrations = []Migration{
|
||||||
NewMigration("add last used passcode column for TOTP", addLastUsedPasscodeTOTP),
|
NewMigration("add last used passcode column for TOTP", addLastUsedPasscodeTOTP),
|
||||||
// v63 -> v64
|
// v63 -> v64
|
||||||
NewMigration("add language column for user setting", addLanguageSetting),
|
NewMigration("add language column for user setting", addLanguageSetting),
|
||||||
|
// v64 -> v65
|
||||||
|
NewMigration("add multiple assignees", addMultipleAssignees),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate database to current version
|
// Migrate database to current version
|
||||||
|
@ -229,7 +231,7 @@ Please try to upgrade to a lower version (>= v0.6.0) first, then upgrade to curr
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dropTableColumns(x *xorm.Engine, tableName string, columnNames ...string) (err error) {
|
func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...string) (err error) {
|
||||||
if tableName == "" || len(columnNames) == 0 {
|
if tableName == "" || len(columnNames) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -245,17 +247,10 @@ func dropTableColumns(x *xorm.Engine, tableName string, columnNames ...string) (
|
||||||
}
|
}
|
||||||
cols += "DROP COLUMN `" + col + "`"
|
cols += "DROP COLUMN `" + col + "`"
|
||||||
}
|
}
|
||||||
if _, err := x.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil {
|
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil {
|
||||||
return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err)
|
return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err)
|
||||||
}
|
}
|
||||||
case setting.UseMSSQL:
|
case setting.UseMSSQL:
|
||||||
sess := x.NewSession()
|
|
||||||
defer sess.Close()
|
|
||||||
|
|
||||||
if err = sess.Begin(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cols := ""
|
cols := ""
|
||||||
for _, col := range columnNames {
|
for _, col := range columnNames {
|
||||||
if cols != "" {
|
if cols != "" {
|
||||||
|
|
|
@ -9,5 +9,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func removeIsOwnerColumnFromOrgUser(x *xorm.Engine) (err error) {
|
func removeIsOwnerColumnFromOrgUser(x *xorm.Engine) (err error) {
|
||||||
return dropTableColumns(x, "org_user", "is_owner", "num_teams")
|
sess := x.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
|
||||||
|
if err = sess.Begin(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dropTableColumns(sess, "org_user", "is_owner", "num_teams"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sess.Commit()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addMultipleAssignees(x *xorm.Engine) error {
|
||||||
|
|
||||||
|
// Redeclare issue struct
|
||||||
|
type Issue struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
|
||||||
|
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
||||||
|
PosterID int64 `xorm:"INDEX"`
|
||||||
|
Title string `xorm:"name"`
|
||||||
|
Content string `xorm:"TEXT"`
|
||||||
|
MilestoneID int64 `xorm:"INDEX"`
|
||||||
|
Priority int
|
||||||
|
AssigneeID int64 `xorm:"INDEX"`
|
||||||
|
IsClosed bool `xorm:"INDEX"`
|
||||||
|
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
||||||
|
NumComments int
|
||||||
|
Ref string
|
||||||
|
|
||||||
|
DeadlineUnix util.TimeStamp `xorm:"INDEX"`
|
||||||
|
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
||||||
|
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
||||||
|
ClosedUnix util.TimeStamp `xorm:"INDEX"`
|
||||||
|
}
|
||||||
|
|
||||||
|
allIssues := []Issue{}
|
||||||
|
err := x.Find(&allIssues)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the table
|
||||||
|
type IssueAssignees struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
AssigneeID int64 `xorm:"INDEX"`
|
||||||
|
IssueID int64 `xorm:"INDEX"`
|
||||||
|
}
|
||||||
|
err = x.Sync2(IssueAssignees{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range over all issues and insert a new entry for each issue/assignee
|
||||||
|
sess := x.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
|
||||||
|
err = sess.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, issue := range allIssues {
|
||||||
|
if issue.AssigneeID != 0 {
|
||||||
|
_, err := sess.Insert(IssueAssignees{IssueID: issue.ID, AssigneeID: issue.AssigneeID})
|
||||||
|
if err != nil {
|
||||||
|
sess.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updated the comment table
|
||||||
|
type Comment struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
Type int
|
||||||
|
PosterID int64 `xorm:"INDEX"`
|
||||||
|
IssueID int64 `xorm:"INDEX"`
|
||||||
|
LabelID int64
|
||||||
|
OldMilestoneID int64
|
||||||
|
MilestoneID int64
|
||||||
|
OldAssigneeID int64
|
||||||
|
AssigneeID int64
|
||||||
|
RemovedAssignee bool
|
||||||
|
OldTitle string
|
||||||
|
NewTitle string
|
||||||
|
|
||||||
|
CommitID int64
|
||||||
|
Line int64
|
||||||
|
Content string `xorm:"TEXT"`
|
||||||
|
RenderedContent string `xorm:"-"`
|
||||||
|
|
||||||
|
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
||||||
|
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
||||||
|
|
||||||
|
// Reference issue in commit message
|
||||||
|
CommitSHA string `xorm:"VARCHAR(40)"`
|
||||||
|
}
|
||||||
|
if err := x.Sync2(Comment{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate comments
|
||||||
|
// First update everything to not have nulls in db
|
||||||
|
if _, err := sess.Where("type = ?", 9).Cols("removed_assignee").Update(Comment{RemovedAssignee: false}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
allAssignementComments := []Comment{}
|
||||||
|
if err := sess.Where("type = ?", 9).Find(&allAssignementComments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, comment := range allAssignementComments {
|
||||||
|
// Everytime where OldAssigneeID is > 0, the assignement was removed.
|
||||||
|
if comment.OldAssigneeID > 0 {
|
||||||
|
_, err = sess.ID(comment.ID).Update(Comment{RemovedAssignee: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dropTableColumns(sess, "issue", "assignee_id"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dropTableColumns(sess, "issue_user", "is_assigned"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sess.Commit()
|
||||||
|
}
|
|
@ -119,6 +119,7 @@ func init() {
|
||||||
new(RepoIndexerStatus),
|
new(RepoIndexerStatus),
|
||||||
new(LFSLock),
|
new(LFSLock),
|
||||||
new(Reaction),
|
new(Reaction),
|
||||||
|
new(IssueAssignees),
|
||||||
)
|
)
|
||||||
|
|
||||||
gonicNames := []string{"SSL", "UID"}
|
gonicNames := []string{"SSL", "UID"}
|
||||||
|
|
|
@ -198,6 +198,7 @@ func (pr *PullRequest) APIFormat() *api.PullRequest {
|
||||||
Labels: apiIssue.Labels,
|
Labels: apiIssue.Labels,
|
||||||
Milestone: apiIssue.Milestone,
|
Milestone: apiIssue.Milestone,
|
||||||
Assignee: apiIssue.Assignee,
|
Assignee: apiIssue.Assignee,
|
||||||
|
Assignees: apiIssue.Assignees,
|
||||||
State: apiIssue.State,
|
State: apiIssue.State,
|
||||||
Comments: apiIssue.Comments,
|
Comments: apiIssue.Comments,
|
||||||
HTMLURL: pr.Issue.HTMLURL(),
|
HTMLURL: pr.Issue.HTMLURL(),
|
||||||
|
@ -719,7 +720,7 @@ func (pr *PullRequest) testPatch() (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPullRequest creates new pull request with labels for repository.
|
// NewPullRequest creates new pull request with labels for repository.
|
||||||
func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte) (err error) {
|
func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte, assigneeIDs []int64) (err error) {
|
||||||
sess := x.NewSession()
|
sess := x.NewSession()
|
||||||
defer sess.Close()
|
defer sess.Close()
|
||||||
if err = sess.Begin(); err != nil {
|
if err = sess.Begin(); err != nil {
|
||||||
|
@ -732,7 +733,11 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
Attachments: uuids,
|
Attachments: uuids,
|
||||||
IsPull: true,
|
IsPull: true,
|
||||||
|
AssigneeIDs: assigneeIDs,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
if IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return fmt.Errorf("newIssue: %v", err)
|
return fmt.Errorf("newIssue: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -600,9 +600,9 @@ func (repo *Repository) GetAssignees() (_ []*User, err error) {
|
||||||
return repo.getAssignees(x)
|
return repo.getAssignees(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssigneeByID returns the user that has write access of repository by given ID.
|
// GetUserIfHasWriteAccess returns the user that has write access of repository by given ID.
|
||||||
func (repo *Repository) GetAssigneeByID(userID int64) (*User, error) {
|
func (repo *Repository) GetUserIfHasWriteAccess(userID int64) (*User, error) {
|
||||||
return GetAssigneeByID(repo, userID)
|
return GetUserIfHasWriteAccess(repo, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMilestoneByID returns the milestone belongs to repository by given ID.
|
// GetMilestoneByID returns the milestone belongs to repository by given ID.
|
||||||
|
|
|
@ -993,7 +993,7 @@ func deleteUser(e *xorm.Session, u *User) error {
|
||||||
// ***** END: PublicKey *****
|
// ***** END: PublicKey *****
|
||||||
|
|
||||||
// Clear assignee.
|
// Clear assignee.
|
||||||
if _, err = e.Exec("UPDATE `issue` SET assignee_id=0 WHERE assignee_id=?", u.ID); err != nil {
|
if err = clearAssigneeByUserID(e, u.ID); err != nil {
|
||||||
return fmt.Errorf("clear assignee: %v", err)
|
return fmt.Errorf("clear assignee: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1110,8 +1110,8 @@ func GetUserByID(id int64) (*User, error) {
|
||||||
return getUserByID(x, id)
|
return getUserByID(x, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssigneeByID returns the user with write access of repository by given ID.
|
// GetUserIfHasWriteAccess returns the user with write access of repository by given ID.
|
||||||
func GetAssigneeByID(repo *Repository, userID int64) (*User, error) {
|
func GetUserIfHasWriteAccess(repo *Repository, userID int64) (*User, error) {
|
||||||
has, err := HasAccess(userID, repo, AccessModeWrite)
|
has, err := HasAccess(userID, repo, AccessModeWrite)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -118,8 +118,12 @@ func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload,
|
||||||
title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
|
title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
|
||||||
text = p.PullRequest.Body
|
text = p.PullRequest.Body
|
||||||
case api.HookIssueAssigned:
|
case api.HookIssueAssigned:
|
||||||
|
list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID})
|
||||||
|
if err != nil {
|
||||||
|
return &DingtalkPayload{}, err
|
||||||
|
}
|
||||||
title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName,
|
title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName,
|
||||||
p.PullRequest.Assignee.UserName, p.Index, p.PullRequest.Title)
|
list, p.Index, p.PullRequest.Title)
|
||||||
text = p.PullRequest.Body
|
text = p.PullRequest.Body
|
||||||
case api.HookIssueUnassigned:
|
case api.HookIssueUnassigned:
|
||||||
title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
|
title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
|
||||||
|
|
|
@ -191,8 +191,12 @@ func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta)
|
||||||
text = p.PullRequest.Body
|
text = p.PullRequest.Body
|
||||||
color = warnColor
|
color = warnColor
|
||||||
case api.HookIssueAssigned:
|
case api.HookIssueAssigned:
|
||||||
|
list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID})
|
||||||
|
if err != nil {
|
||||||
|
return &DiscordPayload{}, err
|
||||||
|
}
|
||||||
title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName,
|
title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName,
|
||||||
p.PullRequest.Assignee.UserName, p.Index, p.PullRequest.Title)
|
list, p.Index, p.PullRequest.Title)
|
||||||
text = p.PullRequest.Body
|
text = p.PullRequest.Body
|
||||||
color = successColor
|
color = successColor
|
||||||
case api.HookIssueUnassigned:
|
case api.HookIssueUnassigned:
|
||||||
|
|
|
@ -172,8 +172,12 @@ func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*S
|
||||||
text = fmt.Sprintf("[%s] Pull request edited: %s by %s", p.Repository.FullName, titleLink, senderLink)
|
text = fmt.Sprintf("[%s] Pull request edited: %s by %s", p.Repository.FullName, titleLink, senderLink)
|
||||||
attachmentText = SlackTextFormatter(p.PullRequest.Body)
|
attachmentText = SlackTextFormatter(p.PullRequest.Body)
|
||||||
case api.HookIssueAssigned:
|
case api.HookIssueAssigned:
|
||||||
|
list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID})
|
||||||
|
if err != nil {
|
||||||
|
return &SlackPayload{}, err
|
||||||
|
}
|
||||||
text = fmt.Sprintf("[%s] Pull request assigned to %s: %s by %s", p.Repository.FullName,
|
text = fmt.Sprintf("[%s] Pull request assigned to %s: %s by %s", p.Repository.FullName,
|
||||||
SlackLinkFormatter(setting.AppURL+p.PullRequest.Assignee.UserName, p.PullRequest.Assignee.UserName),
|
SlackLinkFormatter(setting.AppURL+list, list),
|
||||||
titleLink, senderLink)
|
titleLink, senderLink)
|
||||||
case api.HookIssueUnassigned:
|
case api.HookIssueUnassigned:
|
||||||
text = fmt.Sprintf("[%s] Pull request unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink)
|
text = fmt.Sprintf("[%s] Pull request unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink)
|
||||||
|
|
|
@ -254,6 +254,7 @@ func (f *NewDingtalkHookForm) Validate(ctx *macaron.Context, errs binding.Errors
|
||||||
type CreateIssueForm struct {
|
type CreateIssueForm struct {
|
||||||
Title string `binding:"Required;MaxSize(255)"`
|
Title string `binding:"Required;MaxSize(255)"`
|
||||||
LabelIDs string `form:"label_ids"`
|
LabelIDs string `form:"label_ids"`
|
||||||
|
AssigneeIDs string `form:"assignee_ids"`
|
||||||
Ref string `form:"ref"`
|
Ref string `form:"ref"`
|
||||||
MilestoneID int64
|
MilestoneID int64
|
||||||
AssigneeID int64
|
AssigneeID int64
|
||||||
|
|
|
@ -99,8 +99,9 @@ func (r *Repository) CanUseTimetracker(issue *models.Issue, user *models.User) b
|
||||||
// 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, _ := models.IsUserAssignedToIssue(issue, user)
|
||||||
return r.Repository.IsTimetrackerEnabled() && (!r.Repository.AllowOnlyContributorsToTrackTime() ||
|
return r.Repository.IsTimetrackerEnabled() && (!r.Repository.AllowOnlyContributorsToTrackTime() ||
|
||||||
r.IsWriter() || issue.IsPoster(user.ID) || issue.AssigneeID == user.ID)
|
r.IsWriter() || issue.IsPoster(user.ID) || isAssigned)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCommitsCount returns cached commit count for current view
|
// GetCommitsCount returns cached commit count for current view
|
||||||
|
|
|
@ -624,9 +624,9 @@ issues.new.no_milestone = No Milestone
|
||||||
issues.new.clear_milestone = Clear milestone
|
issues.new.clear_milestone = Clear milestone
|
||||||
issues.new.open_milestone = Open Milestones
|
issues.new.open_milestone = Open Milestones
|
||||||
issues.new.closed_milestone = Closed Milestones
|
issues.new.closed_milestone = Closed Milestones
|
||||||
issues.new.assignee = Assignee
|
issues.new.assignees = Assignees
|
||||||
issues.new.clear_assignee = Clear assignee
|
issues.new.clear_assignees = Clear assignees
|
||||||
issues.new.no_assignee = No assignee
|
issues.new.no_assignees = Nobody assigned
|
||||||
issues.no_ref = No Branch/Tag Specified
|
issues.no_ref = No Branch/Tag Specified
|
||||||
issues.create = Create Issue
|
issues.create = Create Issue
|
||||||
issues.new_label = New Label
|
issues.new_label = New Label
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -179,27 +179,53 @@ function initCommentForm() {
|
||||||
initBranchSelector();
|
initBranchSelector();
|
||||||
initCommentPreviewTab($('.comment.form'));
|
initCommentPreviewTab($('.comment.form'));
|
||||||
|
|
||||||
// Labels
|
// Listsubmit
|
||||||
var $list = $('.ui.labels.list');
|
function initListSubmits(selector, outerSelector) {
|
||||||
|
var $list = $('.ui.' + outerSelector + '.list');
|
||||||
var $noSelect = $list.find('.no-select');
|
var $noSelect = $list.find('.no-select');
|
||||||
var $labelMenu = $('.select-label .menu');
|
var $listMenu = $('.' + selector + ' .menu');
|
||||||
var hasLabelUpdateAction = $labelMenu.data('action') == 'update';
|
var hasLabelUpdateAction = $listMenu.data('action') == 'update';
|
||||||
|
|
||||||
$('.select-label').dropdown('setting', 'onHide', function(){
|
$('.' + selector).dropdown('setting', 'onHide', function(){
|
||||||
|
hasLabelUpdateAction = $listMenu.data('action') == 'update'; // Update the var
|
||||||
if (hasLabelUpdateAction) {
|
if (hasLabelUpdateAction) {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$labelMenu.find('.item:not(.no-select)').click(function () {
|
$listMenu.find('.item:not(.no-select)').click(function () {
|
||||||
|
|
||||||
|
// we don't need the action attribute when updating assignees
|
||||||
|
if (selector == 'select-assignees-modify') {
|
||||||
|
|
||||||
|
// UI magic. We need to do this here, otherwise it would destroy the functionality of
|
||||||
|
// adding/removing labels
|
||||||
|
if ($(this).hasClass('checked')) {
|
||||||
|
$(this).removeClass('checked');
|
||||||
|
$(this).find('.octicon').removeClass('octicon-check');
|
||||||
|
} else {
|
||||||
|
$(this).addClass('checked');
|
||||||
|
$(this).find('.octicon').addClass('octicon-check');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateIssuesMeta(
|
||||||
|
$listMenu.data('update-url'),
|
||||||
|
"",
|
||||||
|
$listMenu.data('issue-id'),
|
||||||
|
$(this).data('id')
|
||||||
|
);
|
||||||
|
$listMenu.data('action', 'update'); // Update to reload the page when we updated items
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if ($(this).hasClass('checked')) {
|
if ($(this).hasClass('checked')) {
|
||||||
$(this).removeClass('checked');
|
$(this).removeClass('checked');
|
||||||
$(this).find('.octicon').removeClass('octicon-check');
|
$(this).find('.octicon').removeClass('octicon-check');
|
||||||
if (hasLabelUpdateAction) {
|
if (hasLabelUpdateAction) {
|
||||||
updateIssuesMeta(
|
updateIssuesMeta(
|
||||||
$labelMenu.data('update-url'),
|
$listMenu.data('update-url'),
|
||||||
"detach",
|
"detach",
|
||||||
$labelMenu.data('issue-id'),
|
$listMenu.data('issue-id'),
|
||||||
$(this).data('id')
|
$(this).data('id')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -208,39 +234,40 @@ function initCommentForm() {
|
||||||
$(this).find('.octicon').addClass('octicon-check');
|
$(this).find('.octicon').addClass('octicon-check');
|
||||||
if (hasLabelUpdateAction) {
|
if (hasLabelUpdateAction) {
|
||||||
updateIssuesMeta(
|
updateIssuesMeta(
|
||||||
$labelMenu.data('update-url'),
|
$listMenu.data('update-url'),
|
||||||
"attach",
|
"attach",
|
||||||
$labelMenu.data('issue-id'),
|
$listMenu.data('issue-id'),
|
||||||
$(this).data('id')
|
$(this).data('id')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelIds = [];
|
var listIds = [];
|
||||||
$(this).parent().find('.item').each(function () {
|
$(this).parent().find('.item').each(function () {
|
||||||
if ($(this).hasClass('checked')) {
|
if ($(this).hasClass('checked')) {
|
||||||
labelIds.push($(this).data('id'));
|
listIds.push($(this).data('id'));
|
||||||
$($(this).data('id-selector')).removeClass('hide');
|
$($(this).data('id-selector')).removeClass('hide');
|
||||||
} else {
|
} else {
|
||||||
$($(this).data('id-selector')).addClass('hide');
|
$($(this).data('id-selector')).addClass('hide');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (labelIds.length == 0) {
|
if (listIds.length == 0) {
|
||||||
$noSelect.removeClass('hide');
|
$noSelect.removeClass('hide');
|
||||||
} else {
|
} else {
|
||||||
$noSelect.addClass('hide');
|
$noSelect.addClass('hide');
|
||||||
}
|
}
|
||||||
$($(this).parent().data('id')).val(labelIds.join(","));
|
$($(this).parent().data('id')).val(listIds.join(","));
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
$labelMenu.find('.no-select.item').click(function () {
|
$listMenu.find('.no-select.item').click(function () {
|
||||||
if (hasLabelUpdateAction) {
|
if (hasLabelUpdateAction || selector == 'select-assignees-modify') {
|
||||||
updateIssuesMeta(
|
updateIssuesMeta(
|
||||||
$labelMenu.data('update-url'),
|
$listMenu.data('update-url'),
|
||||||
"clear",
|
"clear",
|
||||||
$labelMenu.data('issue-id'),
|
$listMenu.data('issue-id'),
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
$listMenu.data('action', 'update'); // Update to reload the page when we updated items
|
||||||
}
|
}
|
||||||
|
|
||||||
$(this).parent().find('.item').each(function () {
|
$(this).parent().find('.item').each(function () {
|
||||||
|
@ -253,7 +280,14 @@ function initCommentForm() {
|
||||||
});
|
});
|
||||||
$noSelect.removeClass('hide');
|
$noSelect.removeClass('hide');
|
||||||
$($(this).parent().data('id')).val('');
|
$($(this).parent().data('id')).val('');
|
||||||
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init labels and assignees
|
||||||
|
initListSubmits('select-label', 'labels');
|
||||||
|
initListSubmits('select-assignees', 'assignees');
|
||||||
|
initListSubmits('select-assignees-modify', 'assignees');
|
||||||
|
|
||||||
function selectItem(select_id, input_id) {
|
function selectItem(select_id, input_id) {
|
||||||
var $menu = $(select_id + ' .menu');
|
var $menu = $(select_id + ' .menu');
|
||||||
|
|
|
@ -119,8 +119,11 @@
|
||||||
}
|
}
|
||||||
.octicon {
|
.octicon {
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: -5px;
|
margin: 5px -7px 0 -5px;
|
||||||
margin-right: -7px;
|
width: 16px;
|
||||||
|
}
|
||||||
|
.text{
|
||||||
|
margin-left: 0.9em;
|
||||||
}
|
}
|
||||||
.menu {
|
.menu {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
|
|
|
@ -178,25 +178,22 @@ func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) {
|
||||||
DeadlineUnix: deadlineUnix,
|
DeadlineUnix: deadlineUnix,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.Repo.IsWriter() {
|
// Get all assignee IDs
|
||||||
if len(form.Assignee) > 0 {
|
assigneeIDs, err := models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees)
|
||||||
assignee, err := models.GetUserByName(form.Assignee)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if models.IsErrUserNotExist(err) {
|
if models.IsErrUserNotExist(err) {
|
||||||
ctx.Error(422, "", fmt.Sprintf("Assignee does not exist: [name: %s]", form.Assignee))
|
ctx.Error(422, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
|
||||||
} else {
|
} else {
|
||||||
ctx.Error(500, "GetUserByName", err)
|
ctx.Error(500, "AddAssigneeByName", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
issue.AssigneeID = assignee.ID
|
|
||||||
}
|
|
||||||
issue.MilestoneID = form.Milestone
|
|
||||||
} else {
|
|
||||||
form.Labels = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := models.NewIssue(ctx.Repo.Repository, issue, form.Labels, nil); err != nil {
|
if err := models.NewIssue(ctx.Repo.Repository, issue, form.Labels, assigneeIDs, nil); err != nil {
|
||||||
|
if models.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||||
|
ctx.Error(400, "UserDoesNotHaveAccessToRepo", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.Error(500, "NewIssue", err)
|
ctx.Error(500, "NewIssue", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -209,7 +206,6 @@ func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refetch from database to assign some automatic values
|
// Refetch from database to assign some automatic values
|
||||||
var err error
|
|
||||||
issue, err = models.GetIssueByID(issue.ID)
|
issue, err = models.GetIssueByID(issue.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(500, "GetIssueByID", err)
|
ctx.Error(500, "GetIssueByID", err)
|
||||||
|
@ -272,6 +268,7 @@ func EditIssue(ctx *context.APIContext, form api.EditIssueOption) {
|
||||||
issue.Content = *form.Body
|
issue.Content = *form.Body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the deadline
|
||||||
var deadlineUnix util.TimeStamp
|
var deadlineUnix util.TimeStamp
|
||||||
if form.Deadline != nil && !form.Deadline.IsZero() {
|
if form.Deadline != nil && !form.Deadline.IsZero() {
|
||||||
deadlineUnix = util.TimeStamp(form.Deadline.Unix())
|
deadlineUnix = util.TimeStamp(form.Deadline.Unix())
|
||||||
|
@ -282,28 +279,28 @@ func EditIssue(ctx *context.APIContext, form api.EditIssueOption) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.Repo.IsWriter() && form.Assignee != nil &&
|
// Add/delete assignees
|
||||||
(issue.Assignee == nil || issue.Assignee.LowerName != strings.ToLower(*form.Assignee)) {
|
|
||||||
if len(*form.Assignee) == 0 {
|
// Deleting is done the Github way (quote from their api documentation):
|
||||||
issue.AssigneeID = 0
|
// https://developer.github.com/v3/issues/#edit-an-issue
|
||||||
} else {
|
// "assignees" (array): Logins for Users to assign to this issue.
|
||||||
assignee, err := models.GetUserByName(*form.Assignee)
|
// Pass one or more user logins to replace the set of assignees on this Issue.
|
||||||
if err != nil {
|
// Send an empty array ([]) to clear all assignees from the Issue.
|
||||||
if models.IsErrUserNotExist(err) {
|
|
||||||
ctx.Error(422, "", fmt.Sprintf("assignee does not exist: [name: %s]", *form.Assignee))
|
if ctx.Repo.IsWriter() && (form.Assignees != nil || form.Assignee != nil) {
|
||||||
} else {
|
|
||||||
ctx.Error(500, "GetUserByName", err)
|
oneAssignee := ""
|
||||||
}
|
if form.Assignee != nil {
|
||||||
return
|
oneAssignee = *form.Assignee
|
||||||
}
|
|
||||||
issue.AssigneeID = assignee.ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = models.UpdateIssueUserByAssignee(issue); err != nil {
|
err = models.UpdateAPIAssignee(issue, oneAssignee, form.Assignees, ctx.User)
|
||||||
ctx.Error(500, "UpdateIssueUserByAssignee", err)
|
if err != nil {
|
||||||
|
ctx.Error(500, "UpdateAPIAssignee", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.Repo.IsWriter() && form.Milestone != nil &&
|
if ctx.Repo.IsWriter() && form.Milestone != nil &&
|
||||||
issue.MilestoneID != *form.Milestone {
|
issue.MilestoneID != *form.Milestone {
|
||||||
oldMilestoneID := issue.MilestoneID
|
oldMilestoneID := issue.MilestoneID
|
||||||
|
|
|
@ -211,26 +211,6 @@ func CreatePullRequest(ctx *context.APIContext, form api.CreatePullRequestOption
|
||||||
milestoneID = milestone.ID
|
milestoneID = milestone.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(form.Assignee) > 0 {
|
|
||||||
assigneeUser, err := models.GetUserByName(form.Assignee)
|
|
||||||
if err != nil {
|
|
||||||
if models.IsErrUserNotExist(err) {
|
|
||||||
ctx.Error(422, "", fmt.Sprintf("assignee does not exist: [name: %s]", form.Assignee))
|
|
||||||
} else {
|
|
||||||
ctx.Error(500, "GetUserByName", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
assignee, err := repo.GetAssigneeByID(assigneeUser.ID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(500, "GetAssigneeByID", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
assigneeID = assignee.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
patch, err := headGitRepo.GetPatch(prInfo.MergeBase, headBranch)
|
patch, err := headGitRepo.GetPatch(prInfo.MergeBase, headBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(500, "GetPatch", err)
|
ctx.Error(500, "GetPatch", err)
|
||||||
|
@ -266,7 +246,22 @@ func CreatePullRequest(ctx *context.APIContext, form api.CreatePullRequestOption
|
||||||
Type: models.PullRequestGitea,
|
Type: models.PullRequestGitea,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := models.NewPullRequest(repo, prIssue, labelIDs, []string{}, pr, patch); err != nil {
|
// Get all assignee IDs
|
||||||
|
assigneeIDs, err := models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees)
|
||||||
|
if err != nil {
|
||||||
|
if models.IsErrUserNotExist(err) {
|
||||||
|
ctx.Error(422, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
|
||||||
|
} else {
|
||||||
|
ctx.Error(500, "AddAssigneeByName", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := models.NewPullRequest(repo, prIssue, labelIDs, []string{}, pr, patch, assigneeIDs); err != nil {
|
||||||
|
if models.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||||
|
ctx.Error(400, "UserDoesNotHaveAccessToRepo", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.Error(500, "NewPullRequest", err)
|
ctx.Error(500, "NewPullRequest", err)
|
||||||
return
|
return
|
||||||
} else if err := pr.PushToBaseRepo(); err != nil {
|
} else if err := pr.PushToBaseRepo(); err != nil {
|
||||||
|
@ -335,6 +330,7 @@ func EditPullRequest(ctx *context.APIContext, form api.EditPullRequestOption) {
|
||||||
issue.Content = form.Body
|
issue.Content = form.Body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Deadline
|
||||||
var deadlineUnix util.TimeStamp
|
var deadlineUnix util.TimeStamp
|
||||||
if form.Deadline != nil && !form.Deadline.IsZero() {
|
if form.Deadline != nil && !form.Deadline.IsZero() {
|
||||||
deadlineUnix = util.TimeStamp(form.Deadline.Unix())
|
deadlineUnix = util.TimeStamp(form.Deadline.Unix())
|
||||||
|
@ -345,28 +341,27 @@ func EditPullRequest(ctx *context.APIContext, form api.EditPullRequestOption) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.Repo.IsWriter() && len(form.Assignee) > 0 &&
|
// Add/delete assignees
|
||||||
(issue.Assignee == nil || issue.Assignee.LowerName != strings.ToLower(form.Assignee)) {
|
|
||||||
if len(form.Assignee) == 0 {
|
// Deleting is done the Github way (quote from their api documentation):
|
||||||
issue.AssigneeID = 0
|
// https://developer.github.com/v3/issues/#edit-an-issue
|
||||||
} else {
|
// "assignees" (array): Logins for Users to assign to this issue.
|
||||||
assignee, err := models.GetUserByName(form.Assignee)
|
// Pass one or more user logins to replace the set of assignees on this Issue.
|
||||||
|
// Send an empty array ([]) to clear all assignees from the Issue.
|
||||||
|
|
||||||
|
if ctx.Repo.IsWriter() && (form.Assignees != nil || len(form.Assignee) > 0) {
|
||||||
|
|
||||||
|
err = models.UpdateAPIAssignee(issue, form.Assignee, form.Assignees, ctx.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if models.IsErrUserNotExist(err) {
|
if models.IsErrUserNotExist(err) {
|
||||||
ctx.Error(422, "", fmt.Sprintf("assignee does not exist: [name: %s]", form.Assignee))
|
ctx.Error(422, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
|
||||||
} else {
|
} else {
|
||||||
ctx.Error(500, "GetUserByName", err)
|
ctx.Error(500, "UpdateAPIAssignee", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
issue.AssigneeID = assignee.ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = models.UpdateIssueUserByAssignee(issue); err != nil {
|
|
||||||
ctx.Error(500, "UpdateIssueUserByAssignee", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ctx.Repo.IsWriter() && form.Milestone != 0 &&
|
if ctx.Repo.IsWriter() && form.Milestone != 0 &&
|
||||||
issue.MilestoneID != form.Milestone {
|
issue.MilestoneID != form.Milestone {
|
||||||
oldMilestoneID := issue.MilestoneID
|
oldMilestoneID := issue.MilestoneID
|
||||||
|
|
|
@ -364,7 +364,7 @@ func NewIssue(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateRepoMetas check and returns repository's meta informations
|
// ValidateRepoMetas check and returns repository's meta informations
|
||||||
func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm) ([]int64, int64, int64) {
|
func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm) ([]int64, []int64, int64) {
|
||||||
var (
|
var (
|
||||||
repo = ctx.Repo.Repository
|
repo = ctx.Repo.Repository
|
||||||
err error
|
err error
|
||||||
|
@ -372,11 +372,11 @@ func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm) ([]int64
|
||||||
|
|
||||||
labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository)
|
labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return nil, 0, 0
|
return nil, nil, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.Repo.IsWriter() {
|
if !ctx.Repo.IsWriter() {
|
||||||
return nil, 0, 0
|
return nil, nil, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelIDs []int64
|
var labelIDs []int64
|
||||||
|
@ -385,7 +385,7 @@ func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm) ([]int64
|
||||||
if len(form.LabelIDs) > 0 {
|
if len(form.LabelIDs) > 0 {
|
||||||
labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
|
labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0
|
return nil, nil, 0
|
||||||
}
|
}
|
||||||
labelIDMark := base.Int64sToMap(labelIDs)
|
labelIDMark := base.Int64sToMap(labelIDs)
|
||||||
|
|
||||||
|
@ -407,23 +407,35 @@ func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm) ([]int64
|
||||||
ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
|
ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetMilestoneByID", err)
|
ctx.ServerError("GetMilestoneByID", err)
|
||||||
return nil, 0, 0
|
return nil, nil, 0
|
||||||
}
|
}
|
||||||
ctx.Data["milestone_id"] = milestoneID
|
ctx.Data["milestone_id"] = milestoneID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check assignee.
|
// Check assignees
|
||||||
assigneeID := form.AssigneeID
|
var assigneeIDs []int64
|
||||||
if assigneeID > 0 {
|
if len(form.AssigneeIDs) > 0 {
|
||||||
ctx.Data["Assignee"], err = repo.GetAssigneeByID(assigneeID)
|
assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetAssigneeByID", err)
|
return nil, nil, 0
|
||||||
return nil, 0, 0
|
|
||||||
}
|
|
||||||
ctx.Data["assignee_id"] = assigneeID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return labelIDs, milestoneID, assigneeID
|
// Check if the passed assignees actually exists and has write access to the repo
|
||||||
|
for _, aID := range assigneeIDs {
|
||||||
|
_, err = repo.GetUserIfHasWriteAccess(aID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUserIfHasWriteAccess", err)
|
||||||
|
return nil, nil, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the old assignee id thingy for compatibility reasons
|
||||||
|
if form.AssigneeID > 0 {
|
||||||
|
assigneeIDs = append(assigneeIDs, form.AssigneeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return labelIDs, assigneeIDs, milestoneID
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIssuePost response for creating new issue
|
// NewIssuePost response for creating new issue
|
||||||
|
@ -440,7 +452,7 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
|
||||||
attachments []string
|
attachments []string
|
||||||
)
|
)
|
||||||
|
|
||||||
labelIDs, milestoneID, assigneeID := ValidateRepoMetas(ctx, form)
|
labelIDs, assigneeIDs, milestoneID := ValidateRepoMetas(ctx, form)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -460,11 +472,14 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
|
||||||
PosterID: ctx.User.ID,
|
PosterID: ctx.User.ID,
|
||||||
Poster: ctx.User,
|
Poster: ctx.User,
|
||||||
MilestoneID: milestoneID,
|
MilestoneID: milestoneID,
|
||||||
AssigneeID: assigneeID,
|
|
||||||
Content: form.Content,
|
Content: form.Content,
|
||||||
Ref: form.Ref,
|
Ref: form.Ref,
|
||||||
}
|
}
|
||||||
if err := models.NewIssue(repo, issue, labelIDs, attachments); err != nil {
|
if err := models.NewIssue(repo, issue, labelIDs, assigneeIDs, attachments); err != nil {
|
||||||
|
if models.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||||
|
ctx.Error(400, "UserDoesNotHaveAccessToRepo", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.ServerError("NewIssue", err)
|
ctx.ServerError("NewIssue", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -702,8 +717,8 @@ func ViewIssue(ctx *context.Context) {
|
||||||
comment.Milestone = ghostMilestone
|
comment.Milestone = ghostMilestone
|
||||||
}
|
}
|
||||||
} else if comment.Type == models.CommentTypeAssignees {
|
} else if comment.Type == models.CommentTypeAssignees {
|
||||||
if err = comment.LoadAssignees(); err != nil {
|
if err = comment.LoadAssigneeUser(); err != nil {
|
||||||
ctx.ServerError("LoadAssignees", err)
|
ctx.ServerError("LoadAssigneeUser", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -912,15 +927,22 @@ func UpdateIssueAssignee(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
assigneeID := ctx.QueryInt64("id")
|
assigneeID := ctx.QueryInt64("id")
|
||||||
|
action := ctx.Query("action")
|
||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
if issue.AssigneeID == assigneeID {
|
switch action {
|
||||||
continue
|
case "clear":
|
||||||
|
if err := models.DeleteNotPassedAssignee(issue, ctx.User, []*models.User{}); err != nil {
|
||||||
|
ctx.ServerError("ClearAssignees", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
if err := issue.ChangeAssignee(ctx.User, assigneeID); err != nil {
|
if err := issue.ChangeAssignee(ctx.User, assigneeID); err != nil {
|
||||||
ctx.ServerError("ChangeAssignee", err)
|
ctx.ServerError("ChangeAssignee", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ctx.JSON(200, map[string]interface{}{
|
ctx.JSON(200, map[string]interface{}{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
})
|
})
|
||||||
|
|
|
@ -775,7 +775,7 @@ func CompareAndPullRequestPost(ctx *context.Context, form auth.CreateIssueForm)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
labelIDs, milestoneID, assigneeID := ValidateRepoMetas(ctx, form)
|
labelIDs, assigneeIDs, milestoneID := ValidateRepoMetas(ctx, form)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -811,7 +811,6 @@ func CompareAndPullRequestPost(ctx *context.Context, form auth.CreateIssueForm)
|
||||||
PosterID: ctx.User.ID,
|
PosterID: ctx.User.ID,
|
||||||
Poster: ctx.User,
|
Poster: ctx.User,
|
||||||
MilestoneID: milestoneID,
|
MilestoneID: milestoneID,
|
||||||
AssigneeID: assigneeID,
|
|
||||||
IsPull: true,
|
IsPull: true,
|
||||||
Content: form.Content,
|
Content: form.Content,
|
||||||
}
|
}
|
||||||
|
@ -828,7 +827,12 @@ func CompareAndPullRequestPost(ctx *context.Context, form auth.CreateIssueForm)
|
||||||
}
|
}
|
||||||
// FIXME: check error in the case two people send pull request at almost same time, give nice error prompt
|
// FIXME: check error in the case two people send pull request at almost same time, give nice error prompt
|
||||||
// instead of 500.
|
// instead of 500.
|
||||||
if err := models.NewPullRequest(repo, pullIssue, labelIDs, attachments, pullRequest, patch); err != nil {
|
|
||||||
|
if err := models.NewPullRequest(repo, pullIssue, labelIDs, attachments, pullRequest, patch, assigneeIDs); err != nil {
|
||||||
|
if models.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||||
|
ctx.Error(400, "UserDoesNotHaveAccessToRepo", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.ServerError("NewPullRequest", err)
|
ctx.ServerError("NewPullRequest", err)
|
||||||
return
|
return
|
||||||
} else if err := pullRequest.PushToBaseRepo(); err != nil {
|
} else if err := pullRequest.PushToBaseRepo(); err != nil {
|
||||||
|
|
|
@ -156,7 +156,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Assignee -->
|
<!-- Assignees -->
|
||||||
<div class="ui {{if not .Assignees}}disabled{{end}} dropdown jump item">
|
<div class="ui {{if not .Assignees}}disabled{{end}} dropdown jump item">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
{{.i18n.Tr "repo.issues.action_assignee"}}
|
{{.i18n.Tr "repo.issues.action_assignee"}}
|
||||||
|
@ -220,9 +220,9 @@
|
||||||
<span class="octicon octicon-calendar"></span>
|
<span class="octicon octicon-calendar"></span>
|
||||||
<span{{if .IsOverdue}} class="overdue"{{end}}>{{.DeadlineUnix.FormatShort}}</span>
|
<span{{if .IsOverdue}} class="overdue"{{end}}>{{.DeadlineUnix.FormatShort}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .Assignee}}
|
{{range .Assignees}}
|
||||||
<a class="ui right assignee poping up" href="{{.Assignee.HomeLink}}" data-content="{{.Assignee.Name}}" data-variation="inverted" data-position="left center">
|
<a class="ui right assignee poping up" href="{{.HomeLink}}" data-content="{{.Name}}" data-variation="inverted" data-position="left center">
|
||||||
<img class="ui avatar image" src="{{.Assignee.RelAvatarLink}}">
|
<img class="ui avatar image" src="{{.RelAvatarLink}}">
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -97,27 +97,56 @@
|
||||||
|
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
|
|
||||||
<input id="assignee_id" name="assignee_id" type="hidden" value="{{.assignee_id}}">
|
<input id="assignee_ids" name="assignee_ids" type="hidden" value="{{.assignee_ids}}">
|
||||||
<div class="ui {{if not .Assignees}}disabled{{end}} floating jump select-assignee dropdown">
|
<div class="ui {{if not .Assignees}}disabled{{end}} floating jump select-assignees dropdown">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
<strong>{{.i18n.Tr "repo.issues.new.assignee"}}</strong>
|
<strong>{{.i18n.Tr "repo.issues.new.assignees"}}</strong>
|
||||||
<span class="octicon octicon-gear"></span>
|
<span class="octicon octicon-gear"></span>
|
||||||
</span>
|
</span>
|
||||||
<div class="menu">
|
<div class="filter menu" data-id="#assignee_ids">
|
||||||
<div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_assignee"}}</div>
|
<div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_assignees"}}</div>
|
||||||
|
{{range .Assignees}}
|
||||||
|
<a class="item" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}">
|
||||||
|
<span class="octicon"></span>
|
||||||
|
<span class="text">
|
||||||
|
<img class="ui avatar image" src="{{.RelAvatarLink}}"> {{.Name}}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui assignees list">
|
||||||
|
<span class="no-select item {{if .HasSelectedLabel}}hide{{end}}">
|
||||||
|
{{.i18n.Tr "repo.issues.new.no_assignees"}}
|
||||||
|
</span>
|
||||||
|
{{range .Assignees}}
|
||||||
|
<a style="padding: 5px;color:rgba(0, 0, 0, 0.87);" class="hide item" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}">
|
||||||
|
<img class="ui avatar image" src="{{.RelAvatarLink}}" style="vertical-align: middle;"> {{.Name}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- input id="assignee_ids" name="assignee_ids" type="hidden" value="{{.assignee_id}}">
|
||||||
|
<div class="ui {{if not .Assignees}}disabled{{end}} floating jump select-assignee dropdown">
|
||||||
|
<span class="text">
|
||||||
|
<strong>{{.i18n.Tr "repo.issues.new.assignees"}}</strong>
|
||||||
|
<span class="octicon octicon-gear"></span>
|
||||||
|
</span>
|
||||||
|
<div class="filter menu">
|
||||||
|
<div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_assignees"}}</div>
|
||||||
{{range .Assignees}}
|
{{range .Assignees}}
|
||||||
<div class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?assignee={{.ID}}" data-avatar="{{.RelAvatarLink}}"><img src="{{.RelAvatarLink}}"> {{.Name}}</div>
|
<div class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?assignee={{.ID}}" data-avatar="{{.RelAvatarLink}}"><img src="{{.RelAvatarLink}}"> {{.Name}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui select-assignee list">
|
<div class="ui select-assignee list">
|
||||||
<span class="no-select item {{if .Assignee}}hide{{end}}">{{.i18n.Tr "repo.issues.new.no_assignee"}}</span>
|
<span class="no-select item {{if .Assignee}}hide{{end}}">{{.i18n.Tr "repo.issues.new.no_assignees"}}</span>
|
||||||
<div class="selected">
|
<div class="selected">
|
||||||
{{if .Assignee}}
|
{{if .Assignee}}
|
||||||
<a class="item" href="{{.RepoLink}}/issues?assignee={{.Assignee.ID}}"><img class="ui avatar image" src="{{.Assignee.RelAvatarLink}}"> {{.Assignee.Name}}</a>
|
<a class="item" href="{{.RepoLink}}/issues?assignee={{.Assignee.ID}}"><img class="ui avatar image" src="{{.Assignee.RelAvatarLink}}"> {{.Assignee.Name}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -118,15 +118,29 @@
|
||||||
{{else if eq .Type 9}}
|
{{else if eq .Type 9}}
|
||||||
<div class="event">
|
<div class="event">
|
||||||
<span class="octicon octicon-primitive-dot"></span>
|
<span class="octicon octicon-primitive-dot"></span>
|
||||||
{{if gt .AssigneeID 0}}{{if eq .Poster.ID .AssigneeID}}<a class="ui avatar image" href="{{.Poster.HomeLink}}">
|
{{if gt .AssigneeID 0}}
|
||||||
<img src="{{.Poster.RelAvatarLink}}">
|
{{if .RemovedAssignee}}
|
||||||
</a> <span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> {{$.i18n.Tr "repo.issues.self_assign_at" $createdStr | Safe}} </span>
|
<a class="ui avatar image" href="{{.Assignee.HomeLink}}">
|
||||||
{{else}}<a class="ui avatar image" href="{{.Assignee.HomeLink}}">
|
|
||||||
<img src="{{.Assignee.RelAvatarLink}}">
|
<img src="{{.Assignee.RelAvatarLink}}">
|
||||||
</a><span class="text grey"><a href="{{.Assignee.HomeLink}}">{{.Assignee.Name}}</a> {{$.i18n.Tr "repo.issues.add_assignee_at" .Poster.Name $createdStr | Safe}} </span>{{end}}{{else if gt .OldAssigneeID 0}}
|
</a>
|
||||||
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
|
<span class="text grey">
|
||||||
<img src="{{.Poster.RelAvatarLink}}">
|
<a href="{{.Assignee.HomeLink}}">{{.Assignee.Name}}</a>
|
||||||
</a> <span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> {{$.i18n.Tr "repo.issues.remove_assignee_at" $createdStr | Safe}} </span>{{end}}
|
{{$.i18n.Tr "repo.issues.remove_assignee_at" $createdStr | Safe}}
|
||||||
|
</span>
|
||||||
|
{{else}}
|
||||||
|
<a class="ui avatar image" href="{{.Assignee.HomeLink}}">
|
||||||
|
<img src="{{.Assignee.RelAvatarLink}}">
|
||||||
|
</a>
|
||||||
|
<span class="text grey">
|
||||||
|
<a href="{{.Assignee.HomeLink}}">{{.Assignee.Name}}</a>
|
||||||
|
{{if eq .Poster.ID .AssigneeID}}
|
||||||
|
{{$.i18n.Tr "repo.issues.self_assign_at" $createdStr | Safe}}
|
||||||
|
{{else}}
|
||||||
|
{{$.i18n.Tr "repo.issues.add_assignee_at" .Poster.Name $createdStr | Safe}}
|
||||||
|
{{end}}
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else if eq .Type 10}}
|
{{else if eq .Type 10}}
|
||||||
<div class="event">
|
<div class="event">
|
||||||
|
|
|
@ -68,23 +68,40 @@
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
|
|
||||||
<input id="assignee_id" name="assignee_id" type="hidden" value="{{.assignee_id}}">
|
<input id="assignee_id" name="assignee_id" type="hidden" value="{{.assignee_id}}">
|
||||||
<div class="ui {{if not .IsRepositoryWriter}}disabled{{end}} floating jump select-assignee dropdown">
|
<div class="ui {{if not .IsRepositoryWriter}}disabled{{end}} floating jump select-assignees-modify dropdown">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
<strong>{{.i18n.Tr "repo.issues.new.assignee"}}</strong>
|
<strong>{{.i18n.Tr "repo.issues.new.assignees"}}</strong>
|
||||||
<span class="octicon octicon-gear"></span>
|
<span class="octicon octicon-gear"></span>
|
||||||
</span>
|
</span>
|
||||||
<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee">
|
<div class="filter menu" data-action="" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee">
|
||||||
<div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_assignee"}}</div>
|
<div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_assignees"}}</div>
|
||||||
{{range .Assignees}}
|
{{range .Assignees}}
|
||||||
<div class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?assignee={{.ID}}" data-avatar="{{.RelAvatarLink}}"><img src="{{.RelAvatarLink}}"> {{.Name}}</div>
|
|
||||||
|
{{$AssigneeID := .ID}}
|
||||||
|
<a class="item{{range $.Issue.Assignees}}
|
||||||
|
{{if eq .ID $AssigneeID}}
|
||||||
|
checked
|
||||||
|
{{end}}
|
||||||
|
{{end}}" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}">
|
||||||
|
<span class="octicon{{range $.Issue.Assignees}}
|
||||||
|
{{if eq .ID $AssigneeID}}
|
||||||
|
octicon-check
|
||||||
|
{{end}}
|
||||||
|
{{end}}"></span>
|
||||||
|
<span class="text">
|
||||||
|
<img class="ui avatar image" src="{{.RelAvatarLink}}"> {{.Name}}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui select-assignee list">
|
<div class="ui assignees list">
|
||||||
<span class="no-select item {{if .Issue.Assignee}}hide{{end}}">{{.i18n.Tr "repo.issues.new.no_assignee"}}</span>
|
<span class="no-select item {{if .Issue.Assignees}}hide{{end}}">{{.i18n.Tr "repo.issues.new.no_assignees"}}</span>
|
||||||
<div class="selected">
|
<div class="selected">
|
||||||
{{if .Issue.Assignee}}
|
{{range .Issue.Assignees}}
|
||||||
<a class="item" href="{{$.RepoLink}}/issues?assignee={{.Issue.Assignee.ID}}"><img class="ui avatar image" src="{{.Issue.Assignee.RelAvatarLink}}"> {{.Issue.Assignee.Name}}</a>
|
<div class="item" style="margin-bottom: 10px;">
|
||||||
|
<a href="{{$.RepoLink}}/issues?assignee={{.ID}}"><img class="ui avatar image" src="{{.RelAvatarLink}}"> {{.Name}}</a>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue