##// END OF EJS Templates
Enable the watching of news (#2549)....
Jean-Philippe Lang -
r12591:1ad33134d300
parent child
Show More
@@ -1,120 +1,126
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class WatchersController < ApplicationController
19 19 before_filter :require_login, :find_watchables, :only => [:watch, :unwatch]
20 20
21 21 def watch
22 22 set_watcher(@watchables, User.current, true)
23 23 end
24 24
25 25 def unwatch
26 26 set_watcher(@watchables, User.current, false)
27 27 end
28 28
29 29 before_filter :find_project, :authorize, :only => [:new, :create, :append, :destroy, :autocomplete_for_user]
30 30 accept_api_auth :create, :destroy
31 31
32 32 def new
33 33 @users = users_for_new_watcher
34 34 end
35 35
36 36 def create
37 37 user_ids = []
38 38 if params[:watcher].is_a?(Hash)
39 39 user_ids << (params[:watcher][:user_ids] || params[:watcher][:user_id])
40 40 else
41 41 user_ids << params[:user_id]
42 42 end
43 43 user_ids.flatten.compact.uniq.each do |user_id|
44 44 Watcher.create(:watchable => @watched, :user_id => user_id)
45 45 end
46 46 respond_to do |format|
47 47 format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
48 48 format.js { @users = users_for_new_watcher }
49 49 format.api { render_api_ok }
50 50 end
51 51 end
52 52
53 53 def append
54 54 if params[:watcher].is_a?(Hash)
55 55 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
56 56 @users = User.active.where(:id => user_ids).all
57 57 end
58 58 end
59 59
60 60 def destroy
61 61 @watched.set_watcher(User.find(params[:user_id]), false)
62 62 respond_to do |format|
63 63 format.html { redirect_to :back }
64 64 format.js
65 65 format.api { render_api_ok }
66 66 end
67 67 end
68 68
69 69 def autocomplete_for_user
70 70 @users = users_for_new_watcher
71 71 render :layout => false
72 72 end
73 73
74 74 private
75 75
76 76 def find_project
77 77 if params[:object_type] && params[:object_id]
78 78 klass = Object.const_get(params[:object_type].camelcase)
79 79 return false unless klass.respond_to?('watched_by')
80 80 @watched = klass.find(params[:object_id])
81 81 @project = @watched.project
82 82 elsif params[:project_id]
83 83 @project = Project.visible.find_by_param(params[:project_id])
84 84 end
85 85 rescue
86 86 render_404
87 87 end
88 88
89 89 def find_watchables
90 90 klass = Object.const_get(params[:object_type].camelcase) rescue nil
91 91 if klass && klass.respond_to?('watched_by')
92 92 @watchables = klass.where(:id => Array.wrap(params[:object_id])).all
93 raise Unauthorized if @watchables.any? {|w| w.respond_to?(:visible?) && !w.visible?}
93 raise Unauthorized if @watchables.any? {|w|
94 if w.respond_to?(:visible?)
95 !w.visible?
96 elsif w.respond_to?(:project) && w.project
97 !w.project.visible?
98 end
99 }
94 100 end
95 101 render_404 unless @watchables.present?
96 102 end
97 103
98 104 def set_watcher(watchables, user, watching)
99 105 watchables.each do |watchable|
100 106 watchable.set_watcher(user, watching)
101 107 end
102 108 respond_to do |format|
103 109 format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
104 110 format.js { render :partial => 'set_watcher', :locals => {:user => user, :watched => watchables} }
105 111 end
106 112 end
107 113
108 114 def users_for_new_watcher
109 115 users = []
110 116 if params[:q].blank? && @project.present?
111 117 users = @project.users.sorted
112 118 else
113 119 users = User.active.sorted.like(params[:q]).limit(100)
114 120 end
115 121 if @watched
116 122 users -= @watched.watcher_users
117 123 end
118 124 users
119 125 end
120 126 end
@@ -1,38 +1,39
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class EnabledModule < ActiveRecord::Base
19 19 belongs_to :project
20 acts_as_watchable
20 21
21 22 validates_presence_of :name
22 23 validates_uniqueness_of :name, :scope => :project_id
23 24
24 25 after_create :module_enabled
25 26
26 27 private
27 28
28 29 # after_create callback used to do things when a module is enabled
29 30 def module_enabled
30 31 case name
31 32 when 'wiki'
32 33 # Create a wiki with a default start page
33 34 if project && project.wiki.nil?
34 35 Wiki.create(:project => project, :start_page => 'Wiki')
35 36 end
36 37 end
37 38 end
38 39 end
@@ -1,494 +1,495
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Mailer < ActionMailer::Base
19 19 layout 'mailer'
20 20 helper :application
21 21 helper :issues
22 22 helper :custom_fields
23 23
24 24 include Redmine::I18n
25 25
26 26 def self.default_url_options
27 27 { :host => Setting.host_name, :protocol => Setting.protocol }
28 28 end
29 29
30 30 # Builds a mail for notifying to_users and cc_users about a new issue
31 31 def issue_add(issue, to_users, cc_users)
32 32 redmine_headers 'Project' => issue.project.identifier,
33 33 'Issue-Id' => issue.id,
34 34 'Issue-Author' => issue.author.login
35 35 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
36 36 message_id issue
37 37 references issue
38 38 @author = issue.author
39 39 @issue = issue
40 40 @users = to_users + cc_users
41 41 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
42 42 mail :to => to_users.map(&:mail),
43 43 :cc => cc_users.map(&:mail),
44 44 :subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
45 45 end
46 46
47 47 # Notifies users about a new issue
48 48 def self.deliver_issue_add(issue)
49 49 to = issue.notified_users
50 50 cc = issue.notified_watchers - to
51 51 issue.each_notification(to + cc) do |users|
52 52 Mailer.issue_add(issue, to & users, cc & users).deliver
53 53 end
54 54 end
55 55
56 56 # Builds a mail for notifying to_users and cc_users about an issue update
57 57 def issue_edit(journal, to_users, cc_users)
58 58 issue = journal.journalized
59 59 redmine_headers 'Project' => issue.project.identifier,
60 60 'Issue-Id' => issue.id,
61 61 'Issue-Author' => issue.author.login
62 62 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
63 63 message_id journal
64 64 references issue
65 65 @author = journal.user
66 66 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
67 67 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
68 68 s << issue.subject
69 69 @issue = issue
70 70 @users = to_users + cc_users
71 71 @journal = journal
72 72 @journal_details = journal.visible_details(@users.first)
73 73 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
74 74 mail :to => to_users.map(&:mail),
75 75 :cc => cc_users.map(&:mail),
76 76 :subject => s
77 77 end
78 78
79 79 # Notifies users about an issue update
80 80 def self.deliver_issue_edit(journal)
81 81 issue = journal.journalized.reload
82 82 to = journal.notified_users
83 83 cc = journal.notified_watchers
84 84 journal.each_notification(to + cc) do |users|
85 85 issue.each_notification(users) do |users2|
86 86 Mailer.issue_edit(journal, to & users2, cc & users2).deliver
87 87 end
88 88 end
89 89 end
90 90
91 91 def reminder(user, issues, days)
92 92 set_language_if_valid user.language
93 93 @issues = issues
94 94 @days = days
95 95 @issues_url = url_for(:controller => 'issues', :action => 'index',
96 96 :set_filter => 1, :assigned_to_id => user.id,
97 97 :sort => 'due_date:asc')
98 98 mail :to => user.mail,
99 99 :subject => l(:mail_subject_reminder, :count => issues.size, :days => days)
100 100 end
101 101
102 102 # Builds a Mail::Message object used to email users belonging to the added document's project.
103 103 #
104 104 # Example:
105 105 # document_added(document) => Mail::Message object
106 106 # Mailer.document_added(document).deliver => sends an email to the document's project recipients
107 107 def document_added(document)
108 108 redmine_headers 'Project' => document.project.identifier
109 109 @author = User.current
110 110 @document = document
111 111 @document_url = url_for(:controller => 'documents', :action => 'show', :id => document)
112 112 mail :to => document.recipients,
113 113 :subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
114 114 end
115 115
116 116 # Builds a Mail::Message object used to email recipients of a project when an attachements are added.
117 117 #
118 118 # Example:
119 119 # attachments_added(attachments) => Mail::Message object
120 120 # Mailer.attachments_added(attachments).deliver => sends an email to the project's recipients
121 121 def attachments_added(attachments)
122 122 container = attachments.first.container
123 123 added_to = ''
124 124 added_to_url = ''
125 125 @author = attachments.first.author
126 126 case container.class.name
127 127 when 'Project'
128 128 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
129 129 added_to = "#{l(:label_project)}: #{container}"
130 130 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
131 131 when 'Version'
132 132 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
133 133 added_to = "#{l(:label_version)}: #{container.name}"
134 134 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
135 135 when 'Document'
136 136 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
137 137 added_to = "#{l(:label_document)}: #{container.title}"
138 138 recipients = container.recipients
139 139 end
140 140 redmine_headers 'Project' => container.project.identifier
141 141 @attachments = attachments
142 142 @added_to = added_to
143 143 @added_to_url = added_to_url
144 144 mail :to => recipients,
145 145 :subject => "[#{container.project.name}] #{l(:label_attachment_new)}"
146 146 end
147 147
148 148 # Builds a Mail::Message object used to email recipients of a news' project when a news item is added.
149 149 #
150 150 # Example:
151 151 # news_added(news) => Mail::Message object
152 152 # Mailer.news_added(news).deliver => sends an email to the news' project recipients
153 153 def news_added(news)
154 154 redmine_headers 'Project' => news.project.identifier
155 155 @author = news.author
156 156 message_id news
157 157 references news
158 158 @news = news
159 159 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
160 160 mail :to => news.recipients,
161 :cc => news.cc_for_added_news,
161 162 :subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
162 163 end
163 164
164 165 # Builds a Mail::Message object used to email recipients of a news' project when a news comment is added.
165 166 #
166 167 # Example:
167 168 # news_comment_added(comment) => Mail::Message object
168 169 # Mailer.news_comment_added(comment) => sends an email to the news' project recipients
169 170 def news_comment_added(comment)
170 171 news = comment.commented
171 172 redmine_headers 'Project' => news.project.identifier
172 173 @author = comment.author
173 174 message_id comment
174 175 references news
175 176 @news = news
176 177 @comment = comment
177 178 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
178 179 mail :to => news.recipients,
179 180 :cc => news.watcher_recipients,
180 181 :subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
181 182 end
182 183
183 184 # Builds a Mail::Message object used to email the recipients of the specified message that was posted.
184 185 #
185 186 # Example:
186 187 # message_posted(message) => Mail::Message object
187 188 # Mailer.message_posted(message).deliver => sends an email to the recipients
188 189 def message_posted(message)
189 190 redmine_headers 'Project' => message.project.identifier,
190 191 'Topic-Id' => (message.parent_id || message.id)
191 192 @author = message.author
192 193 message_id message
193 194 references message.root
194 195 recipients = message.recipients
195 196 cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
196 197 @message = message
197 198 @message_url = url_for(message.event_url)
198 199 mail :to => recipients,
199 200 :cc => cc,
200 201 :subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
201 202 end
202 203
203 204 # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was added.
204 205 #
205 206 # Example:
206 207 # wiki_content_added(wiki_content) => Mail::Message object
207 208 # Mailer.wiki_content_added(wiki_content).deliver => sends an email to the project's recipients
208 209 def wiki_content_added(wiki_content)
209 210 redmine_headers 'Project' => wiki_content.project.identifier,
210 211 'Wiki-Page-Id' => wiki_content.page.id
211 212 @author = wiki_content.author
212 213 message_id wiki_content
213 214 recipients = wiki_content.recipients
214 215 cc = wiki_content.page.wiki.watcher_recipients - recipients
215 216 @wiki_content = wiki_content
216 217 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
217 218 :project_id => wiki_content.project,
218 219 :id => wiki_content.page.title)
219 220 mail :to => recipients,
220 221 :cc => cc,
221 222 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
222 223 end
223 224
224 225 # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was updated.
225 226 #
226 227 # Example:
227 228 # wiki_content_updated(wiki_content) => Mail::Message object
228 229 # Mailer.wiki_content_updated(wiki_content).deliver => sends an email to the project's recipients
229 230 def wiki_content_updated(wiki_content)
230 231 redmine_headers 'Project' => wiki_content.project.identifier,
231 232 'Wiki-Page-Id' => wiki_content.page.id
232 233 @author = wiki_content.author
233 234 message_id wiki_content
234 235 recipients = wiki_content.recipients
235 236 cc = wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients
236 237 @wiki_content = wiki_content
237 238 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
238 239 :project_id => wiki_content.project,
239 240 :id => wiki_content.page.title)
240 241 @wiki_diff_url = url_for(:controller => 'wiki', :action => 'diff',
241 242 :project_id => wiki_content.project, :id => wiki_content.page.title,
242 243 :version => wiki_content.version)
243 244 mail :to => recipients,
244 245 :cc => cc,
245 246 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
246 247 end
247 248
248 249 # Builds a Mail::Message object used to email the specified user their account information.
249 250 #
250 251 # Example:
251 252 # account_information(user, password) => Mail::Message object
252 253 # Mailer.account_information(user, password).deliver => sends account information to the user
253 254 def account_information(user, password)
254 255 set_language_if_valid user.language
255 256 @user = user
256 257 @password = password
257 258 @login_url = url_for(:controller => 'account', :action => 'login')
258 259 mail :to => user.mail,
259 260 :subject => l(:mail_subject_register, Setting.app_title)
260 261 end
261 262
262 263 # Builds a Mail::Message object used to email all active administrators of an account activation request.
263 264 #
264 265 # Example:
265 266 # account_activation_request(user) => Mail::Message object
266 267 # Mailer.account_activation_request(user).deliver => sends an email to all active administrators
267 268 def account_activation_request(user)
268 269 # Send the email to all active administrators
269 270 recipients = User.active.where(:admin => true).collect { |u| u.mail }.compact
270 271 @user = user
271 272 @url = url_for(:controller => 'users', :action => 'index',
272 273 :status => User::STATUS_REGISTERED,
273 274 :sort_key => 'created_on', :sort_order => 'desc')
274 275 mail :to => recipients,
275 276 :subject => l(:mail_subject_account_activation_request, Setting.app_title)
276 277 end
277 278
278 279 # Builds a Mail::Message object used to email the specified user that their account was activated by an administrator.
279 280 #
280 281 # Example:
281 282 # account_activated(user) => Mail::Message object
282 283 # Mailer.account_activated(user).deliver => sends an email to the registered user
283 284 def account_activated(user)
284 285 set_language_if_valid user.language
285 286 @user = user
286 287 @login_url = url_for(:controller => 'account', :action => 'login')
287 288 mail :to => user.mail,
288 289 :subject => l(:mail_subject_register, Setting.app_title)
289 290 end
290 291
291 292 def lost_password(token)
292 293 set_language_if_valid(token.user.language)
293 294 @token = token
294 295 @url = url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
295 296 mail :to => token.user.mail,
296 297 :subject => l(:mail_subject_lost_password, Setting.app_title)
297 298 end
298 299
299 300 def register(token)
300 301 set_language_if_valid(token.user.language)
301 302 @token = token
302 303 @url = url_for(:controller => 'account', :action => 'activate', :token => token.value)
303 304 mail :to => token.user.mail,
304 305 :subject => l(:mail_subject_register, Setting.app_title)
305 306 end
306 307
307 308 def test_email(user)
308 309 set_language_if_valid(user.language)
309 310 @url = url_for(:controller => 'welcome')
310 311 mail :to => user.mail,
311 312 :subject => 'Redmine test'
312 313 end
313 314
314 315 # Sends reminders to issue assignees
315 316 # Available options:
316 317 # * :days => how many days in the future to remind about (defaults to 7)
317 318 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
318 319 # * :project => id or identifier of project to process (defaults to all projects)
319 320 # * :users => array of user/group ids who should be reminded
320 321 def self.reminders(options={})
321 322 days = options[:days] || 7
322 323 project = options[:project] ? Project.find(options[:project]) : nil
323 324 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
324 325 user_ids = options[:users]
325 326
326 327 scope = Issue.open.where("#{Issue.table_name}.assigned_to_id IS NOT NULL" +
327 328 " AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +
328 329 " AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date
329 330 )
330 331 scope = scope.where(:assigned_to_id => user_ids) if user_ids.present?
331 332 scope = scope.where(:project_id => project.id) if project
332 333 scope = scope.where(:tracker_id => tracker.id) if tracker
333 334 issues_by_assignee = scope.includes(:status, :assigned_to, :project, :tracker).
334 335 group_by(&:assigned_to)
335 336 issues_by_assignee.keys.each do |assignee|
336 337 if assignee.is_a?(Group)
337 338 assignee.users.each do |user|
338 339 issues_by_assignee[user] ||= []
339 340 issues_by_assignee[user] += issues_by_assignee[assignee]
340 341 end
341 342 end
342 343 end
343 344
344 345 issues_by_assignee.each do |assignee, issues|
345 346 reminder(assignee, issues, days).deliver if assignee.is_a?(User) && assignee.active?
346 347 end
347 348 end
348 349
349 350 # Activates/desactivates email deliveries during +block+
350 351 def self.with_deliveries(enabled = true, &block)
351 352 was_enabled = ActionMailer::Base.perform_deliveries
352 353 ActionMailer::Base.perform_deliveries = !!enabled
353 354 yield
354 355 ensure
355 356 ActionMailer::Base.perform_deliveries = was_enabled
356 357 end
357 358
358 359 # Sends emails synchronously in the given block
359 360 def self.with_synched_deliveries(&block)
360 361 saved_method = ActionMailer::Base.delivery_method
361 362 if m = saved_method.to_s.match(%r{^async_(.+)$})
362 363 synched_method = m[1]
363 364 ActionMailer::Base.delivery_method = synched_method.to_sym
364 365 ActionMailer::Base.send "#{synched_method}_settings=", ActionMailer::Base.send("async_#{synched_method}_settings")
365 366 end
366 367 yield
367 368 ensure
368 369 ActionMailer::Base.delivery_method = saved_method
369 370 end
370 371
371 372 def mail(headers={}, &block)
372 373 headers.merge! 'X-Mailer' => 'Redmine',
373 374 'X-Redmine-Host' => Setting.host_name,
374 375 'X-Redmine-Site' => Setting.app_title,
375 376 'X-Auto-Response-Suppress' => 'OOF',
376 377 'Auto-Submitted' => 'auto-generated',
377 378 'From' => Setting.mail_from,
378 379 'List-Id' => "<#{Setting.mail_from.to_s.gsub('@', '.')}>"
379 380
380 381 # Removes the author from the recipients and cc
381 382 # if the author does not want to receive notifications
382 383 # about what the author do
383 384 if @author && @author.logged? && @author.pref.no_self_notified
384 385 headers[:to].delete(@author.mail) if headers[:to].is_a?(Array)
385 386 headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array)
386 387 end
387 388
388 389 if @author && @author.logged?
389 390 redmine_headers 'Sender' => @author.login
390 391 end
391 392
392 393 # Blind carbon copy recipients
393 394 if Setting.bcc_recipients?
394 395 headers[:bcc] = [headers[:to], headers[:cc]].flatten.uniq.reject(&:blank?)
395 396 headers[:to] = nil
396 397 headers[:cc] = nil
397 398 end
398 399
399 400 if @message_id_object
400 401 headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
401 402 end
402 403 if @references_objects
403 404 headers[:references] = @references_objects.collect {|o| "<#{self.class.references_for(o)}>"}.join(' ')
404 405 end
405 406
406 407 m = if block_given?
407 408 super headers, &block
408 409 else
409 410 super headers do |format|
410 411 format.text
411 412 format.html unless Setting.plain_text_mail?
412 413 end
413 414 end
414 415 set_language_if_valid @initial_language
415 416
416 417 m
417 418 end
418 419
419 420 def initialize(*args)
420 421 @initial_language = current_language
421 422 set_language_if_valid Setting.default_language
422 423 super
423 424 end
424 425
425 426 def self.deliver_mail(mail)
426 427 return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank?
427 428 begin
428 429 # Log errors when raise_delivery_errors is set to false, Rails does not
429 430 mail.raise_delivery_errors = true
430 431 super
431 432 rescue Exception => e
432 433 if ActionMailer::Base.raise_delivery_errors
433 434 raise e
434 435 else
435 436 Rails.logger.error "Email delivery error: #{e.message}"
436 437 end
437 438 end
438 439 end
439 440
440 441 def self.method_missing(method, *args, &block)
441 442 if m = method.to_s.match(%r{^deliver_(.+)$})
442 443 ActiveSupport::Deprecation.warn "Mailer.deliver_#{m[1]}(*args) is deprecated. Use Mailer.#{m[1]}(*args).deliver instead."
443 444 send(m[1], *args).deliver
444 445 else
445 446 super
446 447 end
447 448 end
448 449
449 450 private
450 451
451 452 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
452 453 def redmine_headers(h)
453 454 h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
454 455 end
455 456
456 457 def self.token_for(object, rand=true)
457 458 timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
458 459 hash = [
459 460 "redmine",
460 461 "#{object.class.name.demodulize.underscore}-#{object.id}",
461 462 timestamp.strftime("%Y%m%d%H%M%S")
462 463 ]
463 464 if rand
464 465 hash << Redmine::Utils.random_hex(8)
465 466 end
466 467 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
467 468 host = "#{::Socket.gethostname}.redmine" if host.empty?
468 469 "#{hash.join('.')}@#{host}"
469 470 end
470 471
471 472 # Returns a Message-Id for the given object
472 473 def self.message_id_for(object)
473 474 token_for(object, true)
474 475 end
475 476
476 477 # Returns a uniq token for a given object referenced by all notifications
477 478 # related to this object
478 479 def self.references_for(object)
479 480 token_for(object, false)
480 481 end
481 482
482 483 def message_id(object)
483 484 @message_id_object = object
484 485 end
485 486
486 487 def references(object)
487 488 @references_objects ||= []
488 489 @references_objects << object
489 490 end
490 491
491 492 def mylogger
492 493 Rails.logger
493 494 end
494 495 end
@@ -1,73 +1,85
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class News < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :project
21 21 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 22 has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on"
23 23
24 24 validates_presence_of :title, :description
25 25 validates_length_of :title, :maximum => 60
26 26 validates_length_of :summary, :maximum => 255
27 27
28 28 acts_as_attachable :delete_permission => :manage_news
29 29 acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project
30 30 acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
31 31 acts_as_activity_provider :find_options => {:include => [:project, :author]},
32 32 :author_key => :author_id
33 33 acts_as_watchable
34 34
35 35 after_create :add_author_as_watcher
36 36 after_create :send_notification
37 37
38 38 scope :visible, lambda {|*args|
39 39 includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_news, *args))
40 40 }
41 41
42 42 safe_attributes 'title', 'summary', 'description'
43 43
44 44 def visible?(user=User.current)
45 45 !user.nil? && user.allowed_to?(:view_news, project)
46 46 end
47 47
48 48 # Returns true if the news can be commented by user
49 49 def commentable?(user=User.current)
50 50 user.allowed_to?(:comment_news, project)
51 51 end
52 52
53 53 def recipients
54 54 project.users.select {|user| user.notify_about?(self)}.map(&:mail)
55 55 end
56 56
57 # Returns the email addresses that should be cc'd when a new news is added
58 def cc_for_added_news
59 cc = []
60 if m = project.enabled_module('news')
61 cc = m.notified_watchers
62 unless project.is_public?
63 cc = cc.select {|user| project.users.include?(user)}
64 end
65 end
66 cc.map(&:mail)
67 end
68
57 69 # returns latest news for projects visible by user
58 70 def self.latest(user = User.current, count = 5)
59 71 visible(user).includes([:author, :project]).order("#{News.table_name}.created_on DESC").limit(count).all
60 72 end
61 73
62 74 private
63 75
64 76 def add_author_as_watcher
65 77 Watcher.create(:watchable => self, :user => author)
66 78 end
67 79
68 80 def send_notification
69 81 if Setting.notified_events.include?('news_added')
70 82 Mailer.news_added(self).deliver
71 83 end
72 84 end
73 85 end
@@ -1,1046 +1,1053
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 # Project statuses
22 22 STATUS_ACTIVE = 1
23 23 STATUS_CLOSED = 5
24 24 STATUS_ARCHIVED = 9
25 25
26 26 # Maximum length for project identifiers
27 27 IDENTIFIER_MAX_LENGTH = 100
28 28
29 29 # Specific overidden Activities
30 30 has_many :time_entry_activities
31 31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 32 has_many :memberships, :class_name => 'Member'
33 33 has_many :member_principals, :class_name => 'Member',
34 34 :include => :principal,
35 35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
36 36
37 37 has_many :enabled_modules, :dependent => :delete_all
38 38 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 39 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
40 40 has_many :issue_changes, :through => :issues, :source => :journals
41 41 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 42 has_many :time_entries, :dependent => :destroy
43 43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
44 44 has_many :documents, :dependent => :destroy
45 45 has_many :news, :dependent => :destroy, :include => :author
46 46 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 47 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 48 has_one :repository, :conditions => ["is_default = ?", true]
49 49 has_many :repositories, :dependent => :destroy
50 50 has_many :changesets, :through => :repository
51 51 has_one :wiki, :dependent => :destroy
52 52 # Custom field for the project issues
53 53 has_and_belongs_to_many :issue_custom_fields,
54 54 :class_name => 'IssueCustomField',
55 55 :order => "#{CustomField.table_name}.position",
56 56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 57 :association_foreign_key => 'custom_field_id'
58 58
59 59 acts_as_nested_set :dependent => :destroy
60 60 acts_as_attachable :view_permission => :view_files,
61 61 :delete_permission => :manage_files
62 62
63 63 acts_as_customizable
64 64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 67 :author => nil
68 68
69 69 attr_protected :status
70 70
71 71 validates_presence_of :name, :identifier
72 72 validates_uniqueness_of :identifier
73 73 validates_associated :repository, :wiki
74 74 validates_length_of :name, :maximum => 255
75 75 validates_length_of :homepage, :maximum => 255
76 76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 77 # donwcase letters, digits, dashes but not digits only
78 78 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
79 79 # reserved words
80 80 validates_exclusion_of :identifier, :in => %w( new )
81 81
82 82 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
83 83 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
84 84 before_destroy :delete_all_members
85 85
86 86 scope :has_module, lambda {|mod|
87 87 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
88 88 }
89 89 scope :active, lambda { where(:status => STATUS_ACTIVE) }
90 90 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
91 91 scope :all_public, lambda { where(:is_public => true) }
92 92 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
93 93 scope :allowed_to, lambda {|*args|
94 94 user = User.current
95 95 permission = nil
96 96 if args.first.is_a?(Symbol)
97 97 permission = args.shift
98 98 else
99 99 user = args.shift
100 100 permission = args.shift
101 101 end
102 102 where(Project.allowed_to_condition(user, permission, *args))
103 103 }
104 104 scope :like, lambda {|arg|
105 105 if arg.blank?
106 106 where(nil)
107 107 else
108 108 pattern = "%#{arg.to_s.strip.downcase}%"
109 109 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
110 110 end
111 111 }
112 112
113 113 def initialize(attributes=nil, *args)
114 114 super
115 115
116 116 initialized = (attributes || {}).stringify_keys
117 117 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
118 118 self.identifier = Project.next_identifier
119 119 end
120 120 if !initialized.key?('is_public')
121 121 self.is_public = Setting.default_projects_public?
122 122 end
123 123 if !initialized.key?('enabled_module_names')
124 124 self.enabled_module_names = Setting.default_projects_modules
125 125 end
126 126 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
127 127 default = Setting.default_projects_tracker_ids
128 128 if default.is_a?(Array)
129 129 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
130 130 else
131 131 self.trackers = Tracker.sorted.all
132 132 end
133 133 end
134 134 end
135 135
136 136 def identifier=(identifier)
137 137 super unless identifier_frozen?
138 138 end
139 139
140 140 def identifier_frozen?
141 141 errors[:identifier].blank? && !(new_record? || identifier.blank?)
142 142 end
143 143
144 144 # returns latest created projects
145 145 # non public projects will be returned only if user is a member of those
146 146 def self.latest(user=nil, count=5)
147 147 visible(user).limit(count).order("created_on DESC").all
148 148 end
149 149
150 150 # Returns true if the project is visible to +user+ or to the current user.
151 151 def visible?(user=User.current)
152 152 user.allowed_to?(:view_project, self)
153 153 end
154 154
155 155 # Returns a SQL conditions string used to find all projects visible by the specified user.
156 156 #
157 157 # Examples:
158 158 # Project.visible_condition(admin) => "projects.status = 1"
159 159 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
160 160 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
161 161 def self.visible_condition(user, options={})
162 162 allowed_to_condition(user, :view_project, options)
163 163 end
164 164
165 165 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
166 166 #
167 167 # Valid options:
168 168 # * :project => limit the condition to project
169 169 # * :with_subprojects => limit the condition to project and its subprojects
170 170 # * :member => limit the condition to the user projects
171 171 def self.allowed_to_condition(user, permission, options={})
172 172 perm = Redmine::AccessControl.permission(permission)
173 173 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
174 174 if perm && perm.project_module
175 175 # If the permission belongs to a project module, make sure the module is enabled
176 176 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
177 177 end
178 178 if options[:project]
179 179 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
180 180 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
181 181 base_statement = "(#{project_statement}) AND (#{base_statement})"
182 182 end
183 183
184 184 if user.admin?
185 185 base_statement
186 186 else
187 187 statement_by_role = {}
188 188 unless options[:member]
189 189 role = user.builtin_role
190 190 if role.allowed_to?(permission)
191 191 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
192 192 end
193 193 end
194 194 if user.logged?
195 195 user.projects_by_role.each do |role, projects|
196 196 if role.allowed_to?(permission) && projects.any?
197 197 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
198 198 end
199 199 end
200 200 end
201 201 if statement_by_role.empty?
202 202 "1=0"
203 203 else
204 204 if block_given?
205 205 statement_by_role.each do |role, statement|
206 206 if s = yield(role, user)
207 207 statement_by_role[role] = "(#{statement} AND (#{s}))"
208 208 end
209 209 end
210 210 end
211 211 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
212 212 end
213 213 end
214 214 end
215 215
216 216 def principals
217 217 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
218 218 end
219 219
220 220 def users
221 221 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
222 222 end
223 223
224 224 # Returns the Systemwide and project specific activities
225 225 def activities(include_inactive=false)
226 226 if include_inactive
227 227 return all_activities
228 228 else
229 229 return active_activities
230 230 end
231 231 end
232 232
233 233 # Will create a new Project specific Activity or update an existing one
234 234 #
235 235 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
236 236 # does not successfully save.
237 237 def update_or_create_time_entry_activity(id, activity_hash)
238 238 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
239 239 self.create_time_entry_activity_if_needed(activity_hash)
240 240 else
241 241 activity = project.time_entry_activities.find_by_id(id.to_i)
242 242 activity.update_attributes(activity_hash) if activity
243 243 end
244 244 end
245 245
246 246 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
247 247 #
248 248 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
249 249 # does not successfully save.
250 250 def create_time_entry_activity_if_needed(activity)
251 251 if activity['parent_id']
252 252 parent_activity = TimeEntryActivity.find(activity['parent_id'])
253 253 activity['name'] = parent_activity.name
254 254 activity['position'] = parent_activity.position
255 255 if Enumeration.overridding_change?(activity, parent_activity)
256 256 project_activity = self.time_entry_activities.create(activity)
257 257 if project_activity.new_record?
258 258 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
259 259 else
260 260 self.time_entries.
261 261 where(["activity_id = ?", parent_activity.id]).
262 262 update_all("activity_id = #{project_activity.id}")
263 263 end
264 264 end
265 265 end
266 266 end
267 267
268 268 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
269 269 #
270 270 # Examples:
271 271 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
272 272 # project.project_condition(false) => "projects.id = 1"
273 273 def project_condition(with_subprojects)
274 274 cond = "#{Project.table_name}.id = #{id}"
275 275 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
276 276 cond
277 277 end
278 278
279 279 def self.find(*args)
280 280 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
281 281 project = find_by_identifier(*args)
282 282 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
283 283 project
284 284 else
285 285 super
286 286 end
287 287 end
288 288
289 289 def self.find_by_param(*args)
290 290 self.find(*args)
291 291 end
292 292
293 293 alias :base_reload :reload
294 294 def reload(*args)
295 295 @principals = nil
296 296 @users = nil
297 297 @shared_versions = nil
298 298 @rolled_up_versions = nil
299 299 @rolled_up_trackers = nil
300 300 @all_issue_custom_fields = nil
301 301 @all_time_entry_custom_fields = nil
302 302 @to_param = nil
303 303 @allowed_parents = nil
304 304 @allowed_permissions = nil
305 305 @actions_allowed = nil
306 306 @start_date = nil
307 307 @due_date = nil
308 308 base_reload(*args)
309 309 end
310 310
311 311 def to_param
312 312 # id is used for projects with a numeric identifier (compatibility)
313 313 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
314 314 end
315 315
316 316 def active?
317 317 self.status == STATUS_ACTIVE
318 318 end
319 319
320 320 def archived?
321 321 self.status == STATUS_ARCHIVED
322 322 end
323 323
324 324 # Archives the project and its descendants
325 325 def archive
326 326 # Check that there is no issue of a non descendant project that is assigned
327 327 # to one of the project or descendant versions
328 328 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
329 329 if v_ids.any? &&
330 330 Issue.
331 331 includes(:project).
332 332 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
333 333 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
334 334 exists?
335 335 return false
336 336 end
337 337 Project.transaction do
338 338 archive!
339 339 end
340 340 true
341 341 end
342 342
343 343 # Unarchives the project
344 344 # All its ancestors must be active
345 345 def unarchive
346 346 return false if ancestors.detect {|a| !a.active?}
347 347 update_attribute :status, STATUS_ACTIVE
348 348 end
349 349
350 350 def close
351 351 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
352 352 end
353 353
354 354 def reopen
355 355 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
356 356 end
357 357
358 358 # Returns an array of projects the project can be moved to
359 359 # by the current user
360 360 def allowed_parents
361 361 return @allowed_parents if @allowed_parents
362 362 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
363 363 @allowed_parents = @allowed_parents - self_and_descendants
364 364 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
365 365 @allowed_parents << nil
366 366 end
367 367 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
368 368 @allowed_parents << parent
369 369 end
370 370 @allowed_parents
371 371 end
372 372
373 373 # Sets the parent of the project with authorization check
374 374 def set_allowed_parent!(p)
375 375 unless p.nil? || p.is_a?(Project)
376 376 if p.to_s.blank?
377 377 p = nil
378 378 else
379 379 p = Project.find_by_id(p)
380 380 return false unless p
381 381 end
382 382 end
383 383 if p.nil?
384 384 if !new_record? && allowed_parents.empty?
385 385 return false
386 386 end
387 387 elsif !allowed_parents.include?(p)
388 388 return false
389 389 end
390 390 set_parent!(p)
391 391 end
392 392
393 393 # Sets the parent of the project
394 394 # Argument can be either a Project, a String, a Fixnum or nil
395 395 def set_parent!(p)
396 396 unless p.nil? || p.is_a?(Project)
397 397 if p.to_s.blank?
398 398 p = nil
399 399 else
400 400 p = Project.find_by_id(p)
401 401 return false unless p
402 402 end
403 403 end
404 404 if p == parent && !p.nil?
405 405 # Nothing to do
406 406 true
407 407 elsif p.nil? || (p.active? && move_possible?(p))
408 408 set_or_update_position_under(p)
409 409 Issue.update_versions_from_hierarchy_change(self)
410 410 true
411 411 else
412 412 # Can not move to the given target
413 413 false
414 414 end
415 415 end
416 416
417 417 # Recalculates all lft and rgt values based on project names
418 418 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
419 419 # Used in BuildProjectsTree migration
420 420 def self.rebuild_tree!
421 421 transaction do
422 422 update_all "lft = NULL, rgt = NULL"
423 423 rebuild!(false)
424 424 all.each { |p| p.set_or_update_position_under(p.parent) }
425 425 end
426 426 end
427 427
428 428 # Returns an array of the trackers used by the project and its active sub projects
429 429 def rolled_up_trackers
430 430 @rolled_up_trackers ||=
431 431 Tracker.
432 432 joins(:projects).
433 433 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
434 434 select("DISTINCT #{Tracker.table_name}.*").
435 435 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
436 436 sorted.
437 437 all
438 438 end
439 439
440 440 # Closes open and locked project versions that are completed
441 441 def close_completed_versions
442 442 Version.transaction do
443 443 versions.where(:status => %w(open locked)).each do |version|
444 444 if version.completed?
445 445 version.update_attribute(:status, 'closed')
446 446 end
447 447 end
448 448 end
449 449 end
450 450
451 451 # Returns a scope of the Versions on subprojects
452 452 def rolled_up_versions
453 453 @rolled_up_versions ||=
454 454 Version.
455 455 includes(:project).
456 456 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
457 457 end
458 458
459 459 # Returns a scope of the Versions used by the project
460 460 def shared_versions
461 461 if new_record?
462 462 Version.
463 463 includes(:project).
464 464 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
465 465 else
466 466 @shared_versions ||= begin
467 467 r = root? ? self : root
468 468 Version.
469 469 includes(:project).
470 470 where("#{Project.table_name}.id = #{id}" +
471 471 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
472 472 " #{Version.table_name}.sharing = 'system'" +
473 473 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
474 474 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
475 475 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
476 476 "))")
477 477 end
478 478 end
479 479 end
480 480
481 481 # Returns a hash of project users grouped by role
482 482 def users_by_role
483 483 members.includes(:user, :roles).inject({}) do |h, m|
484 484 m.roles.each do |r|
485 485 h[r] ||= []
486 486 h[r] << m.user
487 487 end
488 488 h
489 489 end
490 490 end
491 491
492 492 # Deletes all project's members
493 493 def delete_all_members
494 494 me, mr = Member.table_name, MemberRole.table_name
495 495 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
496 496 Member.delete_all(['project_id = ?', id])
497 497 end
498 498
499 499 # Users/groups issues can be assigned to
500 500 def assignable_users
501 501 assignable = Setting.issue_group_assignment? ? member_principals : members
502 502 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
503 503 end
504 504
505 505 # Returns the mail adresses of users that should be always notified on project events
506 506 def recipients
507 507 notified_users.collect {|user| user.mail}
508 508 end
509 509
510 510 # Returns the users that should be notified on project events
511 511 def notified_users
512 512 # TODO: User part should be extracted to User#notify_about?
513 513 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
514 514 end
515 515
516 516 # Returns a scope of all custom fields enabled for project issues
517 517 # (explictly associated custom fields and custom fields enabled for all projects)
518 518 def all_issue_custom_fields
519 519 @all_issue_custom_fields ||= IssueCustomField.
520 520 sorted.
521 521 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
522 522 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
523 523 " WHERE cfp.project_id = ?)", true, id)
524 524 end
525 525
526 526 # Returns an array of all custom fields enabled for project time entries
527 527 # (explictly associated custom fields and custom fields enabled for all projects)
528 528 def all_time_entry_custom_fields
529 529 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
530 530 end
531 531
532 532 def project
533 533 self
534 534 end
535 535
536 536 def <=>(project)
537 537 name.downcase <=> project.name.downcase
538 538 end
539 539
540 540 def to_s
541 541 name
542 542 end
543 543
544 544 # Returns a short description of the projects (first lines)
545 545 def short_description(length = 255)
546 546 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
547 547 end
548 548
549 549 def css_classes
550 550 s = 'project'
551 551 s << ' root' if root?
552 552 s << ' child' if child?
553 553 s << (leaf? ? ' leaf' : ' parent')
554 554 unless active?
555 555 if archived?
556 556 s << ' archived'
557 557 else
558 558 s << ' closed'
559 559 end
560 560 end
561 561 s
562 562 end
563 563
564 564 # The earliest start date of a project, based on it's issues and versions
565 565 def start_date
566 566 @start_date ||= [
567 567 issues.minimum('start_date'),
568 568 shared_versions.minimum('effective_date'),
569 569 Issue.fixed_version(shared_versions).minimum('start_date')
570 570 ].compact.min
571 571 end
572 572
573 573 # The latest due date of an issue or version
574 574 def due_date
575 575 @due_date ||= [
576 576 issues.maximum('due_date'),
577 577 shared_versions.maximum('effective_date'),
578 578 Issue.fixed_version(shared_versions).maximum('due_date')
579 579 ].compact.max
580 580 end
581 581
582 582 def overdue?
583 583 active? && !due_date.nil? && (due_date < Date.today)
584 584 end
585 585
586 586 # Returns the percent completed for this project, based on the
587 587 # progress on it's versions.
588 588 def completed_percent(options={:include_subprojects => false})
589 589 if options.delete(:include_subprojects)
590 590 total = self_and_descendants.collect(&:completed_percent).sum
591 591
592 592 total / self_and_descendants.count
593 593 else
594 594 if versions.count > 0
595 595 total = versions.collect(&:completed_percent).sum
596 596
597 597 total / versions.count
598 598 else
599 599 100
600 600 end
601 601 end
602 602 end
603 603
604 604 # Return true if this project allows to do the specified action.
605 605 # action can be:
606 606 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
607 607 # * a permission Symbol (eg. :edit_project)
608 608 def allows_to?(action)
609 609 if archived?
610 610 # No action allowed on archived projects
611 611 return false
612 612 end
613 613 unless active? || Redmine::AccessControl.read_action?(action)
614 614 # No write action allowed on closed projects
615 615 return false
616 616 end
617 617 # No action allowed on disabled modules
618 618 if action.is_a? Hash
619 619 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
620 620 else
621 621 allowed_permissions.include? action
622 622 end
623 623 end
624 624
625 def module_enabled?(module_name)
626 module_name = module_name.to_s
627 enabled_modules.detect {|m| m.name == module_name}
625 # Return the enabled module with the given name
626 # or nil if the module is not enabled for the project
627 def enabled_module(name)
628 name = name.to_s
629 enabled_modules.detect {|m| m.name == name}
630 end
631
632 # Return true if the module with the given name is enabled
633 def module_enabled?(name)
634 enabled_module(name).present?
628 635 end
629 636
630 637 def enabled_module_names=(module_names)
631 638 if module_names && module_names.is_a?(Array)
632 639 module_names = module_names.collect(&:to_s).reject(&:blank?)
633 640 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
634 641 else
635 642 enabled_modules.clear
636 643 end
637 644 end
638 645
639 646 # Returns an array of the enabled modules names
640 647 def enabled_module_names
641 648 enabled_modules.collect(&:name)
642 649 end
643 650
644 651 # Enable a specific module
645 652 #
646 653 # Examples:
647 654 # project.enable_module!(:issue_tracking)
648 655 # project.enable_module!("issue_tracking")
649 656 def enable_module!(name)
650 657 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
651 658 end
652 659
653 660 # Disable a module if it exists
654 661 #
655 662 # Examples:
656 663 # project.disable_module!(:issue_tracking)
657 664 # project.disable_module!("issue_tracking")
658 665 # project.disable_module!(project.enabled_modules.first)
659 666 def disable_module!(target)
660 667 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
661 668 target.destroy unless target.blank?
662 669 end
663 670
664 671 safe_attributes 'name',
665 672 'description',
666 673 'homepage',
667 674 'is_public',
668 675 'identifier',
669 676 'custom_field_values',
670 677 'custom_fields',
671 678 'tracker_ids',
672 679 'issue_custom_field_ids'
673 680
674 681 safe_attributes 'enabled_module_names',
675 682 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
676 683
677 684 safe_attributes 'inherit_members',
678 685 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
679 686
680 687 # Returns an array of projects that are in this project's hierarchy
681 688 #
682 689 # Example: parents, children, siblings
683 690 def hierarchy
684 691 parents = project.self_and_ancestors || []
685 692 descendants = project.descendants || []
686 693 project_hierarchy = parents | descendants # Set union
687 694 end
688 695
689 696 # Returns an auto-generated project identifier based on the last identifier used
690 697 def self.next_identifier
691 698 p = Project.order('id DESC').first
692 699 p.nil? ? nil : p.identifier.to_s.succ
693 700 end
694 701
695 702 # Copies and saves the Project instance based on the +project+.
696 703 # Duplicates the source project's:
697 704 # * Wiki
698 705 # * Versions
699 706 # * Categories
700 707 # * Issues
701 708 # * Members
702 709 # * Queries
703 710 #
704 711 # Accepts an +options+ argument to specify what to copy
705 712 #
706 713 # Examples:
707 714 # project.copy(1) # => copies everything
708 715 # project.copy(1, :only => 'members') # => copies members only
709 716 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
710 717 def copy(project, options={})
711 718 project = project.is_a?(Project) ? project : Project.find(project)
712 719
713 720 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
714 721 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
715 722
716 723 Project.transaction do
717 724 if save
718 725 reload
719 726 to_be_copied.each do |name|
720 727 send "copy_#{name}", project
721 728 end
722 729 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
723 730 save
724 731 end
725 732 end
726 733 end
727 734
728 735 # Returns a new unsaved Project instance with attributes copied from +project+
729 736 def self.copy_from(project)
730 737 project = project.is_a?(Project) ? project : Project.find(project)
731 738 # clear unique attributes
732 739 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
733 740 copy = Project.new(attributes)
734 741 copy.enabled_modules = project.enabled_modules
735 742 copy.trackers = project.trackers
736 743 copy.custom_values = project.custom_values.collect {|v| v.clone}
737 744 copy.issue_custom_fields = project.issue_custom_fields
738 745 copy
739 746 end
740 747
741 748 # Yields the given block for each project with its level in the tree
742 749 def self.project_tree(projects, &block)
743 750 ancestors = []
744 751 projects.sort_by(&:lft).each do |project|
745 752 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
746 753 ancestors.pop
747 754 end
748 755 yield project, ancestors.size
749 756 ancestors << project
750 757 end
751 758 end
752 759
753 760 private
754 761
755 762 def after_parent_changed(parent_was)
756 763 remove_inherited_member_roles
757 764 add_inherited_member_roles
758 765 end
759 766
760 767 def update_inherited_members
761 768 if parent
762 769 if inherit_members? && !inherit_members_was
763 770 remove_inherited_member_roles
764 771 add_inherited_member_roles
765 772 elsif !inherit_members? && inherit_members_was
766 773 remove_inherited_member_roles
767 774 end
768 775 end
769 776 end
770 777
771 778 def remove_inherited_member_roles
772 779 member_roles = memberships.map(&:member_roles).flatten
773 780 member_role_ids = member_roles.map(&:id)
774 781 member_roles.each do |member_role|
775 782 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
776 783 member_role.destroy
777 784 end
778 785 end
779 786 end
780 787
781 788 def add_inherited_member_roles
782 789 if inherit_members? && parent
783 790 parent.memberships.each do |parent_member|
784 791 member = Member.find_or_new(self.id, parent_member.user_id)
785 792 parent_member.member_roles.each do |parent_member_role|
786 793 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
787 794 end
788 795 member.save!
789 796 end
790 797 end
791 798 end
792 799
793 800 # Copies wiki from +project+
794 801 def copy_wiki(project)
795 802 # Check that the source project has a wiki first
796 803 unless project.wiki.nil?
797 804 wiki = self.wiki || Wiki.new
798 805 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
799 806 wiki_pages_map = {}
800 807 project.wiki.pages.each do |page|
801 808 # Skip pages without content
802 809 next if page.content.nil?
803 810 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
804 811 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
805 812 new_wiki_page.content = new_wiki_content
806 813 wiki.pages << new_wiki_page
807 814 wiki_pages_map[page.id] = new_wiki_page
808 815 end
809 816
810 817 self.wiki = wiki
811 818 wiki.save
812 819 # Reproduce page hierarchy
813 820 project.wiki.pages.each do |page|
814 821 if page.parent_id && wiki_pages_map[page.id]
815 822 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
816 823 wiki_pages_map[page.id].save
817 824 end
818 825 end
819 826 end
820 827 end
821 828
822 829 # Copies versions from +project+
823 830 def copy_versions(project)
824 831 project.versions.each do |version|
825 832 new_version = Version.new
826 833 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
827 834 self.versions << new_version
828 835 end
829 836 end
830 837
831 838 # Copies issue categories from +project+
832 839 def copy_issue_categories(project)
833 840 project.issue_categories.each do |issue_category|
834 841 new_issue_category = IssueCategory.new
835 842 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
836 843 self.issue_categories << new_issue_category
837 844 end
838 845 end
839 846
840 847 # Copies issues from +project+
841 848 def copy_issues(project)
842 849 # Stores the source issue id as a key and the copied issues as the
843 850 # value. Used to map the two togeather for issue relations.
844 851 issues_map = {}
845 852
846 853 # Store status and reopen locked/closed versions
847 854 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
848 855 version_statuses.each do |version, status|
849 856 version.update_attribute :status, 'open'
850 857 end
851 858
852 859 # Get issues sorted by root_id, lft so that parent issues
853 860 # get copied before their children
854 861 project.issues.reorder('root_id, lft').each do |issue|
855 862 new_issue = Issue.new
856 863 new_issue.copy_from(issue, :subtasks => false, :link => false)
857 864 new_issue.project = self
858 865 # Changing project resets the custom field values
859 866 # TODO: handle this in Issue#project=
860 867 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
861 868 # Reassign fixed_versions by name, since names are unique per project
862 869 if issue.fixed_version && issue.fixed_version.project == project
863 870 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
864 871 end
865 872 # Reassign the category by name, since names are unique per project
866 873 if issue.category
867 874 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
868 875 end
869 876 # Parent issue
870 877 if issue.parent_id
871 878 if copied_parent = issues_map[issue.parent_id]
872 879 new_issue.parent_issue_id = copied_parent.id
873 880 end
874 881 end
875 882
876 883 self.issues << new_issue
877 884 if new_issue.new_record?
878 885 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
879 886 else
880 887 issues_map[issue.id] = new_issue unless new_issue.new_record?
881 888 end
882 889 end
883 890
884 891 # Restore locked/closed version statuses
885 892 version_statuses.each do |version, status|
886 893 version.update_attribute :status, status
887 894 end
888 895
889 896 # Relations after in case issues related each other
890 897 project.issues.each do |issue|
891 898 new_issue = issues_map[issue.id]
892 899 unless new_issue
893 900 # Issue was not copied
894 901 next
895 902 end
896 903
897 904 # Relations
898 905 issue.relations_from.each do |source_relation|
899 906 new_issue_relation = IssueRelation.new
900 907 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
901 908 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
902 909 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
903 910 new_issue_relation.issue_to = source_relation.issue_to
904 911 end
905 912 new_issue.relations_from << new_issue_relation
906 913 end
907 914
908 915 issue.relations_to.each do |source_relation|
909 916 new_issue_relation = IssueRelation.new
910 917 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
911 918 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
912 919 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
913 920 new_issue_relation.issue_from = source_relation.issue_from
914 921 end
915 922 new_issue.relations_to << new_issue_relation
916 923 end
917 924 end
918 925 end
919 926
920 927 # Copies members from +project+
921 928 def copy_members(project)
922 929 # Copy users first, then groups to handle members with inherited and given roles
923 930 members_to_copy = []
924 931 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
925 932 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
926 933
927 934 members_to_copy.each do |member|
928 935 new_member = Member.new
929 936 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
930 937 # only copy non inherited roles
931 938 # inherited roles will be added when copying the group membership
932 939 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
933 940 next if role_ids.empty?
934 941 new_member.role_ids = role_ids
935 942 new_member.project = self
936 943 self.members << new_member
937 944 end
938 945 end
939 946
940 947 # Copies queries from +project+
941 948 def copy_queries(project)
942 949 project.queries.each do |query|
943 950 new_query = IssueQuery.new
944 951 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
945 952 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
946 953 new_query.project = self
947 954 new_query.user_id = query.user_id
948 955 self.queries << new_query
949 956 end
950 957 end
951 958
952 959 # Copies boards from +project+
953 960 def copy_boards(project)
954 961 project.boards.each do |board|
955 962 new_board = Board.new
956 963 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
957 964 new_board.project = self
958 965 self.boards << new_board
959 966 end
960 967 end
961 968
962 969 def allowed_permissions
963 970 @allowed_permissions ||= begin
964 971 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
965 972 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
966 973 end
967 974 end
968 975
969 976 def allowed_actions
970 977 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
971 978 end
972 979
973 980 # Returns all the active Systemwide and project specific activities
974 981 def active_activities
975 982 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
976 983
977 984 if overridden_activity_ids.empty?
978 985 return TimeEntryActivity.shared.active
979 986 else
980 987 return system_activities_and_project_overrides
981 988 end
982 989 end
983 990
984 991 # Returns all the Systemwide and project specific activities
985 992 # (inactive and active)
986 993 def all_activities
987 994 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
988 995
989 996 if overridden_activity_ids.empty?
990 997 return TimeEntryActivity.shared
991 998 else
992 999 return system_activities_and_project_overrides(true)
993 1000 end
994 1001 end
995 1002
996 1003 # Returns the systemwide active activities merged with the project specific overrides
997 1004 def system_activities_and_project_overrides(include_inactive=false)
998 1005 t = TimeEntryActivity.table_name
999 1006 scope = TimeEntryActivity.where(
1000 1007 "(#{t}.project_id IS NULL AND #{t}.id NOT IN (?)) OR (#{t}.project_id = ?)",
1001 1008 time_entry_activities.map(&:parent_id), id
1002 1009 )
1003 1010 unless include_inactive
1004 1011 scope = scope.active
1005 1012 end
1006 1013 scope
1007 1014 end
1008 1015
1009 1016 # Archives subprojects recursively
1010 1017 def archive!
1011 1018 children.each do |subproject|
1012 1019 subproject.send :archive!
1013 1020 end
1014 1021 update_attribute :status, STATUS_ARCHIVED
1015 1022 end
1016 1023
1017 1024 def update_position_under_parent
1018 1025 set_or_update_position_under(parent)
1019 1026 end
1020 1027
1021 1028 public
1022 1029
1023 1030 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1024 1031 def set_or_update_position_under(target_parent)
1025 1032 parent_was = parent
1026 1033 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1027 1034 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1028 1035
1029 1036 if to_be_inserted_before
1030 1037 move_to_left_of(to_be_inserted_before)
1031 1038 elsif target_parent.nil?
1032 1039 if sibs.empty?
1033 1040 # move_to_root adds the project in first (ie. left) position
1034 1041 move_to_root
1035 1042 else
1036 1043 move_to_right_of(sibs.last) unless self == sibs.last
1037 1044 end
1038 1045 else
1039 1046 # move_to_child_of adds the project in last (ie.right) position
1040 1047 move_to_child_of(target_parent)
1041 1048 end
1042 1049 if parent_was != target_parent
1043 1050 after_parent_changed(parent_was)
1044 1051 end
1045 1052 end
1046 1053 end
@@ -1,46 +1,47
1 1 <div class="contextual">
2 2 <%= link_to(l(:label_news_new),
3 3 new_project_news_path(@project),
4 4 :class => 'icon icon-add',
5 5 :onclick => 'showAndScrollTo("add-news", "news_title"); return false;') if @project && User.current.allowed_to?(:manage_news, @project) %>
6 <%= watcher_link(@project.enabled_module('news'), User.current) %>
6 7 </div>
7 8
8 9 <div id="add-news" style="display:none;">
9 10 <h2><%=l(:label_news_new)%></h2>
10 11 <%= labelled_form_for @news, :url => project_news_index_path(@project),
11 12 :html => { :id => 'news-form', :multipart => true } do |f| %>
12 13 <%= render :partial => 'news/form', :locals => { :f => f } %>
13 14 <%= submit_tag l(:button_create) %>
14 15 <%= preview_link preview_news_path(:project_id => @project), 'news-form' %> |
15 16 <%= link_to l(:button_cancel), "#", :onclick => '$("#add-news").hide()' %>
16 17 <% end if @project %>
17 18 <div id="preview" class="wiki"></div>
18 19 </div>
19 20
20 21 <h2><%=l(:label_news_plural)%></h2>
21 22
22 23 <% if @newss.empty? %>
23 24 <p class="nodata"><%= l(:label_no_data) %></p>
24 25 <% else %>
25 26 <% @newss.each do |news| %>
26 27 <h3><%= avatar(news.author, :size => "24") %><%= link_to_project(news.project) + ': ' unless news.project == @project %>
27 28 <%= link_to h(news.title), news_path(news) %>
28 29 <%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %></h3>
29 30 <p class="author"><%= authoring news.created_on, news.author %></p>
30 31 <div class="wiki">
31 32 <%= textilizable(news, :description) %>
32 33 </div>
33 34 <% end %>
34 35 <% end %>
35 36 <p class="pagination"><%= pagination_links_full @news_pages %></p>
36 37
37 38 <% other_formats_links do |f| %>
38 39 <%= f.link_to 'Atom', :url => {:project_id => @project, :key => User.current.rss_key} %>
39 40 <% end %>
40 41
41 42 <% content_for :header_tags do %>
42 43 <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
43 44 <%= stylesheet_link_tag 'scm' %>
44 45 <% end %>
45 46
46 47 <% html_title(l(:label_news_plural)) -%>
@@ -1,228 +1,249
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class WatchersControllerTest < ActionController::TestCase
21 21 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
22 22 :issues, :trackers, :projects_trackers, :issue_statuses, :enumerations, :watchers
23 23
24 24 def setup
25 25 User.current = nil
26 26 end
27 27
28 28 def test_watch_a_single_object
29 29 @request.session[:user_id] = 3
30 30 assert_difference('Watcher.count') do
31 31 xhr :post, :watch, :object_type => 'issue', :object_id => '1'
32 32 assert_response :success
33 33 assert_include '$(".issue-1-watcher")', response.body
34 34 end
35 35 assert Issue.find(1).watched_by?(User.find(3))
36 36 end
37 37
38 38 def test_watch_a_collection_with_a_single_object
39 39 @request.session[:user_id] = 3
40 40 assert_difference('Watcher.count') do
41 41 xhr :post, :watch, :object_type => 'issue', :object_id => ['1']
42 42 assert_response :success
43 43 assert_include '$(".issue-1-watcher")', response.body
44 44 end
45 45 assert Issue.find(1).watched_by?(User.find(3))
46 46 end
47 47
48 48 def test_watch_a_collection_with_multiple_objects
49 49 @request.session[:user_id] = 3
50 50 assert_difference('Watcher.count', 2) do
51 51 xhr :post, :watch, :object_type => 'issue', :object_id => ['1', '3']
52 52 assert_response :success
53 53 assert_include '$(".issue-bulk-watcher")', response.body
54 54 end
55 55 assert Issue.find(1).watched_by?(User.find(3))
56 56 assert Issue.find(3).watched_by?(User.find(3))
57 57 end
58 58
59 def test_watch_a_news_module_should_add_watcher
60 @request.session[:user_id] = 7
61 assert_not_nil m = Project.find(1).enabled_module('news')
62
63 assert_difference 'Watcher.count' do
64 xhr :post, :watch, :object_type => 'enabled_module', :object_id => m.id.to_s
65 assert_response :success
66 end
67 assert m.reload.watched_by?(User.find(7))
68 end
69
70 def test_watch_a_private_news_module_without_permission_should_fail
71 @request.session[:user_id] = 7
72 assert_not_nil m = Project.find(2).enabled_module('news')
73
74 assert_no_difference 'Watcher.count' do
75 xhr :post, :watch, :object_type => 'enabled_module', :object_id => m.id.to_s
76 assert_response 403
77 end
78 end
79
59 80 def test_watch_should_be_denied_without_permission
60 81 Role.find(2).remove_permission! :view_issues
61 82 @request.session[:user_id] = 3
62 83 assert_no_difference('Watcher.count') do
63 84 xhr :post, :watch, :object_type => 'issue', :object_id => '1'
64 85 assert_response 403
65 86 end
66 87 end
67 88
68 89 def test_watch_invalid_class_should_respond_with_404
69 90 @request.session[:user_id] = 3
70 91 assert_no_difference('Watcher.count') do
71 92 xhr :post, :watch, :object_type => 'foo', :object_id => '1'
72 93 assert_response 404
73 94 end
74 95 end
75 96
76 97 def test_watch_invalid_object_should_respond_with_404
77 98 @request.session[:user_id] = 3
78 99 assert_no_difference('Watcher.count') do
79 100 xhr :post, :watch, :object_type => 'issue', :object_id => '999'
80 101 assert_response 404
81 102 end
82 103 end
83 104
84 105 def test_unwatch
85 106 @request.session[:user_id] = 3
86 107 assert_difference('Watcher.count', -1) do
87 108 xhr :delete, :unwatch, :object_type => 'issue', :object_id => '2'
88 109 assert_response :success
89 110 assert_include '$(".issue-2-watcher")', response.body
90 111 end
91 112 assert !Issue.find(1).watched_by?(User.find(3))
92 113 end
93 114
94 115 def test_unwatch_a_collection_with_multiple_objects
95 116 @request.session[:user_id] = 3
96 117 Watcher.create!(:user_id => 3, :watchable => Issue.find(1))
97 118 Watcher.create!(:user_id => 3, :watchable => Issue.find(3))
98 119
99 120 assert_difference('Watcher.count', -2) do
100 121 xhr :delete, :unwatch, :object_type => 'issue', :object_id => ['1', '3']
101 122 assert_response :success
102 123 assert_include '$(".issue-bulk-watcher")', response.body
103 124 end
104 125 assert !Issue.find(1).watched_by?(User.find(3))
105 126 assert !Issue.find(3).watched_by?(User.find(3))
106 127 end
107 128
108 129 def test_new
109 130 @request.session[:user_id] = 2
110 131 xhr :get, :new, :object_type => 'issue', :object_id => '2'
111 132 assert_response :success
112 133 assert_match /ajax-modal/, response.body
113 134 end
114 135
115 136 def test_new_for_new_record_with_project_id
116 137 @request.session[:user_id] = 2
117 138 xhr :get, :new, :project_id => 1
118 139 assert_response :success
119 140 assert_equal Project.find(1), assigns(:project)
120 141 assert_match /ajax-modal/, response.body
121 142 end
122 143
123 144 def test_new_for_new_record_with_project_identifier
124 145 @request.session[:user_id] = 2
125 146 xhr :get, :new, :project_id => 'ecookbook'
126 147 assert_response :success
127 148 assert_equal Project.find(1), assigns(:project)
128 149 assert_match /ajax-modal/, response.body
129 150 end
130 151
131 152 def test_create
132 153 @request.session[:user_id] = 2
133 154 assert_difference('Watcher.count') do
134 155 xhr :post, :create, :object_type => 'issue', :object_id => '2',
135 156 :watcher => {:user_id => '4'}
136 157 assert_response :success
137 158 assert_match /watchers/, response.body
138 159 assert_match /ajax-modal/, response.body
139 160 end
140 161 assert Issue.find(2).watched_by?(User.find(4))
141 162 end
142 163
143 164 def test_create_multiple
144 165 @request.session[:user_id] = 2
145 166 assert_difference('Watcher.count', 2) do
146 167 xhr :post, :create, :object_type => 'issue', :object_id => '2',
147 168 :watcher => {:user_ids => ['4', '7']}
148 169 assert_response :success
149 170 assert_match /watchers/, response.body
150 171 assert_match /ajax-modal/, response.body
151 172 end
152 173 assert Issue.find(2).watched_by?(User.find(4))
153 174 assert Issue.find(2).watched_by?(User.find(7))
154 175 end
155 176
156 177 def test_autocomplete_on_watchable_creation
157 178 @request.session[:user_id] = 2
158 179 xhr :get, :autocomplete_for_user, :q => 'mi', :project_id => 'ecookbook'
159 180 assert_response :success
160 181 assert_select 'input', :count => 4
161 182 assert_select 'input[name=?][value=1]', 'watcher[user_ids][]'
162 183 assert_select 'input[name=?][value=2]', 'watcher[user_ids][]'
163 184 assert_select 'input[name=?][value=8]', 'watcher[user_ids][]'
164 185 assert_select 'input[name=?][value=9]', 'watcher[user_ids][]'
165 186 end
166 187
167 188 def test_search_non_member_on_create
168 189 @request.session[:user_id] = 2
169 190 project = Project.find_by_name("ecookbook")
170 191 user = User.generate!(:firstname => 'issue15622')
171 192 membership = user.membership(project)
172 193 assert_nil membership
173 194 xhr :get, :autocomplete_for_user, :q => 'issue15622', :project_id => 'ecookbook'
174 195 assert_response :success
175 196 assert_select 'input', :count => 1
176 197 end
177 198
178 199 def test_autocomplete_on_watchable_update
179 200 @request.session[:user_id] = 2
180 201 xhr :get, :autocomplete_for_user, :q => 'mi', :object_id => '2',
181 202 :object_type => 'issue', :project_id => 'ecookbook'
182 203 assert_response :success
183 204 assert_select 'input', :count => 3
184 205 assert_select 'input[name=?][value=2]', 'watcher[user_ids][]'
185 206 assert_select 'input[name=?][value=8]', 'watcher[user_ids][]'
186 207 assert_select 'input[name=?][value=9]', 'watcher[user_ids][]'
187 208 end
188 209
189 210 def test_search_and_add_non_member_on_update
190 211 @request.session[:user_id] = 2
191 212 project = Project.find_by_name("ecookbook")
192 213 user = User.generate!(:firstname => 'issue15622')
193 214 membership = user.membership(project)
194 215 assert_nil membership
195 216 xhr :get, :autocomplete_for_user, :q => 'issue15622', :object_id => '2',
196 217 :object_type => 'issue', :project_id => 'ecookbook'
197 218 assert_response :success
198 219 assert_select 'input', :count => 1
199 220 assert_difference('Watcher.count', 1) do
200 221 xhr :post, :create, :object_type => 'issue', :object_id => '2',
201 222 :watcher => {:user_ids => ["#{user.id}"]}
202 223 assert_response :success
203 224 assert_match /watchers/, response.body
204 225 assert_match /ajax-modal/, response.body
205 226 end
206 227 assert Issue.find(2).watched_by?(user)
207 228 end
208 229
209 230 def test_append
210 231 @request.session[:user_id] = 2
211 232 assert_no_difference 'Watcher.count' do
212 233 xhr :post, :append, :watcher => {:user_ids => ['4', '7']}, :project_id => 'ecookbook'
213 234 assert_response :success
214 235 assert_include 'watchers_inputs', response.body
215 236 assert_include 'issue[watcher_user_ids][]', response.body
216 237 end
217 238 end
218 239
219 240 def test_remove_watcher
220 241 @request.session[:user_id] = 2
221 242 assert_difference('Watcher.count', -1) do
222 243 xhr :delete, :destroy, :object_type => 'issue', :object_id => '2', :user_id => '3'
223 244 assert_response :success
224 245 assert_match /watchers/, response.body
225 246 end
226 247 assert !Issue.find(2).watched_by?(User.find(3))
227 248 end
228 249 end
@@ -1,757 +1,768
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class MailerTest < ActiveSupport::TestCase
21 21 include Redmine::I18n
22 22 include ActionDispatch::Assertions::SelectorAssertions
23 23 fixtures :projects, :enabled_modules, :issues, :users, :members,
24 24 :member_roles, :roles, :documents, :attachments, :news,
25 25 :tokens, :journals, :journal_details, :changesets,
26 26 :trackers, :projects_trackers,
27 27 :issue_statuses, :enumerations, :messages, :boards, :repositories,
28 28 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
29 29 :versions,
30 30 :comments
31 31
32 32 def setup
33 33 ActionMailer::Base.deliveries.clear
34 34 Setting.host_name = 'mydomain.foo'
35 35 Setting.protocol = 'http'
36 36 Setting.plain_text_mail = '0'
37 37 end
38 38
39 39 def test_generated_links_in_emails
40 40 Setting.default_language = 'en'
41 41 Setting.host_name = 'mydomain.foo'
42 42 Setting.protocol = 'https'
43 43
44 44 journal = Journal.find(3)
45 45 assert Mailer.deliver_issue_edit(journal)
46 46
47 47 mail = last_email
48 48 assert_not_nil mail
49 49
50 50 assert_select_email do
51 51 # link to the main ticket
52 52 assert_select 'a[href=?]',
53 53 'https://mydomain.foo/issues/2#change-3',
54 54 :text => 'Feature request #2: Add ingredients categories'
55 55 # link to a referenced ticket
56 56 assert_select 'a[href=?][title=?]',
57 57 'https://mydomain.foo/issues/1',
58 58 'Can&#x27;t print recipes (New)',
59 59 :text => '#1'
60 60 # link to a changeset
61 61 assert_select 'a[href=?][title=?]',
62 62 'https://mydomain.foo/projects/ecookbook/repository/revisions/2',
63 63 'This commit fixes #1, #2 and references #1 &amp; #3',
64 64 :text => 'r2'
65 65 # link to a description diff
66 66 assert_select 'a[href=?][title=?]',
67 67 'https://mydomain.foo/journals/diff/3?detail_id=4',
68 68 'View differences',
69 69 :text => 'diff'
70 70 # link to an attachment
71 71 assert_select 'a[href=?]',
72 72 'https://mydomain.foo/attachments/download/4/source.rb',
73 73 :text => 'source.rb'
74 74 end
75 75 end
76 76
77 77 def test_generated_links_with_prefix
78 78 Setting.default_language = 'en'
79 79 relative_url_root = Redmine::Utils.relative_url_root
80 80 Setting.host_name = 'mydomain.foo/rdm'
81 81 Setting.protocol = 'http'
82 82
83 83 journal = Journal.find(3)
84 84 assert Mailer.deliver_issue_edit(journal)
85 85
86 86 mail = last_email
87 87 assert_not_nil mail
88 88
89 89 assert_select_email do
90 90 # link to the main ticket
91 91 assert_select 'a[href=?]',
92 92 'http://mydomain.foo/rdm/issues/2#change-3',
93 93 :text => 'Feature request #2: Add ingredients categories'
94 94 # link to a referenced ticket
95 95 assert_select 'a[href=?][title=?]',
96 96 'http://mydomain.foo/rdm/issues/1',
97 97 'Can&#x27;t print recipes (New)',
98 98 :text => '#1'
99 99 # link to a changeset
100 100 assert_select 'a[href=?][title=?]',
101 101 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
102 102 'This commit fixes #1, #2 and references #1 &amp; #3',
103 103 :text => 'r2'
104 104 # link to a description diff
105 105 assert_select 'a[href=?][title=?]',
106 106 'http://mydomain.foo/rdm/journals/diff/3?detail_id=4',
107 107 'View differences',
108 108 :text => 'diff'
109 109 # link to an attachment
110 110 assert_select 'a[href=?]',
111 111 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
112 112 :text => 'source.rb'
113 113 end
114 114 end
115 115
116 116 def test_issue_edit_should_generate_url_with_hostname_for_relations
117 117 journal = Journal.new(:journalized => Issue.find(1), :user => User.find(1), :created_on => Time.now)
118 118 journal.details << JournalDetail.new(:property => 'relation', :prop_key => 'label_relates_to', :value => 2)
119 119 Mailer.deliver_issue_edit(journal)
120 120 assert_not_nil last_email
121 121 assert_select_email do
122 122 assert_select 'a[href=?]', 'http://mydomain.foo/issues/2', :text => 'Feature request #2'
123 123 end
124 124 end
125 125
126 126 def test_generated_links_with_prefix_and_no_relative_url_root
127 127 Setting.default_language = 'en'
128 128 relative_url_root = Redmine::Utils.relative_url_root
129 129 Setting.host_name = 'mydomain.foo/rdm'
130 130 Setting.protocol = 'http'
131 131 Redmine::Utils.relative_url_root = nil
132 132
133 133 journal = Journal.find(3)
134 134 assert Mailer.deliver_issue_edit(journal)
135 135
136 136 mail = last_email
137 137 assert_not_nil mail
138 138
139 139 assert_select_email do
140 140 # link to the main ticket
141 141 assert_select 'a[href=?]',
142 142 'http://mydomain.foo/rdm/issues/2#change-3',
143 143 :text => 'Feature request #2: Add ingredients categories'
144 144 # link to a referenced ticket
145 145 assert_select 'a[href=?][title=?]',
146 146 'http://mydomain.foo/rdm/issues/1',
147 147 'Can&#x27;t print recipes (New)',
148 148 :text => '#1'
149 149 # link to a changeset
150 150 assert_select 'a[href=?][title=?]',
151 151 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
152 152 'This commit fixes #1, #2 and references #1 &amp; #3',
153 153 :text => 'r2'
154 154 # link to a description diff
155 155 assert_select 'a[href=?][title=?]',
156 156 'http://mydomain.foo/rdm/journals/diff/3?detail_id=4',
157 157 'View differences',
158 158 :text => 'diff'
159 159 # link to an attachment
160 160 assert_select 'a[href=?]',
161 161 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
162 162 :text => 'source.rb'
163 163 end
164 164 ensure
165 165 # restore it
166 166 Redmine::Utils.relative_url_root = relative_url_root
167 167 end
168 168
169 169 def test_email_headers
170 170 issue = Issue.find(1)
171 171 Mailer.deliver_issue_add(issue)
172 172 mail = last_email
173 173 assert_not_nil mail
174 174 assert_equal 'OOF', mail.header['X-Auto-Response-Suppress'].to_s
175 175 assert_equal 'auto-generated', mail.header['Auto-Submitted'].to_s
176 176 assert_equal '<redmine.example.net>', mail.header['List-Id'].to_s
177 177 end
178 178
179 179 def test_email_headers_should_include_sender
180 180 issue = Issue.find(1)
181 181 Mailer.deliver_issue_add(issue)
182 182 mail = last_email
183 183 assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s
184 184 end
185 185
186 186 def test_plain_text_mail
187 187 Setting.plain_text_mail = 1
188 188 journal = Journal.find(2)
189 189 Mailer.deliver_issue_edit(journal)
190 190 mail = last_email
191 191 assert_equal "text/plain; charset=UTF-8", mail.content_type
192 192 assert_equal 0, mail.parts.size
193 193 assert !mail.encoded.include?('href')
194 194 end
195 195
196 196 def test_html_mail
197 197 Setting.plain_text_mail = 0
198 198 journal = Journal.find(2)
199 199 Mailer.deliver_issue_edit(journal)
200 200 mail = last_email
201 201 assert_equal 2, mail.parts.size
202 202 assert mail.encoded.include?('href')
203 203 end
204 204
205 205 def test_from_header
206 206 with_settings :mail_from => 'redmine@example.net' do
207 207 Mailer.test_email(User.find(1)).deliver
208 208 end
209 209 mail = last_email
210 210 assert_equal 'redmine@example.net', mail.from_addrs.first
211 211 end
212 212
213 213 def test_from_header_with_phrase
214 214 with_settings :mail_from => 'Redmine app <redmine@example.net>' do
215 215 Mailer.test_email(User.find(1)).deliver
216 216 end
217 217 mail = last_email
218 218 assert_equal 'redmine@example.net', mail.from_addrs.first
219 219 assert_equal 'Redmine app <redmine@example.net>', mail.header['From'].to_s
220 220 end
221 221
222 222 def test_should_not_send_email_without_recipient
223 223 news = News.first
224 224 user = news.author
225 225 # Remove members except news author
226 226 news.project.memberships.each {|m| m.destroy unless m.user == user}
227 227
228 228 user.pref.no_self_notified = false
229 229 user.pref.save
230 230 User.current = user
231 231 Mailer.news_added(news.reload).deliver
232 232 assert_equal 1, last_email.bcc.size
233 233
234 234 # nobody to notify
235 235 user.pref.no_self_notified = true
236 236 user.pref.save
237 237 User.current = user
238 238 ActionMailer::Base.deliveries.clear
239 239 Mailer.news_added(news.reload).deliver
240 240 assert ActionMailer::Base.deliveries.empty?
241 241 end
242 242
243 243 def test_issue_add_message_id
244 244 issue = Issue.find(2)
245 245 Mailer.deliver_issue_add(issue)
246 246 mail = last_email
247 247 assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id
248 248 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
249 249 end
250 250
251 251 def test_issue_edit_message_id
252 252 journal = Journal.find(3)
253 253 journal.issue = Issue.find(2)
254 254
255 255 Mailer.deliver_issue_edit(journal)
256 256 mail = last_email
257 257 assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
258 258 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
259 259 assert_select_email do
260 260 # link to the update
261 261 assert_select "a[href=?]",
262 262 "http://mydomain.foo/issues/#{journal.journalized_id}#change-#{journal.id}"
263 263 end
264 264 end
265 265
266 266 def test_message_posted_message_id
267 267 message = Message.find(1)
268 268 Mailer.message_posted(message).deliver
269 269 mail = last_email
270 270 assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
271 271 assert_include "redmine.message-1.20070512151532@example.net", mail.references
272 272 assert_select_email do
273 273 # link to the message
274 274 assert_select "a[href=?]",
275 275 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}",
276 276 :text => message.subject
277 277 end
278 278 end
279 279
280 280 def test_reply_posted_message_id
281 281 message = Message.find(3)
282 282 Mailer.message_posted(message).deliver
283 283 mail = last_email
284 284 assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
285 285 assert_include "redmine.message-1.20070512151532@example.net", mail.references
286 286 assert_select_email do
287 287 # link to the reply
288 288 assert_select "a[href=?]",
289 289 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.root.id}?r=#{message.id}#message-#{message.id}",
290 290 :text => message.subject
291 291 end
292 292 end
293 293
294 294 test "#issue_add should notify project members" do
295 295 issue = Issue.find(1)
296 296 assert Mailer.deliver_issue_add(issue)
297 297 assert last_email.bcc.include?('dlopper@somenet.foo')
298 298 end
299 299
300 300 test "#issue_add should not notify project members that are not allow to view the issue" do
301 301 issue = Issue.find(1)
302 302 Role.find(2).remove_permission!(:view_issues)
303 303 assert Mailer.deliver_issue_add(issue)
304 304 assert !last_email.bcc.include?('dlopper@somenet.foo')
305 305 end
306 306
307 307 test "#issue_add should notify issue watchers" do
308 308 issue = Issue.find(1)
309 309 user = User.find(9)
310 310 # minimal email notification options
311 311 user.pref.no_self_notified = '1'
312 312 user.pref.save
313 313 user.mail_notification = false
314 314 user.save
315 315
316 316 Watcher.create!(:watchable => issue, :user => user)
317 317 assert Mailer.deliver_issue_add(issue)
318 318 assert last_email.bcc.include?(user.mail)
319 319 end
320 320
321 321 test "#issue_add should not notify watchers not allowed to view the issue" do
322 322 issue = Issue.find(1)
323 323 user = User.find(9)
324 324 Watcher.create!(:watchable => issue, :user => user)
325 325 Role.non_member.remove_permission!(:view_issues)
326 326 assert Mailer.deliver_issue_add(issue)
327 327 assert !last_email.bcc.include?(user.mail)
328 328 end
329 329
330 330 def test_issue_add_should_include_enabled_fields
331 331 Setting.default_language = 'en'
332 332 issue = Issue.find(2)
333 333 assert Mailer.deliver_issue_add(issue)
334 334 assert_mail_body_match '* Target version: 1.0', last_email
335 335 assert_select_email do
336 336 assert_select 'li', :text => 'Target version: 1.0'
337 337 end
338 338 end
339 339
340 340 def test_issue_add_should_not_include_disabled_fields
341 341 Setting.default_language = 'en'
342 342 issue = Issue.find(2)
343 343 tracker = issue.tracker
344 344 tracker.core_fields -= ['fixed_version_id']
345 345 tracker.save!
346 346 assert Mailer.deliver_issue_add(issue)
347 347 assert_mail_body_no_match 'Target version', last_email
348 348 assert_select_email do
349 349 assert_select 'li', :text => /Target version/, :count => 0
350 350 end
351 351 end
352 352
353 353 # test mailer methods for each language
354 354 def test_issue_add
355 355 issue = Issue.find(1)
356 356 valid_languages.each do |lang|
357 357 Setting.default_language = lang.to_s
358 358 assert Mailer.deliver_issue_add(issue)
359 359 end
360 360 end
361 361
362 362 def test_issue_edit
363 363 journal = Journal.find(1)
364 364 valid_languages.each do |lang|
365 365 Setting.default_language = lang.to_s
366 366 assert Mailer.deliver_issue_edit(journal)
367 367 end
368 368 end
369 369
370 370 def test_issue_edit_should_send_private_notes_to_users_with_permission_only
371 371 journal = Journal.find(1)
372 372 journal.private_notes = true
373 373 journal.save!
374 374
375 375 Role.find(2).add_permission! :view_private_notes
376 376 Mailer.deliver_issue_edit(journal)
377 377 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
378 378
379 379 Role.find(2).remove_permission! :view_private_notes
380 380 Mailer.deliver_issue_edit(journal)
381 381 assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
382 382 end
383 383
384 384 def test_issue_edit_should_send_private_notes_to_watchers_with_permission_only
385 385 Issue.find(1).set_watcher(User.find_by_login('someone'))
386 386 journal = Journal.find(1)
387 387 journal.private_notes = true
388 388 journal.save!
389 389
390 390 Role.non_member.add_permission! :view_private_notes
391 391 Mailer.deliver_issue_edit(journal)
392 392 assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
393 393
394 394 Role.non_member.remove_permission! :view_private_notes
395 395 Mailer.deliver_issue_edit(journal)
396 396 assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
397 397 end
398 398
399 399 def test_issue_edit_should_mark_private_notes
400 400 journal = Journal.find(2)
401 401 journal.private_notes = true
402 402 journal.save!
403 403
404 404 with_settings :default_language => 'en' do
405 405 Mailer.deliver_issue_edit(journal)
406 406 end
407 407 assert_mail_body_match '(Private notes)', last_email
408 408 end
409 409
410 410 def test_issue_edit_with_relation_should_notify_users_who_can_see_the_related_issue
411 411 issue = Issue.generate!
412 412 private_issue = Issue.generate!(:is_private => true)
413 413 IssueRelation.create!(:issue_from => issue, :issue_to => private_issue, :relation_type => 'relates')
414 414 issue.reload
415 415 assert_equal 1, issue.journals.size
416 416 journal = issue.journals.first
417 417 ActionMailer::Base.deliveries.clear
418 418
419 419 Mailer.deliver_issue_edit(journal)
420 420 last_email.bcc.each do |email|
421 421 user = User.find_by_mail(email)
422 422 assert private_issue.visible?(user), "Issue was not visible to #{user}"
423 423 end
424 424 end
425 425
426 426 def test_document_added
427 427 document = Document.find(1)
428 428 valid_languages.each do |lang|
429 429 Setting.default_language = lang.to_s
430 430 assert Mailer.document_added(document).deliver
431 431 end
432 432 end
433 433
434 434 def test_attachments_added
435 435 attachements = [ Attachment.find_by_container_type('Document') ]
436 436 valid_languages.each do |lang|
437 437 Setting.default_language = lang.to_s
438 438 assert Mailer.attachments_added(attachements).deliver
439 439 end
440 440 end
441 441
442 442 def test_version_file_added
443 443 attachements = [ Attachment.find_by_container_type('Version') ]
444 444 assert Mailer.attachments_added(attachements).deliver
445 445 assert_not_nil last_email.bcc
446 446 assert last_email.bcc.any?
447 447 assert_select_email do
448 448 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
449 449 end
450 450 end
451 451
452 452 def test_project_file_added
453 453 attachements = [ Attachment.find_by_container_type('Project') ]
454 454 assert Mailer.attachments_added(attachements).deliver
455 455 assert_not_nil last_email.bcc
456 456 assert last_email.bcc.any?
457 457 assert_select_email do
458 458 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
459 459 end
460 460 end
461 461
462 462 def test_news_added
463 463 news = News.first
464 464 valid_languages.each do |lang|
465 465 Setting.default_language = lang.to_s
466 466 assert Mailer.news_added(news).deliver
467 467 end
468 468 end
469 469
470 def test_news_added_should_notify_project_news_watchers
471 user1 = User.generate!
472 user2 = User.generate!
473 news = News.first
474 news.project.enabled_module('news').add_watcher(user1)
475
476 Mailer.news_added(news).deliver
477 assert_include user1.mail, last_email.bcc
478 assert_not_include user2.mail, last_email.bcc
479 end
480
470 481 def test_news_comment_added
471 482 comment = Comment.find(2)
472 483 valid_languages.each do |lang|
473 484 Setting.default_language = lang.to_s
474 485 assert Mailer.news_comment_added(comment).deliver
475 486 end
476 487 end
477 488
478 489 def test_message_posted
479 490 message = Message.first
480 491 recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
481 492 recipients = recipients.compact.uniq
482 493 valid_languages.each do |lang|
483 494 Setting.default_language = lang.to_s
484 495 assert Mailer.message_posted(message).deliver
485 496 end
486 497 end
487 498
488 499 def test_wiki_content_added
489 500 content = WikiContent.find(1)
490 501 valid_languages.each do |lang|
491 502 Setting.default_language = lang.to_s
492 503 assert_difference 'ActionMailer::Base.deliveries.size' do
493 504 assert Mailer.wiki_content_added(content).deliver
494 505 assert_select_email do
495 506 assert_select 'a[href=?]',
496 507 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
497 508 :text => 'CookBook documentation'
498 509 end
499 510 end
500 511 end
501 512 end
502 513
503 514 def test_wiki_content_updated
504 515 content = WikiContent.find(1)
505 516 valid_languages.each do |lang|
506 517 Setting.default_language = lang.to_s
507 518 assert_difference 'ActionMailer::Base.deliveries.size' do
508 519 assert Mailer.wiki_content_updated(content).deliver
509 520 assert_select_email do
510 521 assert_select 'a[href=?]',
511 522 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
512 523 :text => 'CookBook documentation'
513 524 end
514 525 end
515 526 end
516 527 end
517 528
518 529 def test_account_information
519 530 user = User.find(2)
520 531 valid_languages.each do |lang|
521 532 user.update_attribute :language, lang.to_s
522 533 user.reload
523 534 assert Mailer.account_information(user, 'pAsswORd').deliver
524 535 end
525 536 end
526 537
527 538 def test_lost_password
528 539 token = Token.find(2)
529 540 valid_languages.each do |lang|
530 541 token.user.update_attribute :language, lang.to_s
531 542 token.reload
532 543 assert Mailer.lost_password(token).deliver
533 544 end
534 545 end
535 546
536 547 def test_register
537 548 token = Token.find(1)
538 549 Setting.host_name = 'redmine.foo'
539 550 Setting.protocol = 'https'
540 551
541 552 valid_languages.each do |lang|
542 553 token.user.update_attribute :language, lang.to_s
543 554 token.reload
544 555 ActionMailer::Base.deliveries.clear
545 556 assert Mailer.register(token).deliver
546 557 mail = last_email
547 558 assert_select_email do
548 559 assert_select "a[href=?]",
549 560 "https://redmine.foo/account/activate?token=#{token.value}",
550 561 :text => "https://redmine.foo/account/activate?token=#{token.value}"
551 562 end
552 563 end
553 564 end
554 565
555 566 def test_test
556 567 user = User.find(1)
557 568 valid_languages.each do |lang|
558 569 user.update_attribute :language, lang.to_s
559 570 assert Mailer.test_email(user).deliver
560 571 end
561 572 end
562 573
563 574 def test_reminders
564 575 Mailer.reminders(:days => 42)
565 576 assert_equal 1, ActionMailer::Base.deliveries.size
566 577 mail = last_email
567 578 assert mail.bcc.include?('dlopper@somenet.foo')
568 579 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
569 580 assert_equal '1 issue(s) due in the next 42 days', mail.subject
570 581 end
571 582
572 583 def test_reminders_should_not_include_closed_issues
573 584 with_settings :default_language => 'en' do
574 585 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 5,
575 586 :subject => 'Closed issue', :assigned_to_id => 3,
576 587 :due_date => 5.days.from_now,
577 588 :author_id => 2)
578 589 ActionMailer::Base.deliveries.clear
579 590
580 591 Mailer.reminders(:days => 42)
581 592 assert_equal 1, ActionMailer::Base.deliveries.size
582 593 mail = last_email
583 594 assert mail.bcc.include?('dlopper@somenet.foo')
584 595 assert_mail_body_no_match 'Closed issue', mail
585 596 end
586 597 end
587 598
588 599 def test_reminders_for_users
589 600 Mailer.reminders(:days => 42, :users => ['5'])
590 601 assert_equal 0, ActionMailer::Base.deliveries.size # No mail for dlopper
591 602 Mailer.reminders(:days => 42, :users => ['3'])
592 603 assert_equal 1, ActionMailer::Base.deliveries.size # No mail for dlopper
593 604 mail = last_email
594 605 assert mail.bcc.include?('dlopper@somenet.foo')
595 606 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
596 607 end
597 608
598 609 def test_reminder_should_include_issues_assigned_to_groups
599 610 with_settings :default_language => 'en' do
600 611 group = Group.generate!
601 612 group.users << User.find(2)
602 613 group.users << User.find(3)
603 614
604 615 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1,
605 616 :subject => 'Assigned to group', :assigned_to => group,
606 617 :due_date => 5.days.from_now,
607 618 :author_id => 2)
608 619 ActionMailer::Base.deliveries.clear
609 620
610 621 Mailer.reminders(:days => 7)
611 622 assert_equal 2, ActionMailer::Base.deliveries.size
612 623 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.map(&:bcc).flatten.sort
613 624 ActionMailer::Base.deliveries.each do |mail|
614 625 assert_mail_body_match 'Assigned to group', mail
615 626 end
616 627 end
617 628 end
618 629
619 630 def test_mailer_should_not_change_locale
620 631 Setting.default_language = 'en'
621 632 # Set current language to italian
622 633 set_language_if_valid 'it'
623 634 # Send an email to a french user
624 635 user = User.find(1)
625 636 user.language = 'fr'
626 637 Mailer.account_activated(user).deliver
627 638 mail = last_email
628 639 assert_mail_body_match 'Votre compte', mail
629 640
630 641 assert_equal :it, current_language
631 642 end
632 643
633 644 def test_with_deliveries_off
634 645 Mailer.with_deliveries false do
635 646 Mailer.test_email(User.find(1)).deliver
636 647 end
637 648 assert ActionMailer::Base.deliveries.empty?
638 649 # should restore perform_deliveries
639 650 assert ActionMailer::Base.perform_deliveries
640 651 end
641 652
642 653 def test_layout_should_include_the_emails_header
643 654 with_settings :emails_header => "*Header content*" do
644 655 with_settings :plain_text_mail => 0 do
645 656 assert Mailer.test_email(User.find(1)).deliver
646 657 assert_select_email do
647 658 assert_select ".header" do
648 659 assert_select "strong", :text => "Header content"
649 660 end
650 661 end
651 662 end
652 663 with_settings :plain_text_mail => 1 do
653 664 assert Mailer.test_email(User.find(1)).deliver
654 665 mail = last_email
655 666 assert_not_nil mail
656 667 assert_include "*Header content*", mail.body.decoded
657 668 end
658 669 end
659 670 end
660 671
661 672 def test_layout_should_not_include_empty_emails_header
662 673 with_settings :emails_header => "", :plain_text_mail => 0 do
663 674 assert Mailer.test_email(User.find(1)).deliver
664 675 assert_select_email do
665 676 assert_select ".header", false
666 677 end
667 678 end
668 679 end
669 680
670 681 def test_layout_should_include_the_emails_footer
671 682 with_settings :emails_footer => "*Footer content*" do
672 683 with_settings :plain_text_mail => 0 do
673 684 assert Mailer.test_email(User.find(1)).deliver
674 685 assert_select_email do
675 686 assert_select ".footer" do
676 687 assert_select "strong", :text => "Footer content"
677 688 end
678 689 end
679 690 end
680 691 with_settings :plain_text_mail => 1 do
681 692 assert Mailer.test_email(User.find(1)).deliver
682 693 mail = last_email
683 694 assert_not_nil mail
684 695 assert_include "\n-- \n", mail.body.decoded
685 696 assert_include "*Footer content*", mail.body.decoded
686 697 end
687 698 end
688 699 end
689 700
690 701 def test_layout_should_not_include_empty_emails_footer
691 702 with_settings :emails_footer => "" do
692 703 with_settings :plain_text_mail => 0 do
693 704 assert Mailer.test_email(User.find(1)).deliver
694 705 assert_select_email do
695 706 assert_select ".footer", false
696 707 end
697 708 end
698 709 with_settings :plain_text_mail => 1 do
699 710 assert Mailer.test_email(User.find(1)).deliver
700 711 mail = last_email
701 712 assert_not_nil mail
702 713 assert_not_include "\n-- \n", mail.body.decoded
703 714 end
704 715 end
705 716 end
706 717
707 718 def test_should_escape_html_templates_only
708 719 Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'Subject with a <tag>')
709 720 mail = last_email
710 721 assert_equal 2, mail.parts.size
711 722 assert_include '<tag>', text_part.body.encoded
712 723 assert_include '&lt;tag&gt;', html_part.body.encoded
713 724 end
714 725
715 726 def test_should_raise_delivery_errors_when_raise_delivery_errors_is_true
716 727 mail = Mailer.test_email(User.find(1))
717 728 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
718 729
719 730 ActionMailer::Base.raise_delivery_errors = true
720 731 assert_raise Exception, "delivery error" do
721 732 mail.deliver
722 733 end
723 734 ensure
724 735 ActionMailer::Base.raise_delivery_errors = false
725 736 end
726 737
727 738 def test_should_log_delivery_errors_when_raise_delivery_errors_is_false
728 739 mail = Mailer.test_email(User.find(1))
729 740 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
730 741
731 742 Rails.logger.expects(:error).with("Email delivery error: delivery error")
732 743 ActionMailer::Base.raise_delivery_errors = false
733 744 assert_nothing_raised do
734 745 mail.deliver
735 746 end
736 747 end
737 748
738 749 def test_mail_should_return_a_mail_message
739 750 assert_kind_of ::Mail::Message, Mailer.test_email(User.find(1))
740 751 end
741 752
742 753 private
743 754
744 755 def last_email
745 756 mail = ActionMailer::Base.deliveries.last
746 757 assert_not_nil mail
747 758 mail
748 759 end
749 760
750 761 def text_part
751 762 last_email.parts.detect {|part| part.content_type.include?('text/plain')}
752 763 end
753 764
754 765 def html_part
755 766 last_email.parts.detect {|part| part.content_type.include?('text/html')}
756 767 end
757 768 end
General Comments 0
You need to be logged in to leave comments. Login now