Feature #13919 » 0001-Allow-users-to-be-mentioned-using-in-issues-and-wiki.patch
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) |