mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Multiple assignees (#3705)
This commit is contained in:
		@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								models/fixtures/issue_assignees.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								models/fixtures/issue_assignees.yml
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										263
									
								
								models/issue_assignees.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								models/issue_assignees.go
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										71
									
								
								models/issue_assignees_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								models/issue_assignees_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -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}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if toBeDeleted {
 | 
				
			||||||
 | 
							_, err = e.Delete(assigneeIn)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return toBeDeleted, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							_, err = e.Insert(assigneeIn)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return toBeDeleted, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// UpdateIssueUserByAssignee updates issue-user relation for assignee.
 | 
						return toBeDeleted, nil
 | 
				
			||||||
func UpdateIssueUserByAssignee(issue *Issue) (err error) {
 | 
					 | 
				
			||||||
	sess := x.NewSession()
 | 
					 | 
				
			||||||
	defer sess.Close()
 | 
					 | 
				
			||||||
	if err = sess.Begin(); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err = updateIssueUserByAssignee(sess, issue); err != 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()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										129
									
								
								models/migrations/v64.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								models/migrations/v64.go
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user