From 0444fa560acd07255960284f19b1de6499cd5910 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 12 Feb 2016 20:28:39 +0530 Subject: [PATCH 1/4] Original implementation to allow users to subscribe to labels 1. Allow subscribing (the current user) to a label - Refactor the `Subscription` coffeescript class - The main change is that it accepts a container, and conducts all DOM queries within its scope. We need this because the labels page has multiple instances of `Subscription` on the same page. 2. Creating an issue or MR with labels notifies users subscribed to those labels - Label `has_many` subscribers through subscriptions. 3. Adding a label to an issue or MR notifies users subscribed to those labels - This only applies to subscribers of the label that has just been added, not all labels for the issue. --- CHANGELOG | 2 + app/assets/javascripts/subscription.js.coffee | 32 +++---- app/assets/stylesheets/pages/labels.scss | 4 + app/controllers/projects/labels_controller.rb | 9 +- app/mailers/emails/issues.rb | 11 ++- app/mailers/emails/merge_requests.rb | 14 ++- app/models/concerns/issuable.rb | 30 ++----- app/models/concerns/subscribable.rb | 43 +++++++++ app/models/label.rb | 2 + app/services/issuable_base_service.rb | 2 +- app/services/issues/update_service.rb | 7 +- app/services/merge_requests/update_service.rb | 7 +- app/services/notification_service.rb | 44 ++++++++++ .../_relabeled_issuable_email.html.haml | 5 ++ .../notify/_relabeled_issuable_email.text.erb | 5 ++ .../notify/relabeled_issue_email.html.haml | 1 + .../notify/relabeled_issue_email.text.erb | 1 + .../relabeled_merge_request_email.html.haml | 1 + .../relabeled_merge_request_email.text.erb | 1 + app/views/projects/labels/_label.html.haml | 12 +++ app/views/shared/issuable/_sidebar.html.haml | 4 +- config/routes.rb | 4 + features/project/labels.feature | 16 ++++ features/steps/project/labels.rb | 34 ++++++++ spec/factories/labels.rb | 2 +- spec/models/concerns/subscribable_spec.rb | 16 ++++ spec/services/issues/update_service_spec.rb | 56 +++++++++++- .../merge_requests/update_service_spec.rb | 56 +++++++++++- spec/services/notification_service_spec.rb | 87 ++++++++++++++++--- spec/spec_helper.rb | 1 + spec/support/email_helpers.rb | 13 +++ 31 files changed, 458 insertions(+), 64 deletions(-) create mode 100644 app/models/concerns/subscribable.rb create mode 100644 app/views/notify/_relabeled_issuable_email.html.haml create mode 100644 app/views/notify/_relabeled_issuable_email.text.erb create mode 100644 app/views/notify/relabeled_issue_email.html.haml create mode 100644 app/views/notify/relabeled_issue_email.text.erb create mode 100644 app/views/notify/relabeled_merge_request_email.html.haml create mode 100644 app/views/notify/relabeled_merge_request_email.text.erb create mode 100644 features/project/labels.feature create mode 100644 features/steps/project/labels.rb create mode 100644 spec/models/concerns/subscribable_spec.rb create mode 100644 spec/support/email_helpers.rb diff --git a/CHANGELOG b/CHANGELOG index 015efa05c6..fb9500e68a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -174,6 +174,8 @@ v 8.5.0 v 8.4.5 - No CE-specific changes + - Allow user subscription to a label; get notified for issues/merge requests related to that label. + - Allow user subscription to a label; get notified for issues/merge requests related to that label. (Timothy Andrew) v 8.4.4 - Update omniauth-saml gem to 1.4.2 diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index 7f41616d4e..afc17338b2 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -1,17 +1,19 @@ class @Subscription - constructor: (url) -> - $(".subscribe-button").unbind("click").click (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>div").toggleClass("hidden") + constructor: (@url, container) -> + @subscribe_button = $(container).find(".subscribe-button") + @subscription_status = $(container).find(".subscription-status") + @subscribe_button.unbind("click").click(@toggleSubscription) - + 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") diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 5ec0966194..1791ba9fbd 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -41,3 +41,7 @@ .color-label { padding: 3px 4px; } + +.label-subscription { + display: inline-block; +} \ No newline at end of file diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index ecac3c395e..d0334c37b6 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -1,8 +1,8 @@ class Projects::LabelsController < Projects::ApplicationController before_action :module_enabled - before_action :label, only: [:edit, :update, :destroy] + before_action :label, only: [:edit, :update, :destroy, :toggle_subscription] before_action :authorize_read_label! - before_action :authorize_admin_labels!, except: [:index] + before_action :authorize_admin_labels!, except: [:index, :toggle_subscription] respond_to :js, :html @@ -60,6 +60,11 @@ class Projects::LabelsController < Projects::ApplicationController end end + def toggle_subscription + @label.toggle_subscription(current_user) + render nothing: true + end + protected def module_enabled diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 4a88cb6113..2838baa1b4 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -16,7 +16,14 @@ module Emails def closed_issue_email(recipient_id, issue_id, updated_by_user_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, updated_by_user_id, label_names) + setup_issue_mail(issue_id, recipient_id) + @label_names = label_names + @updated_by = User.find(updated_by_user_id) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) end @@ -24,7 +31,7 @@ module Emails setup_issue_mail(issue_id, recipient_id) @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)) end diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 325996e2e1..680ce975c7 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -19,10 +19,20 @@ module Emails subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) end + def relabeled_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, label_names) + setup_merge_request_mail(merge_request_id, recipient_id) + @label_names = label_names + @updated_by = User.find(updated_by_user_id) + mail_answer_thread(@merge_request, + from: sender(@merge_request.author_id), + to: recipient(recipient_id), + subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + end + def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_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, from: sender(updated_by_user_id), to: recipient(recipient_id), @@ -42,7 +52,7 @@ module Emails setup_merge_request_mail(merge_request_id, recipient_id) @mr_status = status - @updated_by = User.find updated_by_user_id + @updated_by = User.find(updated_by_user_id) mail_answer_thread(@merge_request, from: sender(updated_by_user_id), to: recipient(recipient_id), diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 3c42f58293..affc4a842a 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -8,6 +8,7 @@ module Issuable extend ActiveSupport::Concern include Participable include Mentionable + include Subscribable include StripAttribute included do @@ -18,7 +19,6 @@ module Issuable has_many :notes, as: :noteable, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy has_many :labels, through: :label_links - has_many :subscriptions, dependent: :destroy, as: :subscribable validates :author, presence: true validates :title, presence: true, length: { within: 0..255 } @@ -149,28 +149,6 @@ module Issuable notes.awards.where(note: "thumbsup").count end - def subscribed?(user) - subscription = subscriptions.find_by_user_id(user.id) - - if subscription - return subscription.subscribed - end - - participants(user).include?(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 - def to_hook_data(user) hook_data = { object_kind: self.class.name.underscore, @@ -201,6 +179,12 @@ module Issuable end end + # Labels that are currently applied to this object + # that are not present in `old_labels` + def added_labels(old_labels) + self.labels - old_labels + end + # Convert this Issuable class name to a format usable by Ability definitions # # Examples: diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb new file mode 100644 index 0000000000..cab9241ac3 --- /dev/null +++ b/app/models/concerns/subscribable.rb @@ -0,0 +1,43 @@ +# == 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) + subscription = subscriptions.find_by_user_id(user.id) + + if subscription + return subscription.subscribed + end + + # FIXME + # Issue/MergeRequest has participants, but Label doesn't. + # Ideally, subscriptions should be separate from participations, + # but that seems like a larger change with farther-reaching + # consequences, so this is a compromise for the time being. + if respond_to?(:participants) + 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 +end diff --git a/app/models/label.rb b/app/models/label.rb index 5ff644b842..f7ffc0b7f3 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -14,6 +14,8 @@ class Label < ActiveRecord::Base include Referable + include Subscribable + # Represents a "No Label" state used for filtering Issues and Merge # Requests that have no label assigned. LabelStruct = Struct.new(:title, :name) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index ca87dca4a7..971c074882 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -95,7 +95,7 @@ class IssuableBaseService < BaseService old_labels = options[:old_labels] if old_labels && (issuable.labels != old_labels) - create_labels_note(issuable, issuable.labels - old_labels, old_labels - issuable.labels) + create_labels_note(issuable, issuable.added_labels(old_labels), old_labels - issuable.labels) end end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 51ef9dfe61..b2e63a4e1a 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -4,7 +4,7 @@ module Issues update(issue) end - def handle_changes(issue, options = {}) + def handle_changes(issue, old_labels: [], new_labels: []) if has_changes?(issue, options) todo_service.mark_pending_todos_as_done(issue, current_user) end @@ -23,6 +23,11 @@ module Issues notification_service.reassigned_issue(issue, current_user) todo_service.reassigned_issue(issue, current_user) end + + new_labels = issue.added_labels(old_labels) + if new_labels.present? + notification_service.relabeled_issue(issue, new_labels, current_user) + end end def reopen_service diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 6319ad805b..6fd569dc30 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -14,7 +14,7 @@ module MergeRequests update(merge_request) end - def handle_changes(merge_request, options = {}) + def handle_changes(issue, old_labels: [], new_labels: []) if has_changes?(merge_request, options) todo_service.mark_pending_todos_as_done(merge_request, current_user) end @@ -44,6 +44,11 @@ module MergeRequests merge_request.previous_changes.include?('source_branch') merge_request.mark_as_unchecked end + + new_labels = merge_request.added_labels(old_labels) + if new_labels.present? + notification_service.relabeled_merge_request(merge_request, new_labels, current_user) + end end def reopen_service diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index ca8a41d93b..e9955cd3e2 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -52,6 +52,14 @@ class NotificationService reassign_resource_email(issue, issue.project, current_user, 'reassigned_issue_email') end + # When we change labels on an issue we should send emails. + # + # We pass in the labels, here, because we only want the labels that + # have been *added* during this relabel, not all of them. + def relabeled_issue(issue, labels, current_user) + relabel_resource_email(issue, issue.project, labels, current_user, 'relabeled_issue_email') + end + # When create a merge request we should send next emails: # @@ -70,6 +78,14 @@ class NotificationService reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email') end + # When we change labels on a merge request we should send emails. + # + # We pass in the labels, here, because we only want the labels that + # have been *added* during this relabel, not all of them. + def relabeled_merge_request(merge_request, labels, current_user) + relabel_resource_email(merge_request, merge_request.project, labels, current_user, 'relabeled_merge_request_email') + end + def close_mr(merge_request, current_user) close_resource_email(merge_request, merge_request.target_project, current_user, 'closed_merge_request_email') end @@ -142,6 +158,7 @@ class NotificationService recipients = reject_muted_users(recipients, note.project) recipients = add_subscribed_users(recipients, note.noteable) + recipients = add_label_subscriptions(recipients, note.noteable) recipients = reject_unsubscribed_users(recipients, note.noteable) recipients.delete(note.author) @@ -359,6 +376,16 @@ class NotificationService end end + def add_label_subscriptions(recipients, target) + return recipients unless target.respond_to? :labels + + target.labels.each do |label| + recipients += label.subscriptions.where(subscribed: true).map(&:user) + end + + recipients + end + def new_resource_email(target, project, method) recipients = build_recipients(target, project, target.author) @@ -392,6 +419,15 @@ class NotificationService end end + def relabel_resource_email(target, project, labels, current_user, method) + recipients = build_relabel_recipients(target, project, labels, current_user) + label_names = labels.map(&:name) + + recipients.each do |recipient| + mailer.send(method, recipient.id, target.id, current_user.id, label_names).deliver_later + end + end + def reopen_resource_email(target, project, current_user, method, status) recipients = build_recipients(target, project, current_user) @@ -416,6 +452,7 @@ class NotificationService recipients = reject_muted_users(recipients, project) recipients = add_subscribed_users(recipients, target) + recipients = add_label_subscriptions(recipients, target) recipients = reject_unsubscribed_users(recipients, target) recipients.delete(current_user) @@ -423,6 +460,13 @@ class NotificationService recipients.uniq end + def build_relabel_recipients(target, project, labels, current_user) + recipients = add_label_subscriptions([], target) + recipients = reject_unsubscribed_users(recipients, target) + recipients.delete(current_user) + recipients.uniq + end + def mailer Notify end diff --git a/app/views/notify/_relabeled_issuable_email.html.haml b/app/views/notify/_relabeled_issuable_email.html.haml new file mode 100644 index 0000000000..a41ff07c30 --- /dev/null +++ b/app/views/notify/_relabeled_issuable_email.html.haml @@ -0,0 +1,5 @@ +%p + #{@updated_by.name} added the + %em= @label_names.to_sentence + #{"label".pluralize(@label_names.count)} to #{issuable.class.model_name.human} #{issuable.iid}. + diff --git a/app/views/notify/_relabeled_issuable_email.text.erb b/app/views/notify/_relabeled_issuable_email.text.erb new file mode 100644 index 0000000000..1a28d7fd35 --- /dev/null +++ b/app/views/notify/_relabeled_issuable_email.text.erb @@ -0,0 +1,5 @@ +<%= issuable.class.model_name.human.titleize %> <%= issuable.iid %> was relabeled. + +Issue <%= issuable.iid %>: <%= url_for(namespace_project_issue_url(issuable.project.namespace, issuable.project, issuable)) %> +Author: <%= issuable.author_name %> +New Labels: <%= @label_names.to_sentence %> diff --git a/app/views/notify/relabeled_issue_email.html.haml b/app/views/notify/relabeled_issue_email.html.haml new file mode 100644 index 0000000000..a3e094e86e --- /dev/null +++ b/app/views/notify/relabeled_issue_email.html.haml @@ -0,0 +1 @@ += render "relabeled_issuable_email", issuable: @issue diff --git a/app/views/notify/relabeled_issue_email.text.erb b/app/views/notify/relabeled_issue_email.text.erb new file mode 100644 index 0000000000..de7f04a323 --- /dev/null +++ b/app/views/notify/relabeled_issue_email.text.erb @@ -0,0 +1 @@ +<%= render "relabeled_issuable_email", issuable: @issue %> diff --git a/app/views/notify/relabeled_merge_request_email.html.haml b/app/views/notify/relabeled_merge_request_email.html.haml new file mode 100644 index 0000000000..3937b449a3 --- /dev/null +++ b/app/views/notify/relabeled_merge_request_email.html.haml @@ -0,0 +1 @@ += render "relabeled_issuable_email", issuable: @merge_request diff --git a/app/views/notify/relabeled_merge_request_email.text.erb b/app/views/notify/relabeled_merge_request_email.text.erb new file mode 100644 index 0000000000..5c4d86e4dc --- /dev/null +++ b/app/views/notify/relabeled_merge_request_email.text.erb @@ -0,0 +1 @@ +<%= render "relabeled_issuable_email", issuable: @merge_request %> diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index f7ddd30c5a..3b14a925c4 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -10,6 +10,18 @@ = link_to_label(label) do = pluralize label.open_issues_count, 'open issue' + - if current_user + %div{class: "label-subscription", data: {id: label.id}} + - subscribed = label.subscribed?(current_user) + - subscription_status = subscribed ? 'subscribed' : 'unsubscribed' + .subscription-status{data: {status: subscription_status}} + %button.btn.btn-sm.btn-info.subscribe-button{:type => 'button'} + %span= subscribed ? 'Unsubscribe' : 'Subscribe' + - 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 '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?"} + +:javascript + new Subscription("#{toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}", + ".label-subscription[data-id='#{label.id}']"); diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 9020a1330a..b44086d420 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -98,7 +98,7 @@ %hr - if current_user - subscribed = issuable.subscribed?(current_user) - .block.light + .block.light.subscription .sidebar-collapsed-icon = icon('rss') .title.hide-collapsed @@ -124,5 +124,5 @@ = clipboard_button(clipboard_text: project_ref) :javascript - new Subscription("#{toggle_subscription_path(issuable)}"); + new Subscription("#{toggle_subscription_path(issuable)}", ".subscription"); new IssuableContext(); diff --git a/config/routes.rb b/config/routes.rb index 780ad757c5..2ae282f48a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -675,6 +675,10 @@ Rails.application.routes.draw do collection do post :generate end + + member do + post :toggle_subscription + end end resources :issues, constraints: { id: /\d+/ }, except: [:destroy] do diff --git a/features/project/labels.feature b/features/project/labels.feature new file mode 100644 index 0000000000..cd3f3a789f --- /dev/null +++ b/features/project/labels.feature @@ -0,0 +1,16 @@ +@labels +Feature: Labels + Background: + Given I sign in as a user + And I own project "Shop" + And I visit project "Shop" issues page + And project "Shop" has labels: "bug", "feature", "enhancement" + + @javascript + Scenario: I can subscribe to a label + When I visit project "Shop" labels page + Then I should see that I am unsubscribed + When I click button "Subscribe" + Then I should see that I am subscribed + When I click button "Unsubscribe" + Then I should see that I am unsubscribed diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb new file mode 100644 index 0000000000..3f800a1059 --- /dev/null +++ b/features/steps/project/labels.rb @@ -0,0 +1,34 @@ +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' do + expect(subscribe_button).to have_content 'Unsubscribe' + end + + step 'I should see that I am unsubscribed' do + expect(subscribe_button).to have_content 'Subscribe' + end + + step 'I click button "Unsubscribe"' do + subscribe_button.click + end + + step 'I click button "Subscribe"' do + subscribe_button.click + end + + private + + def subscribe_button + first('.subscribe-button span') + end +end diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb index 6e70af10af..91a7400afa 100644 --- a/spec/factories/labels.rb +++ b/spec/factories/labels.rb @@ -13,7 +13,7 @@ FactoryGirl.define do factory :label do - title "Bug" + title { FFaker::Color.name } color "#990000" project end diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb new file mode 100644 index 0000000000..9ee60426a5 --- /dev/null +++ b/spec/models/concerns/subscribable_spec.rb @@ -0,0 +1,16 @@ +require "spec_helper" + +describe Subscribable, "Subscribable" do + let(:resource) { create(:issue) } + let(:user) { create(:user) } + + describe "#subscribed?" do + it do + expect(resource.subscribed?(user)).to be_falsey + resource.toggle_subscription(user) + expect(resource.subscribed?(user)).to be_truthy + resource.toggle_subscription(user) + expect(resource.subscribed?(user)).to be_falsey + end + end +end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index e579e49dfa..dc9d832975 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -48,7 +48,7 @@ describe Issues::UpdateService, services: true do it { expect(@issue.assignee).to eq(user2) } it { expect(@issue).to be_closed } 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 deliveries = ActionMailer::Base.deliveries @@ -148,6 +148,60 @@ describe Issues::UpdateService, services: true do end end + context "when the issue is relabeled" do + it "sends notifications for subscribers of newly added labels" do + subscriber, non_subscriber = create_list(:user, 2) + label.toggle_subscription(subscriber) + 2.times { label.toggle_subscription(non_subscriber) } + + opts = { label_ids: [label.id] } + + perform_enqueued_jobs do + @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + end + + @issue.reload + should_email(subscriber) + should_not_email(non_subscriber) + end + + it "does send notifications for existing labels" do + second_label = create(:label) + issue.labels << label + subscriber, non_subscriber = create_list(:user, 2) + label.toggle_subscription(subscriber) + 2.times { label.toggle_subscription(non_subscriber) } + + opts = { label_ids: [label.id, second_label.id] } + + perform_enqueued_jobs do + @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + end + + @issue.reload + should_email(subscriber) + should_not_email(non_subscriber) + end + + it "does not send notifications for removed labels" do + second_label = create(:label) + issue.labels << label + subscriber, non_subscriber = create_list(:user, 2) + label.toggle_subscription(subscriber) + 2.times { label.toggle_subscription(non_subscriber) } + + opts = { label_ids: [second_label.id] } + + perform_enqueued_jobs do + @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + end + + @issue.reload + should_not_email(subscriber) + should_not_email(non_subscriber) + end + end + context 'when Issue has tasks' do before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 99703c7a8e..104e63ccfe 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -53,7 +53,7 @@ describe MergeRequests::UpdateService, services: true do it { expect(@merge_request.assignee).to eq(user2) } it { expect(@merge_request).to be_closed } 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 'should execute hooks with update action' do @@ -176,6 +176,60 @@ describe MergeRequests::UpdateService, services: true do end end + context "when the merge request is relabeled" do + it "sends notifications for subscribers of newly added labels" do + subscriber, non_subscriber = create_list(:user, 2) + label.toggle_subscription(subscriber) + 2.times { label.toggle_subscription(non_subscriber) } + + opts = { label_ids: [label.id] } + + perform_enqueued_jobs do + @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + end + + @merge_request.reload + should_email(subscriber) + should_not_email(non_subscriber) + end + + it "does send notifications for existing labels" do + second_label = create(:label) + merge_request.labels << label + subscriber, non_subscriber = create_list(:user, 2) + label.toggle_subscription(subscriber) + 2.times { label.toggle_subscription(non_subscriber) } + + opts = { label_ids: [label.id, second_label.id] } + + perform_enqueued_jobs do + @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + end + + @merge_request.reload + should_email(subscriber) + should_not_email(non_subscriber) + end + + it "does not send notifications for removed labels" do + second_label = create(:label) + merge_request.labels << label + subscriber, non_subscriber = create_list(:user, 2) + label.toggle_subscription(subscriber) + 2.times { label.toggle_subscription(non_subscriber) } + + opts = { label_ids: [second_label.id] } + + perform_enqueued_jobs do + @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + end + + @merge_request.reload + should_not_email(subscriber) + should_not_email(non_subscriber) + end + end + context 'when MergeRequest has tasks' do before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) } diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 2d0b5df422..35afa76805 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -224,6 +224,16 @@ describe NotificationService, services: true do should_not_email(issue.assignee) end + + it "should email subscribers of the issue's labels" do + subscriber, non_subscriber = create_list(:user, 2) + label = create(:label, issues: [issue]) + label.toggle_subscription(subscriber) + 2.times { label.toggle_subscription(non_subscriber) } + notification.new_issue(issue, @u_disabled) + should_email(subscriber) + should_not_email(non_subscriber) + end end describe :reassigned_issue do @@ -326,6 +336,32 @@ describe NotificationService, services: true do should_not_email(@u_participating) end end + + describe :relabel_issue do + it "sends email to subscribers of the given labels" do + subscriber, non_subscriber = create_list(:user, 2) + label = create(:label, issues: [issue]) + label.toggle_subscription(subscriber) + 2.times { label.toggle_subscription(non_subscriber) } + notification.relabeled_issue(issue, [label], @u_disabled) + should_email(subscriber) + should_not_email(non_subscriber) + end + + it "doesn't send email to anyone but subscribers of the given labels" do + label = create(:label, issues: [issue]) + notification.relabeled_issue(issue, [label], @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) + end + end end describe 'Merge Requests' do @@ -349,6 +385,17 @@ describe NotificationService, services: true do should_not_email(@u_participating) should_not_email(@u_disabled) end + + it "should email subscribers of the MR's labels" do + subscriber, non_subscriber = create_list(:user, 2) + label = create(:label) + merge_request.labels << label + label.toggle_subscription(subscriber) + 2.times { label.toggle_subscription(non_subscriber) } + notification.new_merge_request(merge_request, @u_disabled) + should_email(subscriber) + should_not_email(non_subscriber) + end end describe :reassigned_merge_request do @@ -410,6 +457,34 @@ describe NotificationService, services: true do should_not_email(@u_disabled) end end + + describe :relabel_merge_request do + it "sends email to subscribers of the given labels" do + subscriber, non_subscriber = create_list(:user, 2) + label = create(:label) + merge_request.labels << label + label.toggle_subscription(subscriber) + 2.times { label.toggle_subscription(non_subscriber) } + notification.relabeled_merge_request(merge_request, [label], @u_disabled) + should_email(subscriber) + should_not_email(non_subscriber) + end + + it "doesn't send email to anyone but subscribers of the given labels" do + label = create(:label) + merge_request.labels << label + notification.relabeled_merge_request(merge_request, [label], @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) + end + end end describe 'Projects' do @@ -467,16 +542,4 @@ describe NotificationService, services: true do # Make the watcher a subscriber to detect dupes issuable.subscriptions.create(user: @watcher_and_subscriber, subscribed: true) 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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7d939ca750..596d607f2a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,6 +32,7 @@ RSpec.configure do |config| config.include LoginHelpers, type: :feature config.include LoginHelpers, type: :request config.include StubConfiguration + config.include EmailHelpers config.include RelativeUrl, type: feature config.include TestEnv config.include ActiveJob::TestHelper diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb new file mode 100644 index 0000000000..a85ab22ce3 --- /dev/null +++ b/spec/support/email_helpers.rb @@ -0,0 +1,13 @@ +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 From 54ec7e959900493b6e9174bf4dfe09ed0afd1e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 1 Mar 2016 17:33:13 +0100 Subject: [PATCH 2/4] Improving the original label-subscribing implementation 1. Make the "subscribed" text in Issuable sidebar reflect the labels subscription status 2. Current user mut be logged-in to toggle issue/MR/label subscription --- CHANGELOG | 3 +- app/assets/javascripts/subscription.js.coffee | 28 ++-- app/assets/stylesheets/pages/labels.scss | 4 +- app/controllers/projects/issues_controller.rb | 2 + app/controllers/projects/labels_controller.rb | 2 + .../projects/merge_requests_controller.rb | 2 + app/helpers/labels_helper.rb | 8 ++ app/mailers/emails/issues.rb | 25 ++-- app/mailers/emails/merge_requests.rb | 55 ++++---- app/models/concerns/issuable.rb | 10 +- app/models/concerns/subscribable.rb | 25 ++-- app/models/subscription.rb | 2 +- app/services/issuable_base_service.rb | 17 ++- app/services/issues/update_service.rb | 10 +- app/services/merge_requests/update_service.rb | 14 +- app/services/notification_service.rb | 72 +++++----- app/views/layouts/notify.html.haml | 13 +- .../_reassigned_issuable_email.text.erb | 2 +- .../_relabeled_issuable_email.html.haml | 4 +- .../notify/_relabeled_issuable_email.text.erb | 6 +- .../notify/relabeled_issue_email.html.haml | 2 +- .../notify/relabeled_issue_email.text.erb | 2 +- .../relabeled_merge_request_email.html.haml | 2 +- .../relabeled_merge_request_email.text.erb | 2 +- app/views/projects/labels/_label.html.haml | 16 +-- app/views/shared/issuable/_sidebar.html.haml | 4 +- features/project/labels.feature | 13 +- features/steps/project/labels.rb | 8 +- spec/factories/labels.rb | 2 +- spec/mailers/notify_spec.rb | 56 ++++++++ spec/mailers/shared/notify.rb | 6 +- spec/models/concerns/issuable_spec.rb | 42 ++++++ spec/models/concerns/subscribable_spec.rb | 51 ++++++- spec/services/issues/update_service_spec.rb | 55 +++----- .../merge_requests/update_service_spec.rb | 55 +++----- spec/services/notification_service_spec.rb | 129 +++++++++--------- 36 files changed, 439 insertions(+), 310 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fb9500e68a..e9e7c88d86 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,7 @@ v 8.6.0 (unreleased) - Update documentation to reflect Guest role not being enforced on internal projects - Allow search for logged out users - 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) - Don't show Issues/MRs from archived projects in Groups view - Fix wrong "iid of max iid" in Issuable sidebar for some merged MRs @@ -174,8 +175,6 @@ v 8.5.0 v 8.4.5 - No CE-specific changes - - Allow user subscription to a label; get notified for issues/merge requests related to that label. - - Allow user subscription to a label; get notified for issues/merge requests related to that label. (Timothy Andrew) v 8.4.4 - Update omniauth-saml gem to 1.4.2 diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index afc17338b2..084f0e0dc6 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -1,19 +1,21 @@ class @Subscription - constructor: (@url, container) -> - @subscribe_button = $(container).find(".subscribe-button") - @subscription_status = $(container).find(".subscription-status") - @subscribe_button.unbind("click").click(@toggleSubscription) + constructor: (container) -> + $container = $(container) + @url = $container.attr('data-url') + @subscribe_button = $container.find('.subscribe-button') + @subscription_status = $container.find('.subscription-status') + @subscribe_button.unbind('click').click(@toggleSubscription) toggleSubscription: (event) => btn = $(event.currentTarget) - action = btn.find("span").text() - current_status = @subscription_status.attr("data-status") - btn.prop("disabled", true) + 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") + 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') diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 1791ba9fbd..61ee34b695 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -43,5 +43,5 @@ } .label-subscription { - display: inline-block; -} \ No newline at end of file + display: inline-block; +} diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 67faa1e443..24a862814b 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -111,6 +111,8 @@ class Projects::IssuesController < Projects::ApplicationController end def toggle_subscription + return unless current_user + @issue.toggle_subscription(current_user) render nothing: true diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index d0334c37b6..e4dea6b065 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -61,6 +61,8 @@ class Projects::LabelsController < Projects::ApplicationController end def toggle_subscription + return unless current_user + @label.toggle_subscription(current_user) render nothing: true end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 03ba289eb9..954ee55a21 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -234,6 +234,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def toggle_subscription + return unless current_user + @merge_request.toggle_subscription(current_user) render nothing: true diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 89a054289e..4455dcd0e2 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -124,6 +124,14 @@ module LabelsHelper options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name]) 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 module_function :render_colored_label, :render_colored_cross_project_label, :text_color_for_bg, :escape_once diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 2838baa1b4..160b6df0b9 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -20,10 +20,11 @@ module Emails mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) end - def relabeled_issue_email(recipient_id, issue_id, updated_by_user_id, label_names) - setup_issue_mail(issue_id, recipient_id) + def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id) + setup_issue_mail(issue_id, recipient_id, sent_notification: false) + @label_names = label_names - @updated_by = User.find(updated_by_user_id) + @labels_url = namespace_project_labels_url(@project.namespace, @project) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) end @@ -37,6 +38,16 @@ module Emails private + def setup_issue_mail(issue_id, recipient_id, sent_notification: true) + @issue = Issue.find(issue_id) + @project = @issue.project + @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) + + if sent_notification + @sent_notification = SentNotification.record(@issue, recipient_id, reply_key) + end + end + def issue_thread_options(sender_id, recipient_id) { from: sender(sender_id), @@ -44,13 +55,5 @@ module Emails subject: subject("#{@issue.title} (##{@issue.iid})") } end - - def setup_issue_mail(issue_id, recipient_id) - @issue = Issue.find(issue_id) - @project = @issue.project - @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) - - @sent_notification = SentNotification.record(@issue, recipient_id, reply_key) - end end end diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 680ce975c7..334bad4e2f 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -3,49 +3,35 @@ module Emails def new_merge_request_email(recipient_id, merge_request_id) setup_merge_request_mail(merge_request_id, recipient_id) - mail_new_thread(@merge_request, - from: sender(@merge_request.author_id), - to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id)) end 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) @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id - mail_answer_thread(@merge_request, - from: sender(updated_by_user_id), - to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) end - def relabeled_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, label_names) - setup_merge_request_mail(merge_request_id, recipient_id) + 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, sent_notification: false) + @label_names = label_names - @updated_by = User.find(updated_by_user_id) - mail_answer_thread(@merge_request, - from: sender(@merge_request.author_id), - to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + @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 def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) setup_merge_request_mail(merge_request_id, recipient_id) @updated_by = User.find(updated_by_user_id) - mail_answer_thread(@merge_request, - from: sender(updated_by_user_id), - to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) end def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) setup_merge_request_mail(merge_request_id, recipient_id) - mail_answer_thread(@merge_request, - from: sender(updated_by_user_id), - to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) end def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id) @@ -53,22 +39,27 @@ module Emails @mr_status = status @updated_by = User.find(updated_by_user_id) - mail_answer_thread(@merge_request, - from: sender(updated_by_user_id), - to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) end private - def setup_merge_request_mail(merge_request_id, recipient_id) + def setup_merge_request_mail(merge_request_id, recipient_id, sent_notification: true) @merge_request = MergeRequest.find(merge_request_id) @project = @merge_request.project - @target_url = namespace_project_merge_request_url(@project.namespace, - @project, - @merge_request) + @target_url = namespace_project_merge_request_url(@project.namespace, @project, @merge_request) - @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key) + if sent_notification + @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 diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index affc4a842a..86ab84615b 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -149,6 +149,10 @@ module Issuable notes.awards.where(note: "thumbsup").count end + def subscribed_without_subscriptions?(user) + participants(user).include?(user) + end + def to_hook_data(user) hook_data = { object_kind: self.class.name.underscore, @@ -179,12 +183,6 @@ module Issuable end end - # Labels that are currently applied to this object - # that are not present in `old_labels` - def added_labels(old_labels) - self.labels - old_labels - end - # Convert this Issuable class name to a format usable by Ability definitions # # Examples: diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb index cab9241ac3..d5a881b244 100644 --- a/app/models/concerns/subscribable.rb +++ b/app/models/concerns/subscribable.rb @@ -13,20 +13,21 @@ module Subscribable end def subscribed?(user) - subscription = subscriptions.find_by_user_id(user.id) - - if subscription - return subscription.subscribed + if subscription = subscriptions.find_by_user_id(user.id) + subscription.subscribed + else + subscribed_without_subscriptions?(user) end + end - # FIXME - # Issue/MergeRequest has participants, but Label doesn't. - # Ideally, subscriptions should be separate from participations, - # but that seems like a larger change with farther-reaching - # consequences, so this is a compromise for the time being. - if respond_to?(:participants) - participants(user).include?(user) - 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) diff --git a/app/models/subscription.rb b/app/models/subscription.rb index dd75d3ab8b..dd800ce110 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -15,7 +15,7 @@ class Subscription < ActiveRecord::Base belongs_to :user belongs_to :subscribable, polymorphic: true - validates :user_id, + validates :user_id, uniqueness: { scope: [:subscribable_id, :subscribable_type] }, presence: true end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 971c074882..18f76d3f65 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -11,7 +11,10 @@ class IssuableBaseService < BaseService issuable, issuable.project, current_user, issuable.milestone) 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( issuable, issuable.project, current_user, added_labels, removed_labels) end @@ -71,20 +74,19 @@ class IssuableBaseService < BaseService end end - def has_changes?(issuable, options = {}) + def has_changes?(issuable, old_labels: []) valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch] attrs_changed = valid_attrs.any? do |attr| issuable.previous_changes.include?(attr.to_s) end - old_labels = options[:old_labels] - labels_changed = old_labels && issuable.labels != old_labels + labels_changed = issuable.labels != old_labels attrs_changed || labels_changed end - def handle_common_system_notes(issuable, options = {}) + def handle_common_system_notes(issuable, old_labels: []) if issuable.previous_changes.include?('title') create_title_change_note(issuable, issuable.previous_changes['title'].first) end @@ -93,9 +95,6 @@ class IssuableBaseService < BaseService create_task_status_note(issuable) end - old_labels = options[:old_labels] - if old_labels && (issuable.labels != old_labels) - create_labels_note(issuable, issuable.added_labels(old_labels), old_labels - issuable.labels) - end + create_labels_note(issuable, old_labels) if issuable.labels != old_labels end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index b2e63a4e1a..3563cbaa99 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -4,8 +4,8 @@ module Issues update(issue) end - def handle_changes(issue, old_labels: [], new_labels: []) - if has_changes?(issue, options) + def handle_changes(issue, old_labels: []) + if has_changes?(issue, old_labels: old_labels) todo_service.mark_pending_todos_as_done(issue, current_user) end @@ -24,9 +24,9 @@ module Issues todo_service.reassigned_issue(issue, current_user) end - new_labels = issue.added_labels(old_labels) - if new_labels.present? - notification_service.relabeled_issue(issue, new_labels, current_user) + added_labels = issue.labels - old_labels + if added_labels.present? + notification_service.relabeled_issue(issue, added_labels, current_user) end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 6fd569dc30..477c64e737 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -14,8 +14,8 @@ module MergeRequests update(merge_request) end - def handle_changes(issue, old_labels: [], new_labels: []) - if has_changes?(merge_request, options) + def handle_changes(merge_request, old_labels: []) + if has_changes?(merge_request, old_labels: old_labels) todo_service.mark_pending_todos_as_done(merge_request, current_user) end @@ -45,9 +45,13 @@ module MergeRequests merge_request.mark_as_unchecked end - new_labels = merge_request.added_labels(old_labels) - if new_labels.present? - notification_service.relabeled_merge_request(merge_request, new_labels, current_user) + added_labels = merge_request.labels - old_labels + if added_labels.present? + notification_service.relabeled_merge_request( + merge_request, + added_labels, + current_user + ) end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index e9955cd3e2..19a6779dea 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -24,16 +24,17 @@ class NotificationService 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 # * project team members with notification level higher then Participating + # * watchers of the issue's labels # def new_issue(issue, current_user) new_resource_email(issue, issue.project, 'new_issue_email') 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 assignee if their notification level is not Disabled @@ -43,7 +44,7 @@ class NotificationService close_resource_email(issue, issue.project, current_user, 'closed_issue_email') 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 new assignee if their notification level is not Disabled @@ -52,24 +53,25 @@ class NotificationService reassign_resource_email(issue, issue.project, current_user, 'reassigned_issue_email') end - # When we change labels on an issue we should send emails. + # When we add labels to an issue we should send an email to: # - # We pass in the labels, here, because we only want the labels that - # have been *added* during this relabel, not all of them. - def relabeled_issue(issue, labels, current_user) - relabel_resource_email(issue, issue.project, labels, current_user, 'relabeled_issue_email') + # * 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 + # * project team members with notification level higher then Participating + # * watchers of the mr's labels # def new_merge_request(merge_request, current_user) new_resource_email(merge_request, merge_request.target_project, 'new_merge_request_email') 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 assignee if their notification level is not Disabled @@ -78,12 +80,12 @@ class NotificationService reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email') end - # When we change labels on a merge request we should send emails. + # When we add labels to a merge request we should send an email to: # - # We pass in the labels, here, because we only want the labels that - # have been *added* during this relabel, not all of them. - def relabeled_merge_request(merge_request, labels, current_user) - relabel_resource_email(merge_request, merge_request.project, labels, current_user, 'relabeled_merge_request_email') + # * 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) @@ -107,7 +109,8 @@ class NotificationService reopen_resource_email( merge_request, merge_request.target_project, - current_user, 'merge_request_status_email', + current_user, + 'merge_request_status_email', 'reopened' ) end @@ -158,7 +161,6 @@ class NotificationService recipients = reject_muted_users(recipients, note.project) recipients = add_subscribed_users(recipients, note.noteable) - recipients = add_label_subscriptions(recipients, note.noteable) recipients = reject_unsubscribed_users(recipients, note.noteable) recipients.delete(note.author) @@ -365,29 +367,23 @@ class NotificationService end def add_subscribed_users(recipients, target) - return recipients unless target.respond_to? :subscriptions + return recipients unless target.respond_to? :subscribers - subscriptions = target.subscriptions - - if subscriptions.any? - recipients + subscriptions.where(subscribed: true).map(&:user) - else - recipients - end + recipients + target.subscribers end - def add_label_subscriptions(recipients, target) + def add_labels_subscribers(recipients, target, labels: nil) return recipients unless target.respond_to? :labels - target.labels.each do |label| - recipients += label.subscriptions.where(subscribed: true).map(&:user) + (labels || target.labels).each do |label| + recipients += label.subscribers end recipients end 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| mailer.send(method, recipient.id, target.id).deliver_later @@ -419,12 +415,12 @@ class NotificationService end end - def relabel_resource_email(target, project, labels, current_user, method) - recipients = build_relabel_recipients(target, project, labels, current_user) + 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, current_user.id, label_names).deliver_later + mailer.send(method, recipient.id, target.id, label_names, current_user.id).deliver_later end end @@ -452,7 +448,11 @@ class NotificationService recipients = reject_muted_users(recipients, project) recipients = add_subscribed_users(recipients, target) - recipients = add_label_subscriptions(recipients, target) + + if action == :new + recipients = add_labels_subscribers(recipients, target) + end + recipients = reject_unsubscribed_users(recipients, target) recipients.delete(current_user) @@ -460,8 +460,8 @@ class NotificationService recipients.uniq end - def build_relabel_recipients(target, project, labels, current_user) - recipients = add_label_subscriptions([], target) + 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 diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 325c68c69d..37b4d56296 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -42,12 +42,15 @@ - else #{link_to "View it on GitLab", @target_url}. %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}. If you'd like to receive fewer emails, you can - - if @sent_notification && @sent_notification.unsubscribable? - = link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification) - from this thread or - adjust your notification settings. + - if @labels_url + adjust your #{link_to 'label subscriptions', @labels_url}. + - else + - 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 diff --git a/app/views/notify/_reassigned_issuable_email.text.erb b/app/views/notify/_reassigned_issuable_email.text.erb index 855d37429d..daf20a226d 100644 --- a/app/views/notify/_reassigned_issuable_email.text.erb +++ b/app/views/notify/_reassigned_issuable_email.text.erb @@ -1,6 +1,6 @@ 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 -%> to <%= "#{issuable.assignee_id ? issuable.assignee_name : 'Unassigned'}" %> diff --git a/app/views/notify/_relabeled_issuable_email.html.haml b/app/views/notify/_relabeled_issuable_email.html.haml index a41ff07c30..80a0de255b 100644 --- a/app/views/notify/_relabeled_issuable_email.html.haml +++ b/app/views/notify/_relabeled_issuable_email.html.haml @@ -1,5 +1,3 @@ %p - #{@updated_by.name} added the + #{'Label'.pluralize(@label_names.size)} added: %em= @label_names.to_sentence - #{"label".pluralize(@label_names.count)} to #{issuable.class.model_name.human} #{issuable.iid}. - diff --git a/app/views/notify/_relabeled_issuable_email.text.erb b/app/views/notify/_relabeled_issuable_email.text.erb index 1a28d7fd35..6a83d79fd6 100644 --- a/app/views/notify/_relabeled_issuable_email.text.erb +++ b/app/views/notify/_relabeled_issuable_email.text.erb @@ -1,5 +1,3 @@ -<%= issuable.class.model_name.human.titleize %> <%= issuable.iid %> was relabeled. +<%= 'Label'.pluralize(@label_names.size) %> added: <%= @label_names.to_sentence %> -Issue <%= issuable.iid %>: <%= url_for(namespace_project_issue_url(issuable.project.namespace, issuable.project, issuable)) %> -Author: <%= issuable.author_name %> -New Labels: <%= @label_names.to_sentence %> +<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %> diff --git a/app/views/notify/relabeled_issue_email.html.haml b/app/views/notify/relabeled_issue_email.html.haml index a3e094e86e..b17b16e181 100644 --- a/app/views/notify/relabeled_issue_email.html.haml +++ b/app/views/notify/relabeled_issue_email.html.haml @@ -1 +1 @@ -= render "relabeled_issuable_email", issuable: @issue += render 'relabeled_issuable_email', issuable: @issue diff --git a/app/views/notify/relabeled_issue_email.text.erb b/app/views/notify/relabeled_issue_email.text.erb index de7f04a323..eeced97f60 100644 --- a/app/views/notify/relabeled_issue_email.text.erb +++ b/app/views/notify/relabeled_issue_email.text.erb @@ -1 +1 @@ -<%= render "relabeled_issuable_email", issuable: @issue %> +<%= render 'relabeled_issuable_email', issuable: @issue %> diff --git a/app/views/notify/relabeled_merge_request_email.html.haml b/app/views/notify/relabeled_merge_request_email.html.haml index 3937b449a3..9eaa9afa5b 100644 --- a/app/views/notify/relabeled_merge_request_email.html.haml +++ b/app/views/notify/relabeled_merge_request_email.html.haml @@ -1 +1 @@ -= render "relabeled_issuable_email", issuable: @merge_request += render 'relabeled_issuable_email', issuable: @merge_request diff --git a/app/views/notify/relabeled_merge_request_email.text.erb b/app/views/notify/relabeled_merge_request_email.text.erb index 5c4d86e4dc..87bc80ead3 100644 --- a/app/views/notify/relabeled_merge_request_email.text.erb +++ b/app/views/notify/relabeled_merge_request_email.text.erb @@ -1 +1 @@ -<%= render "relabeled_issuable_email", issuable: @merge_request %> +<%= render 'relabeled_issuable_email', issuable: @merge_request %> diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 3b14a925c4..4927d239c1 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -11,17 +11,15 @@ = pluralize label.open_issues_count, 'open issue' - if current_user - %div{class: "label-subscription", data: {id: label.id}} - - subscribed = label.subscribed?(current_user) - - subscription_status = subscribed ? 'subscribed' : 'unsubscribed' - .subscription-status{data: {status: subscription_status}} - %button.btn.btn-sm.btn-info.subscribe-button{:type => 'button'} - %span= subscribed ? 'Unsubscribe' : 'Subscribe' + .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 = 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?"} -:javascript - new Subscription("#{toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}", - ".label-subscription[data-id='#{label.id}']"); +- if current_user + :javascript + new Subscription('##{dom_id(label)} .label-subscription'); diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index b44086d420..23b1ed1e51 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -98,7 +98,7 @@ %hr - if current_user - subscribed = issuable.subscribed?(current_user) - .block.light.subscription + .block.light.subscription{data: {url: toggle_subscription_path(issuable)}} .sidebar-collapsed-icon = icon('rss') .title.hide-collapsed @@ -124,5 +124,5 @@ = clipboard_button(clipboard_text: project_ref) :javascript - new Subscription("#{toggle_subscription_path(issuable)}", ".subscription"); + new Subscription('.subscription'); new IssuableContext(); diff --git a/features/project/labels.feature b/features/project/labels.feature index cd3f3a789f..955bc3d8b1 100644 --- a/features/project/labels.feature +++ b/features/project/labels.feature @@ -3,14 +3,13 @@ Feature: Labels Background: Given I sign in as a user And I own project "Shop" - And I visit project "Shop" issues page And project "Shop" has labels: "bug", "feature", "enhancement" + When I visit project "Shop" labels page @javascript Scenario: I can subscribe to a label - When I visit project "Shop" labels page - Then I should see that I am unsubscribed - When I click button "Subscribe" - Then I should see that I am subscribed - When I click button "Unsubscribe" - Then I should see that I am unsubscribed + 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 diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb index 3f800a1059..17944527e3 100644 --- a/features/steps/project/labels.rb +++ b/features/steps/project/labels.rb @@ -10,19 +10,19 @@ class Spinach::Features::Labels < Spinach::FeatureSteps visit namespace_project_labels_path(project.namespace, project) end - step 'I should see that I am subscribed' do + 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 unsubscribed' do + 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"' do + step 'I click button "Unsubscribe" for the "bug" label' do subscribe_button.click end - step 'I click button "Subscribe"' do + step 'I click button "Subscribe" for the "bug" label' do subscribe_button.click end diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb index 91a7400afa..ea2be8928d 100644 --- a/spec/factories/labels.rb +++ b/spec/factories/labels.rb @@ -13,7 +13,7 @@ FactoryGirl.define do factory :label do - title { FFaker::Color.name } + sequence(:title) { |n| "label#{n}" } color "#990000" project end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 232a11245a..f910424d85 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -100,6 +100,34 @@ describe Notify do 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 let(:status) { 'closed' } subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) } @@ -219,6 +247,34 @@ describe Notify do 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 let(:status) { 'reopened' } subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) } diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb index 48c851ebbd..6019af544d 100644 --- a/spec/mailers/shared/notify.rb +++ b/spec/mailers/shared/notify.rb @@ -112,6 +112,10 @@ shared_examples 'an unsubscribeable thread' do it { is_expected.to have_body_text /unsubscribe/ } 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/ } end + +shared_examples 'an email with a labels subscriptions link in its footer' do + it { is_expected.to have_body_text /label subscriptions/ } +end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index aff384c294..be29b6d66f 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -113,6 +113,48 @@ describe Issue, "Issuable" do 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 let(:data) { issue.to_hook_data(user) } let(:project) { issue.project } diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb index 9ee60426a5..e31fdb0bff 100644 --- a/spec/models/concerns/subscribable_spec.rb +++ b/spec/models/concerns/subscribable_spec.rb @@ -1,15 +1,56 @@ -require "spec_helper" +require 'spec_helper' -describe Subscribable, "Subscribable" do +describe Subscribable, 'Subscribable' do let(:resource) { create(:issue) } let(:user) { create(:user) } - describe "#subscribed?" do - it do + describe '#subscribed?' do + it 'returns false when no subcription exists' do expect(resource.subscribed?(user)).to be_falsey - resource.toggle_subscription(user) + 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 diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index dc9d832975..4ffe753fef 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -6,6 +6,7 @@ describe Issues::UpdateService, services: true do let(:user3) { create(:user) } let(:issue) { create(:issue, title: 'Old title', assignee_id: user3.id) } let(:label) { create(:label) } + let(:label2) { create(:label) } let(:project) { issue.project } before do @@ -148,57 +149,45 @@ describe Issues::UpdateService, services: true do end end - context "when the issue is relabeled" do - it "sends notifications for subscribers of newly added labels" do - subscriber, non_subscriber = create_list(:user, 2) - label.toggle_subscription(subscriber) - 2.times { label.toggle_subscription(non_subscriber) } + 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 - @issue.reload should_email(subscriber) should_not_email(non_subscriber) end - it "does send notifications for existing labels" do - second_label = create(:label) - issue.labels << label - subscriber, non_subscriber = create_list(:user, 2) - label.toggle_subscription(subscriber) - 2.times { label.toggle_subscription(non_subscriber) } + context 'when issue has the `label` label' do + before { issue.labels << label } - opts = { label_ids: [label.id, second_label.id] } + 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) + perform_enqueued_jobs do + @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + end + + should_not_email(subscriber) + should_not_email(non_subscriber) end - @issue.reload - should_email(subscriber) - should_not_email(non_subscriber) - end + it 'does not send notifications for removed labels' do + opts = { label_ids: [label2.id] } - it "does not send notifications for removed labels" do - second_label = create(:label) - issue.labels << label - subscriber, non_subscriber = create_list(:user, 2) - label.toggle_subscription(subscriber) - 2.times { label.toggle_subscription(non_subscriber) } + perform_enqueued_jobs do + @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + end - opts = { label_ids: [second_label.id] } - - perform_enqueued_jobs do - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + should_not_email(subscriber) + should_not_email(non_subscriber) end - - @issue.reload - should_not_email(subscriber) - should_not_email(non_subscriber) end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 104e63ccfe..cb8cff2fa8 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -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(:project) { merge_request.project } let(:label) { create(:label) } + let(:label2) { create(:label) } before do project.team << [user, :master] @@ -176,57 +177,45 @@ describe MergeRequests::UpdateService, services: true do end end - context "when the merge request is relabeled" do - it "sends notifications for subscribers of newly added labels" do - subscriber, non_subscriber = create_list(:user, 2) - label.toggle_subscription(subscriber) - 2.times { label.toggle_subscription(non_subscriber) } + 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 - @merge_request.reload should_email(subscriber) should_not_email(non_subscriber) end - it "does send notifications for existing labels" do - second_label = create(:label) - merge_request.labels << label - subscriber, non_subscriber = create_list(:user, 2) - label.toggle_subscription(subscriber) - 2.times { label.toggle_subscription(non_subscriber) } + context 'when issue has the `label` label' do + before { merge_request.labels << label } - opts = { label_ids: [label.id, second_label.id] } + 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) + 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 - @merge_request.reload - should_email(subscriber) - should_not_email(non_subscriber) - end + it 'does not send notifications for removed labels' do + opts = { label_ids: [label2.id] } - it "does not send notifications for removed labels" do - second_label = create(:label) - merge_request.labels << label - subscriber, non_subscriber = create_list(:user, 2) - label.toggle_subscription(subscriber) - 2.times { label.toggle_subscription(non_subscriber) } + perform_enqueued_jobs do + @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + end - opts = { label_ids: [second_label.id] } - - perform_enqueued_jobs do - @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + should_not_email(subscriber) + should_not_email(non_subscriber) end - - @merge_request.reload - should_not_email(subscriber) - should_not_email(non_subscriber) end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 35afa76805..b5407397c1 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -225,14 +225,13 @@ describe NotificationService, services: true do should_not_email(issue.assignee) end - it "should email subscribers of the issue's labels" do - subscriber, non_subscriber = create_list(:user, 2) + it "emails subscribers of the issue's labels" do + subscriber = create(:user) label = create(:label, issues: [issue]) label.toggle_subscription(subscriber) - 2.times { label.toggle_subscription(non_subscriber) } notification.new_issue(issue, @u_disabled) + should_email(subscriber) - should_not_email(non_subscriber) end end @@ -306,6 +305,35 @@ describe NotificationService, services: true do 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 it 'should sent email to issue assignee and issue author' do notification.close_issue(issue, @u_disabled) @@ -336,32 +364,6 @@ describe NotificationService, services: true do should_not_email(@u_participating) end end - - describe :relabel_issue do - it "sends email to subscribers of the given labels" do - subscriber, non_subscriber = create_list(:user, 2) - label = create(:label, issues: [issue]) - label.toggle_subscription(subscriber) - 2.times { label.toggle_subscription(non_subscriber) } - notification.relabeled_issue(issue, [label], @u_disabled) - should_email(subscriber) - should_not_email(non_subscriber) - end - - it "doesn't send email to anyone but subscribers of the given labels" do - label = create(:label, issues: [issue]) - notification.relabeled_issue(issue, [label], @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) - end - end end describe 'Merge Requests' do @@ -386,15 +388,13 @@ describe NotificationService, services: true do should_not_email(@u_disabled) end - it "should email subscribers of the MR's labels" do - subscriber, non_subscriber = create_list(:user, 2) - label = create(:label) - merge_request.labels << label + it "emails subscribers of the merge request's labels" do + subscriber = create(:user) + label = create(:label, merge_requests: [merge_request]) label.toggle_subscription(subscriber) - 2.times { label.toggle_subscription(non_subscriber) } notification.new_merge_request(merge_request, @u_disabled) + should_email(subscriber) - should_not_email(non_subscriber) end end @@ -413,6 +413,35 @@ describe NotificationService, services: true do 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 it do notification.close_mr(merge_request, @u_disabled) @@ -457,34 +486,6 @@ describe NotificationService, services: true do should_not_email(@u_disabled) end end - - describe :relabel_merge_request do - it "sends email to subscribers of the given labels" do - subscriber, non_subscriber = create_list(:user, 2) - label = create(:label) - merge_request.labels << label - label.toggle_subscription(subscriber) - 2.times { label.toggle_subscription(non_subscriber) } - notification.relabeled_merge_request(merge_request, [label], @u_disabled) - should_email(subscriber) - should_not_email(non_subscriber) - end - - it "doesn't send email to anyone but subscribers of the given labels" do - label = create(:label) - merge_request.labels << label - notification.relabeled_merge_request(merge_request, [label], @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) - end - end end describe 'Projects' do From c98089b2b29bbcb44cdce7ce23bff3fa5d10d3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 15 Mar 2016 17:18:47 +0100 Subject: [PATCH 3/4] Move the #toggle_subscription controller method to a concern --- app/controllers/projects/issues_controller.rb | 13 ++++--------- app/controllers/projects/labels_controller.rb | 18 ++++++++---------- .../projects/merge_requests_controller.rb | 12 +++--------- 3 files changed, 15 insertions(+), 28 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 24a862814b..b0a03ee45c 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -1,6 +1,8 @@ class Projects::IssuesController < Projects::ApplicationController + include ToggleSubscriptionAction + before_action :module_enabled - before_action :issue, only: [:edit, :update, :show, :toggle_subscription] + before_action :issue, only: [:edit, :update, :show] # Allow read any issue before_action :authorize_read_issue! @@ -110,14 +112,6 @@ class Projects::IssuesController < Projects::ApplicationController redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" }) end - def toggle_subscription - return unless current_user - - @issue.toggle_subscription(current_user) - - render nothing: true - end - def closed_by_merge_requests @closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user) end @@ -131,6 +125,7 @@ class Projects::IssuesController < Projects::ApplicationController redirect_old end end + alias_method :subscribable_resource, :issue def authorize_update_issue! return render_404 unless can?(current_user, :update_issue, @issue) diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index e4dea6b065..40d8098690 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -1,8 +1,12 @@ class Projects::LabelsController < Projects::ApplicationController + include ToggleSubscriptionAction + before_action :module_enabled - before_action :label, only: [:edit, :update, :destroy, :toggle_subscription] + before_action :label, only: [:edit, :update, :destroy] before_action :authorize_read_label! - before_action :authorize_admin_labels!, except: [:index, :toggle_subscription] + before_action :authorize_admin_labels!, only: [ + :new, :create, :edit, :update, :generate, :destroy + ] respond_to :js, :html @@ -60,13 +64,6 @@ class Projects::LabelsController < Projects::ApplicationController end end - def toggle_subscription - return unless current_user - - @label.toggle_subscription(current_user) - render nothing: true - end - protected def module_enabled @@ -80,8 +77,9 @@ class Projects::LabelsController < Projects::ApplicationController end def label - @label = @project.labels.find(params[:id]) + @label ||= @project.labels.find(params[:id]) end + alias_method :subscribable_resource, :label def authorize_admin_labels! return render_404 unless can?(current_user, :admin_label, @project) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 954ee55a21..61b82c9db4 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -1,10 +1,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController + include ToggleSubscriptionAction include DiffHelper before_action :module_enabled before_action :merge_request, only: [ :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 :validates_merge_request, only: [:show, :diffs, :commits, :builds] @@ -233,14 +234,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController render json: response end - def toggle_subscription - return unless current_user - - @merge_request.toggle_subscription(current_user) - - render nothing: true - end - protected def selected_target_project @@ -254,6 +247,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def merge_request @merge_request ||= @project.merge_requests.find_by!(iid: params[:id]) end + alias_method :subscribable_resource, :merge_request def closes_issues @closes_issues ||= @merge_request.closes_issues From e90d6ec1d83ff86743e70931b49647eaf3e3e67a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 15 Mar 2016 17:24:55 +0100 Subject: [PATCH 4/4] Create a SentNotification record for #relabeled_issue_email / #relabeled_merge_request_email --- .../concerns/toggle_subscription_action.rb | 17 +++++++++++++++++ app/mailers/emails/issues.rb | 8 +++----- app/mailers/emails/merge_requests.rb | 8 +++----- 3 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 app/controllers/concerns/toggle_subscription_action.rb diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb new file mode 100644 index 0000000000..8a43c0b93c --- /dev/null +++ b/app/controllers/concerns/toggle_subscription_action.rb @@ -0,0 +1,17 @@ +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 diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 160b6df0b9..5f9adb32e0 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -21,7 +21,7 @@ module Emails end def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id) - setup_issue_mail(issue_id, recipient_id, sent_notification: false) + setup_issue_mail(issue_id, recipient_id) @label_names = label_names @labels_url = namespace_project_labels_url(@project.namespace, @project) @@ -38,14 +38,12 @@ module Emails private - def setup_issue_mail(issue_id, recipient_id, sent_notification: true) + def setup_issue_mail(issue_id, recipient_id) @issue = Issue.find(issue_id) @project = @issue.project @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) - if sent_notification - @sent_notification = SentNotification.record(@issue, recipient_id, reply_key) - end + @sent_notification = SentNotification.record(@issue, recipient_id, reply_key) end def issue_thread_options(sender_id, recipient_id) diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 334bad4e2f..55bb4f6527 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -14,7 +14,7 @@ module Emails end 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, sent_notification: false) + setup_merge_request_mail(merge_request_id, recipient_id) @label_names = label_names @labels_url = namespace_project_labels_url(@project.namespace, @project) @@ -44,14 +44,12 @@ module Emails private - def setup_merge_request_mail(merge_request_id, recipient_id, sent_notification: true) + def setup_merge_request_mail(merge_request_id, recipient_id) @merge_request = MergeRequest.find(merge_request_id) @project = @merge_request.project @target_url = namespace_project_merge_request_url(@project.namespace, @project, @merge_request) - if sent_notification - @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key) - end + @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key) end def merge_request_thread_options(sender_id, recipient_id)