diff options
author | Timothy Andrew <mail@timothyandrew.net> | 2016-02-12 20:28:39 +0530 |
---|---|---|
committer | Rémy Coutable <remy@rymai.me> | 2016-03-15 17:25:37 +0100 |
commit | 0444fa560acd07255960284f19b1de6499cd5910 (patch) | |
tree | 005ce576bbe661242ee58c1e1ddebd9e665bd9ff /app | |
parent | 178c80a561fa10a157bae6e5d4682b232ae727c7 (diff) | |
download | gitlab-ce-0444fa560acd07255960284f19b1de6499cd5910.tar.gz |
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.
Diffstat (limited to 'app')
20 files changed, 186 insertions, 49 deletions
diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index 7f41616d4e7..afc17338b26 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 5ec0966194c..1791ba9fbd8 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 ecac3c395ec..d0334c37b67 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 4a88cb61132..2838baa1b4e 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 325996e2e16..680ce975c79 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 3c42f582937..affc4a842a7 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 00000000000..cab9241ac3d --- /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 5ff644b8426..f7ffc0b7f36 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 ca87dca4a70..971c074882e 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 51ef9dfe610..b2e63a4e1af 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 6319ad805b6..6fd569dc302 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 ca8a41d93b8..e9955cd3e2d 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 00000000000..a41ff07c306 --- /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 00000000000..1a28d7fd352 --- /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 00000000000..a3e094e86eb --- /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 00000000000..de7f04a323a --- /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 00000000000..3937b449a37 --- /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 00000000000..5c4d86e4dc2 --- /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 f7ddd30c5a9..3b14a925c46 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 9020a1330a3..b44086d4205 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(); |