Project

General

Profile

Feature #13919 » 0001-Allow-users-to-be-mentioned-using-in-issues-and-wiki.patch

Marius BĂLTEANU, 2022-01-24 18:40

View differences:

app/controllers/watchers_controller.rb
28 28
    set_watcher(@watchables, User.current, false)
29 29
  end
30 30

  
31
  before_action :find_project, :authorize, :only => [:new, :create, :append, :destroy, :autocomplete_for_user]
31
  before_action :find_project, :authorize, :only => [:new, :create, :append, :destroy, :autocomplete_for_user, :autocomplete_for_mention]
32 32
  accept_api_auth :create, :destroy
33 33

  
34 34
  def new
......
93 93
    render :layout => false
94 94
  end
95 95

  
96
  def autocomplete_for_mention
97
    users = users_for_mention
98
    render :json => format_users_json(users)
99
  end
100

  
96 101
  private
97 102

  
98 103
  def find_project
......
151 156
    users
152 157
  end
153 158

  
159
  def users_for_mention
160
    users = []
161
    q = params[:q].to_s.strip
162

  
163
    scope = nil
164
    if params[:q].blank? && @project.present?
165
      scope = @project.principals.assignable_watchers
166
    else
167
      scope = Principal.assignable_watchers.limit(10)
168
    end
169
    # Exclude Group principal for now
170
    scope = scope.where(:type => ['User'])
171

  
172
    users = scope.sorted.like(params[:q]).to_a
173

  
174
    if @watchables && @watchables.size == 1
175
      object = @watchables.first
176
      if object.respond_to?(:visible?)
177
        users.reject! {|user| user.is_a?(User) && !object.visible?(user)}
178
      end
179
    end
180

  
181
    users
182
  end
183

  
184
  def format_users_json(users)
185
    users.map do |user|
186
      {
187
        'firstname' => user.firstname,
188
        'lastname' => user.lastname,
189
        'name' => user.name,
190
        'login' => user.login
191
      }
192
    end
193
  end
194

  
154 195
  def find_objects_from_params
155 196
    klass =
156 197
      begin
app/helpers/application_helper.rb
1819 1819
    end
1820 1820
  end
1821 1821

  
1822
  def autocomplete_data_sources(project)
1823
    {
1824
      issues: auto_complete_issues_path(:project_id => project, :q => ''),
1825
      wiki_pages: auto_complete_wiki_pages_path(:project_id => project, :q => '')
1826
    }
1827
  end
1828

  
1829 1822
  def heads_for_auto_complete(project)
1830 1823
    data_sources = autocomplete_data_sources(project)
1831 1824
    javascript_tag(
1832 1825
      "rm = window.rm || {};" \
1833 1826
      "rm.AutoComplete = rm.AutoComplete || {};" \
1834
      "rm.AutoComplete.dataSources = '#{data_sources.to_json}';"
1827
      "rm.AutoComplete.dataSources = JSON.parse('#{data_sources.to_json}');"
1828
    )
1829
  end
1830

  
1831
  def update_data_sources_for_auto_complete(data_sources)
1832
    javascript_tag(
1833
      "const currentDataSources = rm.AutoComplete.dataSources;" \
1834
      "const newDataSources = JSON.parse('#{data_sources.to_json}'); " \
1835
      "rm.AutoComplete.dataSources = Object.assign(currentDataSources, newDataSources);"
1835 1836
    )
1836 1837
  end
1837 1838

  
......
1866 1867
    name = identifier.gsub(%r{^"(.*)"$}, "\\1")
1867 1868
    return CGI.unescapeHTML(name)
1868 1869
  end
1870

  
1871
  def autocomplete_data_sources(project)
1872
    {
1873
      issues: auto_complete_issues_path(project_id: project, q: ''),
1874
      wiki_pages: auto_complete_wiki_pages_path(project_id: project, q: ''),
1875
    }
1876
  end
1869 1877
end
app/models/issue.rb
54 54
  acts_as_activity_provider :scope => proc {preload(:project, :author, :tracker, :status)},
