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) |