Commit bc590ce6 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch '12743-subscribe-to-label' into 'master'

Allow subscribing to labels

This implement #12743 and supersedes !2799 (thanks @timothyandrew!).

- Subscribe (and unsubscribe) for labels
- When an issue/merge request is created, notify all subscribers of all its labels
- When an issue/merge request is edited, notify all subscribers of all its newly-added labels

## Done

- [x] Verify that a user is signed in to subscribe/unsubscribe from a label.
- [x] Merge conflicts
- [x] Integration tests
- [x] `issuable.subscribed?` should check subscribers and participants
- [x] When an issuable is relabeled, notify subscribers of the *newly added* labels only
- [x] Screenshots:

### Labels page

![Screen_Shot_2016-03-07_at_19.05.44](/uploads/3e69064e9e1a06ee2e27c778776c1407/Screen_Shot_2016-03-07_at_19.05.44.png)

### HTML email

![Screen_Shot_2016-03-07_at_19.07.36](/uploads/e484e06366a738385f1f6d71b52eecf7/Screen_Shot_2016-03-07_at_19.07.36.png)

### Plain text email

![Screen_Shot_2016-03-07_at_19.07.50](/uploads/dc05f710a8f7ab5eae9f72aa2110e741/Screen_Shot_2016-03-07_at_19.07.50.png)

PS: I've set the milestone to 8.6 since it's getting late for 8.5...

