| @@ -733,6 +733,22 @@ func (err ErrRepoFileAlreadyExist) Error() string { | |||||
| return fmt.Sprintf("repository file already exists [file_name: %s]", err.FileName) | return fmt.Sprintf("repository file already exists [file_name: %s]", err.FileName) | ||||
| } | } | ||||
| // ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo | |||||
| type ErrUserDoesNotHaveAccessToRepo struct { | |||||
| UserID int64 | |||||
| RepoName string | |||||
| } | |||||
| // IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExist. | |||||
| func IsErrUserDoesNotHaveAccessToRepo(err error) bool { | |||||
| _, ok := err.(ErrUserDoesNotHaveAccessToRepo) | |||||
| return ok | |||||
| } | |||||
| func (err ErrUserDoesNotHaveAccessToRepo) Error() string { | |||||
| return fmt.Sprintf("user doesn't have acces to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName) | |||||
| } | |||||
| // __________ .__ | // __________ .__ | ||||
| // \______ \____________ ____ ____ | |__ | // \______ \____________ ____ ____ | |__ | ||||
| // | | _/\_ __ \__ \ / \_/ ___\| | \ | // | | _/\_ __ \__ \ / \_/ ___\| | \ | ||||
| @@ -3,7 +3,6 @@ | |||||
| repo_id: 1 | repo_id: 1 | ||||
| index: 1 | index: 1 | ||||
| poster_id: 1 | poster_id: 1 | ||||
| assignee_id: 1 | |||||
| name: issue1 | name: issue1 | ||||
| content: content for the first issue | content: content for the first issue | ||||
| is_closed: false | is_closed: false | ||||
| @@ -67,7 +66,6 @@ | |||||
| repo_id: 3 | repo_id: 3 | ||||
| index: 1 | index: 1 | ||||
| poster_id: 1 | poster_id: 1 | ||||
| assignee_id: 1 | |||||
| name: issue6 | name: issue6 | ||||
| content: content6 | content: content6 | ||||
| is_closed: false | is_closed: false | ||||
| @@ -0,0 +1,8 @@ | |||||
| - | |||||
| id: 1 | |||||
| assignee_id: 1 | |||||
| issue_id: 1 | |||||
| - | |||||
| id: 2 | |||||
| assignee_id: 1 | |||||
| issue_id: 6 | |||||
| @@ -3,7 +3,6 @@ | |||||
| uid: 1 | uid: 1 | ||||
| issue_id: 1 | issue_id: 1 | ||||
| is_read: true | is_read: true | ||||
| is_assigned: true | |||||
| is_mentioned: false | is_mentioned: false | ||||
| - | - | ||||
| @@ -11,7 +10,6 @@ | |||||
| uid: 2 | uid: 2 | ||||
| issue_id: 1 | issue_id: 1 | ||||
| is_read: true | is_read: true | ||||
| is_assigned: false | |||||
| is_mentioned: false | is_mentioned: false | ||||
| - | - | ||||
| @@ -19,5 +17,4 @@ | |||||
| uid: 4 | uid: 4 | ||||
| issue_id: 1 | issue_id: 1 | ||||
| is_read: false | is_read: false | ||||
| is_assigned: false | |||||
| is_mentioned: false | is_mentioned: false | ||||
| @@ -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 { | |||||
| apiIssue.Assignee = issue.Assignee.APIFormat() | |||||
| if len(issue.Assignees) > 0 { | |||||
| 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 { | |||||
| valid, err := hasAccess(e, assigneeID, opts.Repo, AccessModeWrite) | |||||
| if err != nil { | |||||
| return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err) | |||||
| // 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 !valid { | |||||
| opts.Issue.AssigneeID = 0 | |||||
| opts.Issue.Assignee = nil | |||||
| 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) | |||||
| if err != nil { | |||||
| return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err) | |||||
| } | |||||
| if !valid { | |||||
| return ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: opts.Repo.Name} | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -931,11 +876,10 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { | |||||
| } | } | ||||
| } | } | ||||
| if opts.Issue.AssigneeID > 0 { | |||||
| if err = opts.Issue.loadRepo(e); err != nil { | |||||
| return err | |||||
| } | |||||
| if _, err = createAssigneeComment(e, doer, opts.Issue.Repo, opts.Issue, -1, opts.Issue.AssigneeID); err != nil { | |||||
| // Insert the assignees | |||||
| for _, assigneeID := range opts.AssigneeIDs { | |||||
| err = opts.Issue.changeAssignee(e, doer, assigneeID) | |||||
| if 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) | |||||
| closedCountSession.And("assignee_id = ?", uid) | |||||
| openCountSession.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). | |||||
| And("issue_assignees.assignee_id = ?", uid) | |||||
| closedCountSession.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). | |||||
| And("issue_assignees.assignee_id = ?", uid) | |||||
| case FilterModeCreate: | case FilterModeCreate: | ||||
| openCountSession.And("poster_id = ?", uid) | openCountSession.And("poster_id = ?", uid) | ||||
| closedCountSession.And("poster_id = ?", uid) | closedCountSession.And("poster_id = ?", uid) | ||||
| @@ -0,0 +1,263 @@ | |||||
| // Copyright 2018 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package models | |||||
| import ( | |||||
| "fmt" | |||||
| "code.gitea.io/gitea/modules/log" | |||||
| api "code.gitea.io/sdk/gitea" | |||||
| "github.com/go-xorm/xorm" | |||||
| ) | |||||
| // IssueAssignees saves all issue assignees | |||||
| type IssueAssignees struct { | |||||
| ID int64 `xorm:"pk autoincr"` | |||||
| AssigneeID int64 `xorm:"INDEX"` | |||||
| IssueID int64 `xorm:"INDEX"` | |||||
| } | |||||
| // This loads all assignees of an issue | |||||
| func (issue *Issue) loadAssignees(e Engine) (err error) { | |||||
| // Reset maybe preexisting assignees | |||||
| issue.Assignees = []*User{} | |||||
| err = e.Table("`user`"). | |||||
| Join("INNER", "issue_assignees", "assignee_id = `user`.id"). | |||||
| Where("issue_assignees.issue_id = ?", issue.ID). | |||||
| Find(&issue.Assignees) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| // Check if we have at least one assignee and if yes put it in as `Assignee` | |||||
| if len(issue.Assignees) > 0 { | |||||
| issue.Assignee = issue.Assignees[0] | |||||
| } | |||||
| return | |||||
| } | |||||
| // GetAssigneesByIssue returns everyone assigned to that issue | |||||
| func GetAssigneesByIssue(issue *Issue) (assignees []*User, err error) { | |||||
| err = issue.loadAssignees(x) | |||||
| if err != nil { | |||||
| return assignees, err | |||||
| } | |||||
| return issue.Assignees, nil | |||||
| } | |||||
| // IsUserAssignedToIssue returns true when the user is assigned to the issue | |||||
| func IsUserAssignedToIssue(issue *Issue, user *User) (isAssigned bool, err error) { | |||||
| isAssigned, err = x.Exist(&IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID}) | |||||
| return | |||||
| } | |||||
| // DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array | |||||
| func DeleteNotPassedAssignee(issue *Issue, doer *User, assignees []*User) (err error) { | |||||
| var found bool | |||||
| for _, assignee := range issue.Assignees { | |||||
| found = false | |||||
| for _, alreadyAssignee := range assignees { | |||||
| if assignee.ID == alreadyAssignee.ID { | |||||
| found = true | |||||
| break | |||||
| } | |||||
| } | |||||
| if !found { | |||||
| // This function also does comments and hooks, which is why we call it seperatly instead of directly removing the assignees here | |||||
| if err := UpdateAssignee(issue, doer, assignee.ID); err != nil { | |||||
| return err | |||||
| } | |||||
| } | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // MakeAssigneeList concats a string with all names of the assignees. Useful for logs. | |||||
| func MakeAssigneeList(issue *Issue) (assigneeList string, err error) { | |||||
| err = issue.loadAssignees(x) | |||||
| if err != nil { | |||||
| return "", err | |||||
| } | |||||
| for in, assignee := range issue.Assignees { | |||||
| assigneeList += assignee.Name | |||||
| if len(issue.Assignees) > (in + 1) { | |||||
| assigneeList += ", " | |||||
| } | |||||
| } | |||||
| return | |||||
| } | |||||
| // ClearAssigneeByUserID deletes all assignments of an user | |||||
| func clearAssigneeByUserID(sess *xorm.Session, userID int64) (err error) { | |||||
| _, err = sess.Delete(&IssueAssignees{AssigneeID: userID}) | |||||
| return | |||||
| } | |||||
| // AddAssigneeIfNotAssigned adds an assignee only if he isn't aleady assigned to the issue | |||||
| func AddAssigneeIfNotAssigned(issue *Issue, doer *User, assigneeID int64) (err error) { | |||||
| // Check if the user is already assigned | |||||
| isAssigned, err := IsUserAssignedToIssue(issue, &User{ID: assigneeID}) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| if !isAssigned { | |||||
| return issue.ChangeAssignee(doer, assigneeID) | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // UpdateAssignee deletes or adds an assignee to an issue | |||||
| func UpdateAssignee(issue *Issue, doer *User, assigneeID int64) (err error) { | |||||
| return issue.ChangeAssignee(doer, assigneeID) | |||||
| } | |||||
| // ChangeAssignee changes the Assignee of this issue. | |||||
| func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) { | |||||
| sess := x.NewSession() | |||||
| defer sess.Close() | |||||
| if err := sess.Begin(); err != nil { | |||||
| return err | |||||
| } | |||||
| if err := issue.changeAssignee(sess, doer, assigneeID); err != nil { | |||||
| return err | |||||
| } | |||||
| return sess.Commit() | |||||
| } | |||||
| func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID int64) (err error) { | |||||
| // Update the assignee | |||||
| removed, err := updateIssueAssignee(sess, issue, assigneeID) | |||||
| if err != nil { | |||||
| return fmt.Errorf("UpdateIssueUserByAssignee: %v", err) | |||||
| } | |||||
| // Repo infos | |||||
| if err = issue.loadRepo(sess); err != nil { | |||||
| return fmt.Errorf("loadRepo: %v", err) | |||||
| } | |||||
| // Comment | |||||
| if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed); err != nil { | |||||
| return fmt.Errorf("createAssigneeComment: %v", err) | |||||
| } | |||||
| if issue.IsPull { | |||||
| issue.PullRequest = &PullRequest{Issue: issue} | |||||
| apiPullRequest := &api.PullRequestPayload{ | |||||
| Index: issue.Index, | |||||
| PullRequest: issue.PullRequest.APIFormat(), | |||||
| Repository: issue.Repo.APIFormat(AccessModeNone), | |||||
| Sender: doer.APIFormat(), | |||||
| } | |||||
| if removed { | |||||
| apiPullRequest.Action = api.HookIssueUnassigned | |||||
| } else { | |||||
| apiPullRequest.Action = api.HookIssueAssigned | |||||
| } | |||||
| if err := PrepareWebhooks(issue.Repo, HookEventPullRequest, apiPullRequest); err != nil { | |||||
| log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err) | |||||
| return nil | |||||
| } | |||||
| } | |||||
| go HookQueue.Add(issue.RepoID) | |||||
| return nil | |||||
| } | |||||
| // UpdateAPIAssignee is a helper function to add or delete one or multiple issue assignee(s) | |||||
| // Deleting is done the Github way (quote from their api documentation): | |||||
| // https://developer.github.com/v3/issues/#edit-an-issue | |||||
| // "assignees" (array): Logins for Users to assign to this issue. | |||||
| // Pass one or more user logins to replace the set of assignees on this Issue. | |||||
| // Send an empty array ([]) to clear all assignees from the Issue. | |||||
| func UpdateAPIAssignee(issue *Issue, oneAssignee string, multipleAssignees []string, doer *User) (err error) { | |||||
| var allNewAssignees []*User | |||||
| // Keep the old assignee thingy for compatibility reasons | |||||
| if oneAssignee != "" { | |||||
| // Prevent double adding assignees | |||||
| var isDouble bool | |||||
| for _, assignee := range multipleAssignees { | |||||
| if assignee == oneAssignee { | |||||
| isDouble = true | |||||
| break | |||||
| } | |||||
| } | |||||
| if !isDouble { | |||||
| multipleAssignees = append(multipleAssignees, oneAssignee) | |||||
| } | |||||
| } | |||||
| // Loop through all assignees to add them | |||||
| for _, assigneeName := range multipleAssignees { | |||||
| assignee, err := GetUserByName(assigneeName) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| allNewAssignees = append(allNewAssignees, assignee) | |||||
| } | |||||
| // Delete all old assignees not passed | |||||
| if err = DeleteNotPassedAssignee(issue, doer, allNewAssignees); err != nil { | |||||
| return err | |||||
| } | |||||
| // Add all new assignees | |||||
| // Update the assignee. The function will check if the user exists, is already | |||||
| // assigned (which he shouldn't as we deleted all assignees before) and | |||||
| // has access to the repo. | |||||
| for _, assignee := range allNewAssignees { | |||||
| // Extra method to prevent double adding (which would result in removing) | |||||
| err = AddAssigneeIfNotAssigned(issue, doer, assignee.ID) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| } | |||||
| return | |||||
| } | |||||
| // MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs | |||||
| func MakeIDsFromAPIAssigneesToAdd(oneAssignee string, multipleAssignees []string) (assigneeIDs []int64, err error) { | |||||
| // Keeping the old assigning method for compatibility reasons | |||||
| if oneAssignee != "" { | |||||
| // Prevent double adding assignees | |||||
| var isDouble bool | |||||
| for _, assignee := range multipleAssignees { | |||||
| if assignee == oneAssignee { | |||||
| isDouble = true | |||||
| break | |||||
| } | |||||
| } | |||||
| if !isDouble { | |||||
| multipleAssignees = append(multipleAssignees, oneAssignee) | |||||
| } | |||||
| } | |||||
| // Get the IDs of all assignees | |||||
| assigneeIDs = GetUserIDsByNames(multipleAssignees) | |||||
| return | |||||
| } | |||||
| @@ -0,0 +1,71 @@ | |||||
| // Copyright 2018 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package models | |||||
| import ( | |||||
| "testing" | |||||
| "github.com/stretchr/testify/assert" | |||||
| ) | |||||
| func TestUpdateAssignee(t *testing.T) { | |||||
| assert.NoError(t, PrepareTestDatabase()) | |||||
| // Fake issue with assignees | |||||
| issue, err := GetIssueByID(1) | |||||
| assert.NoError(t, err) | |||||
| // Assign multiple users | |||||
| user2, err := GetUserByID(2) | |||||
| assert.NoError(t, err) | |||||
| err = UpdateAssignee(issue, &User{ID: 1}, user2.ID) | |||||
| assert.NoError(t, err) | |||||
| user3, err := GetUserByID(3) | |||||
| assert.NoError(t, err) | |||||
| err = UpdateAssignee(issue, &User{ID: 1}, user3.ID) | |||||
| assert.NoError(t, err) | |||||
| user1, err := GetUserByID(1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him | |||||
| assert.NoError(t, err) | |||||
| err = UpdateAssignee(issue, &User{ID: 1}, user1.ID) | |||||
| assert.NoError(t, err) | |||||
| // Check if he got removed | |||||
| isAssigned, err := IsUserAssignedToIssue(issue, user1) | |||||
| assert.NoError(t, err) | |||||
| assert.False(t, isAssigned) | |||||
| // Check if they're all there | |||||
| assignees, err := GetAssigneesByIssue(issue) | |||||
| assert.NoError(t, err) | |||||
| var expectedAssignees []*User | |||||
| expectedAssignees = append(expectedAssignees, user2) | |||||
| expectedAssignees = append(expectedAssignees, user3) | |||||
| for in, assignee := range assignees { | |||||
| assert.Equal(t, assignee.ID, expectedAssignees[in].ID) | |||||
| } | |||||
| // Check if the user is assigned | |||||
| isAssigned, err = IsUserAssignedToIssue(issue, user2) | |||||
| assert.NoError(t, err) | |||||
| assert.True(t, isAssigned) | |||||
| // This user should not be assigned | |||||
| isAssigned, err = IsUserAssignedToIssue(issue, &User{ID: 4}) | |||||
| assert.NoError(t, err) | |||||
| assert.False(t, isAssigned) | |||||
| // Clean everyone | |||||
| err = DeleteNotPassedAssignee(issue, user1, []*User{}) | |||||
| assert.NoError(t, err) | |||||
| // Check they're gone | |||||
| assignees, err = GetAssigneesByIssue(issue) | |||||
| assert.NoError(t, err) | |||||
| assert.Equal(t, 0, len(assignees)) | |||||
| } | |||||
| @@ -81,23 +81,22 @@ const ( | |||||
| // Comment represents a comment in commit and issue page. | // Comment represents a comment in commit and issue page. | ||||
| type Comment struct { | type Comment struct { | ||||
| ID int64 `xorm:"pk autoincr"` | |||||
| Type CommentType | |||||
| PosterID int64 `xorm:"INDEX"` | |||||
| Poster *User `xorm:"-"` | |||||
| IssueID int64 `xorm:"INDEX"` | |||||
| LabelID int64 | |||||
| Label *Label `xorm:"-"` | |||||
| OldMilestoneID int64 | |||||
| MilestoneID int64 | |||||
| OldMilestone *Milestone `xorm:"-"` | |||||
| Milestone *Milestone `xorm:"-"` | |||||
| OldAssigneeID int64 | |||||
| AssigneeID int64 | |||||
| Assignee *User `xorm:"-"` | |||||
| OldAssignee *User `xorm:"-"` | |||||
| OldTitle string | |||||
| NewTitle string | |||||
| ID int64 `xorm:"pk autoincr"` | |||||
| Type CommentType | |||||
| PosterID int64 `xorm:"INDEX"` | |||||
| Poster *User `xorm:"-"` | |||||
| IssueID int64 `xorm:"INDEX"` | |||||
| LabelID int64 | |||||
| Label *Label `xorm:"-"` | |||||
| OldMilestoneID int64 | |||||
| MilestoneID int64 | |||||
| OldMilestone *Milestone `xorm:"-"` | |||||
| Milestone *Milestone `xorm:"-"` | |||||
| AssigneeID int64 | |||||
| RemovedAssignee bool | |||||
| Assignee *User `xorm:"-"` | |||||
| OldTitle string | |||||
| NewTitle string | |||||
| CommitID int64 | CommitID int64 | ||||
| Line int64 | Line int64 | ||||
| @@ -247,18 +246,9 @@ func (c *Comment) LoadMilestone() error { | |||||
| return nil | return nil | ||||
| } | } | ||||
| // LoadAssignees if comment.Type is CommentTypeAssignees, then load assignees | |||||
| func (c *Comment) LoadAssignees() error { | |||||
| // LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees | |||||
| 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) | ||||
| @@ -324,21 +314,21 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err | |||||
| LabelID = opts.Label.ID | LabelID = opts.Label.ID | ||||
| } | } | ||||
| comment := &Comment{ | comment := &Comment{ | ||||
| Type: opts.Type, | |||||
| PosterID: opts.Doer.ID, | |||||
| Poster: opts.Doer, | |||||
| IssueID: opts.Issue.ID, | |||||
| LabelID: LabelID, | |||||
| OldMilestoneID: opts.OldMilestoneID, | |||||
| MilestoneID: opts.MilestoneID, | |||||
| OldAssigneeID: opts.OldAssigneeID, | |||||
| AssigneeID: opts.AssigneeID, | |||||
| CommitID: opts.CommitID, | |||||
| CommitSHA: opts.CommitSHA, | |||||
| Line: opts.LineNum, | |||||
| Content: opts.Content, | |||||
| OldTitle: opts.OldTitle, | |||||
| NewTitle: opts.NewTitle, | |||||
| Type: opts.Type, | |||||
| PosterID: opts.Doer.ID, | |||||
| Poster: opts.Doer, | |||||
| IssueID: opts.Issue.ID, | |||||
| LabelID: LabelID, | |||||
| OldMilestoneID: opts.OldMilestoneID, | |||||
| MilestoneID: opts.MilestoneID, | |||||
| RemovedAssignee: opts.RemovedAssignee, | |||||
| AssigneeID: opts.AssigneeID, | |||||
| CommitID: opts.CommitID, | |||||
| CommitSHA: opts.CommitSHA, | |||||
| Line: opts.LineNum, | |||||
| Content: opts.Content, | |||||
| OldTitle: opts.OldTitle, | |||||
| NewTitle: opts.NewTitle, | |||||
| } | } | ||||
| if _, err = e.Insert(comment); err != nil { | if _, err = e.Insert(comment); err != nil { | ||||
| return nil, err | return nil, err | ||||
| @@ -480,14 +470,14 @@ 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, | |||||
| Doer: doer, | |||||
| Repo: repo, | |||||
| Issue: issue, | |||||
| OldAssigneeID: oldAssigneeID, | |||||
| AssigneeID: assigneeID, | |||||
| Type: CommentTypeAssignees, | |||||
| Doer: doer, | |||||
| Repo: repo, | |||||
| Issue: issue, | |||||
| RemovedAssignee: removedAssignee, | |||||
| AssigneeID: assigneeID, | |||||
| }) | }) | ||||
| } | } | ||||
| @@ -548,17 +538,17 @@ type CreateCommentOptions struct { | |||||
| Issue *Issue | Issue *Issue | ||||
| Label *Label | Label *Label | ||||
| OldMilestoneID int64 | |||||
| MilestoneID int64 | |||||
| OldAssigneeID int64 | |||||
| AssigneeID int64 | |||||
| OldTitle string | |||||
| NewTitle string | |||||
| CommitID int64 | |||||
| CommitSHA string | |||||
| LineNum int64 | |||||
| Content string | |||||
| Attachments []string // UUIDs of attachments | |||||
| OldMilestoneID int64 | |||||
| MilestoneID int64 | |||||
| AssigneeID int64 | |||||
| RemovedAssignee bool | |||||
| OldTitle string | |||||
| NewTitle string | |||||
| CommitID int64 | |||||
| CommitSHA string | |||||
| LineNum int64 | |||||
| Content string | |||||
| Attachments []string // UUIDs of attachments | |||||
| } | } | ||||
| // CreateComment creates comment of issue or commit. | // CreateComment creates comment of issue or commit. | ||||
| @@ -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(assigneeIDs) == 0 { | |||||
| if len(issues) == 0 { | |||||
| return nil | return nil | ||||
| } | } | ||||
| assigneeMaps := make(map[int64]*User, len(assigneeIDs)) | |||||
| err := e. | |||||
| In("id", assigneeIDs). | |||||
| Find(&assigneeMaps) | |||||
| type AssigneeIssue struct { | |||||
| IssueAssignee *IssueAssignees `xorm:"extends"` | |||||
| Assignee *User `xorm:"extends"` | |||||
| } | |||||
| 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 { | if err != nil { | ||||
| return err | return err | ||||
| } | } | ||||
| defer rows.Close() | |||||
| for _, issue := range issues { | |||||
| if issue.AssigneeID <= 0 { | |||||
| continue | |||||
| } | |||||
| var ok bool | |||||
| if issue.Assignee, ok = assigneeMaps[issue.AssigneeID]; !ok { | |||||
| issue.Assignee = NewGhostUser() | |||||
| for rows.Next() { | |||||
| var assigneeIssue AssigneeIssue | |||||
| err = rows.Scan(&assigneeIssue) | |||||
| if err != nil { | |||||
| return err | |||||
| } | } | ||||
| assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee) | |||||
| } | |||||
| for _, issue := range issues { | |||||
| issue.Assignees = assignees[issue.ID] | |||||
| } | } | ||||
| 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 | |||||
| if issue.Assignee != nil && issue.AssigneeID > 0 && issue.AssigneeID != doer.ID { | |||||
| participants = append(participants, issue.Assignee) | |||||
| // Assignees must receive any communications | |||||
| assignees, err := GetAssigneesByIssue(issue) | |||||
| 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 | ||||
| } | } | ||||
| @@ -32,9 +33,8 @@ func newIssueUsers(e Engine, repo *Repository, issue *Issue) error { | |||||
| issueUsers := make([]*IssueUser, 0, len(assignees)+1) | issueUsers := make([]*IssueUser, 0, len(assignees)+1) | ||||
| for _, assignee := range assignees { | for _, assignee := range assignees { | ||||
| issueUsers = append(issueUsers, &IssueUser{ | issueUsers = append(issueUsers, &IssueUser{ | ||||
| IssueID: issue.ID, | |||||
| UID: assignee.ID, | |||||
| IsAssigned: assignee.ID == issue.AssigneeID, | |||||
| IssueID: issue.ID, | |||||
| UID: assignee.ID, | |||||
| }) | }) | ||||
| 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) { | |||||
| if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?", false, issue.ID); err != nil { | |||||
| return err | |||||
| func updateIssueAssignee(e *xorm.Session, issue *Issue, assigneeID int64) (removed bool, err error) { | |||||
| // Check if the user exists | |||||
| _, err = GetUserByID(assigneeID) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | } | ||||
| // Assignee ID equals to 0 means clear assignee. | |||||
| if issue.AssigneeID > 0 { | |||||
| if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE uid = ? AND issue_id = ?", true, issue.AssigneeID, issue.ID); err != nil { | |||||
| return err | |||||
| // Check if the submitted user is already assigne, if yes delete him otherwise add him | |||||
| var toBeDeleted bool | |||||
| for _, assignee := range issue.Assignees { | |||||
| if assignee.ID == assigneeID { | |||||
| toBeDeleted = true | |||||
| break | |||||
| } | } | ||||
| } | } | ||||
| return updateIssue(e, issue) | |||||
| } | |||||
| assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID} | |||||
| // UpdateIssueUserByAssignee updates issue-user relation for assignee. | |||||
| 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 | |||||
| if toBeDeleted { | |||||
| _, err = e.Delete(assigneeIn) | |||||
| if err != nil { | |||||
| return toBeDeleted, err | |||||
| } | |||||
| } else { | |||||
| _, err = e.Insert(assigneeIn) | |||||
| if err != nil { | |||||
| return toBeDeleted, err | |||||
| } | |||||
| } | } | ||||
| return sess.Commit() | |||||
| return toBeDeleted, nil | |||||
| } | } | ||||
| // UpdateIssueUserByRead updates issue-user relation for reading. | // UpdateIssueUserByRead updates issue-user relation for reading. | ||||
| @@ -32,23 +32,6 @@ func Test_newIssueUsers(t *testing.T) { | |||||
| AssertExistsAndLoadBean(t, &IssueUser{IssueID: newIssue.ID, UID: repo.OwnerID}) | AssertExistsAndLoadBean(t, &IssueUser{IssueID: newIssue.ID, UID: repo.OwnerID}) | ||||
| } | } | ||||
| func TestUpdateIssueUserByAssignee(t *testing.T) { | |||||
| assert.NoError(t, PrepareTestDatabase()) | |||||
| issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) | |||||
| // artificially change assignee in issue_user table | |||||
| AssertSuccessfulInsert(t, &IssueUser{IssueID: issue.ID, UID: 5, IsAssigned: true}) | |||||
| _, err := x.Cols("is_assigned"). | |||||
| Update(&IssueUser{IsAssigned: false}, &IssueUser{IssueID: issue.ID, UID: issue.AssigneeID}) | |||||
| assert.NoError(t, err) | |||||
| assert.NoError(t, UpdateIssueUserByAssignee(issue)) | |||||
| // issue_user table should now be correct again | |||||
| AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: issue.AssigneeID}, "is_assigned=1") | |||||
| AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: 5}, "is_assigned=0") | |||||
| } | |||||
| func TestUpdateIssueUserByRead(t *testing.T) { | func TestUpdateIssueUserByRead(t *testing.T) { | ||||
| assert.NoError(t, PrepareTestDatabase()) | assert.NoError(t, PrepareTestDatabase()) | ||||
| issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) | issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) | ||||
| @@ -180,6 +180,8 @@ var migrations = []Migration{ | |||||
| NewMigration("add last used passcode column for TOTP", addLastUsedPasscodeTOTP), | NewMigration("add last used passcode column for TOTP", addLastUsedPasscodeTOTP), | ||||
| // v63 -> v64 | // v63 -> v64 | ||||
| NewMigration("add language column for user setting", addLanguageSetting), | NewMigration("add language column for user setting", addLanguageSetting), | ||||
| // v64 -> v65 | |||||
| NewMigration("add multiple assignees", addMultipleAssignees), | |||||
| } | } | ||||
| // Migrate database to current version | // Migrate database to current version | ||||
| @@ -229,7 +231,7 @@ Please try to upgrade to a lower version (>= v0.6.0) first, then upgrade to curr | |||||
| return nil | return nil | ||||
| } | } | ||||
| func dropTableColumns(x *xorm.Engine, tableName string, columnNames ...string) (err error) { | |||||
| func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...string) (err error) { | |||||
| if tableName == "" || len(columnNames) == 0 { | if tableName == "" || len(columnNames) == 0 { | ||||
| return nil | return nil | ||||
| } | } | ||||
| @@ -245,17 +247,10 @@ func dropTableColumns(x *xorm.Engine, tableName string, columnNames ...string) ( | |||||
| } | } | ||||
| cols += "DROP COLUMN `" + col + "`" | cols += "DROP COLUMN `" + col + "`" | ||||
| } | } | ||||
| if _, err := x.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil { | |||||
| if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil { | |||||
| return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err) | return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err) | ||||
| } | } | ||||
| case setting.UseMSSQL: | case setting.UseMSSQL: | ||||
| sess := x.NewSession() | |||||
| defer sess.Close() | |||||
| if err = sess.Begin(); err != nil { | |||||
| return err | |||||
| } | |||||
| cols := "" | cols := "" | ||||
| for _, col := range columnNames { | for _, col := range columnNames { | ||||
| if cols != "" { | if cols != "" { | ||||
| @@ -9,5 +9,15 @@ import ( | |||||
| ) | ) | ||||
| func removeIsOwnerColumnFromOrgUser(x *xorm.Engine) (err error) { | func removeIsOwnerColumnFromOrgUser(x *xorm.Engine) (err error) { | ||||
| return dropTableColumns(x, "org_user", "is_owner", "num_teams") | |||||
| sess := x.NewSession() | |||||
| defer sess.Close() | |||||
| if err = sess.Begin(); err != nil { | |||||
| return err | |||||
| } | |||||
| if err := dropTableColumns(sess, "org_user", "is_owner", "num_teams"); err != nil { | |||||
| return err | |||||
| } | |||||
| return sess.Commit() | |||||
| } | } | ||||
| @@ -0,0 +1,129 @@ | |||||
| // Copyright 2018 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package migrations | |||||
| import ( | |||||
| "code.gitea.io/gitea/modules/util" | |||||
| "github.com/go-xorm/xorm" | |||||
| ) | |||||
| func addMultipleAssignees(x *xorm.Engine) error { | |||||
| // Redeclare issue struct | |||||
| type Issue struct { | |||||
| ID int64 `xorm:"pk autoincr"` | |||||
| RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"` | |||||
| Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository. | |||||
| PosterID int64 `xorm:"INDEX"` | |||||
| Title string `xorm:"name"` | |||||
| Content string `xorm:"TEXT"` | |||||
| MilestoneID int64 `xorm:"INDEX"` | |||||
| Priority int | |||||
| AssigneeID int64 `xorm:"INDEX"` | |||||
| IsClosed bool `xorm:"INDEX"` | |||||
| IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not. | |||||
| NumComments int | |||||
| Ref string | |||||
| DeadlineUnix util.TimeStamp `xorm:"INDEX"` | |||||
| CreatedUnix util.TimeStamp `xorm:"INDEX created"` | |||||
| UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` | |||||
| ClosedUnix util.TimeStamp `xorm:"INDEX"` | |||||
| } | |||||
| allIssues := []Issue{} | |||||
| err := x.Find(&allIssues) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| // Create the table | |||||
| type IssueAssignees struct { | |||||
| ID int64 `xorm:"pk autoincr"` | |||||
| AssigneeID int64 `xorm:"INDEX"` | |||||
| IssueID int64 `xorm:"INDEX"` | |||||
| } | |||||
| err = x.Sync2(IssueAssignees{}) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| // Range over all issues and insert a new entry for each issue/assignee | |||||
| sess := x.NewSession() | |||||
| defer sess.Close() | |||||
| err = sess.Begin() | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| for _, issue := range allIssues { | |||||
| if issue.AssigneeID != 0 { | |||||
| _, err := sess.Insert(IssueAssignees{IssueID: issue.ID, AssigneeID: issue.AssigneeID}) | |||||
| if err != nil { | |||||
| sess.Rollback() | |||||
| return err | |||||
| } | |||||
| } | |||||
| } | |||||
| // Updated the comment table | |||||
| type Comment struct { | |||||
| ID int64 `xorm:"pk autoincr"` | |||||
| Type int | |||||
| PosterID int64 `xorm:"INDEX"` | |||||
| IssueID int64 `xorm:"INDEX"` | |||||
| LabelID int64 | |||||
| OldMilestoneID int64 | |||||
| MilestoneID int64 | |||||
| OldAssigneeID int64 | |||||
| AssigneeID int64 | |||||
| RemovedAssignee bool | |||||
| OldTitle string | |||||
| NewTitle string | |||||
| CommitID int64 | |||||
| Line int64 | |||||
| Content string `xorm:"TEXT"` | |||||
| RenderedContent string `xorm:"-"` | |||||
| CreatedUnix util.TimeStamp `xorm:"INDEX created"` | |||||
| UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` | |||||
| // Reference issue in commit message | |||||
| CommitSHA string `xorm:"VARCHAR(40)"` | |||||
| } | |||||
| if err := x.Sync2(Comment{}); err != nil { | |||||
| return err | |||||
| } | |||||
| // Migrate comments | |||||
| // First update everything to not have nulls in db | |||||
| if _, err := sess.Where("type = ?", 9).Cols("removed_assignee").Update(Comment{RemovedAssignee: false}); err != nil { | |||||
| return err | |||||
| } | |||||
| allAssignementComments := []Comment{} | |||||
| if err := sess.Where("type = ?", 9).Find(&allAssignementComments); err != nil { | |||||
| return err | |||||
| } | |||||
| for _, comment := range allAssignementComments { | |||||
| // Everytime where OldAssigneeID is > 0, the assignement was removed. | |||||
| if comment.OldAssigneeID > 0 { | |||||
| _, err = sess.ID(comment.ID).Update(Comment{RemovedAssignee: true}) | |||||
| } | |||||
| } | |||||
| if err := dropTableColumns(sess, "issue", "assignee_id"); err != nil { | |||||
| return err | |||||
| } | |||||
| if err := dropTableColumns(sess, "issue_user", "is_assigned"); err != nil { | |||||
| return err | |||||
| } | |||||
| return sess.Commit() | |||||
| } | |||||
| @@ -119,6 +119,7 @@ func init() { | |||||
| new(RepoIndexerStatus), | new(RepoIndexerStatus), | ||||
| new(LFSLock), | new(LFSLock), | ||||
| new(Reaction), | new(Reaction), | ||||
| new(IssueAssignees), | |||||
| ) | ) | ||||
| gonicNames := []string{"SSL", "UID"} | gonicNames := []string{"SSL", "UID"} | ||||
| @@ -198,6 +198,7 @@ func (pr *PullRequest) APIFormat() *api.PullRequest { | |||||
| Labels: apiIssue.Labels, | Labels: apiIssue.Labels, | ||||
| Milestone: apiIssue.Milestone, | Milestone: apiIssue.Milestone, | ||||
| Assignee: apiIssue.Assignee, | Assignee: apiIssue.Assignee, | ||||
| Assignees: apiIssue.Assignees, | |||||
| State: apiIssue.State, | State: apiIssue.State, | ||||
| Comments: apiIssue.Comments, | Comments: apiIssue.Comments, | ||||
| HTMLURL: pr.Issue.HTMLURL(), | HTMLURL: pr.Issue.HTMLURL(), | ||||
| @@ -719,7 +720,7 @@ func (pr *PullRequest) testPatch() (err error) { | |||||
| } | } | ||||
| // NewPullRequest creates new pull request with labels for repository. | // NewPullRequest creates new pull request with labels for repository. | ||||
| func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte) (err error) { | |||||
| func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte, assigneeIDs []int64) (err error) { | |||||
| sess := x.NewSession() | sess := x.NewSession() | ||||
| defer sess.Close() | defer sess.Close() | ||||
| if err = sess.Begin(); err != nil { | if err = sess.Begin(); err != nil { | ||||
| @@ -732,7 +733,11 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str | |||||
| LabelIDs: labelIDs, | LabelIDs: labelIDs, | ||||
| Attachments: uuids, | Attachments: uuids, | ||||
| IsPull: true, | IsPull: true, | ||||
| AssigneeIDs: assigneeIDs, | |||||
| }); err != nil { | }); err != nil { | ||||
| if IsErrUserDoesNotHaveAccessToRepo(err) { | |||||
| return err | |||||
| } | |||||
| return fmt.Errorf("newIssue: %v", err) | return fmt.Errorf("newIssue: %v", err) | ||||
| } | } | ||||
| @@ -600,9 +600,9 @@ func (repo *Repository) GetAssignees() (_ []*User, err error) { | |||||
| return repo.getAssignees(x) | return repo.getAssignees(x) | ||||
| } | } | ||||
| // GetAssigneeByID returns the user that has write access of repository by given ID. | |||||
| func (repo *Repository) GetAssigneeByID(userID int64) (*User, error) { | |||||
| return GetAssigneeByID(repo, userID) | |||||
| // GetUserIfHasWriteAccess returns the user that has write access of repository by given ID. | |||||
| func (repo *Repository) GetUserIfHasWriteAccess(userID int64) (*User, error) { | |||||
| 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. | |||||
| func GetAssigneeByID(repo *Repository, userID int64) (*User, error) { | |||||
| // GetUserIfHasWriteAccess returns the user with write access of repository by given ID. | |||||
| 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.clear_assignee = Clear assignee | |||||
| issues.new.no_assignee = No assignee | |||||
| issues.new.assignees = Assignees | |||||
| issues.new.clear_assignees = Clear assignees | |||||
| 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 | ||||
| @@ -179,81 +179,115 @@ function initCommentForm() { | |||||
| initBranchSelector(); | initBranchSelector(); | ||||
| initCommentPreviewTab($('.comment.form')); | initCommentPreviewTab($('.comment.form')); | ||||
| // Labels | |||||
| var $list = $('.ui.labels.list'); | |||||
| var $noSelect = $list.find('.no-select'); | |||||
| var $labelMenu = $('.select-label .menu'); | |||||
| var hasLabelUpdateAction = $labelMenu.data('action') == 'update'; | |||||
| $('.select-label').dropdown('setting', 'onHide', function(){ | |||||
| if (hasLabelUpdateAction) { | |||||
| location.reload(); | |||||
| } | |||||
| }); | |||||
| $labelMenu.find('.item:not(.no-select)').click(function () { | |||||
| if ($(this).hasClass('checked')) { | |||||
| $(this).removeClass('checked'); | |||||
| $(this).find('.octicon').removeClass('octicon-check'); | |||||
| // Listsubmit | |||||
| function initListSubmits(selector, outerSelector) { | |||||
| var $list = $('.ui.' + outerSelector + '.list'); | |||||
| var $noSelect = $list.find('.no-select'); | |||||
| var $listMenu = $('.' + selector + ' .menu'); | |||||
| var hasLabelUpdateAction = $listMenu.data('action') == 'update'; | |||||
| $('.' + selector).dropdown('setting', 'onHide', function(){ | |||||
| hasLabelUpdateAction = $listMenu.data('action') == 'update'; // Update the var | |||||
| if (hasLabelUpdateAction) { | if (hasLabelUpdateAction) { | ||||
| updateIssuesMeta( | |||||
| $labelMenu.data('update-url'), | |||||
| "detach", | |||||
| $labelMenu.data('issue-id'), | |||||
| $(this).data('id') | |||||
| ); | |||||
| location.reload(); | |||||
| } | } | ||||
| } else { | |||||
| $(this).addClass('checked'); | |||||
| $(this).find('.octicon').addClass('octicon-check'); | |||||
| if (hasLabelUpdateAction) { | |||||
| }); | |||||
| $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( | updateIssuesMeta( | ||||
| $labelMenu.data('update-url'), | |||||
| "attach", | |||||
| $labelMenu.data('issue-id'), | |||||
| $listMenu.data('update-url'), | |||||
| "", | |||||
| $listMenu.data('issue-id'), | |||||
| $(this).data('id') | $(this).data('id') | ||||
| ); | ); | ||||
| $listMenu.data('action', 'update'); // Update to reload the page when we updated items | |||||
| return false; | |||||
| } | } | ||||
| } | |||||
| var labelIds = []; | |||||
| $(this).parent().find('.item').each(function () { | |||||
| if ($(this).hasClass('checked')) { | if ($(this).hasClass('checked')) { | ||||
| labelIds.push($(this).data('id')); | |||||
| $($(this).data('id-selector')).removeClass('hide'); | |||||
| $(this).removeClass('checked'); | |||||
| $(this).find('.octicon').removeClass('octicon-check'); | |||||
| if (hasLabelUpdateAction) { | |||||
| updateIssuesMeta( | |||||
| $listMenu.data('update-url'), | |||||
| "detach", | |||||
| $listMenu.data('issue-id'), | |||||
| $(this).data('id') | |||||
| ); | |||||
| } | |||||
| } else { | |||||
| $(this).addClass('checked'); | |||||
| $(this).find('.octicon').addClass('octicon-check'); | |||||
| if (hasLabelUpdateAction) { | |||||
| updateIssuesMeta( | |||||
| $listMenu.data('update-url'), | |||||
| "attach", | |||||
| $listMenu.data('issue-id'), | |||||
| $(this).data('id') | |||||
| ); | |||||
| } | |||||
| } | |||||
| var listIds = []; | |||||
| $(this).parent().find('.item').each(function () { | |||||
| if ($(this).hasClass('checked')) { | |||||
| listIds.push($(this).data('id')); | |||||
| $($(this).data('id-selector')).removeClass('hide'); | |||||
| } else { | |||||
| $($(this).data('id-selector')).addClass('hide'); | |||||
| } | |||||
| }); | |||||
| if (listIds.length == 0) { | |||||
| $noSelect.removeClass('hide'); | |||||
| } else { | } else { | ||||
| $($(this).data('id-selector')).addClass('hide'); | |||||
| $noSelect.addClass('hide'); | |||||
| } | } | ||||
| $($(this).parent().data('id')).val(listIds.join(",")); | |||||
| return false; | |||||
| }); | }); | ||||
| if (labelIds.length == 0) { | |||||
| $listMenu.find('.no-select.item').click(function () { | |||||
| if (hasLabelUpdateAction || selector == 'select-assignees-modify') { | |||||
| updateIssuesMeta( | |||||
| $listMenu.data('update-url'), | |||||
| "clear", | |||||
| $listMenu.data('issue-id'), | |||||
| "" | |||||
| ); | |||||
| $listMenu.data('action', 'update'); // Update to reload the page when we updated items | |||||
| } | |||||
| $(this).parent().find('.item').each(function () { | |||||
| $(this).removeClass('checked'); | |||||
| $(this).find('.octicon').removeClass('octicon-check'); | |||||
| }); | |||||
| $list.find('.item').each(function () { | |||||
| $(this).addClass('hide'); | |||||
| }); | |||||
| $noSelect.removeClass('hide'); | $noSelect.removeClass('hide'); | ||||
| } else { | |||||
| $noSelect.addClass('hide'); | |||||
| } | |||||
| $($(this).parent().data('id')).val(labelIds.join(",")); | |||||
| return false; | |||||
| }); | |||||
| $labelMenu.find('.no-select.item').click(function () { | |||||
| if (hasLabelUpdateAction) { | |||||
| updateIssuesMeta( | |||||
| $labelMenu.data('update-url'), | |||||
| "clear", | |||||
| $labelMenu.data('issue-id'), | |||||
| "" | |||||
| ); | |||||
| } | |||||
| $($(this).parent().data('id')).val(''); | |||||
| $(this).parent().find('.item').each(function () { | |||||
| $(this).removeClass('checked'); | |||||
| $(this).find('.octicon').removeClass('octicon-check'); | |||||
| }); | }); | ||||
| } | |||||
| $list.find('.item').each(function () { | |||||
| $(this).addClass('hide'); | |||||
| }); | |||||
| $noSelect.removeClass('hide'); | |||||
| $($(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-right: -7px; | |||||
| margin: 5px -7px 0 -5px; | |||||
| width: 16px; | |||||
| } | |||||
| .text{ | |||||
| margin-left: 0.9em; | |||||
| } | } | ||||
| .menu { | .menu { | ||||
| max-height: 300px; | max-height: 300px; | ||||
| @@ -1745,4 +1748,4 @@ tbody.commit-list { | |||||
| #repo-topic { | #repo-topic { | ||||
| margin-top: 5px; | margin-top: 5px; | ||||
| } | |||||
| } | |||||
| @@ -178,25 +178,22 @@ func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) { | |||||
| DeadlineUnix: deadlineUnix, | DeadlineUnix: deadlineUnix, | ||||
| } | } | ||||
| if ctx.Repo.IsWriter() { | |||||
| if len(form.Assignee) > 0 { | |||||
| assignee, 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 | |||||
| } | |||||
| issue.AssigneeID = assignee.ID | |||||
| // 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) | |||||
| } | } | ||||
| issue.MilestoneID = form.Milestone | |||||
| } else { | |||||
| form.Labels = nil | |||||
| return | |||||
| } | } | ||||
| 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 && | |||||
| (issue.Assignee == nil || issue.Assignee.LowerName != strings.ToLower(*form.Assignee)) { | |||||
| if len(*form.Assignee) == 0 { | |||||
| issue.AssigneeID = 0 | |||||
| } else { | |||||
| assignee, 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 | |||||
| } | |||||
| issue.AssigneeID = assignee.ID | |||||
| // Add/delete assignees | |||||
| // 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. | |||||
| if ctx.Repo.IsWriter() && (form.Assignees != nil || form.Assignee != nil) { | |||||
| oneAssignee := "" | |||||
| if form.Assignee != nil { | |||||
| oneAssignee = *form.Assignee | |||||
| } | } | ||||
| if err = models.UpdateIssueUserByAssignee(issue); err != nil { | |||||
| ctx.Error(500, "UpdateIssueUserByAssignee", err) | |||||
| err = models.UpdateAPIAssignee(issue, oneAssignee, form.Assignees, ctx.User) | |||||
| 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 && | |||||
| (issue.Assignee == nil || issue.Assignee.LowerName != strings.ToLower(form.Assignee)) { | |||||
| if len(form.Assignee) == 0 { | |||||
| issue.AssigneeID = 0 | |||||
| } else { | |||||
| assignee, 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 | |||||
| } | |||||
| issue.AssigneeID = assignee.ID | |||||
| } | |||||
| // Add/delete assignees | |||||
| if err = models.UpdateIssueUserByAssignee(issue); err != nil { | |||||
| ctx.Error(500, "UpdateIssueUserByAssignee", err) | |||||
| // 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. | |||||
| 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 models.IsErrUserNotExist(err) { | |||||
| ctx.Error(422, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) | |||||
| } else { | |||||
| ctx.Error(500, "UpdateAPIAssignee", err) | |||||
| } | |||||
| return | 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. | |||||
| assigneeID := form.AssigneeID | |||||
| if assigneeID > 0 { | |||||
| ctx.Data["Assignee"], err = repo.GetAssigneeByID(assigneeID) | |||||
| // Check assignees | |||||
| var assigneeIDs []int64 | |||||
| if len(form.AssigneeIDs) > 0 { | |||||
| assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ",")) | |||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("GetAssigneeByID", err) | |||||
| return nil, 0, 0 | |||||
| return nil, nil, 0 | |||||
| } | |||||
| // 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 | |||||
| } | |||||
| } | } | ||||
| ctx.Data["assignee_id"] = assigneeID | |||||
| } | } | ||||
| return labelIDs, milestoneID, assigneeID | |||||
| // 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 { | |||||
| ctx.ServerError("LoadAssignees", err) | |||||
| if err = comment.LoadAssigneeUser(); err != nil { | |||||
| ctx.ServerError("LoadAssigneeUser", err) | |||||
| return | return | ||||
| } | } | ||||
| } | } | ||||
| @@ -912,13 +927,20 @@ 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 { | |||||
| continue | |||||
| } | |||||
| if err := issue.ChangeAssignee(ctx.User, assigneeID); err != nil { | |||||
| ctx.ServerError("ChangeAssignee", err) | |||||
| return | |||||
| switch action { | |||||
| 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 { | |||||
| ctx.ServerError("ChangeAssignee", err) | |||||
| return | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| ctx.JSON(200, map[string]interface{}{ | ctx.JSON(200, map[string]interface{}{ | ||||
| @@ -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}} | |||||
| <a class="ui right assignee poping up" href="{{.Assignee.HomeLink}}" data-content="{{.Assignee.Name}}" data-variation="inverted" data-position="left center"> | |||||
| <img class="ui avatar image" src="{{.Assignee.RelAvatarLink}}"> | |||||
| {{range .Assignees}} | |||||
| <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="{{.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-assignees dropdown"> | |||||
| <span class="text"> | |||||
| <strong>{{.i18n.Tr "repo.issues.new.assignees"}}</strong> | |||||
| <span class="octicon octicon-gear"></span> | |||||
| </span> | |||||
| <div class="filter menu" data-id="#assignee_ids"> | |||||
| <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"> | <div class="ui {{if not .Assignees}}disabled{{end}} floating jump select-assignee 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="no-select item">{{.i18n.Tr "repo.issues.new.clear_assignee"}}</div> | |||||
| <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}}"> | |||||
| <img src="{{.Poster.RelAvatarLink}}"> | |||||
| </a> <span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> {{$.i18n.Tr "repo.issues.self_assign_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> {{$.i18n.Tr "repo.issues.add_assignee_at" .Poster.Name $createdStr | Safe}} </span>{{end}}{{else if gt .OldAssigneeID 0}} | |||||
| <a class="ui avatar image" href="{{.Poster.HomeLink}}"> | |||||
| <img src="{{.Poster.RelAvatarLink}}"> | |||||
| </a> <span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> {{$.i18n.Tr "repo.issues.remove_assignee_at" $createdStr | Safe}} </span>{{end}} | |||||
| {{if gt .AssigneeID 0}} | |||||
| {{if .RemovedAssignee}} | |||||
| <a class="ui avatar image" href="{{.Assignee.HomeLink}}"> | |||||
| <img src="{{.Assignee.RelAvatarLink}}"> | |||||
| </a> | |||||
| <span class="text grey"> | |||||
| <a href="{{.Assignee.HomeLink}}">{{.Assignee.Name}}</a> | |||||
| {{$.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="no-select item">{{.i18n.Tr "repo.issues.new.clear_assignee"}}</div> | |||||
| <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_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"> | |||||
| <span class="no-select item {{if .Issue.Assignee}}hide{{end}}">{{.i18n.Tr "repo.issues.new.no_assignee"}}</span> | |||||
| <div class="ui assignees list"> | |||||
| <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}} | |||||
| <a class="item" href="{{$.RepoLink}}/issues?assignee={{.Issue.Assignee.ID}}"><img class="ui avatar image" src="{{.Issue.Assignee.RelAvatarLink}}"> {{.Issue.Assignee.Name}}</a> | |||||
| {{range .Issue.Assignees}} | |||||
| <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> | ||||