55 55
                            :author_key => :author_id
56 56

  
57
  acts_as_mentionable :attributes => ['description']
58

  
57 59
  DONE_RATIO_OPTIONS = %w(issue_field issue_status)
58 60

  
59 61
  attr_reader :transition_warning
app/models/journal.rb
58 58
                  " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')").distinct
59 59
      end
60 60
  )
61
  acts_as_mentionable :attributes => ['notes']
61 62
  before_create :split_private_notes
62 63
  after_create_commit :send_notification
63 64

  
......
172 173

  
173 174
  def notified_watchers
174 175
    notified = journalized.notified_watchers
175
    if private_notes?
176
      notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
177
    end
178
    notified
176
    select_journal_visible_user(notified)
177
  end
178

  
179
  def notified_mentions
180
    notified = super
181
    select_journal_visible_user(notified)
179 182
  end
180 183

  
181 184
  def watcher_recipients
......
337 340
      Mailer.deliver_issue_edit(self)
338 341
    end
339 342
  end
343

  
344
  def select_journal_visible_user(notified)
345
    if private_notes?
346
      notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
347
    end
348
    notified
349
  end
340 350
end
app/models/mailer.rb
94 94
  # Example:
95 95
  #   Mailer.deliver_issue_add(issue)
96 96
  def self.deliver_issue_add(issue)
97
    users = issue.notified_users | issue.notified_watchers
97
    users = issue.notified_users | issue.notified_watchers | issue.notified_mentions
98 98
    users.each do |user|
99 99
      issue_add(user, issue).deliver_later
100 100
    end
......
129 129
  # Example:
130 130
  #   Mailer.deliver_issue_edit(journal)
131 131
  def self.deliver_issue_edit(journal)
132
    users  = journal.notified_users | journal.notified_watchers
132
    users  = journal.notified_users | journal.notified_watchers | journal.notified_mentions | journal.journalized.notified_mentions
133 133
    users.select! do |user|
134 134
      journal.notes? || journal.visible_details(user).any?
135 135
    end
......
306 306
  # Example:
307 307
  #   Mailer.deliver_wiki_content_added(wiki_content)
308 308
  def self.deliver_wiki_content_added(wiki_content)
309
    users = wiki_content.notified_users | wiki_content.page.wiki.notified_watchers
309
    users = wiki_content.notified_users | wiki_content.page.wiki.notified_watchers | wiki_content.notified_mentions
310 310
    users.each do |user|
311 311
      wiki_content_added(user, wiki_content).deliver_later
312 312
    end
......
343 343
    users  = wiki_content.notified_users
344 344
    users |= wiki_content.page.notified_watchers
345 345
    users |= wiki_content.page.wiki.notified_watchers
346
    users |= wiki_content.notified_mentions
346 347

  
347 348
    users.each do |user|
348 349
      wiki_content_updated(user, wiki_content).deliver_later
app/models/wiki_content.rb
24 24
  belongs_to :page, :class_name => 'WikiPage'
25 25
  belongs_to :author, :class_name => 'User'
26 26
  has_many :versions, :class_name => 'WikiContentVersion', :dependent => :delete_all
27

  
28
  acts_as_mentionable :attributes => ['text']
29

  
27 30
  validates_presence_of :text
28 31
  validates_length_of :comments, :maximum => 1024, :allow_nil => true
29 32

  
app/views/issues/_form.html.erb
53 53
<% end %>
54 54

  
55 55
<% heads_for_wiki_formatter %>
56
<%= heads_for_auto_complete(@issue.project) %>
56

  
57
<% if User.current.allowed_to?(:add_issue_watchers, @issue.project)%>
58
  <%= update_data_sources_for_auto_complete({users: watchers_autocomplete_for_mention_path(project_id: @issue.project, q: '', object_type: 'issue',
59
   object_id: @issue.id)}) %>
