Commit b9270cb9 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'multiple_assignee_backend1' of gitlab.com:gitlab-org/gitlab-ee...

Merge branch 'multiple_assignee_backend1' of gitlab.com:gitlab-org/gitlab-ee into multiple_assignees_review
parents 615e51bb 00abd3a1
......@@ -231,6 +231,17 @@ class IssuableFinder
klass.all
end
def by_scope(items)
case params[:scope]
when 'created-by-me', 'authored'
items.where(author_id: current_user.id)
when 'assigned-to-me'
items.assigned_to(current_user)
else
items
end
end
def by_state(items)
case params[:state].to_s
when 'closed'
......
......@@ -28,40 +28,26 @@ class IssuesFinder < IssuableFinder
def by_assignee(items)
if assignee
items = items.where("issue_assignees.user_id = ?", assignee.id)
items.assigned_to(assignee)
elsif no_assignee?
items = items.where("issue_assignees.user_id is NULL")
items.unassigned
elsif assignee_id? || assignee_username? # assignee not found
items = items.none
end
items
end
def by_scope(items)
case params[:scope]
when 'created-by-me', 'authored'
items.where(author_id: current_user.id)
when 'assigned-to-me'
items.where("issue_assignees.user_id = ?", current_user.id)
items.none
else
items
end
end
def self.not_restricted_by_confidentiality(user)
issues = Issue.with_assignees
return issues.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
return Issue.where('issues.confidential IS NOT TRUE') if user.blank?
return issues.all if user.admin_or_auditor?
return Issue.all if user.admin_or_auditor?
issues.where('
issues.confidential IS NULL
OR issues.confidential IS FALSE
Issue.where('
issues.confidential IS NOT TRUE
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
OR issue_assignees.user_id = :user_id
OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
OR issues.project_id IN(:project_ids)))',
user_id: user.id,
project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
......
......@@ -23,17 +23,6 @@ class MergeRequestsFinder < IssuableFinder
private
def by_scope(items)
case params[:scope]
when 'created-by-me', 'authored'
items.where(author_id: current_user.id)
when 'assigned-to-me'
items.where(assignee_id: current_user.id)
else
items
end
end
def item_project_ids(items)
items&.reorder(nil)&.select(:target_project_id)
end
......
......@@ -40,7 +40,7 @@ module Milestoneish
def issues_visible_to_user(user)
memoize_per_user(user, :issues_visible_to_user) do
IssuesFinder.new(user, issues_finder_params)
.execute.where(milestone_id: milestoneish_ids)
.execute.includes(:assignees).where(milestone_id: milestoneish_ids)
end
end
......
......@@ -36,7 +36,7 @@ class GlobalMilestone
closed = count_by_state(milestones_by_state_and_title, 'closed')
all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
{
{
opened: opened,
closed: closed,
all: all
......@@ -94,7 +94,7 @@ class GlobalMilestone
end
def participants
@participants ||= milestones.includes(:participants).map(&:participants).flatten.compact.uniq
@participants ||= milestones.map(&:participants).flatten.uniq
end
def labels
......
......@@ -34,13 +34,11 @@ class Issue < ActiveRecord::Base
validates :project, presence: true
scope :cared, ->(user) { with_assignees.where("issue_assignees.user_id IN(?)", user.id) }
scope :open_for, ->(user) { opened.assigned_to(user) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
scope :with_assignees, -> { joins("LEFT JOIN issue_assignees ON issue_id = issues.id") }
scope :assigned, -> { with_assignees.where('issue_assignees.user_id IS NOT NULL') }
scope :unassigned, -> { with_assignees.where('issue_assignees.user_id IS NULL') }
scope :assigned_to, ->(u) { with_assignees.where('issue_assignees.user_id = ?', u.id)}
scope :assigned, -> { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)}
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
......
......@@ -106,7 +106,7 @@ class User < ActiveRecord::Base
has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ProtectedBranch::PushAccessLevel
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id
has_many :issue_assignees, dependent: :destroy
has_many :issue_assignees
has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
......
......@@ -22,9 +22,9 @@ module Issues
end
def filter_assignee(issuable)
return if params[:assignee_ids].to_a.empty?
return if params[:assignee_ids].blank?
assignee_ids = params[:assignee_ids].select{ |assignee_id| assignee_can_read?(issuable, assignee_id)}
assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE]
params[:assignee_ids] = []
......
......@@ -21,7 +21,7 @@ module Issues
def csv_builder
@csv_builder ||=
CsvBuilder.new(@issues.includes(:author), header_to_value_hash)
CsvBuilder.new(@issues.includes(:author, :assignees), header_to_value_hash)
end
private
......@@ -35,8 +35,8 @@ module Issues
'Description' => 'description',
'Author' => 'author_name',
'Author Username' => -> (issue) { issue.author&.username },
'Assignee' => -> (issue) { issue.assignees.pluck(:name).join(', ') },
'Assignee Username' => -> (issue) { issue.assignees.pluck(:username).join(', ') },
'Assignee' => -> (issue) { issue.assignees.map(&:name).join(', ') },
'Assignee Username' => -> (issue) { issue.assignees.map(&:username).join(', ') },
'Confidential' => -> (issue) { issue.confidential? ? 'Yes' : 'No' },
'Due Date' => -> (issue) { issue.due_date&.to_s(:csv) },
'Created At (UTC)' => -> (issue) { issue.created_at&.to_s(:csv) },
......
......@@ -22,7 +22,7 @@ module Issues
end
if issue.previous_changes.include?('title') ||
issue.previous_changes.include?('description')
issue.previous_changes.include?('description')
todo_service.update_issue(issue, current_user)
end
......
......@@ -4,7 +4,7 @@ module MergeRequests
@assignable_issues ||= begin
if current_user == merge_request.author
closes_issues.select do |issue|
!issue.is_a?(ExternalIssue) && !issue.assignees.any? && can?(current_user, :admin_issue, issue)
!issue.is_a?(ExternalIssue) && !issue.assignees.present? && can?(current_user, :admin_issue, issue)
end
else
[]
......
......@@ -29,7 +29,7 @@ class NotificationRecipientService
recipients << target.assignee
when :reassign_issue
previous_assignees = Array(previous_assignee)
recipients.concat(previous_assignees) if previous_assignees.any?
recipients.concat(previous_assignees)
recipients.concat(target.assignees)
end
......
......@@ -86,15 +86,18 @@ module SlashCommands
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :assign do |assignee_param|
user = extract_references(assignee_param, :user).first
user ||= User.find_by(username: assignee_param)
user_ids = extract_references(assignee_param, :user).map(&:id)
next unless user
if user_ids.empty?
user_ids = User.where(username: assignee_param.split(' ').map(&:strip)).pluck(:id)
end
next if user_ids.empty?
if issuable.is_a?(Issue)
@updates[:assignee_ids] = [user.id]
@updates[:assignee_ids] = user_ids
else
@updates[:assignee_id] = user.id
@updates[:assignee_id] = user_ids.last
end
end
......
......@@ -54,7 +54,7 @@ module SystemNoteService
# issue - Issue object
# project - Project owning noteable
# author - User performing the change
# assignees - User being assigned, or nil
# assignees - Users being assigned, or nil
#
# Example Note text:
#
......
Reassigned Issue <%= @issue.iid %>
<%= url_for([@issue.project.namespace.becomes(Namespace), @issue.project, @issue, { only_path: false }]) %>
Assignee changed <%= "from #{@previous_assignees.map(&:name).to_sentence}" if @previous_assignees.any? -%>
to <%= "#{@issue.assignees.any? ? @issue.assignee_list : 'Unassigned'}" %>
......@@ -7,6 +7,6 @@
- if @issue.description
= markdown(@issue.description, pipeline: :email, author: @issue.author)
- if @issue.assignees.any?
- if @issue.assignees.present?
%p
Assignee: #{@issue.assignee_list}
......@@ -8,4 +8,3 @@
%strong= @issue.assignee_list
- else
%strong Unassigned
<%= render 'reassigned_issuable_email', issuable: @issue %>
Reassigned Issue <%= @issue.iid %>
<%= url_for([@issue.project.namespace.becomes(Namespace), @issue.project, @issue, { only_path: false }]) %>
Assignee changed <%= "from #{@previous_assignees.map(&:name).to_sentence}" if @previous_assignees.any? -%>
to <%= "#{@issue.assignees.any? ? @issue.assignee_list : 'Unassigned'}" %>
= render 'reassigned_issuable_email', issuable: @merge_request
Reassigned Merge Request #{ @merge_request.iid }
= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }])
Assignee changed
- if @previous_assignee
from #{@previous_assignee.name}
to
= @merge_request.assignee_id ? @merge_request.assignee_name : 'Unassigned'
......@@ -26,7 +26,7 @@ class CreateIssueAssigneesTable < ActiveRecord::Migration
# disable_ddl_transaction!
def up
create_table :issue_assignees do |t|
create_table :issue_assignees, id: false do |t|
t.references :user, foreign_key: { on_delete: :cascade }, index: true, null: false
t.references :issue, foreign_key: { on_delete: :cascade }, null: false
end
......@@ -35,10 +35,6 @@ class CreateIssueAssigneesTable < ActiveRecord::Migration
end
def down
if index_exists?(:issue_assignees, name: INDEX_NAME)
remove_index :issue_assignees, name: INDEX_NAME
end
drop_table :issue_assignees
end
end
......@@ -21,9 +21,23 @@ class MigrateAssignees < ActiveRecord::Migration
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
disable_ddl_transaction!
def up
# Optimisation: this accounts for most of the invalid assignee IDs on GitLab.com
update_column_in_batches(:issues, :assignee_id, nil) do |table, query|
query.where(table[:assignee_id].eq(0))
end
users = Arel::Table.new(:users)
update_column_in_batches(:issues, :assignee_id, nil) do |table, query|
query.where(table[:assignee_id].not_eq(nil)\
.and(
users.project("true").where(users[:id].eq(table[:assignee_id])).exists.not
))
end
execute <<-EOF
INSERT INTO issue_assignees(issue_id, user_id)
SELECT id, assignee_id FROM issues WHERE assignee_id IS NOT NULL
......
......@@ -495,7 +495,7 @@ ActiveRecord::Schema.define(version: 20170320173259) do
add_index "index_statuses", ["project_id"], name: "index_index_statuses_on_project_id", unique: true, using: :btree
create_table "issue_assignees", force: :cascade do |t|
create_table "issue_assignees", id: false, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "issue_id", null: false
end
......
......@@ -68,14 +68,14 @@ Example response:
"updated_at" : "2016-01-04T15:31:39.996Z"
},
"project_id" : 1,
"assignee" : {
"assignees" : [{
"state" : "active",
"id" : 1,
"name" : "Administrator",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root"
},
}],
"updated_at" : "2016-01-04T15:31:51.081Z",
"id" : 76,
"title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
......@@ -150,14 +150,14 @@ Example response:
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
"state" : "closed",
"iid" : 1,
"assignee" : {
"assignees" : [{
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
"name" : "Dr. Luella Kovacek"
},
}],
"labels" : [],
"id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
......@@ -231,14 +231,14 @@ Example response:
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
"state" : "closed",
"iid" : 1,
"assignee" : {
"assignees" : [{
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
"name" : "Dr. Luella Kovacek"
},
}],
"labels" : [],
"id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
......@@ -297,14 +297,14 @@ Example response:
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
"state" : "closed",
"iid" : 1,
"assignee" : {
"assignees" : [{
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
"name" : "Dr. Luella Kovacek"
},
}],
"labels" : [],
"id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
......@@ -333,7 +333,7 @@ POST /projects/:id/issues
| `title` | string | yes | The title of an issue |
| `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
| `assignee_id` | integer | no | The ID of a user to assign issue |
| `assignee_ids` | Array[integer] | no | The ID of a user to assign issue |
| `milestone_id` | integer | no | The ID of a milestone to assign issue |
| `labels` | string | no | Comma-separated label names for an issue |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
......@@ -356,7 +356,7 @@ Example response:
"iid" : 14,
"title" : "Issues with auth",
"state" : "opened",
"assignee" : null,
"assignees" : [],
"labels" : [
"bug"
],
......@@ -396,7 +396,7 @@ PUT /projects/:id/issues/:issue_iid
| `title` | string | no | The title of an issue |
| `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Updates an issue to be confidential |
| `assignee_id` | integer | no | The ID of a user to assign the issue to |
| `assignee_ids` | Array[integer] | no | The ID of a user to assign the issue to |
| `milestone_id` | integer | no | The ID of a milestone to assign the issue to |
| `labels` | string | no | Comma-separated label names for an issue |
| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
......@@ -431,7 +431,7 @@ Example response:
"bug"
],
"id" : 85,
"assignee" : null,
"assignees" : [],
"milestone" : null,
"subscribed" : true,
"user_notes_count": 0,
......@@ -496,14 +496,14 @@ Example response:
"updated_at": "2016-04-07T12:20:17.596Z",
"labels": [],
"milestone": null,
"assignee": {
"assignees": [{
"name": "Miss Monserrate Beier",
"username": "axel.block",
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
"web_url": "https://gitlab.example.com/axel.block"
},
}],
"author": {
"name": "Kris Steuber",
"username": "solon.cremin",
......@@ -552,14 +552,14 @@ Example response:
"updated_at": "2016-04-07T12:20:17.596Z",
"labels": [],
"milestone": null,
"assignee": {
"assignees": [{
"name": "Miss Monserrate Beier",
"username": "axel.block",
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
"web_url": "https://gitlab.example.com/axel.block"
},
}],
"author": {
"name": "Kris Steuber",
"username": "solon.cremin",
......@@ -656,14 +656,14 @@ Example response:
"updated_at": "2016-06-17T07:47:33.832Z",
"due_date": null
},
"assignee": {
"assignees": [{
"name": "Jarret O'Keefe",
"username": "francisca",
"id": 14,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
"web_url": "https://gitlab.example.com/francisca"
},
}],
"author": {
"name": "Maxie Medhurst",
"username": "craig_rutherford",
......
......@@ -232,7 +232,7 @@ X-Gitlab-Event: Issue Hook
"object_attributes": {
"id": 301,
"title": "New API: create/update/delete file",
"assignee_id": 51,
"assignee_ids": [51],
"author_id": 51,
"project_id": 14,
"created_at": "2013-12-03T17:15:43Z",
......@@ -246,11 +246,11 @@ X-Gitlab-Event: Issue Hook
"url": "http://example.com/diaspora/issues/23",
"action": "open"
},
"assignee": {
"assignees": [{
"name": "User1",
"username": "user1",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
}],
"labels": [{
"id": 206,
"title": "API",
......@@ -544,7 +544,7 @@ X-Gitlab-Event: Note Hook
"issue": {
"id": 92,
"title": "test",
"assignee_id": null,
"assignee_ids": [],
"author_id": 1,
"project_id": 5,
"created_at": "2015-04-12 14:53:17 UTC",
......
......@@ -295,6 +295,13 @@ module API
expose :project_id, :issues_events, :merge_requests_events
expose :note_events, :build_events, :pipeline_events, :wiki_page_events
end
class Issue < ::API::Entities::Issue
unexpose :assignees
expose :assignee do |issue, options|
::API::Entities::UserBasic.represent(issue.assignees.first, options)
end
end
end
end
end
......@@ -14,6 +14,14 @@ module API
authorize! access_level, merge_request
merge_request
end
def convert_parameters_from_legacy_format(params)
if params[:assignee_id].present?
params[:assignee_ids] = [params.delete(:assignee_id)]
end
params
end
end
end
end
......@@ -8,6 +8,7 @@ module API
helpers do
def find_issues(args = {})
args = params.merge(args)
args = convert_parameters_from_legacy_format(args)
args.delete(:id)
args[:milestone_title] = args.delete(:milestone)
......@@ -53,7 +54,7 @@ module API
resource :issues do
desc "Get currently authenticated user's issues" do
success ::API::Entities::Issue
success ::API::V3::Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
......@@ -62,7 +63,7 @@ module API
end
get do
issues = find_issues(scope: 'authored')
present paginate(issues), with: ::API::Entities::Issue, current_user: current_user
present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user
end
end
......@@ -71,7 +72,7 @@ module API
end
resource :groups, requirements: { id: %r{[^/]+} } do
desc 'Get a list of group issues' do
success ::API::Entities::Issue
success ::API::V3::Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'opened',
......@@ -83,7 +84,7 @@ module API
issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true)
present paginate(issues), with: ::API::Entities::Issue, current_user: current_user
present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user
end
end
......@@ -95,7 +96,7 @@ module API
desc 'Get a list of project issues' do
detail 'iid filter is deprecated have been removed on V4'
success ::API::Entities::Issue
success ::API::V3::Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
......@@ -108,22 +109,22 @@ module API
issues = find_issues(project_id: project.id)
present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project
present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
end
desc 'Get a single project issue' do
success ::API::Entities::Issue
success ::API::V3::Entities::Issue
end
params do
requires :issue_id, type: Integer, desc: 'The ID of a project issue'
end
get ":id/issues/:issue_id" do
issue = find_project_issue(params[:issue_id])
present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
end
desc 'Create a new project issue' do
success ::API::Entities::Issue
success ::API::V3::Entities::Issue
end
params do
requires :title, type: String, desc: 'The title of an issue'
......@@ -141,6 +142,7 @@ module API
issue_params = declared_params(include_missing: false)
issue_params = issue_params.merge(merge_request_to_resolve_discussions_of: issue_params.delete(:merge_request_for_resolving_discussions))
issue_params = convert_parameters_from_legacy_format(issue_params)
issue = ::Issues::CreateService.new(user_project,
current_user,
......@@ -148,14 +150,14 @@ module API
render_spam_error! if issue.spam?
if issue.valid?
present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
else
render_validation_error!(issue)
end
end
desc 'Update an existing issue' do
success ::API::Entities::Issue
success ::API::V3::Entities::Issue
end
params do
requires :issue_id, type: Integer, desc: 'The ID of a project issue'
......@@ -178,6 +180,7 @@ module API
end
update_params = declared_params(include_missing: false).merge(request: request, api: true)
update_params = convert_parameters_from_legacy_format(update_params)
issue = ::Issues::UpdateService.new(user_project,
current_user,
......@@ -186,14 +189,14 @@ module API
render_spam_error! if issue.spam?
if issue.valid?
present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
else
render_validation_error!(issue)
end
end
desc 'Move an existing issue' do
success ::API::Entities::Issue
success ::API::V3::Entities::Issue
end
params do
requires :issue_id, type: Integer, desc: 'The ID of a project issue'
......@@ -208,7 +211,7 @@ module API
begin
issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project)
present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
rescue ::Issues::MoveService::MoveError => error
render_api_error!(error.message, 400)
end
......
......@@ -32,7 +32,7 @@ module API
if project.has_external_issue_tracker?
::API::Entities::ExternalIssue
else
::API::Entities::Issue
::API::V3::Entities::Issue
end
end
......
......@@ -39,7 +39,7 @@ module API
end
desc 'Get all issues for a single project milestone' do
success ::API::Entities::Issue
success ::API::V3::Entities::Issue
end
params do
requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
......@@ -56,7 +56,7 @@ module API
}
issues = IssuesFinder.new(current_user, finder_params).execute
present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project
present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
end
end
end
......
......@@ -1329,7 +1329,7 @@ describe Projects::MergeRequestsController do
end
it 'correctly pluralizes flash message on success' do
issue2.update!(assignees: [user])
issue2.assignees = [user]
post_assign_issues
......
......@@ -41,7 +41,7 @@ describe "Dashboard Issues Feed", feature: true do
expect(entry).to be_present
expect(entry).to have_selector('author email', text: issue2.author_public_email)
expect(entry).to have_selector('assignees email', text: issue2.assignees.first.public_email)
expect(entry).to have_selector('assignees email', text: assignee.public_email)
expect(entry).not_to have_selector('labels')
expect(entry).not_to have_selector('milestone')
expect(entry).to have_selector('description', text: issue2.description)
......@@ -64,7 +64,7 @@ describe "Dashboard Issues Feed", feature: true do
expect(entry).to be_present
expect(entry).to have_selector('author email', text: issue1.author_public_email)
expect(entry).to have_selector('assignees email', text: issue1.assignees.first.public_email)
expect(entry).to have_selector('assignees email', text: assignee.public_email)
expect(entry).to have_selector('labels label', text: label1.title)
expect(entry).to have_selector('milestone', text: milestone1.title)
expect(entry).not_to have_selector('description')
......
......@@ -22,7 +22,7 @@ describe 'Issues Feed', feature: true do
to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{project.name} issues")
expect(body).to have_selector('author email', text: issue.author_public_email)
expect(body).to have_selector('assignee email', text: issue.author_public_email)
expect(body).to have_selector('assignees email', text: issue.author_public_email)
expect(body).to have_selector('entry summary', text: issue.title)
end
end
......@@ -36,7 +36,7 @@ describe 'Issues Feed', feature: true do
to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{project.name} issues")
expect(body).to have_selector('author email', text: issue.author_public_email)
expect(body).to have_selector('assignee email', text: issue.author_public_email)
expect(body).to have_selector('assignees email', text: issue.author_public_email)
expect(body).to have_selector('entry summary', text: issue.title)
end
end
......
......@@ -80,6 +80,6 @@ describe 'Issues csv', feature: true do
author: user,
milestone: milestone,
labels: [feature_label, idea_label])
expect{ request_csv }.not_to exceed_query_limit(control_count + 23)
expect{ request_csv }.not_to exceed_query_limit(control_count + 5)
end
end
......@@ -6,7 +6,7 @@ describe 'Unsubscribe links', feature: true do
let(:recipient) { create(:user) }
let(:author) { create(:user) }
let(:project) { create(:empty_project, :public) }
let(:params) { { title: 'A bug!', description: 'Fix it!', assignee: recipient } }
let(:params) { { title: 'A bug!', description: 'Fix it!', assignees: [recipient] } }
let(:issue) { Issues::CreateService.new(project, author, params).execute }
let(:mail) { ActionMailer::Base.deliveries.last }
......
......@@ -31,13 +31,14 @@ describe Issue, elastic: true do
end
it "returns json with all needed elements" do
issue = create :issue, project: project
assignee = create(:user)
issue = create :issue, project: project, assignees: [assignee]
expected_hash = issue.attributes.extract!('id', 'iid', 'title', 'description', 'created_at',
'updated_at', 'state', 'project_id', 'author_id',
'confidential')
expected_hash['assignee_id'] = []
expected_hash['assignee_id'] = [assignee.id]
expect(issue.as_indexed_json).to eq(expected_hash)
end
......
......@@ -56,84 +56,6 @@ describe Issue, "Issuable" do
end
end
describe "before_save" do
describe "#update_cache_counts when an issue is reassigned" do
context "when previous assignee exists" do
before do
assignee = create(:user)
issue.project.team << [assignee, :developer]
issue.assignees << assignee
end
it "updates cache counts for new assignee" do
user = create(:user)
expect(user).to receive(:update_cache_counts)
issue.assignees << user
end
it "updates cache counts for previous assignee" do
old_assignee = issue.assignees.first
expect_any_instance_of(User).to receive(:update_cache_counts)
issue.assignees.destroy_all
end
end
context "when previous assignee does not exist" do
before{ issue.assignees = [] }
it "updates cache count for the new assignee" do
expect_any_instance_of(User).to receive(:update_cache_counts)
issue.assignees << user
end
end
end
describe "#update_cache_counts when a merge request is reassigned" do
let(:project) { create :project }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
context "when previous assignee exists" do
before do
assignee = create(:user)
project.team << [assignee, :developer]
merge_request.update(assignee: assignee)
end
it "updates cache counts for new assignee" do
user = create(:user)
expect(user).to receive(:update_cache_counts)
merge_request.update(assignee: user)
end
it "updates cache counts for previous assignee" do
old_assignee = merge_request.assignee
allow(User).to receive(:find_by_id).with(old_assignee.id).and_return(old_assignee)
expect(old_assignee).to receive(:update_cache_counts)
merge_request.update(assignee: nil)
end
end
context "when previous assignee does not exist" do
before { merge_request.update(assignee: nil) }
it "updates cache count for the new assignee" do
expect_any_instance_of(User).to receive(:update_cache_counts)
merge_request.update(assignee: user)
end
end
end
end
describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
......
......@@ -38,6 +38,46 @@ describe Issue, models: true do
end
end
describe "before_save" do
describe "#update_cache_counts when an issue is reassigned" do
let(:issue) { create(:issue) }
let(:assignee) { create(:user) }
context "when previous assignee exists" do
before do
issue.project.team << [assignee, :developer]
issue.assignees << assignee
end
it "updates cache counts for new assignee" do
user = create(:user)
expect(user).to receive(:update_cache_counts)
issue.assignees << user
end
it "updates cache counts for previous assignee" do
issue.assignees.first
expect_any_instance_of(User).to receive(:update_cache_counts)
issue.assignees.destroy_all
end
end
context "when previous assignee does not exist" do
it "updates cache count for the new assignee" do
issue.assignees = []
expect_any_instance_of(User).to receive(:update_cache_counts)
issue.assignees << assignee
end
end
end
end
describe '#card_attributes' do
it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
......
......@@ -88,6 +88,48 @@ describe MergeRequest, models: true do
end
end
describe "before_save" do
describe "#update_cache_counts when a merge request is reassigned" do
let(:project) { create :project }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:assignee) { create :user }
context "when previous assignee exists" do
before do
project.team << [assignee, :developer]
merge_request.update(assignee: assignee)
end
it "updates cache counts for new assignee" do
user = create(:user)
expect(user).to receive(:update_cache_counts)
merge_request.update(assignee: user)
end
it "updates cache counts for previous assignee" do
old_assignee = merge_request.assignee
allow(User).to receive(:find_by_id).with(old_assignee.id).and_return(old_assignee)
expect(old_assignee).to receive(:update_cache_counts)
merge_request.update(assignee: nil)
end
end
context "when previous assignee does not exist" do
it "updates cache count for the new assignee" do
merge_request.update(assignee: nil)
expect_any_instance_of(User).to receive(:update_cache_counts)
merge_request.update(assignee: assignee)
end
end
end
end
describe '#card_attributes' do
it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
......
......@@ -15,14 +15,14 @@ describe MergeRequests::AssignIssuesService, services: true do
expect(service.assignable_issues.map(&:id)).to include(issue.id)
end
it 'ignores issues already assigned to any user' do
issue.assignees = [create(:user)]
it 'ignores issues the user cannot update assignee on' do
project.team.truncate
expect(service.assignable_issues).to be_empty
end
it 'ignores issues the user cannot update assignee on' do
project.team.truncate
it 'ignores issues already assigned to any user' do
issue.assignees = [create(:user)]
expect(service.assignable_issues).to be_empty
end
......
......@@ -460,7 +460,7 @@ describe NotificationService, services: true do
it do
notification.new_issue(issue, @u_disabled)
should_email(issue.assignees.first)
should_email(assignee)
should_email(@u_watcher)
should_email(@u_guest_watcher)
should_email(@u_guest_custom)
......
......@@ -3,6 +3,7 @@ require 'spec_helper'
describe SlashCommands::InterpretService, services: true do
let(:project) { create(:project, :public) }
let(:developer) { create(:user) }
let(:developer2) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:milestone) { create(:milestone, project: project, title: '9.10') }
let(:inprogress) { create(:label, project: project, title: 'In Progress') }
......@@ -388,6 +389,28 @@ describe SlashCommands::InterpretService, services: true do
end
end
context 'assign command with multiple assignees' do
let(:content) { "/assign @#{developer.username} @#{developer2.username}" }
before{ project.team << [developer2, :developer] }
context 'Issue' do
it 'fetches assignee and populates assignee_id if content contains /assign' do
_, updates = service.execute(content, issue)
expect(updates[:assignee_ids]).to match_array([developer.id, developer2.id])
end
end
context 'Merge Request' do
it 'fetches assignee and populates assignee_id if content contains /assign' do
_, updates = service.execute(content, merge_request)
expect(updates).to eq(assignee_id: developer.id)
end
end
end
it_behaves_like 'empty command' do
let(:content) { '/assign @abcd1234' }
let(:issuable) { issue }
......
......@@ -63,7 +63,7 @@ shared_examples 'issuable record that supports slash commands in its description
note = issuable.notes.user.first
expect(note.note).to eq "Awesome!"
expect(issuable.assignee).to eq assignee
expect(issuable.assignees).to eq [assignee]
expect(issuable.labels).to eq [label_bug]
expect(issuable.milestone).to eq milestone
end
......@@ -81,7 +81,7 @@ shared_examples 'issuable record that supports slash commands in its description
issuable.reload
expect(issuable.notes.user).to be_empty
expect(issuable.assignee).to eq assignee
expect(issuable.assignees).to eq [assignee]
expect(issuable.labels).to eq [label_bug]
expect(issuable.milestone).to eq milestone
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment