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