60
<% end %>
57 61

  
58 62
<%= javascript_tag do %>
59 63
$(document).ready(function(){
app/views/wiki/edit.html.erb
64 64
  <%= link_to l(:button_cancel), wiki_page_edit_cancel_path(@page) %>
65 65
 </p>
66 66
<%= wikitoolbar_for 'content_text', preview_project_wiki_page_path(:project_id => @project, :id => @page.title) %>
67

  
68
<% if User.current.allowed_to?(:add_wiki_page_watchers, @project)%>
69
  <%= update_data_sources_for_auto_complete({users: watchers_autocomplete_for_mention_path(project_id: @project, q: '', object_type: 'wiki_page', object_id: @page.id)}) %>
70
  <% end %>
67 71
<% end %>
68 72

  
69 73
<% content_for :header_tags do %>
config/routes.rb
46 46
  post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
47 47
  post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
48 48

  
49
  # Auto complate routes
49
  # Auto complete routes
50 50
  match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
51 51
  match '/wiki_pages/auto_complete', :to => 'auto_completes#wiki_pages', :via => :get, :as => 'auto_complete_wiki_pages'
52 52

  
......
119 119
  post 'watchers', :to => 'watchers#create'
120 120
  post 'watchers/append', :to => 'watchers#append'
121 121
  delete 'watchers', :to => 'watchers#destroy'
122
  get 'watchers/autocomplete_for_mention', to: 'watchers#autocomplete_for_mention', via: [:get]
122 123
  get 'watchers/autocomplete_for_user', :to => 'watchers#autocomplete_for_user'
123 124
  # Specific routes for issue watchers API
124 125
  post 'issues/:object_id/watchers', :to => 'watchers#create', :object_type => 'issue'
lib/redmine/acts/mentionable.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-2022 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
          class_attribute :mentionable_attributes
30
          self.mentionable_attributes = options[:attributes]
31

  
32
          attr_accessor :mentioned_users
33

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

  
36
          after_save :parse_mentions
37
        end
38
      end
39

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

  
45
        def notified_mentions
46
          notified = mentioned_users.to_a
47
          notified.reject! {|user| user.mail.blank? || user.mail_notification == 'none'}
48
          if respond_to?(:visible?)
49
            notified.select! {|user| visible?(user)}
50
          end
51
          notified
52
        end
53

  
54
        private
55

  
56
        def parse_mentions
57
          mentionable_attrs = self.mentionable_attributes
58
          saved_mentionable_attrs = self.saved_changes.select{|a| mentionable_attrs.include?(a)}
59

  
60
          saved_mentionable_attrs.each do |key, attr|
61
            old_value, new_value =  attr
62
            get_mentioned_users(old_value, new_value)
63
          end
64
        end
65

  
66
        def get_mentioned_users(old_content, new_content)
67
          self.mentioned_users = []
68

  
69
          previous_matches =  scan_for_mentioned_users(old_content)
70
          current_matches = scan_for_mentioned_users(new_content)
71
          new_matches = (current_matches - previous_matches).flatten
72

  
73
          if new_matches.any?
74
            self.mentioned_users = User.visible.active.where(login: new_matches)
75
          end
76
        end
77

  
78
        def scan_for_mentioned_users(content)
79
          return [] if content.nil?
80

  
81
          # remove quoted text
82
          content = content.gsub(%r{\r\n(?:\>\s)+(.*?)\r\n}m, '')
83

  
84
          text_formatting = Setting.text_formatting
85
          # Remove text wrapped in pre tags based on text formatting
86
          case text_formatting
87
          when 'textile'
88
            content = content.gsub(%r{<pre>(.*?)</pre>}m, '')
89
          when 'markdown', 'common_mark'
90
            content = content.gsub(%r{(~~~|```)(.*?)(~~~|```)}m, '')
91
          end
92

  
93
          users = content.scan(MENTION_PATTERN).flatten
94
        end
95

  
96
        MENTION_PATTERN = /
97
          (?:^|\W)                    # beginning of string or non-word char
98
          @((?>[a-z0-9][a-z0-9-]*))   # @username
99
          (?!\/)                      # without a trailing slash
100
          (?=
101
            \.+[ \t\W]|               # dots followed by space or non-word character
102
            \.+$|                     # dots at end of line
103
            [^0-9a-zA-Z_.]|           # non-word character except dot
104
            $                         # end of line
105
          )
106
        /ix
107
      end
108
    end
109
  end
110
end
lib/redmine/preparation.rb
21 21
  module Preparation
22 22
    def self.prepare
23 23
      ActiveRecord::Base.include Redmine::Acts::Positioned
24
      ActiveRecord::Base.include Redmine::Acts::Mentionable
24 25
      ActiveRecord::Base.include Redmine::I18n
25 26

  
26 27
      Scm::Base.add "Subversion"
......
71 72
          map.permission :view_private_notes, {}, :read => true, :require => :member
72 73
          map.permission :set_notes_private, {}, :require => :member
73 74
          map.permission :delete_issues, {:issues => :destroy}, :require => :member
75
          map.permission :mention_users, {}
74 76
          # Watchers
75 77
          map.permission :view_issue_watchers, {}, :read => true
76
          map.permission :add_issue_watchers, {:watchers => [:new, :create, :append, :autocomplete_for_user]}
78
          map.permission :add_issue_watchers, {:watchers => [:new, :create, :append, :autocomplete_for_user, :autocomplete_for_mention]}
77 79
          map.permission :delete_issue_watchers, {:watchers => :destroy}
78 80
          map.permission :import_issues, {}
79 81
          # Issue categories
......
123 125
          map.permission :delete_wiki_pages, {:wiki => [:destroy, :destroy_version]}, :require => :member
124 126
          map.permission :delete_wiki_pages_attachments, {}
125 127
          map.permission :view_wiki_page_watchers, {}, :read => true
126
          map.permission :add_wiki_page_watchers, {:watchers => [:new, :create, :autocomplete_for_user]}
128
          map.permission :add_wiki_page_watchers, {:watchers => [:new, :create, :autocomplete_for_user, :autocomplete_for_mention]}
127 129
          map.permission :delete_wiki_page_watchers, {:watchers => :destroy}
128 130
          map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
129 131
          map.permission :manage_wiki, {:wikis => :destroy, :wiki => :rename}, :require => :member
......
145 147
          map.permission :delete_messages, {:messages => :destroy}, :require => :member
146 148
          map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
147 149
          map.permission :view_message_watchers, {}, :read => true
148
          map.permission :add_message_watchers, {:watchers => [:new, :create, :autocomplete_for_user]}
150
          map.permission :add_message_watchers, {:watchers => [:new, :create, :autocomplete_for_user, :autocomplete_for_mention]}
149 151
          map.permission :delete_message_watchers, {:watchers => :destroy}
150 152
          map.permission :manage_boards, {:projects => :settings, :boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
151 153
        end
public/javascripts/application.js
1127 1127
    if (element.dataset.tribute === 'true') {return};
1128 1128

  
1129 1129
    const getDataSource = function(entity) {
1130
      const dataSources = JSON.parse(rm.AutoComplete.dataSources);
1130
      const dataSources = rm.AutoComplete.dataSources;
1131 1131

  
1132
      return dataSources[entity];
1132
      if (dataSources[entity]) {
1133
        return dataSources[entity];
1134
      } else {
1135
        return false;
1136
      }
1133 1137
    }
1134 1138

  
1135 1139
    const remoteSearch = function(url, cb) {
......
1187 1191
          menuItemTemplate: function (wikiPage) {
1188 1192
            return sanitizeHTML(wikiPage.original.label);
1189 1193
          }
1194
        },
1195
        {
1196
          trigger: '@',
1197
          lookup: function (user, mentionText) {
1198
            return user.name + user.firstname + user.lastname + user.login;
1199
          },
1200
          values: function (text, cb) {
1201
            const url = getDataSource('users');
1202
            if (url) {
1203
              remoteSearch(url + text, function (users) {
1204
                return cb(users);
1205
              });
1206
            }
1207
          },
1208
          menuItemTemplate: function (user) {
1209
            return user.original.name;
1210
          },
1211
          selectTemplate: function (user) {
1212
            return '@' + user.original.login;
1213
          }
1190 1214
        }
1191 1215
      ],
1192 1216
      noMatchTemplate: ""
test/functional/auto_completes_controller_test.rb
79 79
    assert_include "Bug #13", response.body
80 80
  end
81 81

  
82
  def test_auto_complete_with_scope_all_should_search_other_projects
82
  def test_issues_with_scope_all_should_search_other_projects
83 83
    get(
84 84
      :issues,
85 85
      :params => {
......
92 92
    assert_include "Bug #13", response.body
93 93
  end
94 94

  
95
  def test_auto_complete_without_project_should_search_all_projects
95
  def test_issues_without_project_should_search_all_projects
96 96
    get(:issues, :params => {:q => '13'})
97 97
    assert_response :success
98 98
    assert_include "Bug #13", response.body
99 99
  end
100 100

  
101
  def test_auto_complete_without_scope_all_should_not_search_other_projects
101
  def test_issues_without_scope_all_should_not_search_other_projects
102 102
    get(
103 103
      :issues,
104 104
      :params => {
......
128 128
    assert_equal 'Bug #13: Subproject issue two', issue['label']
129 129
  end
130 130

  
131
  def test_auto_complete_with_status_o_should_return_open_issues_only
131
  def test_issues_with_status_o_should_return_open_issues_only
132 132
    get(
133 133
      :issues,
134 134
      :params => {
......
142 142
    assert_not_include "closed", response.body
143 143
  end
144 144

  
145
  def test_auto_complete_with_status_c_should_return_closed_issues_only
145
  def test_issues_with_status_c_should_return_closed_issues_only
146 146
    get(
147 147
      :issues,
148 148
      :params => {
......
156 156
    assert_not_include "Issue due today", response.body
157 157
  end
158 158

  
159
  def test_auto_complete_with_issue_id_should_not_return_that_issue
159
  def test_issues_with_issue_id_should_not_return_that_issue
160 160
    get(
161 161
      :issues,
162 162
      :params => {
......
182 182
    assert_include 'application/json', response.headers['Content-Type']
183 183
  end
184 184

  
185
  def test_auto_complete_without_term_should_return_last_10_issues
185
  def test_issue_without_term_should_return_last_10_issues
186 186
    # There are 9 issues generated by fixtures
187 187
    # and we need two more to test the 10 limit
188 188
    %w(1..2).each do
test/unit/journal_test.rb
236 236
      assert_equal "image#{i}.png", attachment.filename
237 237
    end
238 238
  end
239

  
240
  def test_notified_mentions_should_not_include_users_who_cannot_view_private_notes
241
    journal = Journal.generate!(journalized: Issue.find(2), user: User.find(1), private_notes: true, notes: 'Hello @dlopper, @jsmith and @admin.')
242

  
243
    # User "dlopper" has "Developer" role on project "eCookbook"
244
    # Role "Developer" does not have the "View private notes" permission
245
    assert_equal [1, 2], journal.notified_mentions.map(&:id)
246
  end
239 247
end
test/unit/lib/redmine/acts/mentionable_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-2022  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
require File.expand_path('../../../../../test_helper', __FILE__)
21

  
22
class Redmine::Acts::MentionableTest < ActiveSupport::TestCase
23
  fixtures :projects, :users, :email_addresses, :members, :member_roles, :roles,
24
         :groups_users,
25
         :trackers, :projects_trackers,
26
         :enabled_modules,
27
         :issue_statuses, :issue_categories, :issue_relations, :workflows,
28
         :enumerations,
29
         :issues
30

  
31
  def test_mentioned_users_with_user_mention
32
    issue = Issue.generate!(project_id: 1, description: '@dlopper')
33

  
34
    assert_equal [User.find(3)], issue.mentioned_users
35
  end
36

  
37
  def test_mentioned_users_with_multiple_mentions
38
    issue = Issue.generate!(project_id: 1, description: 'Hello @dlopper, @jsmith.')
39

  
40
    assert_equal [User.find(2), User.find(3)], issue.mentioned_users
41
  end
42

  
43
  def test_mentioned_users_should_not_mention_same_user_multiple_times
44
    issue = Issue.generate!(project_id: 1, description: '@dlopper @jsmith @dlopper')
45

  
46
    assert_equal [User.find(2), User.find(3)], issue.mentioned_users
47
  end
48

  
49
  def test_mentioned_users_should_include_only_active_users
50
    # disable dlopper account
51
    user = User.find(3)
52
    user.status = User::STATUS_LOCKED
53
    user.save
54

  
55
    issue = Issue.generate!(project_id: 1, description: '@dlopper @jsmith')
56

  
57
    assert_equal [User.find(2)], issue.mentioned_users
58
  end
59

  
60
  def test_mentioned_users_should_include_only_visible_users
61
    User.current = nil
62
    Role.non_member.update! users_visibility: 'members_of_visible_projects'
63
    Role.anonymous.update! users_visibility: 'members_of_visible_projects'
64
    user = User.generate!
65

  
66
    issue = Issue.generate!(project_id: 1, description: "@jsmith @#{user.login}")
67

  
68
    assert_equal [User.find(2)], issue.mentioned_users
69
  end
70

  
71
  def test_mentioned_users_should_not_include_mentioned_users_in_existing_content
72
    issue = Issue.generate!(project_id: 1, description: 'Hello @dlopper')
73

  
74
    assert issue.save
75
    assert_equal [User.find(3)], issue.mentioned_users
76

  
77
    issue.description = 'Hello @dlopper and @jsmith'
78
    issue.save
79

  
80
    assert_equal [User.find(2)], issue.mentioned_users
81
  end
82

  
83
  def test_mentioned_users_should_not_include_users_wrapped_in_pre_tags_for_textile
84
    description = <<~STR
85
      <pre>
86
      Hello @jsmith
87
      </pre>
88
    STR
89

  
90
    with_settings text_formatting: 'textile' do
91
      issue = Issue.generate!(project_id: 1, description: description)
92

  
93
      assert_equal [], issue.mentioned_users
94
    end
95
  end
96

  
97
  def test_mentioned_users_should_not_include_users_wrapped_in_pre_tags_for_markdown
98
    description = <<~STR
99
      ```
100
      Hello @jsmith
101
      ```
102
    STR
103

  
104
    with_settings text_formatting: 'markdown' do
105
      issue = Issue.generate!(project_id: 1, description: description)
106

  
107
      assert_equal [], issue.mentioned_users
108
    end
109
  end
110

  
111
  def test_mentioned_users_should_not_include_users_wrapped_in_pre_tags_for_common_mark
112
    description = <<~STR
113
      ```
114
      Hello @jsmith
115
      ```
116
    STR
117

  
118
    with_settings text_formatting: 'common_mark' do
119
      issue = Issue.generate!(project_id: 1, description: description)
120

  
121
      assert_equal [], issue.mentioned_users
122
    end
123
  end
124

  
125
  def test_notified_mentions
126
    issue = Issue.generate!(project_id: 1, description: 'Hello @dlopper, @jsmith.')
127

  
128
    assert_equal [User.find(2), User.find(3)], issue.notified_mentions
129
  end
130

  
131
  def test_notified_mentions_should_not_include_users_who_out_of_all_email
132
    User.find(3).update!(mail_notification: :none)
133
    issue = Issue.generate!(project_id: 1, description: "Hello @dlopper, @jsmith.")
134

  
135
    assert_equal [User.find(2)], issue.notified_mentions
136
  end
137

  
138
  def test_notified_mentions_should_not_include_users_who_cannot_view_the_object
139
    user = User.find(3)
140

  
141
    # User dlopper does not have access to project "Private child of eCookbook"
142
    issue = Issue.generate!(project_id: 5, description: "Hello @dlopper, @jsmith.")
143

  
144
    assert !issue.notified_mentions.include?(user)
145
  end
146
end
test/unit/mailer_test.rb
464 464
    assert_not_include user.mail, recipients
465 465
  end
466 466

  
467
  def test_issue_add_should_notify_mentioned_users_in_issue_description
468
    User.find(1).mail_notification = 'only_my_events'
469

  
470
    issue = Issue.generate!(project_id: 1, description: 'Hello @dlopper and @admin.')
471

  
472
    assert Mailer.deliver_issue_add(issue)
473
    # @jsmith and @dlopper are members of the project
474
    # admin is mentioned
475
    # @dlopper won't receive duplicated notifications
476
    assert_equal 3, ActionMailer::Base.deliveries.size
477
    assert_include User.find(1).mail, recipients
478
  end
479

  
467 480
  def test_issue_add_should_include_enabled_fields
468 481
    issue = Issue.find(2)
469 482
    assert Mailer.deliver_issue_add(issue)
......
608 621
    end
609 622
  end
610 623

  
624
  def test_issue_edit_should_notify_mentioned_users_in_issue_updated_description
625
    User.find(1).mail_notification = 'only_my_events'
626

  
627
    issue = Issue.find(3)
628
    issue.init_journal(User.current)
629
    issue.update(description: "Hello @admin")
630
    journal = issue.journals.last
631

  
632
    ActionMailer::Base.deliveries.clear
633
    Mailer.deliver_issue_edit(journal)
634

  
635
    # @jsmith and @dlopper are members of the project
636
    # admin is mentioned in the updated description
637
    # @dlopper won't receive duplicated notifications
638
    assert_equal 3, ActionMailer::Base.deliveries.size
639
    assert_include User.find(1).mail, recipients
640
  end
641

  
642
  def test_issue_edit_should_notify_mentioned_users_in_notes
643
    User.find(1).mail_notification = 'only_my_events'
644

  
645
    journal = Journal.generate!(journalized: Issue.find(3), user: User.find(1), notes: 'Hello @admin.')
646

  
647
    ActionMailer::Base.deliveries.clear
648
    Mailer.deliver_issue_edit(journal)
649

  
650
    # @jsmith and @dlopper are members of the project
651
    # admin is mentioned in the notes
652
    # @dlopper won't receive duplicated notifications
653
    assert_equal 3, ActionMailer::Base.deliveries.size
654
    assert_include User.find(1).mail, recipients
655
  end
656

  
611 657
  def test_issue_should_send_email_notification_with_suppress_empty_fields
612 658
    ActionMailer::Base.deliveries.clear
613 659
    with_settings :notified_events => %w(issue_added) do
......
703 749
    end
704 750
  end
705 751

  
752
  def test_wiki_content_added_should_notify_mentioned_users_in_content
753
    content = WikiContent.new(text: 'Hello @admin.', author_id: 1, page_id: 1)
754
    content.save!
755

  
756
    ActionMailer::Base.deliveries.clear
757
    Mailer.deliver_wiki_content_added(content)
758

  
759
    # @jsmith and @dlopper are members of the project
760
    # admin is mentioned in the notes
761
    # @dlopper won't receive duplicated notifications
762
    assert_equal 3, ActionMailer::Base.deliveries.size
763
    assert_include User.find(1).mail, recipients
764
  end
765

  
706 766
  def test_wiki_content_updated
707 767
    content = WikiContent.find(1)
708 768
    assert Mailer.deliver_wiki_content_updated(content)
......
713 773
    end
714 774
  end
715 775

  
776
  def test_wiki_content_updated_should_notify_mentioned_users_in_updated_content
777
    content = WikiContent.find(1)
778
    content.update(text: 'Hello @admin.')
779
    content.save!
780

  
781
    ActionMailer::Base.deliveries.clear
782
    Mailer.deliver_wiki_content_updated(content)
783

  
784
    # @jsmith and @dlopper are members of the project
785
    # admin is mentioned in the notes
786
    # @dlopper won't receive duplicated notifications
787
    assert_equal 3, ActionMailer::Base.deliveries.size
788
    assert_include User.find(1).mail, recipients
789
  end
790

  
716 791
  def test_register
717 792
    token = Token.find(1)
718 793
    assert Mailer.deliver_register(token.user, token)
(11-11/12)