Commit 54ec7e95 authored by Rémy Coutable's avatar Rémy Coutable
Browse files

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
parent 0444fa56
......@@ -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
......
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')
......@@ -43,5 +43,5 @@
}
.label-subscription {
display: inline-block;
}
\ No newline at end of file
display: inline-block;
}
......@@ -111,6 +111,8 @@ def bulk_update
end
def toggle_subscription
return unless current_user
@issue.toggle_subscription(current_user)
render nothing: true
......
......@@ -61,6 +61,8 @@ def destroy
end
def toggle_subscription
return unless current_user
@label.toggle_subscription(current_user)
render nothing: true
end
......
......@@ -234,6 +234,8 @@ def ci_status
end
def toggle_subscription
return unless current_user
@merge_request.toggle_subscription(current_user)
render nothing: true
......
......@@ -124,6 +124,14 @@ def projects_labels_options
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
......
......@@ -20,10 +20,11 @@ def closed_issue_email(recipient_id, issue_id, 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)
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 @@ def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_i
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 @@ def issue_thread_options(sender_id, recipient_id)
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
......@@ -3,49 +3,35 @@ module MergeRequests
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 @@ def merge_request_status_email(recipient_id, merge_request_id, status, updated_b
@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)
if sent_notification
@sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
end
end
@sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
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
......@@ -149,6 +149,10 @@ def upvotes
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 @@ def add_labels_by_names(label_names)
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:
......
......@@ -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)
......
......@@ -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
......@@ -11,7 +11,10 @@ def create_milestone_note(issuable)
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 @@ def change_state(issuable)
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 @@ def handle_common_system_notes(issuable, options = {})
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
......@@ -4,8 +4,8 @@ def execute(issue)
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 @@ def handle_changes(issue, old_labels: [], new_labels: [])
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
......
......@@ -14,8 +14,8 @@ def execute(merge_request)
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 @@ def handle_changes(issue, old_labels: [], new_labels: [])
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
......
......@@ -24,16 +24,17 @@ def new_email(email)
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 @@ def close_issue(issue, current_user)
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 @@ def reassigned_issue(issue, current_user)
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 @@ def reassigned_merge_request(merge_request, current_user)
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 @@ def reopen_mr(merge_request, current_user)
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 @@ def new_note(note)
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 @@ def reject_unsubscribed_users(recipients, target)
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 @@ def reassign_resource_email(target, project, current_user, method)
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 @@ def build_recipients(target, project, current_user, action: nil, previous_assign
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 @@ def build_recipients(target, project, current_user, action: nil, previous_assign
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
......
......@@ -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.