Project

General

Profile

Feature #13919 » draft-acts_as_mentionable.patch

Patch developed based on 0001-Demo-for-acts_as_mentionable.patch - Mizuki ISHIKAWA, 2020-02-13 02:23

View differences:

app/helpers/application_helper.rb
53 53
      name = h(user.name(options[:format]))
54 54
      if user.active? || (User.current.admin? && user.logged?)
55 55
        only_path = options[:only_path].nil? ? true : options[:only_path]
56
        link_to name, user_url(user, :only_path => only_path), :class => user.css_classes
56
        css_classes = options[:class] ? "#{user.css_classes} #{options[:class]}" : user.css_classes
57
        link_to name, user_url(user, :only_path => only_path), :class => css_classes
57 58
      else
58 59
        name
59 60
      end
......
1080 1081
              if p = Project.visible.find_by_id(oid)
1081 1082
                link = link_to_project(p, {:only_path => only_path}, :class => 'project')
1082 1083
              end
1083
            when 'user'
1084
              u = User.visible.find_by(:id => oid, :type => 'User')
1085
              link = link_to_user(u, :only_path => only_path) if u
1086 1084
            end
1087 1085
          elsif sep == ':'
1088 1086
            name = remove_double_quotes(identifier)
......
1157 1155
              if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
1158 1156
                link = link_to_project(p, {:only_path => only_path}, :class => 'project')
1159 1157
              end
1160
            when 'user'
1161
              u = User.visible.find_by("LOWER(login) = :s AND type = 'User'", :s => name.downcase)
1162
              link = link_to_user(u, :only_path => only_path) if u
1163 1158
            end
1164
          elsif sep == "@"
1165
            name = remove_double_quotes(identifier)
1166
            u = User.visible.find_by("LOWER(login) = :s AND type = 'User'", :s => name.downcase)
1167
            link = link_to_user(u, :only_path => only_path) if u
1159
          end
1160
          if link.nil? && $~
1161
            user = User.mentioned_user($~.named_captures.symbolize_keys)
1162
            if user
1163
              css_classes = (user.notify_mentioned_user?(obj) ? 'notified' : nil)
1164
              link = link_to_user(user, :only_path => only_path, :class => css_classes)
1165
            end
1168 1166
          end
1169 1167
        end
