Commit a6218f1b authored by Nick Thomas's avatar Nick Thomas

Merge branch 'osw-multi-assignees-merge-requests' into 'master'

[Backport] Support multiple assignees for merge requests

See merge request gitlab-org/gitlab-ce!27089
parents 12e35b49 ca884980
......@@ -81,9 +81,6 @@ export default {
const formData = {
update: {
state_event: this.form.find('input[name="update[state_event]"]').val(),
// For Merge Requests
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
// For Issues
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
......
......@@ -74,8 +74,7 @@ export default {
}
if (!this.users.length) {
const emptyTooltipLabel =
this.issuableType === 'issue' ? __('Assignee(s)') : __('Assignee');
const emptyTooltipLabel = __('Assignee(s)');
names.push(emptyTooltipLabel);
}
......@@ -90,6 +89,27 @@ export default {
return counter;
},
mergeNotAllowedTooltipMessage() {
const assigneesCount = this.users.length;
if (this.issuableType !== 'merge_request' || assigneesCount === 0) {
return null;
}
const cannotMergeCount = this.users.filter(u => u.can_merge === false).length;
const canMergeCount = assigneesCount - cannotMergeCount;
if (canMergeCount === assigneesCount) {
// Everyone can merge
return null;
} else if (cannotMergeCount === assigneesCount && assigneesCount > 1) {
return 'No one can merge';
} else if (assigneesCount === 1) {
return 'Cannot merge';
}
return `${canMergeCount}/${assigneesCount} can merge`;
},
},
methods: {
assignSelf() {
......@@ -154,6 +174,15 @@ export default {
</button>
</div>
<div class="value hide-collapsed">
<span
v-if="mergeNotAllowedTooltipMessage"
v-tooltip
:title="mergeNotAllowedTooltipMessage"
data-placement="left"
class="float-right cannot-be-merged"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-exclamation-triangle"></i>
</span>
<template v-if="hasNoUsers">
<span class="assign-yourself no-value">
No assignee
......
......@@ -498,6 +498,16 @@
flex: 1;
}
.issuable-meta {
.author-link {
display: inline-block;
}
.issuable-comments {
height: 18px;
}
}
.merge-request-title {
margin-bottom: 2px;
......
......@@ -1158,6 +1158,8 @@ pre.light-well {
.cannot-be-merged:hover {
color: $red-500;
margin-top: 2px;
position: relative;
z-index: 2;
}
.private-forks-notice .private-fork-icon {
......
......@@ -192,12 +192,7 @@ module IssuableActions
def bulk_update_params
permitted_keys_array = permitted_keys.dup
if resource_name == 'issue'
permitted_keys_array << { assignee_ids: [] }
else
permitted_keys_array.unshift(:assignee_id)
end
permitted_keys_array << { assignee_ids: [] }
params.require(:update).permit(permitted_keys_array)
end
......
......@@ -190,15 +190,15 @@ module IssuableCollections
end
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def preload_for_collection
common_attributes = [:author, :assignees, :labels, :milestone]
@preload_for_collection ||= case collection_type
when 'Issue'
[:project, :author, :assignees, :labels, :milestone, project: :namespace]
common_attributes + [:project, project: :namespace]
when 'MergeRequest'
[
:target_project, :author, :assignee, :labels, :milestone,
source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
]
common_attributes + [:target_project, source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits]
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
......@@ -20,7 +20,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
def merge_request_params_attributes
[
:allow_collaboration,
:assignee_id,
:description,
:force_remove_source_branch,
:lock_version,
......@@ -35,6 +34,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:title,
:discussion_locked,
label_ids: [],
assignee_ids: [],
update_task: [:index, :checked, :line_number, :line_source]
]
end
......
......@@ -439,22 +439,6 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def by_assignee(items)
if filter_by_no_assignee?
items.where(assignee_id: nil)
elsif filter_by_any_assignee?
items.where('assignee_id IS NOT NULL')
elsif assignee
items.where(assignee_id: assignee.id)
elsif assignee_id? || assignee_username? # assignee not found
items.none
else
items
end
end
# rubocop: enable CodeReuse/ActiveRecord
def filter_by_no_assignee?
# Assignee_id takes precedence over assignee_username
[NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE
......@@ -478,6 +462,20 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
def by_assignee(items)
if filter_by_no_assignee?
items.unassigned
elsif filter_by_any_assignee?
items.assigned
elsif assignee
items.assigned_to(assignee)
elsif assignee_id? || assignee_username? # assignee not found
items.none
else
items
end
end
# rubocop: disable CodeReuse/ActiveRecord
def by_milestone(items)
if milestones?
......
......@@ -144,18 +144,4 @@ class IssuesFinder < IssuableFinder
current_user.blank?
end
def by_assignee(items)
if filter_by_no_assignee?
items.unassigned
elsif filter_by_any_assignee?
items.assigned
elsif assignee
items.assigned_to(assignee)
elsif assignee_id? || assignee_username? # assignee not found
items.none
else
items
end
end
end
......@@ -69,7 +69,7 @@ module BoardsHelper
end
def board_sidebar_user_data
dropdown_options = issue_assignees_dropdown_options
dropdown_options = assignees_dropdown_options('issue')
{
toggle: 'dropdown',
......
......@@ -17,8 +17,8 @@ module FormHelper
end
end
def issue_assignees_dropdown_options
{
def assignees_dropdown_options(issuable_type)
dropdown_data = {
toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data',
title: 'Select assignee',
filter: true,
......@@ -28,8 +28,8 @@ module FormHelper
first_user: current_user&.username,
null_user: true,
current_user: true,
project_id: @project&.id,
field_name: 'issue[assignee_ids][]',
project_id: (@target_project || @project)&.id,
field_name: "#{issuable_type}[assignee_ids][]",
default_label: 'Unassigned',
'max-select': 1,
'dropdown-header': 'Assignee',
......@@ -39,5 +39,36 @@ module FormHelper
current_user_info: UserSerializer.new.represent(current_user)
}
}
type = issuable_type.to_s
if type == 'issue' && issue_supports_multiple_assignees? ||
type == 'merge_request' && merge_request_supports_multiple_assignees?
dropdown_data = multiple_assignees_dropdown_options(dropdown_data)
end
dropdown_data
end
# Overwritten
def issue_supports_multiple_assignees?
false
end
# Overwritten
def merge_request_supports_multiple_assignees?
false
end
private
def multiple_assignees_dropdown_options(options)
new_options = options.dup
new_options[:title] = 'Select assignee(s)'
new_options[:data][:'dropdown-header'] = 'Assignee(s)'
new_options[:data].delete(:'max-select')
new_options
end
end
......@@ -15,11 +15,14 @@ module IssuablesHelper
sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar')
end
def sidebar_assignee_tooltip_label(issuable)
if issuable.assignee
issuable.assignee.name
def assignees_label(issuable, include_value: true)
label = 'Assignee'.pluralize(issuable.assignees.count)
if include_value
sanitized_list = sanitize_name(issuable.assignee_list)
"#{label}: #{sanitized_list}"
else
issuable.allows_multiple_assignees? ? _('Assignee(s)') : _('Assignee')
label
end
end
......
......@@ -24,10 +24,12 @@ module Emails
end
# rubocop: disable CodeReuse/ActiveRecord
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id, reason = nil)
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_ids, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
@previous_assignees = []
@previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any?
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
# rubocop: enable CodeReuse/ActiveRecord
......
......@@ -4,6 +4,7 @@ class Notify < BaseMailer
include ActionDispatch::Routing::PolymorphicRoutes
include GitlabRoutingHelper
include EmailsHelper
include IssuablesHelper
include Emails::Issues
include Emails::MergeRequests
......@@ -24,6 +25,7 @@ class Notify < BaseMailer
helper MembersHelper
helper AvatarsHelper
helper GitlabRoutingHelper
helper IssuablesHelper
def test_email(recipient_email, subject, body)
mail(to: recipient_email,
......
# frozen_string_literal: true
# This module handles backward compatibility for import/export of Merge Requests after
# multiple assignees feature was introduced. Also, it handles the scenarios where
# the #26496 background migration hasn't finished yet.
# Ideally, most of this code should be removed at #59457.
module DeprecatedAssignee
extend ActiveSupport::Concern
def assignee_ids=(ids)
nullify_deprecated_assignee
super
end
def assignees=(users)
nullify_deprecated_assignee
super
end
def assignee_id=(id)
self.assignee_ids = Array(id)
end
def assignee=(user)
self.assignees = Array(user)
end
def assignee
assignees.first
end
def assignee_id
assignee_ids.first
end
def assignee_ids
if Gitlab::Database.read_only? && pending_assignees_population?
return Array(deprecated_assignee_id)
end
update_assignees_relation
super
end
def assignees
if Gitlab::Database.read_only? && pending_assignees_population?
return User.where(id: deprecated_assignee_id)
end
update_assignees_relation
super
end
private
# This will make the background migration process quicker (#26496) as it'll have less
# assignee_id rows to look through.
def nullify_deprecated_assignee
return unless persisted? && Gitlab::Database.read_only?
update_column(:assignee_id, nil)
end
# This code should be removed in the clean-up phase of the
# background migration (#59457).
def pending_assignees_population?
persisted? && deprecated_assignee_id && merge_request_assignees.empty?
end
# If there's an assignee_id and no relation, it means the background
# migration at #26496 didn't reach this merge request yet.
# This code should be removed in the clean-up phase of the
# background migration (#59457).
def update_assignees_relation
if pending_assignees_population?
transaction do
merge_request_assignees.create!(user_id: deprecated_assignee_id, merge_request_id: id)
update_column(:assignee_id, nil)
end
end
end
def deprecated_assignee_id
read_attribute(:assignee_id)
end
end
......@@ -67,13 +67,6 @@ module Issuable
allow_nil: true,
prefix: true
delegate :name,
:email,
:public_email,
to: :assignee,
allow_nil: true,
prefix: true
validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 }
validate :milestone_is_valid
......@@ -88,6 +81,19 @@ module Issuable
scope :only_opened, -> { with_state(:opened) }
scope :closed, -> { with_state(:closed) }
# rubocop:disable GitlabSecurity/SqlInjection
# The `to_ability_name` method is not an user input.
scope :assigned, -> do
where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
end
scope :unassigned, -> do
where("NOT EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
end
scope :assigned_to, ->(u) do
where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE user_id = ? AND #{to_ability_name}_id = #{to_ability_name}s.id)", u.id)
end
# rubocop:enable GitlabSecurity/SqlInjection
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC') }
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC') }
......@@ -104,6 +110,7 @@ module Issuable
participant :author
participant :notes_with_associations
participant :assignees
strip_attributes :title
......@@ -270,6 +277,10 @@ module Issuable
end
end
def assignee_or_author?(user)
author_id == user.id || assignees.exists?(user.id)
end
def today?
Date.today == created_at.to_date
end
......@@ -314,11 +325,7 @@ module Issuable
end
if old_assignees != assignees
if self.is_a?(Issue)
changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
else
changes[:assignee] = [old_assignees&.first&.hook_attrs, assignee&.hook_attrs]
end
changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
end
if self.respond_to?(:total_time_spent)
......@@ -355,10 +362,18 @@ module Issuable
def card_attributes
{
'Author' => author.try(:name),
'Assignee' => assignee.try(:name)
'Assignee' => assignee_list
}
end
def assignee_list
assignees.map(&:name).to_sentence
end
def assignee_username_list
assignees.map(&:username).to_sentence
end
def notes_with_associations
# If A has_many Bs, and B has_many Cs, and you do
# `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord
......
......@@ -49,10 +49,6 @@ class Issue < ApplicationRecord
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
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 :with_due_date, -> { where.not(due_date: nil) }
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
......@@ -75,8 +71,6 @@ class Issue < ApplicationRecord
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
participant :assignees
state_machine :state, initial: :opened do
event :close do
transition [:opened] => :closed
......@@ -155,22 +149,6 @@ class Issue < ApplicationRecord
Gitlab::HookData::IssueBuilder.new(self).build
end
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
'Author' => author.try(:name),
'Assignee' => assignee_list
}
end
def assignee_or_author?(user)
author_id == user.id || assignees.exists?(user.id)
end
def assignee_list
assignees.map(&:name).to_sentence
end
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
......
......@@ -16,6 +16,7 @@ class MergeRequest < ApplicationRecord
include LabelEventable
include ReactiveCaching
include FromUnion
include DeprecatedAssignee
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
self.reactive_cache_refresh_interval = 10.minutes
......@@ -69,8 +70,7 @@ class MergeRequest < ApplicationRecord
has_many :suggestions, through: :notes
has_many :merge_request_assignees
# Will be deprecated at https://gitlab.com/gitlab-org/gitlab-ce/issues/59457
belongs_to :assignee, class_name: "User"
has_many :assignees, class_name: "User", through: :merge_request_assignees
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
......@@ -79,10 +79,6 @@ class MergeRequest < ApplicationRecord
after_update :reload_diff_if_branch_changed
after_save :ensure_metrics
# Required until the codebase starts using this relation for single or multiple assignees.
# TODO: Remove at gitlab-ee#2004 implementation.
after_save :refresh_merge_request_assignees, if: :assignee_id_changed?
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
attr_accessor :allow_broken
......@@ -188,19 +184,14 @@ class MergeRequest < ApplicationRecord
end
scope :join_project, -> { joins(:target_project) }
scope :references_project, -> { references(:target_project) }
scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") }
scope :assigned_to, ->(u) { where(assignee_id: u.id)}
scope :with_api_entity_associations, -> {
preload(:author, :assignee, :notes, :labels, :milestone, :timelogs,
preload(:assignees, :author, :notes, :labels, :milestone, :timelogs,
latest_merge_request_diff: [:merge_request_diff_commits],
metrics: [:latest_closed_by, :merged_by],
target_project: [:route, { namespace: :route }],
source_project: [:route, { namespace: :route }])
}
participant :assignee
after_save :keep_around_commit
alias_attribute :project, :target_project
......@@ -337,31 +328,6 @@ class MergeRequest < ApplicationRecord
Gitlab::HookData::MergeRequestBuilder.new(self).build
end
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
'Author' => author.try(:name),
'Assignee' => assignee.try(:name)
}
end
# These method are needed for compatibility with issues to not mess view and other code
def assignees
Array(assignee)
end
def assignee_ids
Array(assignee_id)
end
def assignee_ids=(ids)
write_attribute(:assignee_id, ids.last)
end
def assignee_or_author?(user)
author_id == user.id || assignee_id == user.id
end
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
......@@ -682,15 +648,6 @@ class MergeRequest < ApplicationRecord
merge_request_diff || create_merge_request_diff
end
def refresh_merge_request_assignees
transaction do
# Using it instead relation.delete_all in order to avoid adding a
# dependent: :delete_all (we already have foreign key cascade deletion).
MergeRequestAssignee.where(merge_request_id: self).delete_all
merge_request_assignees.create(user_id: assignee_id) if assignee_id
end
end
def create_merge_request_diff
fetch_ref!
......@@ -1208,7 +1165,7 @@ class MergeRequest < ApplicationRecord
variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', value: project.web_url)
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', value: target_branch.to_s)
variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title)
variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee.username) if assignee
variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee_username_list) if assignees.any?
variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone
variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present?
variables.concat(source_project_variables)
......
......@@ -674,6 +674,10 @@ class Project < ApplicationRecord
{ scope: :project, status: auto_devops&.enabled || Feature.enabled?(:force_autodevops_on_by_default, self) }
end
def multiple_mr_assignees_enabled?
Feature.enabled?(:multiple_merge_request_assignees, self)
end
def daily_statistics_enabled?
Feature.enabled?(:project_daily_statistics, self, default_enabled: true)
end
......
......@@ -11,4 +11,6 @@ class IssuableSidebarExtrasEntity < Grape::Entity
expose :subscribed do |issuable|
issuable.subscribed?(request.current_user, issuable.project)
end
expose :assignees, using: API::Entities::UserBasic
end
# frozen_string_literal: true
class IssueSidebarExtrasEntity < IssuableSidebarExtrasEntity
expose :assignees, using: API::Entities::UserBasic
end
# frozen_string_literal: true
class MergeRequestAssigneeEntity < ::API::Entities::UserBasic
expose :can_merge do |assignee, options|
options[:merge_request]&.can_be_merged_by?(assignee)
end
end
# frozen_string_literal: true
class MergeRequestBasicEntity < Grape::Entity
expose :assignee_id
expose :merge_status
expose :merge_error
expose :state
......@@ -9,7 +8,7 @@ class MergeRequestBasicEntity < Grape::Entity
expose :rebase_in_progress?, as: :rebase_in_progress
expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity
expose :assignee, using: API::Entities::UserBasic
expose :assignees, using: API::Entities::UserBasic
expose :task_status, :task_status_short
expose :lock_version, :lock_version
end
......@@ -8,9 +8,9 @@ class MergeRequestSerializer < BaseSerializer
entity =
case opts[:serializer]
when 'sidebar'
MergeRequestSidebarBasicEntity
IssuableSidebarBasicEntity
when 'sidebar_extras'
IssuableSidebarExtrasEntity
MergeRequestSidebarExtrasEntity
when 'basic'
MergeRequestBasicEntity
else
......
# frozen_string_literal: true
class MergeRequestSidebarBasicEntity < IssuableSidebarBasicEntity
expose :assignee, if: lambda { |issuable| issuable.assignee } do
expose :assignee, merge: true, using: API::Entities::UserBasic
expose :can_merge do |issuable|
issuable.can_be_merged_by?(issuable.assignee)
end
end
end
# frozen_string_literal: true
class MergeRequestSidebarExtrasEntity < IssuableSidebarExtrasEntity
expose :assignees do |merge_request|
MergeRequestAssigneeEntity.represent(merge_request.assignees, merge_request: merge_request)
end
end
......@@ -34,14 +34,20 @@ class IssuableBaseService < BaseService
end
def filter_assignee(issuable)
return unless params[:assignee_id].present?
return if params[:assignee_ids].blank?
assignee_id = params[:assignee_id]
unless issuable.allows_multiple_assignees?
params[:assignee_ids] = params[:assignee_ids].first(1)
end
assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
if assignee_id.to_s == IssuableFinder::NONE
params[:assignee_id] = ""
if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE]
params[:assignee_ids] = []
elsif assignee_ids.any?
params[:assignee_ids] = assignee_ids
else
params.delete(:assignee_id) unless assignee_can_read?(issuable, assignee_id)
params.delete(:assignee_ids)
end
end
......@@ -352,7 +358,7 @@ class IssuableBaseService < BaseService
end
def has_changes?(issuable, old_labels: [], old_assignees: [])
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
valid_attrs = [:title, :description, :assignee_ids, :milestone_id, :target_branch]
attrs_changed = valid_attrs.any? do |attr|
issuable.previous_changes.include?(attr.to_s)
......
......@@ -20,7 +20,7 @@ module Issues
private
def create_assignee_note(issue, old_assignees)
SystemNoteService.change_issue_assignees(
SystemNoteService.change_issuable_assignees(
issue, issue.project, current_user, old_assignees)
end
......@@ -31,26 +31,6 @@ module Issues
issue.project.execute_services(issue_data, hooks_scope)
end
# rubocop: disable CodeReuse/ActiveRecord
def filter_assignee(issuable)
return if params[:assignee_ids].blank?
unless issuable.allows_multiple_assignees?
params[:assignee_ids] = params[:assignee_ids].take(1)
end
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] = []
elsif assignee_ids.any?
params[:assignee_ids] = assignee_ids
else
params.delete(:assignee_ids)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def update_project_counter_caches?(issue)
super || issue.confidential_changed?
end
......
......@@ -39,7 +39,7 @@ module Issues
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
notification_service.async.reassigned_issue(issue, current_user, old_assignees)
todo_service.reassigned_issue(issue, current_user, old_assignees)
todo_service.reassigned_issuable(issue, current_user, old_assignees)
end
if issue.previous_changes.include?('confidential')
......
......@@ -49,9 +49,9 @@ module MergeRequests
MergeRequestMetricsService.new(merge_request.metrics)
end
def create_assignee_note(merge_request)
SystemNoteService.change_assignee(
merge_request, merge_request.project, current_user, merge_request.assignee)
def create_assignee_note(merge_request, old_assignees)
SystemNoteService.change_issuable_assignees(
merge_request, merge_request.project, current_user, old_assignees)
end
def create_pipeline_for(merge_request, user)
......
......@@ -24,13 +24,13 @@ module MergeRequests
update_task_event(merge_request) || update(merge_request)
end
# rubocop:disable Metrics/AbcSize
def handle_changes(merge_request, options)
old_associations = options.fetch(:old_associations, {})
old_labels = old_associations.fetch(:labels, [])
old_mentioned_users = old_associations.fetch(:mentioned_users, [])
old_assignees = old_associations.fetch(:assignees, [])
if has_changes?(merge_request, old_labels: old_labels)
if has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
end
......@@ -45,15 +45,10 @@ module MergeRequests
merge_request.target_branch)
end
if merge_request.previous_changes.include?('assignee_id')
reassigned_merge_request_args = [merge_request, current_user]
old_assignee_id = merge_request.previous_changes['assignee_id'].first
reassigned_merge_request_args << User.find(old_assignee_id) if old_assignee_id
create_assignee_note(merge_request)
notification_service.async.reassigned_merge_request(*reassigned_merge_request_args)
todo_service.reassigned_merge_request(merge_request, current_user)
if merge_request.assignees != old_assignees
create_assignee_note(merge_request, old_assignees)
notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignees)
todo_service.reassigned_issuable(merge_request, current_user, old_assignees)
end
if merge_request.previous_changes.include?('target_branch') ||
......@@ -81,7 +76,6 @@ module MergeRequests
)
end
end
# rubocop:enable Metrics/AbcSize
def handle_task_changes(merge_request)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
......
......@@ -247,15 +247,15 @@ module NotificationRecipientService
attr_reader :target
attr_reader :current_user
attr_reader :action
attr_reader :previous_assignee
attr_reader :previous_assignees
attr_reader :skip_current_user
def initialize(target, current_user, action:, custom_action: nil, previous_assignee: nil, skip_current_user: true)
def initialize(target, current_user, action:, custom_action: nil, previous_assignees: nil, skip_current_user: true)
@target = target
@current_user = current_user
@action = action
@custom_action = custom_action
@previous_assignee = previous_assignee
@previous_assignees = previous_assignees
@skip_current_user = skip_current_user
end
......@@ -270,11 +270,7 @@ module NotificationRecipientService
# Re-assign is considered as a mention of the new assignee
case custom_action
when :reassign_merge_request
add_recipients(previous_assignee, :mention, nil)
add_recipients(target.assignee, :mention, NotificationReason::ASSIGNED)
when :reassign_issue
previous_assignees = Array(previous_assignee)
when :reassign_merge_request, :reassign_issue
add_recipients(previous_assignees, :mention, nil)
add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED)
end
......@@ -287,17 +283,11 @@ module NotificationRecipientService
# receive them, too.
add_mentions(current_user, target: target)
# Add the assigned users, if any
assignees = case custom_action
when :new_issue
target.assignees
else
target.assignee
end
# We use the `:participating` notification level in order to match existing legacy behavior as captured
# in existing specs (notification_service_spec.rb ~ line 507)
add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees
if target.is_a?(Issuable)
add_recipients(target.assignees, :participating, NotificationReason::ASSIGNED)
end
add_labels_subscribers
end
......
......@@ -95,8 +95,8 @@ class NotificationService
# When we reassign an issue we should send an email to:
#
# * issue old assignee if their notification level is not Disabled
# * issue new assignee if their notification level is not Disabled
# * issue old assignees if their notification level is not Disabled
# * issue new assignees if their notification level is not Disabled
# * users with custom level checked with "reassign issue"
#
def reassigned_issue(issue, current_user, previous_assignees = [])
......@@ -104,7 +104,7 @@ class NotificationService
issue,
current_user,
action: "reassign",
previous_assignee: previous_assignees
previous_assignees: previous_assignees
)
previous_assignee_ids = previous_assignees.map(&:id)
......@@ -140,7 +140,7 @@ class NotificationService
# When create a merge request we should send an email to:
#
# * mr author
# * mr assignee if their notification level is not Disabled
# * mr assignees if their notification level is not Disabled
# * project team members with notification level higher then Participating
# * watchers of the mr's labels
# * users with custom level checked with "new merge request"
......@@ -184,23 +184,25 @@ class NotificationService
# When we reassign a merge_request we should send an email to:
#
# * merge_request old assignee if their notification level is not Disabled
# * merge_request assignee if their notification level is not Disabled
# * merge_request old assignees if their notification level is not Disabled
# * merge_request new assignees if their notification level is not Disabled
# * users with custom level checked with "reassign merge request"
#
def reassigned_merge_request(merge_request, current_user, previous_assignee = nil)
def reassigned_merge_request(merge_request, current_user, previous_assignees = [])
recipients = NotificationRecipientService.build_recipients(
merge_request,
current_user,
action: "reassign",
previous_assignee: previous_assignee
previous_assignees: previous_assignees
)
previous_assignee_ids = previous_assignees.map(&:id)
recipients.each do |recipient|
mailer.reassigned_merge_request_email(
recipient.user.id,
merge_request.id,
previous_assignee&.id,
previous_assignee_ids,
current_user.id,
recipient.reason
).deliver_later
......
......@@ -69,7 +69,7 @@ module SystemNoteService
# Called when the assignees of an Issue is changed or removed
#
# issue - Issue object
# issuable - Issuable object (responds to assignees)
# project - Project owning noteable
# author - User performing the change
# assignees - Users being assigned, or nil
......@@ -85,9 +85,9 @@ module SystemNoteService
# "assigned to @user1 and @user2"
#
# Returns the created Note object
def change_issue_assignees(issue, project, author, old_assignees)
unassigned_users = old_assignees - issue.assignees
added_users = issue.assignees.to_a - old_assignees
def change_issuable_assignees(issuable, project, author, old_assignees)
unassigned_users = old_assignees - issuable.assignees
added_users = issuable.assignees.to_a - old_assignees
text_parts = []
text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
......@@ -95,7 +95,7 @@ module SystemNoteService
body = text_parts.join(' and ')
create_note(NoteSummary.new(issue, project, author, body, action: 'assignee'))
create_note(NoteSummary.new(issuable, project, author, body, action: 'assignee'))
end
# Called when the milestone of a Noteable is changed
......
......@@ -49,12 +49,12 @@ class TodoService
todo_users.each(&:update_todos_count_cache)
end
# When we reassign an issue we should:
# When we reassign an issuable we should:
#
# * create a pending todo for new assignee if issue is assigned
# * create a pending todo for new assignee if issuable is assigned
#
def reassigned_issue(issue, current_user, old_assignees = [])
create_assignment_todo(issue, current_user, old_assignees)
def reassigned_issuable(issuable, current_user, old_assignees = [])
create_assignment_todo(issuable, current_user, old_assignees)
end
# When create a merge request we should:
......@@ -82,14 +82,6 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user)
end
# When we reassign a merge request we should:
#
# * creates a pending todo for new assignee if merge request is assigned
#
def reassigned_merge_request(merge_request, current_user)
create_assignment_todo(merge_request, current_user)
end
# When merge a merge request we should:
#
# * mark all pending todos related to the target for the current user as done
......
%p
Assignee changed
- if previous_assignees.any?
from
%strong= sanitize_name(previous_assignees.map(&:name).to_sentence)
to
- if issuable.assignees.any?
%strong= sanitize_name(issuable.assignee_list)
- else
%strong Unassigned
......@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{sanitize_name(@merge_request.assignee_name)}
= assignees_label(@merge_request)
......@@ -3,7 +3,7 @@
- if @issue.assignees.any?
%p
Assignee: #{@issue.assignee_list}
= assignees_label(@issue)
%p
This issue is due on: #{@issue.due_date.to_s(:medium)}
......
......@@ -2,6 +2,6 @@ The following issue is due on <%= @issue.due_date %>:
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= @issue.author_name %>
Assignee: <%= @issue.assignee_list %>
<%= assignees_label(@issue) %>
<%= @issue.description %>
......@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{sanitize_name(@merge_request.assignee_name)}
= assignees_label(@merge_request)
......@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{sanitize_name(@merge_request.assignee_name)}
= assignees_label(@merge_request)
......@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{sanitize_name(@merge_request.assignee_name)}
= assignees_label(@merge_request)
......@@ -4,7 +4,7 @@
- if @issue.assignees.any?
%p
Assignee: #{@issue.assignee_list}
= assignees_label(@issue)
- if @issue.description
%div
......
......@@ -2,6 +2,6 @@ New Issue was created.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= sanitize_name(@issue.author_name) %>
Assignee: <%= @issue.assignee_list %>
<%= assignees_label(@issue) %>
<%= @issue.description %>
......@@ -2,6 +2,6 @@ You have been mentioned in an issue.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= sanitize_name(@issue.author_name) %>
Assignee: <%= sanitize_name(@issue.assignee_list) %>
<%= assignees_label(@issue) %>
<%= @issue.description %>
......@@ -4,6 +4,6 @@ You have been mentioned in Merge Request <%= @merge_request.to_reference %>
<%= merge_path_description(@merge_request, 'to') %>
Author: <%= sanitize_name(@merge_request.author_name) %>
Assignee: <%= sanitize_name(@merge_request.assignee_name) %>
= assignees_label(@merge_request)
<%= @merge_request.description %>
......@@ -5,9 +5,9 @@
%p.details
!= merge_path_description(@merge_request, '&rarr;')
- if @merge_request.assignee_id.present?
- if @merge_request.assignees.any?
%p
Assignee: #{sanitize_name(@merge_request.assignee_name)}
= assignees_label(@merge_request)
= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter
......
......@@ -4,7 +4,7 @@ New Merge Request <%= @merge_request.to_reference %>
<%= merge_path_description(@merge_request, 'to') %>
Author: <%= @merge_request.author_name %>
Assignee: <%= @merge_request.assignee_name %>
<%= assignees_label(@merge_request) %>
<%= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter %>
<%= @merge_request.description %>
%p
Assignee changed
- if @previous_assignees.any?
from
%strong= sanitize_name(@previous_assignees.map(&:name).to_sentence)
to
- if @issue.assignees.any?
%strong= @issue.assignee_list
- else
%strong Unassigned
= render 'reassigned_issuable_email', issuable: @issue, previous_assignees: @previous_assignees
%p
Assignee changed
- if @previous_assignee
from
%strong= sanitize_name(@previous_assignee.name)
to
- if @merge_request.assignee_id
%strong= sanitize_name(@merge_request.assignee_name)
- else
%strong Unassigned
= render 'reassigned_issuable_email', issuable: @merge_request, previous_assignees: @previous_assignees
......@@ -2,5 +2,5 @@ Reassigned Merge Request <%= @merge_request.iid %>
<%= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }]) %>
Assignee changed <%= "from #{sanitize_name(@previous_assignee.name)}" if @previous_assignee -%>
to <%= "#{@merge_request.assignee_id ? sanitize_name(@merge_request.assignee_name) : 'Unassigned'}" %>
Assignee changed <%= "from #{sanitize_name(@previous_assignees.map(&:name).to_sentence)}" if @previous_assignees.any? -%>
to <%= "#{@merge_request.assignees.any? ? @merge_request.assignee_list : 'Unassigned'}" %>
......@@ -46,7 +46,7 @@
CLOSED
- if issue.assignees.any?
%li
= render 'shared/issuable/assignees', project: @project, issue: issue
= render 'shared/issuable/assignees', project: @project, issuable: issue
= render 'shared/issuable_meta_data', issuable: issue
......
......@@ -53,9 +53,9 @@
%li.issuable-pipeline-broken.d-none.d-sm-inline-block
= link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do
= icon('exclamation-triangle')
- if merge_request.assignee
- if merge_request.assignees.any?
%li
= link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: _('Assigned to :name'))
= render 'shared/issuable/assignees', project: merge_request.project, issuable: merge_request
= render_if_exists 'projects/merge_requests/approvals_count', merge_request: merge_request
= render 'shared/issuable_meta_data', issuable: merge_request
......
......@@ -19,7 +19,7 @@
":data-name" => "assignee.name",
":data-username" => "assignee.username" }
.dropdown
- dropdown_options = issue_assignees_dropdown_options
- dropdown_options = assignees_dropdown_options('issue')
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: board_sidebar_user_data,
":data-issuable-id" => "issue.iid" }
= dropdown_options[:title]
......
- max_render = 4
- assignees_rendering_overflow = issue.assignees.size > max_render
- assignees_rendering_overflow = issuable.assignees.size > max_render
- render_count = assignees_rendering_overflow ? max_render - 1 : max_render
- more_assignees_count = issue.assignees.size - render_count
- more_assignees_count = issuable.assignees.size - render_count
- issue.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord
- issuable.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord
= link_to_member(@project, assignee, name: false, title: "Assigned to :name")
- if more_assignees_count.positive?
......
......@@ -21,10 +21,7 @@
.title
Assignee
.filter-item
- if type == :issues
- field_name = "update[assignee_ids][]"
- else
- field_name = "update[assignee_id]"
- field_name = "update[assignee_ids][]"
= dropdown_tag("Select assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
.block
......
- issuable_type = issuable_sidebar[:type]
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
- if issuable_type == "issue"
#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}[assignee_ids]", signed_in: signed_in } }
.title.hide-collapsed
= _('Assignee')
= icon('spinner spin')
- else
- assignee = assignees.first
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body", boundary: 'viewport' }, title: (issuable_sidebar.dig(:assignee, :name) || _('Assignee')) }
- if issuable_sidebar[:assignee]
= link_to_member(@project, assignee, size: 24)
- else
= icon('user', 'aria-hidden': 'true')
#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}[assignee_ids]", signed_in: signed_in } }
.title.hide-collapsed
= _('Assignee')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
- if !signed_in
%a.gutter-toggle.float-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') }
= sidebar_gutter_toggle_icon
.value.hide-collapsed
- if issuable_sidebar[:assignee]
= link_to_member(@project, assignee, size: 32, extra_class: 'bold') do
- unless issuable_sidebar[:assignee][:can_merge]
%span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') }
= icon('exclamation-triangle', 'aria-hidden': 'true')
%span.username
@#{issuable_sidebar[:assignee][:username]}
- else
%span.assign-yourself.no-value
= _('No assignee')
- if can_edit_issuable
\-
%a.js-assign-yourself{ href: '#' }
= _('assign yourself')
= icon('spinner spin')
.selectbox.hide-collapsed
- if assignees.none?
......@@ -59,17 +27,15 @@
ability_name: issuable_type,
null_user: true,
display: 'static' } }
- title = _('Select assignee')
- if issuable_type == "issue"
- dropdown_options = issue_assignees_dropdown_options
- title = dropdown_options[:title]
- options[:toggle_class] += ' js-multiselect js-save-user-data'
- data = { field_name: "#{issuable_type}[assignee_ids][]" }
- data[:multi_select] = true
- data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- options[:data].merge!(data)
- dropdown_options = assignees_dropdown_options(issuable_type)
- title = dropdown_options[:title]
- options[:toggle_class] += ' js-multiselect js-save-user-data'
- data = { field_name: "#{issuable_type}[assignee_ids][]" }
- data[:multi_select] = true
- data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- options[:data].merge!(data)
= dropdown_tag(title, options: options)
- merge_request = issuable
.block.assignee
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 24)
- else
= icon('user', 'aria-hidden': 'true')
.title.hide-collapsed
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
- if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 32, extra_class: 'bold') do
- unless merge_request.can_be_merged_by?(merge_request.assignee)
%span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
= icon('exclamation-triangle', 'aria-hidden': 'true')
%span.username
= merge_request.assignee.to_reference
- else
%span.assign-yourself.no-value
No assignee
- if can_edit_issuable
\-
%a.js-assign-yourself{ href: '#' }
assign yourself
.selectbox.hide-collapsed
= f.hidden_field 'assignee_id', value: merge_request.assignee_id, id: 'issue_assignee_id'
= dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: @project&.id, author_id: merge_request.author_id, field_name: 'merge_request[assignee_id]', issue_update: issuable_json_path(merge_request), ability_name: 'merge_request', null_user: true } })
......@@ -8,11 +8,8 @@
%hr
.row
%div{ class: (has_due_date ? "col-lg-6" : "col-12") }
.form-group.row.issue-assignee
- if issuable.is_a?(Issue)
= render "shared/issuable/form/metadata_issue_assignee", issuable: issuable, form: form, has_due_date: has_due_date
- else
= render "shared/issuable/form/metadata_merge_request_assignee", issuable: issuable, form: form, has_due_date: has_due_date
.form-group.row.merge-request-assignee
= render "shared/issuable/form/metadata_issuable_assignee", issuable: issuable, form: form, has_due_date: has_due_date
.form-group.row.issue-milestone
= form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
.col-sm-10{ class: ("col-md-8" if has_due_date) }
......
= form.label :assignee_ids, "Assignee", class: "col-form-label #{"col-md-2 col-lg-4" if has_due_date}"
= form.label :assignee_id, "Assignee", class: "col-form-label #{has_due_date ? "col-lg-4" : "col-sm-2"}"
.col-sm-10{ class: ("col-md-8" if has_due_date) }
.issuable-form-select-holder.selectbox
- issuable.assignees.each do |assignee|
......@@ -7,5 +7,5 @@
- if issuable.assignees.length === 0
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
= dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_assignees_dropdown_options)
= link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
= dropdown_tag(users_dropdown_label(issuable.assignees), options: assignees_dropdown_options(issuable.to_ability_name))
= link_to 'Assign to me', '#', class: "assign-to-me-link qa-assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
......@@ -21,7 +21,7 @@ Gitlab::Seeder.quiet do
title: FFaker::Lorem.sentence(6),
description: FFaker::Lorem.sentences(3).join(" "),
milestone: project.milestones.sample,
assignee: project.team.users.sample,
assignees: [project.team.users.sample],
label_ids: label_ids
}
......
......@@ -663,7 +663,11 @@ module API
expose(:user_notes_count) { |merge_request, options| issuable_metadata(merge_request, options, :user_notes_count) }
expose(:upvotes) { |merge_request, options| issuable_metadata(merge_request, options, :upvotes) }
expose(:downvotes) { |merge_request, options| issuable_metadata(merge_request, options, :downvotes) }
expose :author, :assignee, using: Entities::UserBasic
expose :assignee, using: ::API::Entities::UserBasic do |merge_request|
merge_request.assignee
end
expose :author, :assignees, using: Entities::UserBasic
expose :source_project_id, :target_project_id
expose :labels do |merge_request|
# Avoids an N+1 query since labels are preloaded
......
......@@ -20,6 +20,7 @@ module API
def self.update_params_at_least_one_of
%i[
assignee_id
assignee_ids
description
labels
milestone_id
......@@ -184,6 +185,7 @@ module API
params :optional_params do
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
......@@ -231,6 +233,7 @@ module API
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch)
mr_params = convert_parameters_from_legacy_format(mr_params)
merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
......@@ -334,6 +337,7 @@ module API
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
mr_params = convert_parameters_from_legacy_format(mr_params)
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
......
......@@ -10,7 +10,7 @@ module Banzai
nodes,
MergeRequest.includes(
:author,
:assignee,
:assignees,
{
# These associations are primarily used for checking permissions.
# Eager loading these ensures we don't end up running dozens of
......
......@@ -20,11 +20,7 @@ module Gitlab
repository: issuable.project.hook_attrs.slice(:name, :url, :description, :homepage)
}
if issuable.is_a?(Issue)
hook_data[:assignees] = issuable.assignees.map(&:hook_attrs) if issuable.assignees.any?
else
hook_data[:assignee] = issuable.assignee.hook_attrs if issuable.assignee
end
hook_data[:assignees] = issuable.assignees.map(&:hook_attrs) if issuable.assignees.any?
hook_data
end
......
......@@ -34,7 +34,6 @@ module Gitlab
end
SAFE_HOOK_RELATIONS = %i[
assignee
labels
total_time_spent
].freeze
......@@ -51,7 +50,9 @@ module Gitlab
work_in_progress: merge_request.work_in_progress?,
total_time_spent: merge_request.total_time_spent,
human_total_time_spent: merge_request.human_total_time_spent,
human_time_estimate: merge_request.human_time_estimate
human_time_estimate: merge_request.human_time_estimate,
assignee_ids: merge_request.assignee_ids,
assignee_id: merge_request.assignee_ids.first # This key is deprecated
}
merge_request.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes)
......
......@@ -987,9 +987,6 @@ msgstr ""
msgid "Assigned Merge Requests"
msgstr ""
msgid "Assigned to :name"
msgstr ""
msgid "Assigned to me"
msgstr ""
......@@ -5474,9 +5471,6 @@ msgstr ""
msgid "No activities found"
msgstr ""
msgid "No assignee"
msgstr ""
msgid "No branches found"
msgstr ""
......@@ -5564,9 +5558,6 @@ msgstr ""
msgid "None"
msgstr ""
msgid "Not allowed to merge"
msgstr ""
msgid "Not available"
msgstr ""
......@@ -7277,9 +7268,6 @@ msgstr ""
msgid "Select an existing Kubernetes cluster or create a new one"
msgstr ""
msgid "Select assignee"
msgstr ""
msgid "Select branch/tag"
msgstr ""
......@@ -8860,9 +8848,6 @@ msgstr ""
msgid "Toggle navigation"
msgstr ""
msgid "Toggle sidebar"
msgstr ""
msgid "ToggleButton|Toggle Status: OFF"
msgstr ""
......@@ -9870,9 +9855,6 @@ msgstr ""
msgid "among other things"
msgstr ""
msgid "assign yourself"
msgstr ""
msgid "attach a new file"
msgstr ""
......
......@@ -26,7 +26,7 @@ module QA
element :issuable_label
end
view 'app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml' do
view 'app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml' do
element :assign_to_me_link
end
......
......@@ -238,11 +238,11 @@ describe Projects::MergeRequestsController do
assignee = create(:user)
project.add_developer(assignee)
update_merge_request({ assignee_id: assignee.id }, format: :json)
update_merge_request({ assignee_ids: [assignee.id] }, format: :json)
body = JSON.parse(response.body)
expect(body['assignee'].keys)
.to match_array(%w(name username avatar_url id state web_url))
expect(body['assignees']).to all(include(*%w(name username avatar_url id state web_url)))
end
end
......
......@@ -8,7 +8,7 @@ describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do
before do
issue.assignees = [user]
merge_request.update(assignee: user)
merge_request.update(assignees: [user])
sign_in(user)
end
......@@ -33,7 +33,7 @@ describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do
expect_counters('merge_requests', '1')
merge_request.update(assignee: nil)
merge_request.update(assignees: [])
user.invalidate_cache_counts
......
......@@ -48,14 +48,14 @@ describe 'Dashboard Merge Requests' do
let!(:assigned_merge_request) do
create(:merge_request,
assignee: current_user,
assignees: [current_user],
source_project: project,
author: create(:user))
end
let!(:assigned_merge_request_from_fork) do
create(:merge_request,
source_branch: 'markdown', assignee: current_user,
source_branch: 'markdown', assignees: [current_user],
target_project: public_project, source_project: forked_project,
author: create(:user))
end
......
......@@ -38,7 +38,7 @@ describe 'Group merge requests page' do
context 'when merge request assignee to user' do
before do
issuable.update!(assignee: user)
issuable.update!(assignees: [user])
visit path
end
......
......@@ -30,8 +30,8 @@ describe 'New/edit issue', :js do
# the original method, resulting in infinite recursion when called.
# This is likely a bug with helper modules included into dynamically generated view classes.
# To work around this, we have to hold on to and call to the original implementation manually.
original_issue_dropdown_options = FormHelper.instance_method(:issue_assignees_dropdown_options)
allow_any_instance_of(FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args|
original_issue_dropdown_options = FormHelper.instance_method(:assignees_dropdown_options)
allow_any_instance_of(FormHelper).to receive(:assignees_dropdown_options).and_wrap_original do |original, *args|
options = original_issue_dropdown_options.bind(original.receiver).call(*args)
options[:data][:per_page] = 2
......
......@@ -68,15 +68,15 @@ describe "User creates a merge request", :js do
fill_in("Title", with: title)
end
click_button("Assignee")
expect(find(".js-assignee-search")["data-project-id"]).to eq(project.id.to_s)
find('.js-assignee-search').click
page.within(".dropdown-menu-user") do
expect(page).to have_content("Unassigned")
.and have_content(user.name)
.and have_content(project.users.first.name)
end
find('.js-assignee-search').click
click_button("Submit merge request")
......
require 'rails_helper'
describe 'Merge request > User creates MR' do
it_behaves_like 'a creatable merge request'
include ProjectForksHelper
context 'from a forked project' do
include ProjectForksHelper
before do
stub_licensed_features(multiple_merge_request_assignees: false)
end
context 'non-fork merge request' do
include_context 'merge request create context'
it_behaves_like 'a creatable merge request'
end
context 'from a forked project' do
let(:canonical_project) { create(:project, :public, :repository) }
let(:source_project) do
......@@ -15,6 +22,7 @@ describe 'Merge request > User creates MR' do
end
context 'to canonical project' do
include_context 'merge request create context'
it_behaves_like 'a creatable merge request'
end
......@@ -25,6 +33,7 @@ describe 'Merge request > User creates MR' do
namespace: user.namespace)
end
include_context 'merge request create context'
it_behaves_like 'a creatable merge request'
end
end
......
require 'rails_helper'
require 'spec_helper'
describe 'Merge request > User edits MR' do
include ProjectForksHelper
it_behaves_like 'an editable merge request'
before do
stub_licensed_features(multiple_merge_request_assignees: false)
end
context 'non-fork merge request' do
include_context 'merge request edit context'
it_behaves_like 'an editable merge request'
end
context 'for a forked project' do
it_behaves_like 'an editable merge request' do
let(:source_project) { fork_project(target_project, nil, repository: true) }
end
let(:source_project) { fork_project(target_project, nil, repository: true) }
include_context 'merge request edit context'
it_behaves_like 'an editable merge request'
end
end
......@@ -7,7 +7,7 @@ describe 'Merge Requests > User filters by assignees', :js do
let(:user) { project.creator }
before do
create(:merge_request, assignee: user, title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1')
create(:merge_request, assignees: [user], title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1')
create(:merge_request, title: 'Bugfix2', source_project: project, target_project: project, source_branch: 'bugfix2')
sign_in(user)
......
......@@ -10,7 +10,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do
before do
sign_in(user)
mr = create(:merge_request, title: 'Bugfix2', author: user, assignee: user, source_project: project, target_project: project, milestone: milestone)
mr = create(:merge_request, title: 'Bugfix2', author: user, assignees: [user], source_project: project, target_project: project, milestone: milestone)
mr.labels << wontfix
visit project_merge_requests_path(project)
......
......@@ -12,7 +12,7 @@ describe 'Merge requests > User lists merge requests' do
title: 'fix',
source_project: project,
source_branch: 'fix',
assignee: user,
assignees: [user],
milestone: create(:milestone, project: project, due_date: '2013-12-11'),
created_at: 1.minute.ago,
updated_at: 1.minute.ago)
......@@ -20,7 +20,7 @@ describe 'Merge requests > User lists merge requests' do
title: 'markdown',
source_project: project,
source_branch: 'markdown',
assignee: user,
assignees: [user],
milestone: create(:milestone, project: project, due_date: '2013-12-12'),
created_at: 2.minutes.ago,
updated_at: 2.minutes.ago)
......
......@@ -54,8 +54,7 @@ describe 'Merge requests > User mass updates', :js do
describe 'remove assignee' do
before do
merge_request.assignee = user
merge_request.save
merge_request.assignees = [user]
visit project_merge_requests_path(project)
end
......
......@@ -36,7 +36,7 @@ describe 'User uses header search field' do
end
context 'when clicking merge requests' do
let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) }
it 'shows assigned merge requests' do
find('.search-input-container .dropdown-menu').click_link('Merge requests assigned to me')
......@@ -100,7 +100,7 @@ describe 'User uses header search field' do
end
context 'when clicking merge requests' do
let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) }
it 'shows assigned merge requests' do
find('.dropdown-menu').click_link('Merge requests assigned to me')
......
......@@ -13,60 +13,32 @@ describe IssuesFinder do
expect(issues).to contain_exactly(issue1, issue2, issue3, issue4)
end
context 'filtering by assignee ID' do
let(:params) { { assignee_id: user.id } }
context 'assignee filtering' do
let(:issuables) { issues }
it 'returns issues assigned to that user' do
expect(issues).to contain_exactly(issue1, issue2)
end
end
context 'filtering by assignee usernames' do
set(:user3) { create(:user) }
let(:params) { { assignee_username: [user2.username, user3.username] } }
before do
project2.add_developer(user3)
issue3.assignees = [user2, user3]
end
it 'returns issues assigned to those users' do
expect(issues).to contain_exactly(issue3)
end
end
context 'filtering by no assignee' do
let(:params) { { assignee_id: 'None' } }
it 'returns issues not assigned to any assignee' do
expect(issues).to contain_exactly(issue4)
end
it 'returns issues not assigned to any assignee' do
params[:assignee_id] = 0
expect(issues).to contain_exactly(issue4)
it_behaves_like 'assignee ID filter' do
let(:params) { { assignee_id: user.id } }
let(:expected_issuables) { [issue1, issue2] }
end
it 'returns issues not assigned to any assignee' do
params[:assignee_id] = 'none'
it_behaves_like 'assignee username filter' do
before do
project2.add_developer(user3)
issue3.assignees = [user2, user3]
end
expect(issues).to contain_exactly(issue4)
set(:user3) { create(:user) }
let(:params) { { assignee_username: [user2.username, user3.username] } }
let(:expected_issuables) { [issue3] }
end
end
context 'filtering by any assignee' do
let(:params) { { assignee_id: 'Any' } }
it 'returns issues assigned to any assignee' do
expect(issues).to contain_exactly(issue1, issue2, issue3)
it_behaves_like 'no assignee filter' do
set(:user3) { create(:user) }
let(:expected_issuables) { [issue4] }
end
it 'returns issues assigned to any assignee' do
params[:assignee_id] = 'any'
expect(issues).to contain_exactly(issue1, issue2, issue3)
it_behaves_like 'any assignee filter' do
let(:expected_issuables) { [issue1, issue2, issue3] }
end
end
......
......@@ -136,21 +136,50 @@ describe MergeRequestsFinder do
end
end
context 'filtering by group milestone' do
let(:group_milestone) { create(:milestone, group: group) }
context 'assignee filtering' do
let(:issuables) { described_class.new(user, params).execute }
before do
project2.update(namespace: group)
merge_request2.update(milestone: group_milestone)
merge_request3.update(milestone: group_milestone)
it_behaves_like 'assignee ID filter' do
let(:params) { { assignee_id: user.id } }
let(:expected_issuables) { [merge_request1, merge_request2] }
end
it 'returns merge requests assigned to that group milestone' do
params = { milestone_title: group_milestone.title }
it_behaves_like 'assignee username filter' do
before do
project2.add_developer(user3)
merge_request3.assignees = [user2, user3]
end
merge_requests = described_class.new(user, params).execute
set(:user3) { create(:user) }
let(:params) { { assignee_username: [user2.username, user3.username] } }
let(:expected_issuables) { [merge_request3] }
end
it_behaves_like 'no assignee filter' do
set(:user3) { create(:user) }
let(:expected_issuables) { [merge_request4, merge_request5] }
end
it_behaves_like 'any assignee filter' do
let(:expected_issuables) { [merge_request1, merge_request2, merge_request3] }
end
context 'filtering by group milestone' do
let(:group_milestone) { create(:milestone, group: group) }
before do
project2.update(namespace: group)
merge_request2.update(milestone: group_milestone)
merge_request3.update(milestone: group_milestone)
end
it 'returns merge requests assigned to that group milestone' do
params = { milestone_title: group_milestone.title }
expect(merge_requests).to contain_exactly(merge_request2, merge_request3)
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request2, merge_request3)
end
end
end
......
......@@ -6,14 +6,14 @@
"source_branch_exists": { "type": "boolean" },
"merge_error": { "type": ["string", "null"] },
"rebase_in_progress": { "type": "boolean" },
"assignee_id": { "type": ["integer", "null"] },
"allow_collaboration": { "type": "boolean"},
"allow_maintainer_to_push": { "type": "boolean"},
"assignee": {
"oneOf": [
{ "type": "null" },
{ "$ref": "user.json" }
]
"assignees": {
"type": ["array"],
"items": {
"type": "object",
"$ref": "../public_api/v4/user/basic.json"
}
},
"milestone": {
"type": [ "object", "null" ]
......
......@@ -64,6 +64,11 @@
},
"additionalProperties": false
},
"assignees": {
"items": {
"$ref": "./merge_request.json"
}
},
"source_project_id": { "type": "integer" },
"target_project_id": { "type": "integer" },
"labels": {
......
......@@ -132,9 +132,94 @@ describe('Assignee component', () => {
-1,
);
});
it('has correct "cannot merge" tooltip when user cannot merge', () => {
const user = Object.assign({}, UsersMock.user, { can_merge: false });
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
users: [user],
editable: true,
issuableType: 'merge_request',
},
}).$mount();
expect(component.mergeNotAllowedTooltipMessage).toEqual('Cannot merge');
});
});
describe('Two or more assignees/users', () => {
it('has correct "cannot merge" tooltip when one user can merge', () => {
const users = UsersMockHelper.createNumberRandomUsers(3);
users[0].can_merge = true;
users[1].can_merge = false;
users[2].can_merge = false;
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
users,
editable: true,
issuableType: 'merge_request',
},
}).$mount();
expect(component.mergeNotAllowedTooltipMessage).toEqual('1/3 can merge');
});
it('has correct "cannot merge" tooltip when no user can merge', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
users[0].can_merge = false;
users[1].can_merge = false;
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
users,
editable: true,
issuableType: 'merge_request',
},
}).$mount();
expect(component.mergeNotAllowedTooltipMessage).toEqual('No one can merge');
});
it('has correct "cannot merge" tooltip when more than one user can merge', () => {
const users = UsersMockHelper.createNumberRandomUsers(3);
users[0].can_merge = false;
users[1].can_merge = true;
users[2].can_merge = true;
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
users,
editable: true,
issuableType: 'merge_request',
},
}).$mount();
expect(component.mergeNotAllowedTooltipMessage).toEqual('2/3 can merge');
});
it('has no "cannot merge" tooltip when every user can merge', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
users[0].can_merge = true;
users[1].can_merge = true;
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
users,
editable: true,
issuableType: 'merge_request',
},
}).$mount();
expect(component.mergeNotAllowedTooltipMessage).toEqual(null);
});
it('displays two assignee icons when collapsed', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
component = new AssigneeComponent({
......
......@@ -97,13 +97,13 @@ describe Gitlab::HookData::IssuableBuilder do
end
context 'merge_request is assigned' do
let(:merge_request) { create(:merge_request, assignee: user) }
let(:merge_request) { create(:merge_request, assignees: [user]) }
let(:data) { described_class.new(merge_request).build(user: user) }
it 'returns correct hook data' do
expect(data[:object_attributes]['assignee_id']).to eq(user.id)
expect(data[:assignee]).to eq(user.hook_attrs)
expect(data).not_to have_key(:assignees)
expect(data[:assignees].first).to eq(user.hook_attrs)
expect(data).not_to have_key(:assignee)
end
end
end
......
......@@ -10,6 +10,7 @@ describe Gitlab::HookData::MergeRequestBuilder do
it 'includes safe attribute' do
%w[
assignee_id
assignee_ids
author_id
created_at
description
......
......@@ -102,6 +102,7 @@ merge_requests:
- merge_request_pipelines
- merge_request_assignees
- suggestions
- assignees
merge_request_diff:
- merge_request
- merge_request_diff_commits
......@@ -336,6 +337,9 @@ push_event_payload:
issue_assignees:
- issue
- assignee
merge_request_assignees:
- merge_request
- assignee
lfs_file_locks:
- user
project_badges:
......
......@@ -621,3 +621,7 @@ Suggestion:
- outdated
- lines_above
- lines_below
MergeRequestAssignee:
- id
- user_id
- merge_request_id
......@@ -19,7 +19,7 @@ describe Gitlab::IssuableMetadata do
let!(:closed_issue) { create(:issue, state: :closed, author: user, project: project) }
let!(:downvote) { create(:award_emoji, :downvote, awardable: closed_issue) }
let!(:upvote) { create(:award_emoji, :upvote, awardable: issue) }
let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") }
let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") }
let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) }
it 'aggregates stats on issues' do
......@@ -39,7 +39,7 @@ describe Gitlab::IssuableMetadata do
end
context 'merge requests' do
let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") }
let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") }
let!(:merge_request_closed) { create(:merge_request, state: "closed", source_project: project, target_project: project, title: "Closed Test") }
let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request) }
let!(:upvote) { create(:award_emoji, :upvote, awardable: merge_request) }
......
......@@ -19,7 +19,7 @@ describe Notify do
create(:merge_request, source_project: project,
target_project: project,
author: current_user,
assignee: assignee,
assignees: [assignee],
description: 'Awesome description')
end
......@@ -275,7 +275,7 @@ describe Notify do
context 'for merge requests' do
describe 'that are new' do
subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) }
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
......@@ -300,7 +300,7 @@ describe Notify do
end
context 'when sent with a reason' do
subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id, NotificationReason::ASSIGNED) }
subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id, NotificationReason::ASSIGNED) }
it_behaves_like 'appearance header and footer enabled'
it_behaves_like 'appearance header and footer not enabled'
......@@ -324,7 +324,7 @@ describe Notify do
describe 'that are reassigned' do
let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id) }
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
......@@ -351,7 +351,7 @@ describe Notify do
end
context 'when sent with a reason' do
subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::ASSIGNED) }
subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::ASSIGNED) }
it_behaves_like 'appearance header and footer enabled'
it_behaves_like 'appearance header and footer not enabled'
......@@ -364,11 +364,11 @@ describe Notify do
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::ASSIGNED)
is_expected.to have_body_text(text)
new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::MENTIONED)
new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::MENTIONED)
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::MENTIONED)
expect(new_subject).to have_body_text(text)
new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, nil)
new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, nil)
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(nil)
expect(new_subject).to have_body_text(text)
end
......@@ -376,7 +376,7 @@ describe Notify do
end
describe 'that are new with a description' do
subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) }
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
......@@ -476,7 +476,7 @@ describe Notify do
source_project: project,
target_project: project,
author: current_user,
assignee: assignee,
assignees: [assignee],
description: 'Awesome description')
end
......
......@@ -684,12 +684,12 @@ describe Ci::Pipeline, :mailer do
source_branch: 'feature',
target_project: project,
target_branch: 'master',
assignee: assignee,
assignees: assignees,
milestone: milestone,
labels: labels)
end
let(:assignee) { create(:user) }
let(:assignees) { create_list(:user, 2) }
let(:milestone) { create(:milestone, project: project) }
let(:labels) { create_list(:label, 2) }
......@@ -710,7 +710,7 @@ describe Ci::Pipeline, :mailer do
'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => pipeline.source_sha.to_s,
'CI_MERGE_REQUEST_TITLE' => merge_request.title,
'CI_MERGE_REQUEST_ASSIGNEES' => assignee.username,
'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).join(','))
end
......@@ -730,7 +730,7 @@ describe Ci::Pipeline, :mailer do
end
context 'without assignee' do
let(:assignee) { nil }
let(:assignees) { [] }
it 'does not expose assignee variable' do
expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES')
......
# frozen_string_literal: true
require 'spec_helper'
describe DeprecatedAssignee do
let(:user) { create(:user) }
describe '#assignee_id=' do
it 'creates the merge_request_assignees relation' do
merge_request = create(:merge_request, assignee_id: user.id)
merge_request.reload
expect(merge_request.merge_request_assignees.count).to eq(1)
end
it 'nullifies the assignee_id column' do
merge_request = create(:merge_request, assignee_id: user.id)
merge_request.reload
expect(merge_request.read_attribute(:assignee_id)).to be_nil
end
context 'when relation already exists' do
it 'overwrites existing assignees' do
other_user = create(:user)
merge_request = create(:merge_request, assignee_id: nil)
merge_request.merge_request_assignees.create!(user_id: user.id)
merge_request.merge_request_assignees.create!(user_id: other_user.id)
expect { merge_request.update!(assignee_id: other_user.id) }
.to change { merge_request.reload.merge_request_assignees.count }
.from(2).to(1)
end
end
end
describe '#assignee=' do
it 'creates the merge_request_assignees relation' do
merge_request = create(:merge_request, assignee: user)
merge_request.reload
expect(merge_request.merge_request_assignees.count).to eq(1)
end
it 'nullifies the assignee_id column' do
merge_request = create(:merge_request, assignee: user)
merge_request.reload
expect(merge_request.read_attribute(:assignee_id)).to be_nil
end
context 'when relation already exists' do
it 'overwrites existing assignees' do
other_user = create(:user)
merge_request = create(:merge_request, assignee: nil)
merge_request.merge_request_assignees.create!(user_id: user.id)
merge_request.merge_request_assignees.create!(user_id: other_user.id)
expect { merge_request.update!(assignee: other_user) }
.to change { merge_request.reload.merge_request_assignees.count }
.from(2).to(1)
end
end
end
describe '#assignee_id' do
it 'returns the first assignee ID' do
other_user = create(:user)
merge_request = create(:merge_request, assignees: [user, other_user])
merge_request.reload
expect(merge_request.assignee_id).to eq(merge_request.assignee_ids.first)
end
end
describe '#assignees' do
context 'when assignee_id exists and there is no relation' do
it 'creates the relation' do
merge_request = create(:merge_request, assignee_id: nil)
merge_request.update_column(:assignee_id, user.id)
expect { merge_request.assignees }.to change { merge_request.merge_request_assignees.count }.from(0).to(1)
end
it 'nullifies the assignee_id' do
merge_request = create(:merge_request, assignee_id: nil)
merge_request.update_column(:assignee_id, user.id)
expect { merge_request.assignees }
.to change { merge_request.read_attribute(:assignee_id) }
.from(user.id).to(nil)
end
end
context 'when DB is read-only' do
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
end
it 'returns a users relation' do
merge_request = create(:merge_request, assignee_id: user.id)
expect(merge_request.assignees).to be_a(ActiveRecord::Relation)
expect(merge_request.assignees).to eq([user])
end
it 'returns an empty relation if no assignee_id is set' do
merge_request = create(:merge_request, assignee_id: nil)
expect(merge_request.assignees).to be_a(ActiveRecord::Relation)
expect(merge_request.assignees).to eq([])
end
end
end
describe '#assignee_ids' do
context 'when assignee_id exists and there is no relation' do
it 'creates the relation' do
merge_request = create(:merge_request, assignee_id: nil)
merge_request.update_column(:assignee_id, user.id)
expect { merge_request.assignee_ids }.to change { merge_request.merge_request_assignees.count }.from(0).to(1)
end
it 'nullifies the assignee_id' do
merge_request = create(:merge_request, assignee_id: nil)
merge_request.update_column(:assignee_id, user.id)
expect { merge_request.assignee_ids }
.to change { merge_request.read_attribute(:assignee_id) }
.from(user.id).to(nil)
end
end
context 'when DB is read-only' do
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
end
it 'returns a list of user IDs' do
merge_request = create(:merge_request, assignee_id: user.id)
expect(merge_request.assignee_ids).to be_a(Array)
expect(merge_request.assignee_ids).to eq([user.id])
end
it 'returns an empty relation if no assignee_id is set' do
merge_request = create(:merge_request, assignee_id: nil)
expect(merge_request.assignee_ids).to be_a(Array)
expect(merge_request.assignee_ids).to eq([])
end
end
end
end
......@@ -502,8 +502,8 @@ describe Issuable do
let(:user2) { create(:user) }
before do
merge_request.update(assignee: user)
merge_request.update(assignee: user2)
merge_request.update(assignees: [user])
merge_request.update(assignees: [user, user2])
expect(Gitlab::HookData::IssuableBuilder)
.to receive(:new).with(merge_request).and_return(builder)
end
......@@ -512,8 +512,7 @@ describe Issuable do
expect(builder).to receive(:build).with(
user: user,
changes: hash_including(
'assignee_id' => [user.id, user2.id],
'assignee' => [user.hook_attrs, user2.hook_attrs]
'assignees' => [[user.hook_attrs], [user.hook_attrs, user2.hook_attrs]]
))
merge_request.to_hook_data(user, old_associations: { assignees: [user] })
......
......@@ -263,7 +263,7 @@ describe Event do
context 'merge request diff note event' do
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project, author: author, assignee: assignee) }
let(:merge_request) { create(:merge_request, source_project: project, author: author, assignees: [assignee]) }
let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) }
let(:target) { note_on_merge_request }
......
......@@ -13,7 +13,7 @@ describe MergeRequest do
it { is_expected.to belong_to(:target_project).class_name('Project') }
it { is_expected.to belong_to(:source_project).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
it { is_expected.to belong_to(:assignee) }
it { is_expected.to have_many(:assignees).through(:merge_request_assignees) }
it { is_expected.to have_many(:merge_request_diffs) }
context 'for forks' do
......@@ -181,31 +181,6 @@ describe MergeRequest do
expect(MergeRequest::Metrics.count).to eq(1)
end
end
describe '#refresh_merge_request_assignees' do
set(:user) { create(:user) }
it 'creates merge request assignees relation upon MR creation' do
merge_request = create(:merge_request, assignee: nil)
expect(merge_request.merge_request_assignees).to be_empty
expect { merge_request.update!(assignee: user) }
.to change { merge_request.reload.merge_request_assignees.count }
.from(0).to(1)
end
it 'updates merge request assignees relation upon MR assignee change' do
another_user = create(:user)
merge_request = create(:merge_request, assignee: user)
expect { merge_request.update!(assignee: another_user) }
.to change { merge_request.reload.merge_request_assignees.first.assignee }
.from(user).to(another_user)
expect(merge_request.merge_request_assignees.count).to eq(1)
end
end
end
describe 'respond to' do
......@@ -337,34 +312,18 @@ describe MergeRequest do
describe '#card_attributes' do
it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
allow(subject).to receive(:assignee).and_return(nil)
allow(subject).to receive(:assignees).and_return([])
expect(subject.card_attributes)
.to eq({ 'Author' => 'Robert', 'Assignee' => nil })
.to eq({ 'Author' => 'Robert', 'Assignee' => "" })
end
it 'includes the assignee name' do
it 'includes the assignees name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
allow(subject).to receive(:assignee).and_return(double(name: 'Douwe'))
allow(subject).to receive(:assignees).and_return([double(name: 'Douwe'), double(name: 'Robert')])
expect(subject.card_attributes)
.to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
end
end
describe '#assignee_ids' do
it 'returns an array of the assigned user id' do
subject.assignee_id = 123
expect(subject.assignee_ids).to eq([123])
end
end
describe '#assignee_ids=' do
it 'sets assignee_id to the last id in the array' do
subject.assignee_ids = [123, 456]
expect(subject.assignee_id).to eq(456)
.to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe and Robert' })
end
end
......@@ -372,7 +331,7 @@ describe MergeRequest do
let(:user) { create(:user) }
it 'returns true for a user that is assigned to a merge request' do
subject.assignee = user
subject.assignees = [user]
expect(subject.assignee_or_author?(user)).to eq(true)
end
......@@ -1949,15 +1908,14 @@ describe MergeRequest do
it 'updates when assignees change' do
user1 = create(:user)
user2 = create(:user)
mr = create(:merge_request, assignee: user1)
mr = create(:merge_request, assignees: [user1])
mr.project.add_developer(user1)
mr.project.add_developer(user2)
expect(user1.assigned_open_merge_requests_count).to eq(1)
expect(user2.assigned_open_merge_requests_count).to eq(0)
mr.assignee = user2
mr.save
mr.assignees = [user2]
expect(user1.assigned_open_merge_requests_count).to eq(0)
expect(user2.assigned_open_merge_requests_count).to eq(1)
......
......@@ -2816,9 +2816,9 @@ describe User do
project = create(:project, :public)
archived_project = create(:project, :public, :archived)
create(:merge_request, source_project: project, author: user, assignee: user)
create(:merge_request, :closed, source_project: project, author: user, assignee: user)
create(:merge_request, source_project: archived_project, author: user, assignee: user)
create(:merge_request, source_project: project, author: user, assignees: [user])
create(:merge_request, :closed, source_project: project, author: user, assignees: [user])
create(:merge_request, source_project: archived_project, author: user, assignees: [user])
expect(user.assigned_open_merge_requests_count(force: true)).to eq 1
end
......
......@@ -270,8 +270,8 @@ describe API::Events do
end
context 'when exists some events' do
let(:merge_request1) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') }
let(:merge_request2) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') }
let(:merge_request1) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
let(:merge_request2) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
before do
create_event(merge_request1)
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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