See merge request !3115
parents ac8c6f24 e90d6ec1
...@@ -28,6 +28,7 @@ v 8.6.0 (unreleased) ...@@ -28,6 +28,7 @@ v 8.6.0 (unreleased)
- Update documentation to reflect Guest role not being enforced on internal projects - Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users - Allow search for logged out users
- Allow to define on which builds the current one depends on - Allow to define on which builds the current one depends on
- Allow user subscription to a label: get notified for issues/merge requests related to that label (Timothy Andrew)
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio) - Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view - Don't show Issues/MRs from archived projects in Groups view
- Fix wrong "iid of max iid" in Issuable sidebar for some merged MRs - Fix wrong "iid of max iid" in Issuable sidebar for some merged MRs
......
class @Subscription class @Subscription
constructor: (url) -> constructor: (container) ->
$(".subscribe-button").unbind("click").click (event)=> $container = $(container)
btn = $(event.currentTarget) @url = $container.attr('data-url')
action = btn.find("span").text() @subscribe_button = $container.find('.subscribe-button')
current_status = $(".subscription-status").attr("data-status") @subscription_status = $container.find('.subscription-status')
btn.prop("disabled", true) @subscribe_button.unbind('click').click(@toggleSubscription)
$.post url, =>
btn.prop("disabled", false)
status = if current_status == "subscribed" then "unsubscribed" else "subscribed"
$(".subscription-status").attr("data-status", status)
action = if status == "subscribed" then "Unsubscribe" else "Subscribe"
btn.find("span").text(action)
$(".subscription-status>div").toggleClass("hidden")
toggleSubscription: (event) =>
btn = $(event.currentTarget)
action = btn.find('span').text()
current_status = @subscription_status.attr('data-status')
btn.prop('disabled', true)
$.post @url, =>
btn.prop('disabled', false)
status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed'
@subscription_status.attr('data-status', status)
action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe'
btn.find('span').text(action)
@subscription_status.find('>div').toggleClass('hidden')
...@@ -41,3 +41,7 @@ ...@@ -41,3 +41,7 @@
.color-label { .color-label {
padding: 3px 4px; padding: 3px 4px;
} }
.label-subscription {
display: inline-block;
}
module ToggleSubscriptionAction
extend ActiveSupport::Concern
def toggle_subscription
return unless current_user
subscribable_resource.toggle_subscription(current_user)
render nothing: true
end
private
def subscribable_resource
raise NotImplementedError
end
end
class Projects::IssuesController < Projects::ApplicationController class Projects::IssuesController < Projects::ApplicationController
include ToggleSubscriptionAction
before_action :module_enabled before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :toggle_subscription] before_action :issue, only: [:edit, :update, :show]
# Allow read any issue # Allow read any issue
before_action :authorize_read_issue! before_action :authorize_read_issue!
...@@ -110,12 +112,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -110,12 +112,6 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" }) redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
end end
def toggle_subscription
@issue.toggle_subscription(current_user)
render nothing: true
end
def closed_by_merge_requests def closed_by_merge_requests
@closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user) @closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
end end
...@@ -129,6 +125,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -129,6 +125,7 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_old redirect_old
end end
end end
alias_method :subscribable_resource, :issue
def authorize_update_issue! def authorize_update_issue!
return render_404 unless can?(current_user, :update_issue, @issue) return render_404 unless can?(current_user, :update_issue, @issue)
......
class Projects::LabelsController < Projects::ApplicationController class Projects::LabelsController < Projects::ApplicationController
include ToggleSubscriptionAction
before_action :module_enabled before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy] before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_read_label! before_action :authorize_read_label!
before_action :authorize_admin_labels!, except: [:index] before_action :authorize_admin_labels!, only: [
:new, :create, :edit, :update, :generate, :destroy
]
respond_to :js, :html respond_to :js, :html
...@@ -73,8 +77,9 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -73,8 +77,9 @@ class Projects::LabelsController < Projects::ApplicationController
end end
def label def label
@label = @project.labels.find(params[:id]) @label ||= @project.labels.find(params[:id])
end end
alias_method :subscribable_resource, :label
def authorize_admin_labels! def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project) return render_404 unless can?(current_user, :admin_label, @project)
......
class Projects::MergeRequestsController < Projects::ApplicationController class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction
include DiffHelper include DiffHelper
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check, :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
:ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds :ci_status, :cancel_merge_when_build_succeeds
] ]
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds] before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
...@@ -233,12 +234,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -233,12 +234,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response render json: response
end end
def toggle_subscription
@merge_request.toggle_subscription(current_user)
render nothing: true
end
protected protected
def selected_target_project def selected_target_project
...@@ -252,6 +247,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -252,6 +247,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge_request def merge_request
@merge_request ||= @project.merge_requests.find_by!(iid: params[:id]) @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end end
alias_method :subscribable_resource, :merge_request
def closes_issues def closes_issues
@closes_issues ||= @merge_request.closes_issues @closes_issues ||= @merge_request.closes_issues
......
...@@ -124,6 +124,14 @@ module LabelsHelper ...@@ -124,6 +124,14 @@ module LabelsHelper
options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name]) options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name])
end end
def label_subscription_status(label)
label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
end
def label_subscription_toggle_button_text(label)
label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
end
# Required for Banzai::Filter::LabelReferenceFilter # Required for Banzai::Filter::LabelReferenceFilter
module_function :render_colored_label, :render_colored_cross_project_label, module_function :render_colored_label, :render_colored_cross_project_label,
:text_color_for_bg, :escape_once :text_color_for_bg, :escape_once
......
...@@ -16,7 +16,15 @@ module Emails ...@@ -16,7 +16,15 @@ module Emails
def closed_issue_email(recipient_id, issue_id, updated_by_user_id) def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
@updated_by = User.find updated_by_user_id @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id)
setup_issue_mail(issue_id, recipient_id)
@label_names = label_names
@labels_url = namespace_project_labels_url(@project.namespace, @project)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end end
...@@ -24,20 +32,12 @@ module Emails ...@@ -24,20 +32,12 @@ module Emails
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
@issue_status = status @issue_status = status
@updated_by = User.find updated_by_user_id @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end end
private private
def issue_thread_options(sender_id, recipient_id)
{
from: sender(sender_id),
to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})")
}
end
def setup_issue_mail(issue_id, recipient_id) def setup_issue_mail(issue_id, recipient_id)
@issue = Issue.find(issue_id) @issue = Issue.find(issue_id)
@project = @issue.project @project = @issue.project
...@@ -45,5 +45,13 @@ module Emails ...@@ -45,5 +45,13 @@ module Emails
@sent_notification = SentNotification.record(@issue, recipient_id, reply_key) @sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end end
def issue_thread_options(sender_id, recipient_id)
{
from: sender(sender_id),
to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})")
}
end
end end
end end
...@@ -3,50 +3,43 @@ module Emails ...@@ -3,50 +3,43 @@ module Emails
def new_merge_request_email(recipient_id, merge_request_id) def new_merge_request_email(recipient_id, merge_request_id)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
mail_new_thread(@merge_request, mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id))
from: sender(@merge_request.author_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
end end
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id) def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
mail_answer_thread(@merge_request, mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
from: sender(updated_by_user_id), end
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id)
@label_names = label_names
@labels_url = namespace_project_labels_url(@project.namespace, @project)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end end
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@updated_by = User.find updated_by_user_id @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@merge_request, mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
end end
def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
mail_answer_thread(@merge_request, mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
end end
def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id) def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@mr_status = status @mr_status = status
@updated_by = User.find updated_by_user_id @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@merge_request, mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
end end
private private
...@@ -54,11 +47,17 @@ module Emails ...@@ -54,11 +47,17 @@ module Emails
def setup_merge_request_mail(merge_request_id, recipient_id) def setup_merge_request_mail(merge_request_id, recipient_id)
@merge_request = MergeRequest.find(merge_request_id) @merge_request = MergeRequest.find(merge_request_id)
@project = @merge_request.project @project = @merge_request.project
@target_url = namespace_project_merge_request_url(@project.namespace, @target_url = namespace_project_merge_request_url(@project.namespace, @project, @merge_request)
@project,
@merge_request)
@sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key) @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
end end
def merge_request_thread_options(sender_id, recipient_id)
{
from: sender(sender_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})")
}
end
end end
end end
...@@ -8,6 +8,7 @@ module Issuable ...@@ -8,6 +8,7 @@ module Issuable
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Participable include Participable
include Mentionable include Mentionable
include Subscribable
include StripAttribute include StripAttribute
included do included do
...@@ -18,7 +19,6 @@ module Issuable ...@@ -18,7 +19,6 @@ module Issuable
has_many :notes, as: :noteable, dependent: :destroy has_many :notes, as: :noteable, dependent: :destroy
has_many :label_links, as: :target, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links has_many :labels, through: :label_links
has_many :subscriptions, dependent: :destroy, as: :subscribable
validates :author, presence: true validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 } validates :title, presence: true, length: { within: 0..255 }
...@@ -149,28 +149,10 @@ module Issuable ...@@ -149,28 +149,10 @@ module Issuable
notes.awards.where(note: "thumbsup").count notes.awards.where(note: "thumbsup").count
end end
def subscribed?(user) def subscribed_without_subscriptions?(user)
subscription = subscriptions.find_by_user_id(user.id)
if subscription
return subscription.subscribed
end
participants(user).include?(user) participants(user).include?(user)
end end
def toggle_subscription(user)
subscriptions.
find_or_initialize_by(user_id: user.id).
update(subscribed: !subscribed?(user))
end
def unsubscribe(user)
subscriptions.
find_or_initialize_by(user_id: user.id).
update(subscribed: false)
end
def to_hook_data(user) def to_hook_data(user)
hook_data = { hook_data = {
object_kind: self.class.name.underscore, object_kind: self.class.name.underscore,
......
# == Subscribable concern
#
# Users can subscribe to these models.
#
# Used by Issue, MergeRequest, Label
#
module Subscribable
extend ActiveSupport::Concern
included do
has_many :subscriptions, dependent: :destroy, as: :subscribable
end
def subscribed?(user)
if subscription = subscriptions.find_by_user_id(user.id)
subscription.subscribed
else
subscribed_without_subscriptions?(user)
end
end
# Override this method to define custom logic to consider a subscribable as
# subscribed without an explicit subscription record.
def subscribed_without_subscriptions?(user)
false
end
def subscribers
subscriptions.where(subscribed: true).map(&:user)
end
def toggle_subscription(user)
subscriptions.
find_or_initialize_by(user_id: user.id).
update(subscribed: !subscribed?(user))
end
def unsubscribe(user)
subscriptions.
find_or_initialize_by(user_id: user.id).
update(subscribed: false)
end
end
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
class Label < ActiveRecord::Base class Label < ActiveRecord::Base
include Referable include Referable
include Subscribable
# Represents a "No Label" state used for filtering Issues and Merge # Represents a "No Label" state used for filtering Issues and Merge
# Requests that have no label assigned. # Requests that have no label assigned.
LabelStruct = Struct.new(:title, :name) LabelStruct = Struct.new(:title, :name)
......
...@@ -15,7 +15,7 @@ class Subscription < ActiveRecord::Base ...@@ -15,7 +15,7 @@ class Subscription < ActiveRecord::Base
belongs_to :user belongs_to :user
belongs_to :subscribable, polymorphic: true belongs_to :subscribable, polymorphic: true
validates :user_id, validates :user_id,
uniqueness: { scope: [:subscribable_id, :subscribable_type] }, uniqueness: { scope: [:subscribable_id, :subscribable_type] },
presence: true presence: true
end end
...@@ -11,7 +11,10 @@ class IssuableBaseService < BaseService ...@@ -11,7 +11,10 @@ class IssuableBaseService < BaseService
issuable, issuable.project, current_user, issuable.milestone) issuable, issuable.project, current_user, issuable.milestone)
end end
def create_labels_note(issuable, added_labels, removed_labels) def create_labels_note(issuable, old_labels)
added_labels = issuable.labels - old_labels
removed_labels = old_labels - issuable.labels
SystemNoteService.change_label( SystemNoteService.change_label(
issuable, issuable.project, current_user, added_labels, removed_labels) issuable, issuable.project, current_user, added_labels, removed_labels)
end end
...@@ -71,20 +74,19 @@ class IssuableBaseService < BaseService ...@@ -71,20 +74,19 @@ class IssuableBaseService < BaseService
end end
end end
def has_changes?(issuable, options = {}) def has_changes?(issuable, old_labels: [])
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch] valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
attrs_changed = valid_attrs.any? do |attr| attrs_changed = valid_attrs.any? do |attr|
issuable.previous_changes.include?(attr.to_s) issuable.previous_changes.include?(attr.to_s)
end end
old_labels = options[:old_labels] labels_changed = issuable.labels != old_labels
labels_changed = old_labels && issuable.labels != old_labels
attrs_changed || labels_changed attrs_changed || labels_changed
end end
def handle_common_system_notes(issuable, options = {}) def handle_common_system_notes(issuable, old_labels: [])
if issuable.previous_changes.include?('title') if issuable.previous_changes.include?('title')
create_title_change_note(issuable, issuable.previous_changes['title'].first) create_title_change_note(issuable, issuable.previous_changes['title'].first)
end end
...@@ -93,9 +95,6 @@ class IssuableBaseService < BaseService ...@@ -93,9 +95,6 @@ class IssuableBaseService < BaseService
create_task_status_note(issuable) create_task_status_note(issuable)
end end
old_labels = options[:old_labels] create_labels_note(issuable, old_labels) if issuable.labels != old_labels
if old_labels && (issuable.labels != old_labels)
create_labels_note(issuable, issuable.labels - old_labels, old_labels - issuable.labels)
end
end end
end end
...@@ -4,8 +4,8 @@ module Issues ...@@ -4,8 +4,8 @@ module Issues
update(issue) update(issue)
end end
def handle_changes(issue, options = {}) def handle_changes(issue, old_labels: [])
if has_changes?(issue, options) if has_changes?(issue, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(issue, current_user) todo_service.mark_pending_todos_as_done(issue, current_user)
end end
...@@ -23,6 +23,11 @@ module Issues ...@@ -23,6 +23,11 @@ module Issues
notification_service.reassigned_issue(issue, current_user) notification_service.reassigned_issue(issue, current_user)
todo_service.reassigned_issue(issue, current_user) todo_service.reassigned_issue(issue, current_user)
end end
added_labels = issue.labels - old_labels
if added_labels.present?
notification_service.relabeled_issue(issue, added_labels, current_user)
end
end end
def reopen_service def reopen_service
......
...@@ -14,8 +14,8 @@ module MergeRequests ...@@ -14,8 +14,8 @@ module MergeRequests
update(merge_request) update(merge_request)
end end
def handle_changes(merge_request, options = {}) def handle_changes(merge_request, old_labels: [])
if has_changes?(merge_request, options) if has_changes?(merge_request, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(merge_request, current_user) todo_service.mark_pending_todos_as_done(merge_request, current_user)
end end
...@@ -44,6 +44,15 @@ module MergeRequests ...@@ -44,6 +44,15 @@ module MergeRequests
merge_request.previous_changes.include?('source_branch') merge_request.previous_changes.include?('source_branch')
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
end end
added_labels = merge_request.labels - old_labels
if added_labels.present?
notification_service.relabeled_merge_request(
merge_request,
added_labels,
current_user
)
end
end end
def reopen_service def reopen_service
......
...@@ -24,16 +24,17 @@ class NotificationService ...@@ -24,16 +24,17 @@ class NotificationService
end end
end end
# When create an issue we should send next emails: # When create an issue we should send an email to:
# #
# * issue assignee if their notification level is not Disabled # * issue assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating # * project team members with notification level higher then Participating
# * watchers of the issue's labels
# #
def new_issue(issue, current_user) def new_issue(issue, current_user)
new_resource_email(issue, issue.project, 'new_issue_email') new_resource_email(issue, issue.project, 'new_issue_email')
end end
# When we close an issue we should send next emails: # When we close an issue we should send an email to:
# #
# * issue author if their notification level is not Disabled # * issue author if their notification level is not Disabled
# * issue assignee if their notification level is not Disabled # * issue assignee if their notification level is not Disabled
...@@ -43,7 +44,7 @@ class NotificationService ...@@ -43,7 +44,7 @@ class NotificationService
close_resource_email(issue, issue.project, current_user, 'closed_issue_email') close_resource_email(issue, issue.project, current_user, 'closed_issue_email')
end end
# When we reassign an issue we should send next emails: # When we reassign an issue we should send an email to:
# #
# * issue old assignee if their notification level is not Disabled # * issue old assignee if their notification level is not Disabled
# * issue new assignee if their notification level is not Disabled # * issue new assignee if their notification level is not Disabled
...@@ -52,16 +53,25 @@ class NotificationService ...@@ -52,16 +53,25 @@ class NotificationService
reassign_resource_email(issue, issue.project, current_user, 'reassigned_issue_email') reassign_resource_email(issue, issue.project, current_user, 'reassigned_issue_email')
end end
# When we add labels to an issue we should send an email to:
#
# * watchers of the issue's labels
#
def relabeled_issue(issue, added_labels, current_user)
relabeled_resource_email(issue, added_labels, current_user, 'relabeled_issue_email')
end
# When create a merge request we should send next emails: # When create a merge request we should send an email to:
# #
# * mr assignee if their notification level is not Disabled # * mr assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
# * watchers of the mr's labels
# #
def new_merge_request(merge_request, current_user) def new_merge_request(merge_request, current_user)
new_resource_email(merge_request, merge_request.target_project, 'new_merge_request_email') new_resource_email(merge_request, merge_request.target_project, 'new_merge_request_email')
end end
# When we reassign a merge_request we should send next emails: # 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 old assignee if their notification level is not Disabled
# * merge_request assignee if their notification level is not Disabled # * merge_request assignee if their notification level is not Disabled
...@@ -70,6 +80,14 @@ class NotificationService ...@@ -70,6 +80,14 @@ class NotificationService
reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email') reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email')
end end
# When we add labels to a merge request we should send an email to:
#
# * watchers of the mr's labels
#
def relabeled_merge_request(merge_request, added_labels, current_user)
relabeled_resource_email(merge_request, added_labels, current_user, 'relabeled_merge_request_email')
end
def close_mr(merge_request, current_user) def close_mr(merge_request, current_user)
close_resource_email(merge_request, merge_request.target_project, current_user, 'closed_merge_request_email') close_resource_email(merge_request, merge_request.target_project, current_user, 'closed_merge_request_email')
end end
...@@ -91,7 +109,8 @@ class NotificationService ...@@ -91,7 +109,8 @@ class NotificationService
reopen_resource_email( reopen_resource_email(
merge_request, merge_request,
merge_request.target_project, merge_request.target_project,
current_user, 'merge_request_status_email', current_user,
'merge_request_status_email',
'reopened' 'reopened'
) )
end end
...@@ -348,19 +367,23 @@ class NotificationService ...@@ -348,19 +367,23 @@ class NotificationService
end end
def add_subscribed_users(recipients, target) def add_subscribed_users(recipients, target)
return recipients unless target.respond_to? :subscriptions return recipients unless target.respond_to? :subscribers
subscriptions = target.subscriptions recipients + target.subscribers
end
if subscriptions.any? def add_labels_subscribers(recipients, target, labels: nil)
recipients + subscriptions.where(subscribed: true).map(&:user) return recipients unless target.respond_to? :labels
else
recipients (labels || target.labels).each do |label|
recipients += label.subscribers
end end
recipients
end end
def new_resource_email(target, project, method) def new_resource_email(target, project, method)
recipients = build_recipients(target, project, target.author) recipients = build_recipients(target, project, target.author, action: :new)
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later mailer.send(method, recipient.id, target.id).deliver_later
...@@ -392,6 +415,15 @@ class NotificationService ...@@ -392,6 +415,15 @@ class NotificationService
end end
end end
def relabeled_resource_email(target, labels, current_user, method)
recipients = build_relabeled_recipients(target, current_user, labels: labels)
label_names = labels.map(&:name)
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, label_names, current_user.id).deliver_later
end
end
def reopen_resource_email(target, project, current_user, method, status) def reopen_resource_email(target, project, current_user, method, status)
recipients = build_recipients(target, project, current_user) recipients = build_recipients(target, project, current_user)
...@@ -416,6 +448,11 @@ class NotificationService ...@@ -416,6 +448,11 @@ class NotificationService
recipients = reject_muted_users(recipients, project) recipients = reject_muted_users(recipients, project)
recipients = add_subscribed_users(recipients, target) recipients = add_subscribed_users(recipients, target)
if action == :new
recipients = add_labels_subscribers(recipients, target)
end
recipients = reject_unsubscribed_users(recipients, target) recipients = reject_unsubscribed_users(recipients, target)
recipients.delete(current_user) recipients.delete(current_user)
...@@ -423,6 +460,13 @@ class NotificationService ...@@ -423,6 +460,13 @@ class NotificationService
recipients.uniq recipients.uniq
end end
def build_relabeled_recipients(target, current_user, labels:)
recipients = add_labels_subscribers([], target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target)
recipients.delete(current_user)
recipients.uniq
end
def mailer def mailer
Notify Notify
end end
......
...@@ -42,12 +42,15 @@ ...@@ -42,12 +42,15 @@
- else - else
#{link_to "View it on GitLab", @target_url}. #{link_to "View it on GitLab", @target_url}.
%br %br
-# Don't link the host is the line below, one link in the email is easier to quickly click than two. -# Don't link the host in the line below, one link in the email is easier to quickly click than two.
You're receiving this email because of your account on #{Gitlab.config.gitlab.host}. You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
If you'd like to receive fewer emails, you can If you'd like to receive fewer emails, you can
- if @sent_notification && @sent_notification.unsubscribable? - if @labels_url
= link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification) adjust your #{link_to 'label subscriptions', @labels_url}.
from this thread or - else
adjust your notification settings. - if @sent_notification && @sent_notification.unsubscribable?
= link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
from this thread or
adjust your notification settings.
= email_action @target_url = email_action @target_url
Reassigned <%= issuable.class.model_name.human.titleize %> <%= issuable.iid %> Reassigned <%= issuable.class.model_name.human.titleize %> <%= issuable.iid %>
<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, {only_path: false}]) %> <%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%> Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%>
to <%= "#{issuable.assignee_id ? issuable.assignee_name : 'Unassigned'}" %> to <%= "#{issuable.assignee_id ? issuable.assignee_name : 'Unassigned'}" %>
%p
#{'Label'.pluralize(@label_names.size)} added:
%em= @label_names.to_sentence
<%= 'Label'.pluralize(@label_names.size) %> added: <%= @label_names.to_sentence %>
<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
= render 'relabeled_issuable_email', issuable: @issue
<%= render 'relabeled_issuable_email', issuable: @issue %>
= render 'relabeled_issuable_email', issuable: @merge_request
<%= render 'relabeled_issuable_email', issuable: @merge_request %>
...@@ -10,6 +10,16 @@ ...@@ -10,6 +10,16 @@
= link_to_label(label) do = link_to_label(label) do
= pluralize label.open_issues_count, 'open issue' = pluralize label.open_issues_count, 'open issue'
- if current_user
.label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
.subscription-status{data: {status: label_subscription_status(label)}}
%button.btn.btn-sm.btn-info.subscribe-button
%span= label_subscription_toggle_button_text(label)
- if can? current_user, :admin_label, @project - if can? current_user, :admin_label, @project
= link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm' = link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm'
= link_to 'Delete', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} = link_to 'Delete', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
- if current_user
:javascript
new Subscription('##{dom_id(label)} .label-subscription');
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
%hr %hr
- if current_user - if current_user
- subscribed = issuable.subscribed?(current_user) - subscribed = issuable.subscribed?(current_user)
.block.light .block.light.subscription{data: {url: toggle_subscription_path(issuable)}}
.sidebar-collapsed-icon .sidebar-collapsed-icon
= icon('rss') = icon('rss')
.title.hide-collapsed .title.hide-collapsed
...@@ -124,5 +124,5 @@ ...@@ -124,5 +124,5 @@
= clipboard_button(clipboard_text: project_ref) = clipboard_button(clipboard_text: project_ref)
:javascript :javascript
new Subscription("#{toggle_subscription_path(issuable)}"); new Subscription('.subscription');
new IssuableContext(); new IssuableContext();
...@@ -675,6 +675,10 @@ Rails.application.routes.draw do ...@@ -675,6 +675,10 @@ Rails.application.routes.draw do
collection do collection do
post :generate post :generate
end end
member do
post :toggle_subscription
end
end end
resources :issues, constraints: { id: /\d+/ }, except: [:destroy] do resources :issues, constraints: { id: /\d+/ }, except: [:destroy] do
......
@labels
Feature: Labels
Background:
Given I sign in as a user
And I own project "Shop"
And project "Shop" has labels: "bug", "feature", "enhancement"
When I visit project "Shop" labels page
@javascript
Scenario: I can subscribe to a label
Then I should see that I am not subscribed to the "bug" label
When I click button "Subscribe" for the "bug" label
Then I should see that I am subscribed to the "bug" label
When I click button "Unsubscribe" for the "bug" label
Then I should see that I am not subscribed to the "bug" label
class Spinach::Features::Labels < Spinach::FeatureSteps
include SharedAuthentication
include SharedIssuable
include SharedProject
include SharedNote
include SharedPaths
include SharedMarkdown
step 'And I visit project "Shop" labels page' do
visit namespace_project_labels_path(project.namespace, project)
end
step 'I should see that I am subscribed to the "bug" label' do
expect(subscribe_button).to have_content 'Unsubscribe'
end
step 'I should see that I am not subscribed to the "bug" label' do
expect(subscribe_button).to have_content 'Subscribe'
end
step 'I click button "Unsubscribe" for the "bug" label' do
subscribe_button.click
end
step 'I click button "Subscribe" for the "bug" label' do
subscribe_button.click
end
private
def subscribe_button
first('.subscribe-button span')
end
end
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
FactoryGirl.define do FactoryGirl.define do
factory :label do factory :label do
title "Bug" sequence(:title) { |n| "label#{n}" }
color "#990000" color "#990000"
project project
end end
......
...@@ -100,6 +100,34 @@ describe Notify do ...@@ -100,6 +100,34 @@ describe Notify do
end end
end end
describe 'that have been relabeled' do
subject { Notify.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(current_user.name)
expect(sender.address).to eq(gitlab_sender)
end
it 'has the correct subject' do
is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
end
it 'contains the names of the added labels' do
is_expected.to have_body_text /foo, bar, and baz/
end
it 'contains a link to the issue' do
is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
end
end
describe 'status changed' do describe 'status changed' do
let(:status) { 'closed' } let(:status) { 'closed' }
subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) } subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
...@@ -219,6 +247,34 @@ describe Notify do ...@@ -219,6 +247,34 @@ describe Notify do
end end
end end
describe 'that have been relabeled' do
subject { Notify.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(current_user.name)
expect(sender.address).to eq(gitlab_sender)
end
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
end
it 'contains the names of the added labels' do
is_expected.to have_body_text /foo, bar, and baz/
end
it 'contains a link to the merge request' do
is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/
end
end
describe 'status changed' do describe 'status changed' do
let(:status) { 'reopened' } let(:status) { 'reopened' }
subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) } subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
......
...@@ -112,6 +112,10 @@ shared_examples 'an unsubscribeable thread' do ...@@ -112,6 +112,10 @@ shared_examples 'an unsubscribeable thread' do
it { is_expected.to have_body_text /unsubscribe/ } it { is_expected.to have_body_text /unsubscribe/ }
end end
shared_examples "a user cannot unsubscribe through footer link" do shared_examples 'a user cannot unsubscribe through footer link' do
it { is_expected.not_to have_body_text /unsubscribe/ } it { is_expected.not_to have_body_text /unsubscribe/ }
end end
shared_examples 'an email with a labels subscriptions link in its footer' do
it { is_expected.to have_body_text /label subscriptions/ }
end
...@@ -113,6 +113,48 @@ describe Issue, "Issuable" do ...@@ -113,6 +113,48 @@ describe Issue, "Issuable" do
end end
end end
describe '#subscribed?' do
context 'user is not a participant in the issue' do
before { allow(issue).to receive(:participants).with(user).and_return([]) }
it 'returns false when no subcription exists' do
expect(issue.subscribed?(user)).to be_falsey
end
it 'returns true when a subcription exists and subscribed is true' do
issue.subscriptions.create(user: user, subscribed: true)
expect(issue.subscribed?(user)).to be_truthy
end
it 'returns false when a subcription exists and subscribed is false' do
issue.subscriptions.create(user: user, subscribed: false)
expect(issue.subscribed?(user)).to be_falsey
end
end
context 'user is a participant in the issue' do
before { allow(issue).to receive(:participants).with(user).and_return([user]) }
it 'returns false when no subcription exists' do
expect(issue.subscribed?(user)).to be_truthy
end
it 'returns true when a subcription exists and subscribed is true' do
issue.subscriptions.create(user: user, subscribed: true)
expect(issue.subscribed?(user)).to be_truthy
end
it 'returns false when a subcription exists and subscribed is false' do
issue.subscriptions.create(user: user, subscribed: false)
expect(issue.subscribed?(user)).to be_falsey
end
end
end
describe "#to_hook_data" do describe "#to_hook_data" do
let(:data) { issue.to_hook_data(user) } let(:data) { issue.to_hook_data(user) }
let(:project) { issue.project } let(:project) { issue.project }
......
require 'spec_helper'
describe Subscribable, 'Subscribable' do
let(:resource) { create(:issue) }
let(:user) { create(:user) }
describe '#subscribed?' do
it 'returns false when no subcription exists' do
expect(resource.subscribed?(user)).to be_falsey
end
it 'returns true when a subcription exists and subscribed is true' do
resource.subscriptions.create(user: user, subscribed: true)
expect(resource.subscribed?(user)).to be_truthy
end
it 'returns false when a subcription exists and subscribed is false' do
resource.subscriptions.create(user: user, subscribed: false)
expect(resource.subscribed?(user)).to be_falsey
end
end
describe '#subscribers' do
it 'returns [] when no subcribers exists' do
expect(resource.subscribers).to be_empty
end
it 'returns the subscribed users' do
resource.subscriptions.create(user: user, subscribed: true)
resource.subscriptions.create(user: create(:user), subscribed: false)
expect(resource.subscribers).to eq [user]
end
end
describe '#toggle_subscription' do
it 'toggles the current subscription state for the given user' do
expect(resource.subscribed?(user)).to be_falsey
resource.toggle_subscription(user)
expect(resource.subscribed?(user)).to be_truthy
end
end
describe '#unsubscribe' do
it 'unsubscribes the given current user' do
resource.subscriptions.create(user: user, subscribed: true)
expect(resource.subscribed?(user)).to be_truthy
resource.unsubscribe(user)
expect(resource.subscribed?(user)).to be_falsey
end
end
end
...@@ -6,6 +6,7 @@ describe Issues::UpdateService, services: true do ...@@ -6,6 +6,7 @@ describe Issues::UpdateService, services: true do
let(:user3) { create(:user) } let(:user3) { create(:user) }
let(:issue) { create(:issue, title: 'Old title', assignee_id: user3.id) } let(:issue) { create(:issue, title: 'Old title', assignee_id: user3.id) }
let(:label) { create(:label) } let(:label) { create(:label) }
let(:label2) { create(:label) }
let(:project) { issue.project } let(:project) { issue.project }
before do before do
...@@ -48,7 +49,7 @@ describe Issues::UpdateService, services: true do ...@@ -48,7 +49,7 @@ describe Issues::UpdateService, services: true do
it { expect(@issue.assignee).to eq(user2) } it { expect(@issue.assignee).to eq(user2) }
it { expect(@issue).to be_closed } it { expect(@issue).to be_closed }
it { expect(@issue.labels.count).to eq(1) } it { expect(@issue.labels.count).to eq(1) }
it { expect(@issue.labels.first.title).to eq('Bug') } it { expect(@issue.labels.first.title).to eq(label.name) }
it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do
deliveries = ActionMailer::Base.deliveries deliveries = ActionMailer::Base.deliveries
...@@ -148,6 +149,48 @@ describe Issues::UpdateService, services: true do ...@@ -148,6 +149,48 @@ describe Issues::UpdateService, services: true do
end end
end end
context 'when the issue is relabeled' do
let!(:non_subscriber) { create(:user) }
let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
it 'sends notifications for subscribers of newly added labels' do
opts = { label_ids: [label.id] }
perform_enqueued_jobs do
@issue = Issues::UpdateService.new(project, user, opts).execute(issue)
end
should_email(subscriber)
should_not_email(non_subscriber)
end
context 'when issue has the `label` label' do
before { issue.labels << label }
it 'does not send notifications for existing labels' do
opts = { label_ids: [label.id, label2.id] }
perform_enqueued_jobs do
@issue = Issues::UpdateService.new(project, user, opts).execute(issue)
end
should_not_email(subscriber)
should_not_email(non_subscriber)
end
it 'does not send notifications for removed labels' do
opts = { label_ids: [label2.id] }
perform_enqueued_jobs do
@issue = Issues::UpdateService.new(project, user, opts).execute(issue)
end
should_not_email(subscriber)
should_not_email(non_subscriber)
end
end
end
context 'when Issue has tasks' do context 'when Issue has tasks' do
before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) } before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
......
...@@ -7,6 +7,7 @@ describe MergeRequests::UpdateService, services: true do ...@@ -7,6 +7,7 @@ describe MergeRequests::UpdateService, services: true do
let(:merge_request) { create(:merge_request, :simple, title: 'Old title', assignee_id: user3.id) } let(:merge_request) { create(:merge_request, :simple, title: 'Old title', assignee_id: user3.id) }
let(:project) { merge_request.project } let(:project) { merge_request.project }
let(:label) { create(:label) } let(:label) { create(:label) }
let(:label2) { create(:label) }
before do before do
project.team << [user, :master] project.team << [user, :master]
...@@ -53,7 +54,7 @@ describe MergeRequests::UpdateService, services: true do ...@@ -53,7 +54,7 @@ describe MergeRequests::UpdateService, services: true do
it { expect(@merge_request.assignee).to eq(user2) } it { expect(@merge_request.assignee).to eq(user2) }
it { expect(@merge_request).to be_closed } it { expect(@merge_request).to be_closed }
it { expect(@merge_request.labels.count).to eq(1) } it { expect(@merge_request.labels.count).to eq(1) }
it { expect(@merge_request.labels.first.title).to eq('Bug') } it { expect(@merge_request.labels.first.title).to eq(label.name) }
it { expect(@merge_request.target_branch).to eq('target') } it { expect(@merge_request.target_branch).to eq('target') }
it 'should execute hooks with update action' do it 'should execute hooks with update action' do
...@@ -176,6 +177,48 @@ describe MergeRequests::UpdateService, services: true do ...@@ -176,6 +177,48 @@ describe MergeRequests::UpdateService, services: true do
end end
end end
context 'when the issue is relabeled' do
let!(:non_subscriber) { create(:user) }
let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
it 'sends notifications for subscribers of newly added labels' do
opts = { label_ids: [label.id] }
perform_enqueued_jobs do
@merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
should_email(subscriber)
should_not_email(non_subscriber)
end
context 'when issue has the `label` label' do
before { merge_request.labels << label }
it 'does not send notifications for existing labels' do
opts = { label_ids: [label.id, label2.id] }
perform_enqueued_jobs do
@merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
should_not_email(subscriber)
should_not_email(non_subscriber)
end
it 'does not send notifications for removed labels' do
opts = { label_ids: [label2.id] }
perform_enqueued_jobs do
@merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
should_not_email(subscriber)
should_not_email(non_subscriber)
end
end
end
context 'when MergeRequest has tasks' do context 'when MergeRequest has tasks' do
before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) } before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
......
...@@ -224,6 +224,15 @@ describe NotificationService, services: true do ...@@ -224,6 +224,15 @@ describe NotificationService, services: true do
should_not_email(issue.assignee) should_not_email(issue.assignee)
end end
it "emails subscribers of the issue's labels" do
subscriber = create(:user)
label = create(:label, issues: [issue])
label.toggle_subscription(subscriber)
notification.new_issue(issue, @u_disabled)
should_email(subscriber)
end
end end
describe :reassigned_issue do describe :reassigned_issue do
...@@ -296,6 +305,35 @@ describe NotificationService, services: true do ...@@ -296,6 +305,35 @@ describe NotificationService, services: true do
end end
end end
describe '#relabeled_issue' do
let(:label) { create(:label, issues: [issue]) }
let(:label2) { create(:label) }
let!(:subscriber_to_label) { create(:user).tap { |u| label.toggle_subscription(u) } }
let!(:subscriber_to_label2) { create(:user).tap { |u| label2.toggle_subscription(u) } }
it "emails subscribers of the issue's added labels only" do
notification.relabeled_issue(issue, [label2], @u_disabled)
should_not_email(subscriber_to_label)
should_email(subscriber_to_label2)
end
it "doesn't send email to anyone but subscribers of the given labels" do
notification.relabeled_issue(issue, [label2], @u_disabled)
should_not_email(issue.assignee)
should_not_email(issue.author)
should_not_email(@u_watcher)
should_not_email(@u_participant_mentioned)
should_not_email(@subscriber)
should_not_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(subscriber_to_label)
should_email(subscriber_to_label2)
end
end
describe :close_issue do describe :close_issue do
it 'should sent email to issue assignee and issue author' do it 'should sent email to issue assignee and issue author' do
notification.close_issue(issue, @u_disabled) notification.close_issue(issue, @u_disabled)
...@@ -349,6 +387,15 @@ describe NotificationService, services: true do ...@@ -349,6 +387,15 @@ describe NotificationService, services: true do
should_not_email(@u_participating) should_not_email(@u_participating)
should_not_email(@u_disabled) should_not_email(@u_disabled)
end end
it "emails subscribers of the merge request's labels" do
subscriber = create(:user)
label = create(:label, merge_requests: [merge_request])
label.toggle_subscription(subscriber)
notification.new_merge_request(merge_request, @u_disabled)
should_email(subscriber)
end
end end
describe :reassigned_merge_request do describe :reassigned_merge_request do
...@@ -366,6 +413,35 @@ describe NotificationService, services: true do ...@@ -366,6 +413,35 @@ describe NotificationService, services: true do
end end
end end
describe :relabel_merge_request do
let(:label) { create(:label, merge_requests: [merge_request]) }
let(:label2) { create(:label) }
let!(:subscriber_to_label) { create(:user).tap { |u| label.toggle_subscription(u) } }
let!(:subscriber_to_label2) { create(:user).tap { |u| label2.toggle_subscription(u) } }
it "emails subscribers of the merge request's added labels only" do
notification.relabeled_merge_request(merge_request, [label2], @u_disabled)
should_not_email(subscriber_to_label)
should_email(subscriber_to_label2)
end
it "doesn't send email to anyone but subscribers of the given labels" do
notification.relabeled_merge_request(merge_request, [label2], @u_disabled)
should_not_email(merge_request.assignee)
should_not_email(merge_request.author)
should_not_email(@u_watcher)
should_not_email(@u_participant_mentioned)
should_not_email(@subscriber)
should_not_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(subscriber_to_label)
should_email(subscriber_to_label2)
end
end
describe :closed_merge_request do describe :closed_merge_request do
it do it do
notification.close_mr(merge_request, @u_disabled) notification.close_mr(merge_request, @u_disabled)
...@@ -467,16 +543,4 @@ describe NotificationService, services: true do ...@@ -467,16 +543,4 @@ describe NotificationService, services: true do
# Make the watcher a subscriber to detect dupes # Make the watcher a subscriber to detect dupes
issuable.subscriptions.create(user: @watcher_and_subscriber, subscribed: true) issuable.subscriptions.create(user: @watcher_and_subscriber, subscribed: true)
end end
def sent_to_user?(user)
ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
end
def should_email(user)
expect(sent_to_user?(user)).to be_truthy
end
def should_not_email(user)
expect(sent_to_user?(user)).to be_falsey
end
end end
...@@ -32,6 +32,7 @@ RSpec.configure do |config| ...@@ -32,6 +32,7 @@ RSpec.configure do |config|
config.include LoginHelpers, type: :feature config.include LoginHelpers, type: :feature
config.include LoginHelpers, type: :request config.include LoginHelpers, type: :request
config.include StubConfiguration config.include StubConfiguration
config.include EmailHelpers
config.include RelativeUrl, type: feature config.include RelativeUrl, type: feature
config.include TestEnv config.include TestEnv
config.include ActiveJob::TestHelper config.include ActiveJob::TestHelper
......
module EmailHelpers
def sent_to_user?(user)
ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
end
def should_email(user)
expect(sent_to_user?(user)).to be_truthy
end
def should_not_email(user)
expect(sent_to_user?(user)).to be_falsey
end
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