1170 1168
        (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
app/models/comment.rb
24 24

  
25 25
  validates_presence_of :commented, :author, :content
26 26

  
27
  acts_as_mentionable :attributes => ['content']
28

  
27 29
  after_create_commit :send_notification
28 30

  
29 31
  safe_attributes 'comments'
app/models/document.rb
63 63
  end
64 64

  
65 65
  def notified_users
66
    project.notified_users.reject {|user| !visible?(user)}
66
    project.notified_users.select {|user| user.allowed_to_view_notify_target?(self) }
67 67
  end
68 68

  
69 69
  private
app/models/issue.rb
43 43
  acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
44 44
  acts_as_customizable
45 45
  acts_as_watchable
46
  acts_as_mentionable :attributes => ['description']
46 47
  acts_as_searchable :columns => ['subject', "#{table_name}.description"],
47 48
                     :preload => [:project, :status, :tracker],
48 49
                     :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
......
1045 1046
    notified += project.users.preload(:preference).select(&:notify_about_high_priority_issues?) if priority.high?
1046 1047
    notified.uniq!
1047 1048
    # Remove users that can not view the issue
1048
    notified.reject! {|user| !visible?(user)}
1049
    notified
1049
    notified.select {|user| user.allowed_to_view_notify_target?(self)}
1050 1050
  end
1051 1051

  
1052 1052
  # Returns the email addresses that should be notified
app/models/journal.rb
29 29
  has_many :details, :class_name => "JournalDetail", :dependent => :delete_all, :inverse_of => :journal
30 30
  attr_accessor :indice
31 31

  
32
  acts_as_mentionable :attributes => ['notes']
32 33
  acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
33 34
                :description => :notes,
34 35
                :author => :user,
35 36
                :group => :issue,
36 37
                :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' },
37 38
                :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
38

  
39 39
  acts_as_activity_provider :type => 'issues',
40 40
                            :author_key => :user_id,
41 41
                            :scope => preload({:issue => :project}, :user).
......
145 145

  
146 146
  def notified_users
147 147
    notified = journalized.notified_users
148
    if private_notes?
149
      notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
150
    end
151
    notified
148
    notified.select{ |u| u.allowed_to_view_notify_target?(self) }
152 149
  end
153 150

  
154 151
  def recipients
app/models/mailer.rb
93 93
  #   Mailer.deliver_issue_add(issue)
94 94
  def self.deliver_issue_add(issue)
95 95
    users = issue.notified_users | issue.notified_watchers
96
    users -= issue.mentioned_users_with_latest_changes
96 97
    users.each do |user|
97 98
      issue_add(user, issue).deliver_later
98 99
    end
......
131 132
    users.select! do |user|
132 133
      journal.notes? || journal.visible_details(user).any?
133 134
    end
135
    users -= journal.mentioned_users_with_latest_changes
136
    users -= journal.issue.mentioned_users_with_latest_changes
137

  
134 138
    users.each do |user|
135 139
      issue_edit(user, journal).deliver_later
136 140
    end
......
221 225
  #   Mailer.deliver_news_added(news)
222 226
  def self.deliver_news_added(news)
223 227
    users = news.notified_users | news.notified_watchers_for_added_news
228
    users -= news.mentioned_users_with_latest_changes
224 229
    users.each do |user|
225 230
      news_added(user, news).deliver_later
226 231
    end
......
248 253
  def self.deliver_news_comment_added(comment)
249 254
    news = comment.commented
250 255
    users = news.notified_users | news.notified_watchers
256
    users -= comment.mentioned_users_with_latest_changes
251 257
    users.each do |user|
252 258
      news_comment_added(user, comment).deliver_later
253 259
    end
......
275 281
    users  = message.notified_users
276 282
    users |= message.root.notified_watchers
277 283
    users |= message.board.notified_watchers
284
    users -= message.mentioned_users_with_latest_changes
278 285

  
279 286
    users.each do |user|
280 287
      message_posted(user, message).deliver_later
......
529 536
    end
530 537
  end
531 538

  
539
  def mail_to_mentioned_users(user, obj, contents)
540
    @contents = contents
541
    mail :to => user,
542
         :subject => "You are mentioned by #{obj.try(:author) || obj.user} in #{obj.class}##{obj.id}"
543
  end
544

  
545
  # Notifies mentioned users.
546
  #
547
  # Example:
548
  #   Mailer.deliver_mail_to_mentioned_users(users, obj, content)
549
  def self.deliver_mail_to_mentioned_users(users, obj, content)
550
    users.each do |user|
551
      mail_to_mentioned_users(user, obj, content).deliver_later
552
    end
553
  end
554

  
532 555
  # Build a test email to user.
533 556
  def test_email(user)
534 557
    @url = url_for(:controller => 'welcome')
app/models/message.rb
114 114
  end
115 115

  
116 116
  def notified_users
117
    project.notified_users.reject {|user| !visible?(user)}
117
    project.notified_users.select {|user| user.allowed_to_view_notify_target?(self) }
118 118
  end
119 119

  
120 120
  private
app/models/news.rb
31 31
                     :delete_permission => :manage_news
32 32
  acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"],
33 33
                     :preload => :project
34
  acts_as_mentionable :attributes => ['description']
34 35
  acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
35 36
  acts_as_activity_provider :scope => preload(:project, :author),
36 37
                            :author_key => :author_id
......
56 57
  end
57 58

  
58 59
  def notified_users
59
    project.users.select {|user| user.notify_about?(self) && user.allowed_to?(:view_news, project)}
60
    project.users.select {|user| user.notify_about?(self) && user.allowed_to_view_notify_target?(self)}
60 61
  end
61 62

  
62 63
  def recipients
app/models/user.rb
823 823
    RequestStore.store[:current_user] ||= User.anonymous
824 824
  end
825 825

  
826
  # Return the mentioned user to based on the match data
827
  #  of ApplicationHelper::LINKS_RE.
828
  #     user:jsmith -> Link to user with login jsmith
829
  #     @jsmith -> Link to user with login jsmith
830
  #     user#2 -> Link to user with id 2
831
  def self.mentioned_user(match_data)
832
    return nil if match_data[:esc]
833
    sep = match_data[:sep1] || match_data[:sep2] || match_data[:sep3] || match_data[:sep4]
834
    identifier = match_data[:identifier1] || match_data[:identifier2] || match_data[:identifier3]
835
    prefix = match_data[:prefix]
836
    if ['#', '##'].include?(sep) && prefix == 'user'
837
      User.visible.find_by(:id => identifier.to_i, :type => 'User')
838
    elsif sep == '@' || (sep == ':' && prefix == 'user')
839
      name = identifier.gsub(%r{^"(.*)"$}, "\\1")
840
      User.find_by_login(CGI.unescapeHTML(name).downcase)
841
    end
842
  end
843

  
844
  # Return true if notify the mentioned user.
845
  def notify_mentioned_user?(object)
846
    self.active? &&
847
      self.mail.present? &&
848
      self.mail_notification.present? && self.mail_notification != 'none' &&
849
      self.allowed_to_view_notify_target?(object)
850
  end
851

  
852
  # Return true if the user is allowed to view the notify target.
853
  def allowed_to_view_notify_target?(object)
854
    case object
855
    when Journal
856
      self.allowed_to_view_notify_target?(object.journalized) &&
857
        (!object.private_notes? || self.allowed_to?(:view_private_notes, object.journalized.project))
858
    when Comment
859
      self.allowed_to_view_notify_target?(object.commented)
860
    when nil
861
      false
862
    else
863
      object.visible?(self)
864
    end
865
  end
866

  
826 867
  # Returns the anonymous user.  If the anonymous user does not exist, it is created.  There can be only
827 868
  # one anonymous user per database.
828 869
  def self.anonymous
app/models/wiki_content.rb
53 53
  end
54 54

  
55 55
  def notified_users
56
    project.notified_users.reject {|user| !visible?(user)}
56
    project.notified_users.select {|user| user.allowed_to_view_notify_target?(self) }
57 57
  end
58 58

  
59 59
  # Returns the mail addresses of users that should be notified
app/views/mailer/mail_to_mentioned_users.html.erb
1
<% @contents.each do |key, content| %>
2
<p><%= textilizable content %></p>
3
<% end %>
app/views/mailer/mail_to_mentioned_users.text.erb
1
<% @contents.each do |key, content| %>
2
<%= textilizable content %>
3
<% end %>
lib/plugins/acts_as_mentionable/init.rb
1
# frozen_string_literal: true
2

  
3
# Include hook code here
4
require File.dirname(__FILE__) + '/lib/acts_as_mentionable'
5
ActiveRecord::Base.send(:include, Redmine::Acts::Mentionable)
lib/plugins/acts_as_mentionable/lib/acts_as_mentionable.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-2019  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
module Redmine
21
  module Acts
22
    module Mentionable
23
      def self.included(base)
24
        base.extend ClassMethods
25
      end
26

  
27
      module ClassMethods
28
        def acts_as_mentionable(options = {})
29
          return if self.included_modules.include?(Redmine::Acts::Mentionable::InstanceMethods)
30

  
31
          cattr_accessor :mentionable_attributes
32
          self.mentionable_attributes = options[:attributes]
33

  
34
          send :include, Redmine::Acts::Mentionable::InstanceMethods
35

  
36
          before_save :was_new_record?
37
          after_save :notify_mentioned_users
38
        end
39
      end
40

  
41
      module InstanceMethods
42
        def self.included(base)
43
          base.extend ClassMethods
44
        end
45

  
46
        def was_new_record?
47
          @was_new_record = self.new_record?
48
        end
49

  
50
        def notify_mentioned_users
51
          attribute_values = mentionable_attributes.map{|attr| [attr, self.saved_changes[attr][1]] if self.saved_changes[attr] }.compact.to_h
52
          users = mentioned_users_with_latest_changes
53
          Mailer.deliver_mail_to_mentioned_users(users, self, attribute_values) if users.present?
54
        end
55

  
56
        def mentioned_users_with_latest_changes
57
          changes = self.saved_changes
58
          if @was_new_record
59
            values = mentionable_attributes.map{|attr| changes[attr] && changes[attr][1] }.compact
60
            users = mentioned_users(values)
61
          else
62
            new_values = mentionable_attributes.map{|attr| changes[attr] && changes[attr][1] }.compact
63
            old_values = mentionable_attributes.map{|attr| changes[attr] && changes[attr][0] }.compact
64
            users = mentioned_users(new_values) - mentioned_users(old_values)
65
          end
66
          users
67
        end
68

  
69
        def mentioned_users(values)
70
          users = []
71
          values.each do |value|
72
            value.scan(ApplicationHelper::LINKS_RE) do |_|
73
              target = User.mentioned_user($~.named_captures.symbolize_keys)
74
              next if target.blank? || users.include?(target)
75
              users << target if target.notify_mentioned_user?(self)
76
            end
77
          end
78
          users.uniq
79
        end
80

  
81
        module ClassMethods
82
        end
83
      end
84
    end
85
  end
86
end
public/stylesheets/application.css
137 137
a, a:link, a:visited{ color: #169; text-decoration: none; }
138 138
a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
139 139
a img{ border: 0; }
140
a.user.notified, a.user.notified:link, a.user.notified:visited {padding: 2px; border-radius: 3px; background-color: #bae9f5}
140 141

  
141 142
a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
142 143
a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
(5-5/12)